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 }