From 33cba43ff2f1760ba0c9be5969b686d2c5181aad Mon Sep 17 00:00:00 2001 From: Mohammad Alsayyedan Date: Sun, 20 Sep 2026 21:31:23 -0500 Subject: [PATCH] Report an empty model response as itself, not as invalid JSON MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the language model returns an empty stream, parseLlmJson throws "No JSON structure found in LLM response". That message matches the /JSON|parse/i test used to classify retry failures, so the attempt is counted as a parse failure and the user is told: LLM returned invalid JSON after 3 attempts. Please try again. Nothing was wrong with the model's JSON — there was no response at all. The suggested remedy ("try again") is also wrong, since the usual cause is an exhausted request quota or a model declining the request, neither of which a retry fixes. Observed with three attempts failing in 11ms with textLen=0, which no amount of retrying would have resolved. Detect the empty stream directly and report it with the model id, so the message names the real cause. An empty response is tracked separately from parse failures rather than being folded into them. Also log the available model catalog when no preferred family matches, so the debug log records which models the selected fallback came from. --- src/webview/panel-llm.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/webview/panel-llm.ts b/src/webview/panel-llm.ts index 5d437f1b..f27ca10c 100644 --- a/src/webview/panel-llm.ts +++ b/src/webview/panel-llm.ts @@ -345,6 +345,8 @@ async function selectModel(): Promise { if (models.length > 0) return models[0]; } const any = await vscode.lm.selectChatModels({}); + runtimeDebug('panel-llm', 'model-catalog', + any.map(m => `${m.vendor}/${m.family}/${m.id} maxIn=${m.maxInputTokens}`).join(' ; ') || '(empty)'); if (any.length > 0) return any[0]; throw new Error('No language model available. Make sure GitHub Copilot is installed and signed in.'); } @@ -411,6 +413,7 @@ export async function callLlmJson(messages: vscode.LanguageModelChatMessage[] let lastError: unknown; let parseFailures = 0; + let emptyResponses = 0; const retryMessages = [...redactMessages(messages)]; for (let attempt = 0; attempt <= LLM_MAX_RETRIES; attempt++) { @@ -419,6 +422,7 @@ export async function callLlmJson(messages: vscode.LanguageModelChatMessage[] try { const response = await model.sendRequest(retryMessages, options, cts.token); for await (const chunk of response.text) text += chunk; + if (text.trim().length === 0) throw new Error('Model returned an empty response'); try { return JSON.parse(text.trim()) as T; } catch { @@ -437,7 +441,9 @@ export async function callLlmJson(messages: vscode.LanguageModelChatMessage[] options.modelOptions = undefined; } // On parse failures, nudge the model to return valid JSON on the next attempt - if (lastError instanceof Error && /JSON|parse/i.test(lastError.message)) { + if (lastError instanceof Error && /empty response/i.test(lastError.message)) { + emptyResponses++; + } else if (lastError instanceof Error && /JSON|parse/i.test(lastError.message)) { parseFailures++; if (retryMessages.length === messages.length) { retryMessages.push(vscode.LanguageModelChatMessage.User( @@ -450,8 +456,11 @@ export async function callLlmJson(messages: vscode.LanguageModelChatMessage[] } } - const label = parseFailures > 0 - ? `LLM returned invalid JSON after ${LLM_MAX_RETRIES + 1} attempts. Please try again.` - : (lastError instanceof Error ? lastError.message : 'LLM request failed after retries'); + const label = emptyResponses > 0 + ? `The language model (${model.id}) returned an empty response ${emptyResponses} time(s). ` + + 'This usually means the request quota is exhausted or the model declined the request.' + : parseFailures > 0 + ? `LLM returned invalid JSON after ${LLM_MAX_RETRIES + 1} attempts. Please try again.` + : (lastError instanceof Error ? lastError.message : 'LLM request failed after retries'); throw new Error(label); } \ No newline at end of file