mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-12 20:49:53 +08:00
fix(chat): clearer provider errors and no empty bubble, found with real models
- An error object sent inside the stream (OpenRouter's { code, message })
showed as "[object Object]"; its message and status code are read now.
- A problem+json "detail" is added to the message: NVIDIA only said
"Gone" for a retired model. 410 counts as model not found.
- "Cannot connect to API" from the SDK gets the connection hint.
- Text that is only whitespace (Kimi K2.6 sends a space before a tool
call) no longer shows an empty bubble.
- allowSystemInMessages stops the warning on every request. Our system
messages carry cache points; a client's own system messages are already
dropped by the empty-content filter.
This commit is contained in:
+33
-4
@@ -62,6 +62,8 @@ const STATUS_CODES: Record<number, LLMErrorCode> = {
|
||||
403: "forbidden",
|
||||
404: "model_not_found",
|
||||
408: "timeout",
|
||||
// A retired model
|
||||
410: "model_not_found",
|
||||
413: "context_too_long",
|
||||
429: "rate_limited",
|
||||
}
|
||||
@@ -76,7 +78,10 @@ const GENERAL_TEXTS: Array<[RegExp, LLMErrorCode]> = [
|
||||
"invalid_api_key",
|
||||
],
|
||||
[/rate limit|too many requests/i, "rate_limited"],
|
||||
[/ECONNREFUSED|ENOTFOUND|ECONNRESET|fetch failed/i, "cannot_connect"],
|
||||
[
|
||||
/Cannot connect to API|ECONNREFUSED|ENOTFOUND|ECONNRESET|ETIMEDOUT|fetch failed/i,
|
||||
"cannot_connect",
|
||||
],
|
||||
]
|
||||
|
||||
/** Secrets a provider may echo back: API keys, Bearer tokens, key=value */
|
||||
@@ -91,6 +96,15 @@ function redact(text: string): string {
|
||||
)
|
||||
}
|
||||
|
||||
function problemDetail(body: string): string | undefined {
|
||||
try {
|
||||
const detail = JSON.parse(body)?.detail
|
||||
return typeof detail === "string" ? detail : undefined
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -106,13 +120,28 @@ export function isToolCallError(error: unknown): boolean {
|
||||
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)
|
||||
// Errors sent inside the stream can be plain objects like OpenRouter's
|
||||
// { code: 503, message }
|
||||
const plain = e as {
|
||||
message?: unknown
|
||||
code?: unknown
|
||||
statusCode?: number
|
||||
}
|
||||
const raw =
|
||||
e instanceof Error
|
||||
? e.message
|
||||
: typeof plain?.message === "string"
|
||||
? plain.message
|
||||
: String(e)
|
||||
const body = APICallError.isInstance(e) ? (e.responseBody ?? "") : ""
|
||||
// A problem+json body names the reason the SDK left out (NVIDIA: "Gone")
|
||||
const detail = problemDetail(body)
|
||||
const message = redact(detail ? `${raw}: ${detail}` : raw).slice(0, 500)
|
||||
const text = `${raw} ${body}`
|
||||
const status = APICallError.isInstance(e)
|
||||
? e.statusCode
|
||||
: (e as { statusCode?: number })?.statusCode
|
||||
: (plain?.statusCode ??
|
||||
(typeof plain?.code === "number" ? plain.code : undefined))
|
||||
|
||||
const find = (rules: Array<[RegExp, LLMErrorCode]>) =>
|
||||
rules.find(([pattern]) => pattern.test(text))?.[1]
|
||||
|
||||
Reference in New Issue
Block a user