fix(ai): classify AI provider error status in logs and UI
Provider auth failures were logged with the provider's opaque message only (e.g. OpenRouter returns "401: User not found." for a bad/missing API key), which reads like a missing wiki user rather than a credentials problem. describeProviderError now prepends a clear, human-readable English label for a small set of well-known HTTP statuses while keeping the original detail (status + provider message + truncated response-body snippet): - 401/403 -> authentication failed (invalid or missing API key) - 402 -> insufficient credits or quota - 429 -> rate limit exceeded Other statuses and status-less errors are formatted exactly as before. The label is a static string and never contains the API key. Benefits every caller (embedding processor, indexer, AI "Test endpoint" UI) at once. Tests: switch the plain status+message case to a non-classified status (500); add 401/403/402/429 cases; keep 502/503 as regression guards for the unchanged path.
This commit is contained in:
@@ -10,6 +10,12 @@
|
||||
* None of these fields contain the API key (it is sent as an Authorization
|
||||
* header and never echoed in the response body), so this is safe to log/return.
|
||||
*
|
||||
* A small set of well-known HTTP statuses (auth / billing / rate limit) are
|
||||
* classified and a clear, human-readable English label is prepended, so the
|
||||
* log/UI states the real cause instead of only the provider's opaque message
|
||||
* (e.g. a 401 "User not found." is really a bad/missing API key). The label is
|
||||
* a static string and never contains the API key.
|
||||
*
|
||||
* `fallback` is used when the error carries no usable message (e.g. a bare
|
||||
* object); defaults to 'Unknown error'.
|
||||
*/
|
||||
@@ -31,9 +37,29 @@ export function describeProviderError(
|
||||
? `${e.statusCode}: ${e.message ?? ''}`.trim()
|
||||
: (e.message ?? fallback);
|
||||
const body = (e.responseBody ?? e.text ?? '').trim();
|
||||
if (!body) return base;
|
||||
// Collapse whitespace so a multi-line HTML body stays on one log line.
|
||||
const oneLine = body.replace(/\s+/g, ' ');
|
||||
const snippet = oneLine.length > 300 ? `${oneLine.slice(0, 300)}…` : oneLine;
|
||||
return `${base} | response body: ${snippet}`;
|
||||
const detail = body ? `${base} | response body: ${snippet}` : base;
|
||||
// Classify well-known HTTP statuses so the log/UI states the real problem
|
||||
// (auth / billing / rate limit) instead of only the provider's opaque message.
|
||||
const label = classifyStatus(e.statusCode);
|
||||
return label ? `${label} — ${detail}` : detail;
|
||||
}
|
||||
|
||||
// Map a small set of well-known provider HTTP statuses to a clear,
|
||||
// human-readable cause. Returns null for anything else so the existing
|
||||
// "<status>: <message> | response body: …" output is preserved unchanged.
|
||||
function classifyStatus(statusCode?: number): string | null {
|
||||
switch (statusCode) {
|
||||
case 401:
|
||||
case 403:
|
||||
return 'AI provider authentication failed (invalid or missing API key)';
|
||||
case 402:
|
||||
return 'AI provider rejected the request: insufficient credits or quota';
|
||||
case 429:
|
||||
return 'AI provider rate limit exceeded';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user