mirror of
https://github.com/cline/cline.git
synced 2026-09-24 23:20:16 +08:00
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: <id>". 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.
This commit is contained in:
committed by
Dominic Cooney
parent
ee0c63a216
commit
82c3d8d771
@@ -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: <id>' 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")
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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: <id>" 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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user