SPB Git forge

spb/doc-api

Public
2commits 1branches 0releases
15.7 MBsize
maindefault branch
13 days agolast push
Python 88.3% TypeScript 7.6% Shell 4.1%
6.3 KB · 88 lines typescript
Raw Blame History
1/**2 * Provider-agnostic error classification helper (OpenAI first, Anthropic shape supported).3 * STATUS: LIVE_VERIFIED offline for the OpenAI shapes captured on 2026-09-18 (self-test: `node classify_error.ts`).4 *5 * Handles OpenAI's three body shapes:6 *   {"error":{"message","type","param","code"}} | {"error":"<bare string>"} (admin 403) | "" (edge 404, no body)7 * and Anthropic's {"type":"error","error":{"type","message"}} (request id header `request-id`).8 */9export type Category = "auth" | "permission" | "not_found" | "bad_request" | "rate_limit" | "quota_billing" | "safety" | "server" | "network" | "unknown";1011export interface ErrorClass {12  provider: string;13  httpStatus: number | null;14  type: string | null;15  code: string | null;16  param: string | null;17  message: string;18  category: Category;19  retryable: boolean;20  retryAfterSec: number | null;21  requestId: string | null;22}2324const NON_RETRYABLE_429 = new Set(["insufficient_quota", "credit_balance_exhausted", "organization_spend_limit_exceeded", "project_spend_limit_exceeded", "organization_usage_limit_exceeded", "billing_hard_limit_reached"]);25const SAFETY = new Set(["misalignment_policy_violation", "cyber_policy", "moderation_blocked", "content_policy_violation", "invalid_prompt"]);26const AUTH = new Set(["invalid_api_key", "mismatched_organization", "invalid_project"]);2728type HeadersLike = Record<string, string | string[] | undefined> | { get(name: string): string | null } | null | undefined;29function hdr(h: HeadersLike, name: string): string | null {30  if (!h) return null;31  if (typeof (h as any).get === "function") return (h as any).get(name);32  for (const [k, v] of Object.entries(h as Record<string, string | string[] | undefined>)) if (k.toLowerCase() === name && v) return Array.isArray(v) ? v[0] : v;33  return null;34}3536export function classify(provider: string, status: number | null, body: unknown, headers?: HeadersLike): ErrorClass {37  let err: any = null;38  if (body && typeof body === "object") err = (body as any).error ?? body;39  else if (typeof body === "string" && body) err = body;40  let type: string | null = null, code: string | null = null, param: string | null = null, message = "";41  if (err && typeof err === "object") { type = err.type ?? null; code = err.code ?? null; param = err.param ?? null; message = String(err.message ?? ""); }42  else if (typeof err === "string") message = err;43  const ms = hdr(headers, "retry-after-ms"); const ra = hdr(headers, "retry-after");44  const retryAfterSec = ms && !Number.isNaN(Number(ms)) ? Number(ms) / 1000 : ra && !Number.isNaN(Number(ra)) ? Number(ra) : null;45  const requestId = hdr(headers, "x-request-id") ?? hdr(headers, "request-id");4647  let category: Category; let retryable: boolean;48  if (status === null) { category = "network"; retryable = true; }49  else if (status === 401 || (code && AUTH.has(code)) || type === "authentication_error") { category = "auth"; retryable = false; }50  else if (status === 403 || type === "permission_error" || code === "insufficient_permissions" || code === "ip_not_authorized") { category = code && SAFETY.has(code) ? "safety" : "permission"; retryable = false; }51  else if (status === 404 || type === "not_found_error" || code === "model_not_found") { category = "not_found"; retryable = false; }52  else if (status === 429) { if ((code && NON_RETRYABLE_429.has(code)) || type === "insufficient_quota") { category = "quota_billing"; retryable = false; } else { category = "rate_limit"; retryable = true; } }53  else if (status >= 500 || status === 408 || status === 409 || status === 425) { category = "server"; retryable = true; }54  else if ((code && SAFETY.has(code)) || type === "image_generation_user_error") { category = "safety"; retryable = false; }55  else if (status >= 400) { category = "bad_request"; retryable = status === 422; }56  else { category = "unknown"; retryable = false; }57  if (message.includes("Missing scopes")) { category = "permission"; retryable = false; }58  return { provider, httpStatus: status, type, code, param, message, category, retryable, retryAfterSec, requestId };59}6061/** Delay (seconds) before retry #attempt (1-based): server hint first, else exponential backoff with jitter (0.5 s .. 8 s). */62export function backoff(e: ErrorClass, attempt: number, base = 0.5, cap = 8): number {63  if (e.retryAfterSec !== null) return e.retryAfterSec + Math.random() * 0.25;64  return Math.min(cap, base * 2 ** (attempt - 1)) * (1 + (Math.random() - 0.5) / 2);65}6667// ---- self-test on captured OpenAI shapes (run: node classify_error.ts) ----68if (import.meta.url === `file://${process.argv[1]}`) {69  const cases: Array<[number | null, unknown, Category, boolean]> = [70    [401, { error: { message: "Incorrect API key provided: sk-inv***", type: "invalid_request_error", param: null, code: "invalid_api_key" } }, "auth", false],71    [403, { error: "You have insufficient permissions for this operation. Missing scopes: api.management.read." }, "permission", false],72    [404, { error: { message: "The model `x` does not exist", type: "invalid_request_error", param: null, code: "model_not_found" } }, "not_found", false],73    [404, "", "not_found", false],74    [400, { error: { message: "Missing required parameter: 'model'.", type: "invalid_request_error", param: "model", code: "missing_required_parameter" } }, "bad_request", false],75    [429, { error: { message: "Rate limit reached", type: "rate_limit_error", param: null, code: "rate_limit_exceeded" } }, "rate_limit", true],76    [429, { error: { message: "You exceeded your current quota", type: "insufficient_quota", param: null, code: "insufficient_quota" } }, "quota_billing", false],77    [503, { error: { message: "overloaded", type: "service_unavailable_error", param: null, code: "server_is_overloaded" } }, "server", true],78    [403, { error: { message: "blocked", type: "invalid_request_error", param: null, code: "misalignment_policy_violation" } }, "safety", false],79    [529, { type: "error", error: { type: "overloaded_error", message: "Overloaded" } }, "server", true],80    [null, null, "network", true],81  ];82  for (const [st, body, cat, retry] of cases) {83    const c = classify(st === 529 ? "anthropic" : "openai", st, body, { "retry-after": "2" });84    if (c.category !== cat || c.retryable !== retry) throw new Error(`FAIL ${st} ${JSON.stringify(body)} -> ${c.category}/${c.retryable}`);85  }86  console.log(`OK — ${cases.length} classification cases passed`);87}88