From 0eac7a680c562e79d186e7e20637487a1542f856 Mon Sep 17 00:00:00 2001 From: Sun-sunshine06 Date: Fri, 31 Jul 2026 11:40:35 +0800 Subject: [PATCH] fix(ci): finalize DeepSeek review retries --- .github/scripts/deepseek-common.mjs | 46 +++++++++++++++++++++-------- tests/deepseek-common.test.ts | 9 ++++-- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/.github/scripts/deepseek-common.mjs b/.github/scripts/deepseek-common.mjs index 51d23f37e..58363c2a9 100644 --- a/.github/scripts/deepseek-common.mjs +++ b/.github/scripts/deepseek-common.mjs @@ -343,9 +343,11 @@ export async function callDeepSeekJson({ const parsed = parseJsonObject(content); if (!parsed || typeof parsed !== 'object') { - throw new Error( + const error = new Error( `DeepSeek returned non-JSON content: ${truncate(content, 1000, 'model output')}` ); + error.modelOutput = content; + throw error; } return { @@ -391,25 +393,34 @@ export async function callDeepSeekJsonWithRetries(options) { ...requestOptions } = options let lastError = null + let previousModelOutput = null for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { const attemptMaxTokens = attempt === 1 ? maxTokens : Math.max(maxTokens, 16384) + const retryInstructions = [ + 'AUTOMATION RETRY NOTE:', + '- Return ONLY valid JSON.', + `- The JSON MUST include a non-empty string field named "${fieldName}".`, + '- Do not wrap the JSON in code fences.', + `- Do not return an empty, null, or missing "${fieldName}" field.`, + ] const attemptPrompt = attempt === 1 ? userPrompt - : [ - userPrompt, - '', - 'AUTOMATION RETRY NOTE:', - '- Your previous response was invalid for automation.', - '- Return ONLY valid JSON.', - `- The JSON MUST include a non-empty string field named "${fieldName}".`, - '- Do not wrap the JSON in code fences.', - `- Do not return an empty, null, or missing "${fieldName}" field.`, - ].join('\n') - + : previousModelOutput + ? [ + 'Finalize the prior model analysis below into the requested JSON response.', + 'Do not repeat the analysis and do not inspect the pull request again.', + ...retryInstructions, + '', + 'PRIOR MODEL ANALYSIS:', + truncate(previousModelOutput, 60000, 'prior model analysis'), + ].join('\n') + : [userPrompt, '', ...retryInstructions].join('\n') + + let result = null try { - const result = await callDeepSeekJson({ + result = await callDeepSeekJson({ ...requestOptions, maxTokens: attemptMaxTokens, userPrompt: attemptPrompt, @@ -417,6 +428,15 @@ export async function callDeepSeekJsonWithRetries(options) { assertNonEmptyParsedString(result.parsed, fieldName) return result } catch (error) { + const modelOutput = + typeof result?.content === 'string' + ? result.content + : typeof error?.modelOutput === 'string' + ? error.modelOutput + : null + if (modelOutput?.trim()) { + previousModelOutput = modelOutput + } lastError = error if (attempt >= maxAttempts || !isRetryableDeepSeekOutputError(error)) { throw error diff --git a/tests/deepseek-common.test.ts b/tests/deepseek-common.test.ts index 230a02a6e..557ac2aa9 100644 --- a/tests/deepseek-common.test.ts +++ b/tests/deepseek-common.test.ts @@ -266,7 +266,7 @@ describe('deepseek-common structured response parsing', () => { { message: { content: '', - reasoning_content: 'Analysis was truncated before the final JSON object.', + reasoning_content: 'Analysis was truncated before the final JSON object.\n{}', role: 'assistant', }, }, @@ -302,7 +302,7 @@ describe('deepseek-common structured response parsing', () => { effort: 'high', model: 'deepseek-v4-flash', systemPrompt: 'Review the pull request.', - userPrompt: 'Return JSON.', + userPrompt: 'ORIGINAL LARGE PR PROMPT', }) ).resolves.toMatchObject({ parsed: { body: 'No findings.' }, @@ -310,5 +310,10 @@ describe('deepseek-common structured response parsing', () => { const requestBodies = fetchMock.mock.calls.map(([, init]) => JSON.parse(String(init?.body))); expect(requestBodies.map((body) => body.max_tokens)).toEqual([8192, 16384]); + expect(requestBodies[1].messages[1].content).toContain('PRIOR MODEL ANALYSIS:'); + expect(requestBodies[1].messages[1].content).toContain( + 'Analysis was truncated before the final JSON object.' + ); + expect(requestBodies[1].messages[1].content).not.toContain('ORIGINAL LARGE PR PROMPT'); }); });