From 82c3d8d771c16d1533bb8dfdabeae22beda45962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Kondratek?= <19799111+mkondratek@users.noreply.github.com> Date: Wed, 10 Jun 2026 12:51:31 +0200 Subject: [PATCH] fix(sdk): make model-not-found API errors actionable in the webview (#11378) When a provider returns a model-not-found error (e.g. Anthropic's HTTP 404 for a retired model such as claude-3-haiku-20240307), the SDK strips the status and delivers only the terse body, which collapses to the bare label "model: ". reshapeErrorForWebview fell through to returning that raw string, so ErrorRow rendered a label-like fragment in red with no hint that the model is gone or how to recover. Detect these in the plain-text branch of reshapeErrorForWebview and rewrite them into a sentence that names the model and tells the user to switch models in API Configuration settings, then retry. The model switch is framed as a precondition rather than a parallel option so users don't loop on Retry. Detection is text-based because the HTTP status is unavailable at this point. The keyword match is anchored to the word "model" with a not-found signal in the same sentence, so unrelated errors that merely mention a model (plan gating, deprecated features) are left untouched. Adds tests for the bare label form, a generic "does not exist" form, and two negative cases (plan gating and an auth error mentioning a model) that must pass through unchanged. --- .../vscode/src/sdk/message-translator.test.ts | 83 +++++++++++++++++++ apps/vscode/src/sdk/message-translator.ts | 40 +++++++-- 2 files changed, 115 insertions(+), 8 deletions(-) diff --git a/apps/vscode/src/sdk/message-translator.test.ts b/apps/vscode/src/sdk/message-translator.test.ts index 302b68f1f3..567167076d 100644 --- a/apps/vscode/src/sdk/message-translator.test.ts +++ b/apps/vscode/src/sdk/message-translator.test.ts @@ -1113,6 +1113,89 @@ describe("translateSessionEvent — agent_event error", () => { expect(parsed.code).toBe("SPEND_LIMIT_EXCEEDED") expect(parsed.providerId).toBe("cline") }) + + it("rewrites Anthropic bare 'model: ' 404 into an actionable message", () => { + const state = new MessageTranslatorState(undefined, () => "anthropic") + const event: CoreSessionEvent = { + type: "agent_event", + payload: { + sessionId: "session-1", + event: { + type: "error", + error: { message: "model: claude-3-haiku-20240307" }, + } as AgentEvent, + }, + } + + const result = translateSessionEvent(event, state) + expect(result.messages).toHaveLength(2) + + const failedText = result.messages[1].text! + expect(failedText).toContain("claude-3-haiku-20240307") + expect(failedText).toContain("was not found") + expect(failedText).toContain("API Configuration settings") + }) + + it("rewrites a 'model not found' message into an actionable message", () => { + const state = new MessageTranslatorState() + const event: CoreSessionEvent = { + type: "agent_event", + payload: { + sessionId: "session-1", + event: { + type: "error", + error: { message: "The model `gpt-foo` does not exist" }, + } as AgentEvent, + }, + } + + const result = translateSessionEvent(event, state) + expect(result.messages).toHaveLength(2) + + const failedText = result.messages[1].text! + expect(failedText).toContain("gpt-foo") + expect(failedText).toContain("API Configuration settings") + }) + + it("does not rewrite an unrelated error that merely mentions a model", () => { + const state = new MessageTranslatorState() + const rawMessage = "The requested feature model is not available in your plan" + const event: CoreSessionEvent = { + type: "agent_event", + payload: { + sessionId: "session-1", + event: { + type: "error", + error: { message: rawMessage }, + } as AgentEvent, + }, + } + + const result = translateSessionEvent(event, state) + const failedText = result.messages[1].text! + expect(failedText).toBe(rawMessage) + expect(failedText).not.toContain("API Configuration settings") + }) + + it("leaves an auth error mentioning a model untouched", () => { + const state = new MessageTranslatorState() + const rawMessage = "Invalid API key for model gpt-4" + const event: CoreSessionEvent = { + type: "agent_event", + payload: { + sessionId: "session-1", + event: { + type: "error", + error: { message: rawMessage }, + } as AgentEvent, + }, + } + + const result = translateSessionEvent(event, state) + const failedText = result.messages[1].text! + expect(failedText).toBe(rawMessage) + expect(failedText).not.toContain("API Configuration settings") + }) }) // --------------------------------------------------------------------------- diff --git a/apps/vscode/src/sdk/message-translator.ts b/apps/vscode/src/sdk/message-translator.ts index a3720e9ec0..48f6ca0e7d 100644 --- a/apps/vscode/src/sdk/message-translator.ts +++ b/apps/vscode/src/sdk/message-translator.ts @@ -1876,15 +1876,35 @@ export function historyItemToSessionFields(item: { } } +const MODEL_NOT_FOUND_GUIDANCE = + "This model may be retired or unavailable on your account. Switch to a different model in API Configuration settings, then retry." + /** - * Reshape an SDK error into the serialized ClineError JSON format the webview - * expects. The webview's ErrorRow uses ClineError.parse() which needs specific - * fields (`code`, `providerId`, `details`) to detect error types and render - * appropriate UI (e.g. "Buy Credits" button for insufficient credits). - * - * The SDK error's `message` may contain embedded JSON from the API response. - * We try to extract it and produce a proper ClineError-serialized payload. - * If parsing fails, we fall back to the raw error message. + * Rewrite a model-not-found error into actionable guidance, or undefined if the + * message is not one. The provider's HTTP status is stripped upstream, so this + * matches on text rather than a status code. + */ +function describeModelNotFoundError(rawMessage: string): string | undefined { + // Anthropic's 404 body collapses to a bare "model: " label. + const bareModelLabel = rawMessage.match(/^\s*model:\s*(\S+)\s*$/i) + if (bareModelLabel) { + return `Model "${bareModelLabel[1]}" was not found. ${MODEL_NOT_FOUND_GUIDANCE}` + } + + // Keep the not-found signal in the same clause as "model" so errors that + // merely mention one (plan gating, deprecated features) are left untouched. + const modelNotFound = /\bmodel\b[^.,;:]*\b(not[ _]?found|does not exist|no such model|unknown model)\b/i + if (modelNotFound.test(rawMessage)) { + return `${rawMessage} ${MODEL_NOT_FOUND_GUIDANCE}` + } + + return undefined +} + +/** + * Reshape an SDK error into the serialized ClineError JSON the webview's + * ErrorRow expects (`code`, `providerId`, `details`), extracting structured + * info from the error message when present and falling back to raw text. */ export function reshapeErrorForWebview( error: { message?: string; status?: number; code?: string }, @@ -1949,6 +1969,10 @@ export function reshapeErrorForWebview( }, }) } + const notFoundMessage = describeModelNotFoundError(rawMessage) + if (notFoundMessage) { + return notFoundMessage + } return rawMessage }