SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
6.2 KB · 123 lines typescript
Raw Blame History
1import { PolyProviderError, type PolyErrorCode, type ProviderId } from "./types";2import { redactSecrets } from "@/lib/crypto/keys";34export function isRetryableStatus(status?: number): boolean {5  return status === 408 || status === 409 || status === 429 || (status !== undefined && status >= 500 && status <= 599);6}78export function codeFromStatus(status: number | undefined): PolyErrorCode {9  switch (status) {10    case 401:11      return "INVALID_API_KEY";12    case 402:13      return "INSUFFICIENT_CREDITS";14    case 403:15      return "PERMISSION_DENIED";16    case 404:17      return "MODEL_NOT_FOUND";18    case 408:19      return "REQUEST_TIMEOUT";20    case 413:21      return "CONTEXT_TOO_LONG";22    case 422:23    case 400:24      return "INVALID_PARAMETER";25    case 429:26      return "RATE_LIMITED";27    case 500:28    case 502:29    case 503:30    case 504:31    case 529:32      return "PROVIDER_UNAVAILABLE";33    default:34      return "UNKNOWN_PROVIDER_ERROR";35  }36}3738/** Refine a 400-ish error using the provider's message text (all providers put the reason in prose). */39export function refineByMessage(code: PolyErrorCode, message: string): PolyErrorCode {40  const m = message.toLowerCase();41  if (/context.length|context_length|too many tokens|maximum context|prompt is too long|exceeds the (model'?s )?(maximum )?(context|token)|input token count|token limit|max_tokens.*(too large|exceed)/.test(m)) return "CONTEXT_TOO_LONG";42  if (/(insufficient|exceeded your current) (quota|credits|balance)|billing|credit balance is too low|payment/.test(m)) return "INSUFFICIENT_CREDITS";43  if (/(model|deployment).*(not found|does not exist|not exist|unknown|unsupported model)|not found for model|no such model/.test(m)) return "MODEL_NOT_FOUND";44  if (/(invalid|incorrect) (api key|x-api-key|authentication)|api key (not valid|invalid|expired)|bad credentials|unauthenticated|authentication_error/.test(m)) return "INVALID_API_KEY";45  if (/rate.?limit|too many requests|resource.?exhausted|overloaded|quota exceeded/.test(m)) return "RATE_LIMITED";46  if (/safety|content (policy|filter|management)|blocked|prohibited|refus|harm_category/.test(m)) return "CONTENT_REJECTED";47  if (/timed? ?out|deadline exceeded/.test(m)) return "REQUEST_TIMEOUT";48  return code;49}5051export function parseRetryAfter(headers?: Headers | Record<string, string | string[] | undefined> | null): number | undefined {52  if (!headers) return undefined;53  const get = (k: string): string | undefined => {54    if (headers instanceof Headers) return headers.get(k) ?? undefined;55    const v = headers[k] ?? headers[k.toLowerCase()];56    return Array.isArray(v) ? v[0] : v;57  };58  const ra = get("retry-after");59  if (ra) {60    const secs = Number(ra);61    if (!Number.isNaN(secs)) return Math.min(secs * 1000, 60_000);62    const date = Date.parse(ra);63    if (!Number.isNaN(date)) return Math.max(0, Math.min(date - Date.now(), 60_000));64  }65  const raMs = get("retry-after-ms");66  if (raMs && !Number.isNaN(Number(raMs))) return Math.min(Number(raMs), 60_000);67  return undefined;68}6970/** Human-readable, non-technical explanations shown in the UI. */71export const ERROR_MESSAGES: Record<PolyErrorCode, string> = {72  INVALID_API_KEY: "The API key for this provider was rejected. Check it in Settings → Providers.",73  PERMISSION_DENIED: "This key isn't allowed to use that model or feature.",74  RATE_LIMITED: "The provider is rate-limiting your key. Wait a moment and try again.",75  MODEL_NOT_FOUND: "This model isn't available for your account or was renamed. Refresh the model list.",76  CONTEXT_TOO_LONG: "The conversation is longer than this model's context window. Start a new chat or pick a long-context model.",77  INSUFFICIENT_CREDITS: "Your provider account has no remaining credit or quota.",78  REQUEST_TIMEOUT: "The provider took too long to respond.",79  PROVIDER_UNAVAILABLE: "The provider is temporarily unavailable. Try again shortly.",80  INVALID_PARAMETER: "A setting isn't accepted by this model. Reset the model configuration and retry.",81  CONTENT_REJECTED: "The provider declined to answer this request (content policy).",82  NETWORK_ERROR: "PolyLLM couldn't reach the provider.",83  CANCELLED: "Generation stopped.",84  UNKNOWN_PROVIDER_ERROR: "The provider returned an unexpected error.",85};8687/**88 * Generic normalizer used by adapters as a fallback: works on SDK error objects89 * exposing `status`, `message`, `headers`, `error.code/type`. Provider-specific90 * adapters call this then refine.91 */92export function normalizeGenericError(provider: ProviderId, error: unknown): PolyProviderError {93  if (error instanceof PolyProviderError) return error;94  const e = error as { status?: number; statusCode?: number; message?: string; name?: string; code?: string; headers?: Headers | Record<string, string>; error?: { code?: string; type?: string; message?: string; status?: string } } | undefined;95  const status = e?.status ?? e?.statusCode;96  const rawMessage = e?.error?.message ?? e?.message ?? String(error);97  const message = redactSecrets(rawMessage).slice(0, 600);9899  if (e?.name === "AbortError" || /aborted|abort/i.test(e?.name ?? "") || (e?.code === "ABORT_ERR")) {100    return new PolyProviderError({ code: "CANCELLED", message: "Cancelled", provider, retryable: false, cause: error });101  }102  if (e?.name === "APIConnectionTimeoutError" || /timeout/i.test(e?.name ?? "")) {103    return new PolyProviderError({ code: "REQUEST_TIMEOUT", message, provider, retryable: true, cause: error });104  }105  if (e?.name === "APIConnectionError" || e?.code === "ECONNRESET" || e?.code === "ENOTFOUND" || e?.code === "ECONNREFUSED" || e?.code === "UND_ERR_SOCKET" || /fetch failed/i.test(message)) {106    return new PolyProviderError({ code: "NETWORK_ERROR", message, provider, retryable: true, cause: error });107  }108109  let code = codeFromStatus(status);110  code = refineByMessage(code, message);111  const retryAfterMs = parseRetryAfter(e?.headers ?? null);112  return new PolyProviderError({113    code,114    message: code === "INVALID_API_KEY" ? "Invalid API key" : message,115    provider,116    status,117    retryable: isRetryableStatus(status) && code !== "INSUFFICIENT_CREDITS" && code !== "INVALID_API_KEY",118    retryAfterMs,119    providerCode: e?.error?.code ?? e?.error?.type ?? e?.error?.status ?? e?.code,120    cause: error,121  });122}123