mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-09 11:17:04 +08:00
feat(errors): classify provider errors and show a hint the user can act on
- lib/llm-errors.ts sorts an error into about a dozen kinds (key
rejected, no access, unknown model, no credit, rate limited, context
too long, no image input, no tool calls, output cut off, provider down,
cannot connect, timeout): first texts that name the cause precisely,
then the HTTP status code, then general texts. It unwraps RetryError and
hides keys and Bearer tokens in the provider's message
- The chat route uses it for errors before the stream and, through
toUIMessageStreamResponse's onError, for errors in the stream. Errors
of the model's own tool call stay as they are: the same text goes back
to the model so it can fix the call
- The chat shows the hint in the user's language, then the provider's
message; a rejected key, missing access or unknown model adds an "Open
model settings" button. The Test button shows the same hints
- Fixes: our message "API key is required when using a custom base URL"
was replaced by "Authentication failed" because it contains "key"; a
provider's "Rate limit exceeded" opened this site's quota toast; an
error body like {"error": ...} was shown as raw JSON; the Test button
matched "401" in the message, where providers rarely put it
- Remove the string matching fallbacks in the chat panel
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
APICallError,
|
||||
InvalidToolInputError,
|
||||
LoadAPIKeyError,
|
||||
NoSuchToolError,
|
||||
RetryError,
|
||||
ToolCallRepairError,
|
||||
} from "ai"
|
||||
|
||||
/**
|
||||
* What went wrong with a model call, for a hint the user can act on. The
|
||||
* provider's own message always goes along, because a guess can be wrong.
|
||||
*/
|
||||
export type LLMErrorCode =
|
||||
| "invalid_api_key"
|
||||
| "forbidden"
|
||||
| "model_not_found"
|
||||
| "insufficient_quota"
|
||||
| "rate_limited"
|
||||
| "context_too_long"
|
||||
| "images_unsupported"
|
||||
| "tools_unsupported"
|
||||
| "output_truncated"
|
||||
| "provider_unavailable"
|
||||
| "cannot_connect"
|
||||
| "timeout"
|
||||
| "unknown"
|
||||
|
||||
export interface LLMError {
|
||||
type: "provider"
|
||||
code: LLMErrorCode
|
||||
message: string
|
||||
}
|
||||
|
||||
// Texts that name the cause more precisely than the status code: a quota
|
||||
// error can come as 403 or 429, a context or image error as a plain 400
|
||||
const SPECIFIC_TEXTS: Array<[RegExp, LLMErrorCode]> = [
|
||||
[
|
||||
/context length|context window|maximum context|prompt is too long|input is too long|too many (?:input )?tokens/i,
|
||||
"context_too_long",
|
||||
],
|
||||
[
|
||||
/image content block|image_url|does not support image|image input is not supported/i,
|
||||
"images_unsupported",
|
||||
],
|
||||
[
|
||||
/does not support tools|tool use is not supported|tools? (?:are|is) not supported|function calling is not supported/i,
|
||||
"tools_unsupported",
|
||||
],
|
||||
// Bedrock, when the output limit cut the tool call's JSON short
|
||||
[/toolUse\.input is invalid/i, "output_truncated"],
|
||||
[
|
||||
/insufficient[_ ]quota|insufficient balance|exceeded your current quota|credit balance is too low|余额不足/i,
|
||||
"insufficient_quota",
|
||||
],
|
||||
]
|
||||
|
||||
const STATUS_CODES: Record<number, LLMErrorCode> = {
|
||||
401: "invalid_api_key",
|
||||
402: "insufficient_quota",
|
||||
// Not "invalid key": a valid key can lack access to a model or region
|
||||
403: "forbidden",
|
||||
404: "model_not_found",
|
||||
408: "timeout",
|
||||
413: "context_too_long",
|
||||
429: "rate_limited",
|
||||
}
|
||||
|
||||
const GENERAL_TEXTS: Array<[RegExp, LLMErrorCode]> = [
|
||||
[
|
||||
/model[_ ]not[_ ]found|model .*does not exist|unknown model|no such model/i,
|
||||
"model_not_found",
|
||||
],
|
||||
[
|
||||
/invalid[_ ]api[_ ]key|incorrect api key|unauthorized/i,
|
||||
"invalid_api_key",
|
||||
],
|
||||
[/rate limit|too many requests/i, "rate_limited"],
|
||||
[/ECONNREFUSED|ENOTFOUND|ECONNRESET|fetch failed/i, "cannot_connect"],
|
||||
]
|
||||
|
||||
/** Secrets a provider may echo back: API keys, Bearer tokens, key=value */
|
||||
function redact(text: string): string {
|
||||
return text
|
||||
.replace(/\b(sk|pk|rk|ak)-[A-Za-z0-9_-]{8,}/g, "$1-[redacted]")
|
||||
.replace(/\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi, "Bearer [redacted]")
|
||||
.replace(/\bAKIA[0-9A-Z]{16}\b/g, "[redacted]")
|
||||
.replace(
|
||||
/\b(api[_-]?key|access[_-]?key|secret|token|password|signature)(["']?\s*[:=]\s*["']?)[^\s"',&}]+/gi,
|
||||
"$1$2[redacted]",
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Model and tool errors the SDK sends back to the model as the tool result,
|
||||
* so it can fix its call. Their text has to stay as it is.
|
||||
*/
|
||||
export function isToolCallError(error: unknown): boolean {
|
||||
return (
|
||||
InvalidToolInputError.isInstance(error) ||
|
||||
NoSuchToolError.isInstance(error) ||
|
||||
ToolCallRepairError.isInstance(error)
|
||||
)
|
||||
}
|
||||
|
||||
export function classifyLLMError(error: unknown): LLMError {
|
||||
// After the SDK's retries, the last attempt says what happened
|
||||
const e = RetryError.isInstance(error) ? error.lastError : error
|
||||
const raw = e instanceof Error ? e.message : String(e)
|
||||
const message = redact(raw).slice(0, 500)
|
||||
const body = APICallError.isInstance(e) ? (e.responseBody ?? "") : ""
|
||||
const text = `${raw} ${body}`
|
||||
const status = APICallError.isInstance(e)
|
||||
? e.statusCode
|
||||
: (e as { statusCode?: number })?.statusCode
|
||||
|
||||
const find = (rules: Array<[RegExp, LLMErrorCode]>) =>
|
||||
rules.find(([pattern]) => pattern.test(text))?.[1]
|
||||
const code =
|
||||
(e instanceof Error && e.name === "TimeoutError" && "timeout") ||
|
||||
(LoadAPIKeyError.isInstance(e) && "invalid_api_key") ||
|
||||
find(SPECIFIC_TEXTS) ||
|
||||
(status && STATUS_CODES[status]) ||
|
||||
(status && status >= 500 && "provider_unavailable") ||
|
||||
find(GENERAL_TEXTS) ||
|
||||
"unknown"
|
||||
return { type: "provider", code, message }
|
||||
}
|
||||
Reference in New Issue
Block a user