copilot-proxy-api
Version:
Self-hosted GitHub Copilot bridge for Claude Code and Codex CLI with OpenAI Responses, Anthropic Messages, streaming resilience, and context management.
1 lines • 6.26 kB
Source Map (JSON)
{"version":3,"file":"error-wiGHep5P.mjs","names":[],"sources":["../src/lib/error.ts"],"sourcesContent":["import type { Context } from \"hono\"\nimport type { ContentfulStatusCode } from \"hono/utils/http-status\"\n\nimport consola from \"consola\"\n\nexport class HTTPError extends Error {\n response: Response\n\n constructor(message: string, response: Response) {\n super(message)\n this.response = response\n }\n}\n\n/**\n * Anthropic API error envelope shape (per docs.anthropic.com/api/errors):\n * { \"type\": \"error\", \"error\": { \"type\": \"<category>\", \"message\": \"...\" } }\n *\n * Error categories Claude Code recognizes (services/api/errors.ts):\n * invalid_request_error — 400 (incl. \"prompt is too long\" → reactive compact)\n * authentication_error — 401\n * permission_error — 403\n * not_found_error — 404\n * request_too_large — 413\n * rate_limit_error — 429\n * api_error — 500 / generic upstream\n * overloaded_error — 529\n */\n\ninterface AnthropicErrorBody {\n type: \"error\"\n error: { type: string; message: string }\n}\n\nfunction statusToErrorType(status: number): string {\n switch (status) {\n case 400: {\n return \"invalid_request_error\"\n }\n case 401: {\n return \"authentication_error\"\n }\n case 403: {\n return \"permission_error\"\n }\n case 404: {\n return \"not_found_error\"\n }\n case 413: {\n return \"request_too_large\"\n }\n case 429: {\n return \"rate_limit_error\"\n }\n case 529: {\n return \"overloaded_error\"\n }\n default: {\n return status >= 500 ? \"api_error\" : \"invalid_request_error\"\n }\n }\n}\n\n/** Detect bodies already shaped as Anthropic errors (don't double-wrap). */\nfunction isAnthropicErrorShape(value: unknown): value is AnthropicErrorBody {\n if (typeof value !== \"object\" || value === null) return false\n const obj = value as Record<string, unknown>\n if (obj.type !== \"error\") return false\n const err = obj.error\n if (typeof err !== \"object\" || err === null) return false\n const e = err as Record<string, unknown>\n return typeof e.type === \"string\" && typeof e.message === \"string\"\n}\n\n/** Strip internal implementation details from leaked messages. */\nfunction sanitizeMessage(msg: string): string {\n // Common internal-only strings we don't want to leak to clients.\n if (/copilot token not found/i.test(msg)) {\n return \"Upstream authentication unavailable\"\n }\n if (/copilot/i.test(msg) && /token/i.test(msg)) {\n return \"Upstream authentication error\"\n }\n // Trim verbose stack-traces; keep only first line.\n const firstLine = msg.split(\"\\n\", 1)[0]\n return firstLine.length > 500 ? firstLine.slice(0, 500) + \"…\" : firstLine\n}\n\nexport async function forwardError(c: Context, error: unknown) {\n consola.error(\"Error occurred:\", error)\n\n if (error instanceof HTTPError) {\n const errorText = await error.response.text()\n const status = error.response.status as ContentfulStatusCode\n\n let parsed: unknown\n try {\n parsed = JSON.parse(errorText)\n } catch {\n parsed = errorText\n }\n consola.error(\"HTTP error body:\", parsed)\n\n // If upstream already shaped it as an Anthropic error, pass through verbatim.\n if (isAnthropicErrorShape(parsed)) {\n return c.json(parsed, status)\n }\n\n // Try to extract a useful message from upstream JSON ({error: {message}}, etc.).\n let message: string\n if (typeof parsed === \"string\") {\n message = parsed\n } else if (parsed && typeof parsed === \"object\") {\n const obj = parsed as Record<string, unknown>\n const err = obj.error as Record<string, unknown> | string | undefined\n if (typeof err === \"string\") {\n message = err\n } else if (err && typeof err.message === \"string\") {\n message = err.message\n } else if (typeof obj.message === \"string\") {\n message = obj.message\n } else {\n message = errorText.slice(0, 500)\n }\n } else {\n message = errorText.slice(0, 500)\n }\n\n const body: AnthropicErrorBody = {\n type: \"error\",\n error: {\n type: statusToErrorType(status),\n message: sanitizeMessage(message),\n },\n }\n return c.json(body, status)\n }\n\n const message =\n error instanceof Error ? error.message : \"An unknown error occurred\"\n const body: AnthropicErrorBody = {\n type: \"error\",\n error: {\n type: \"api_error\",\n message: sanitizeMessage(message),\n },\n }\n return c.json(body, 500)\n}\n"],"mappings":"uBAKA,IAAa,EAAb,cAA+B,KAAM,CACnC,SAEA,YAAY,EAAiB,EAAoB,CAC/C,MAAM,EAAQ,CACd,KAAK,SAAW,IAwBpB,SAAS,EAAkB,EAAwB,CACjD,OAAQ,EAAR,CACE,IAAK,KACH,MAAO,wBAET,IAAK,KACH,MAAO,uBAET,IAAK,KACH,MAAO,mBAET,IAAK,KACH,MAAO,kBAET,IAAK,KACH,MAAO,oBAET,IAAK,KACH,MAAO,mBAET,IAAK,KACH,MAAO,mBAET,QACE,OAAO,GAAU,IAAM,YAAc,yBAM3C,SAAS,EAAsB,EAA6C,CAC1E,GAAI,OAAO,GAAU,WAAY,EAAgB,MAAO,GACxD,IAAM,EAAM,EACZ,GAAI,EAAI,OAAS,QAAS,MAAO,GACjC,IAAM,EAAM,EAAI,MAChB,GAAI,OAAO,GAAQ,WAAY,EAAc,MAAO,GACpD,IAAM,EAAI,EACV,OAAO,OAAO,EAAE,MAAS,UAAY,OAAO,EAAE,SAAY,SAI5D,SAAS,EAAgB,EAAqB,CAE5C,GAAI,2BAA2B,KAAK,EAAI,CACtC,MAAO,sCAET,GAAI,WAAW,KAAK,EAAI,EAAI,SAAS,KAAK,EAAI,CAC5C,MAAO,gCAGT,IAAM,EAAY,EAAI,MAAM;EAAM,EAAE,CAAC,GACrC,OAAO,EAAU,OAAS,IAAM,EAAU,MAAM,EAAG,IAAI,CAAG,IAAM,EAGlE,eAAsB,EAAa,EAAY,EAAgB,CAG7D,GAFA,EAAQ,MAAM,kBAAmB,EAAM,CAEnC,aAAiB,EAAW,CAC9B,IAAM,EAAY,MAAM,EAAM,SAAS,MAAM,CACvC,EAAS,EAAM,SAAS,OAE1B,EACJ,GAAI,CACF,EAAS,KAAK,MAAM,EAAU,MACxB,CACN,EAAS,EAKX,GAHA,EAAQ,MAAM,mBAAoB,EAAO,CAGrC,EAAsB,EAAO,CAC/B,OAAO,EAAE,KAAK,EAAQ,EAAO,CAI/B,IAAI,EACJ,GAAI,OAAO,GAAW,SACpB,EAAU,UACD,GAAU,OAAO,GAAW,SAAU,CAC/C,IAAM,EAAM,EACN,EAAM,EAAI,MAChB,AAOE,EAPE,OAAO,GAAQ,SACP,EACD,GAAO,OAAO,EAAI,SAAY,SAC7B,EAAI,QACL,OAAO,EAAI,SAAY,SACtB,EAAI,QAEJ,EAAU,MAAM,EAAG,IAAI,MAGnC,EAAU,EAAU,MAAM,EAAG,IAAI,CAGnC,IAAM,EAA2B,CAC/B,KAAM,QACN,MAAO,CACL,KAAM,EAAkB,EAAO,CAC/B,QAAS,EAAgB,EAAQ,CAClC,CACF,CACD,OAAO,EAAE,KAAK,EAAM,EAAO,CAK7B,IAAM,EAA2B,CAC/B,KAAM,QACN,MAAO,CACL,KAAM,YACN,QAAS,EALX,aAAiB,MAAQ,EAAM,QAAU,4BAKN,CAClC,CACF,CACD,OAAO,EAAE,KAAK,EAAM,IAAI"}