copillm 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +1 -0
  2. package/dist/agentconfig/apply.js +4 -3
  3. package/dist/agentconfig/load.js +34 -1
  4. package/dist/agentconfig/schema.js +22 -0
  5. package/dist/agents/registry.js +80 -6
  6. package/dist/cli/auth/ensure.js +30 -0
  7. package/dist/cli/auth/runAuth.js +38 -0
  8. package/dist/cli/commands/agents/claude.js +47 -0
  9. package/dist/cli/commands/agents/codex.js +48 -0
  10. package/dist/cli/commands/agents/copilot.js +49 -0
  11. package/dist/cli/commands/agents/pi.js +47 -0
  12. package/dist/cli/commands/agents/shared.js +28 -0
  13. package/dist/cli/commands/auth.js +99 -0
  14. package/dist/cli/commands/daemon.js +357 -0
  15. package/dist/cli/commands/env.js +135 -0
  16. package/dist/cli/commands/models.js +80 -0
  17. package/dist/cli/configCommands.js +10 -0
  18. package/dist/cli/copillmFlags.js +101 -0
  19. package/dist/cli/daemon/ensureRunning.js +66 -0
  20. package/dist/cli/daemon/lifecycle.js +61 -0
  21. package/dist/cli/daemon/probes.js +68 -0
  22. package/dist/cli/daemon/runDaemon.js +102 -0
  23. package/dist/cli/daemon/spawnEnv.js +12 -0
  24. package/dist/cli/index.js +43 -0
  25. package/dist/cli/integrations/banner.js +51 -0
  26. package/dist/cli/integrations/claudeExport.js +14 -0
  27. package/dist/cli/integrations/refreshCodex.js +19 -0
  28. package/dist/cli/integrations/refreshPi.js +17 -0
  29. package/dist/cli/shared/backends.js +31 -0
  30. package/dist/cli/shared/debug.js +44 -0
  31. package/dist/cli/shared/deprecation.js +7 -0
  32. package/dist/cli/shared/exitCodes.js +9 -0
  33. package/dist/cli/shared/output.js +14 -0
  34. package/dist/cli/shared/parseAgent.js +6 -0
  35. package/dist/cli.js +1 -1348
  36. package/dist/server/errors.js +195 -0
  37. package/dist/server/proxy.js +50 -885
  38. package/dist/server/routes/debug.js +65 -0
  39. package/dist/server/routes/health.js +32 -0
  40. package/dist/server/routes/models.js +41 -0
  41. package/dist/server/routes/proxyForward.js +108 -0
  42. package/dist/server/routes/shared.js +161 -0
  43. package/dist/server/upstream/copilotClient.js +137 -0
  44. package/dist/server/upstream/streaming.js +146 -0
  45. package/package.json +7 -2
@@ -0,0 +1,65 @@
1
+ import { getGithubUserSummary, GithubUserFetchError } from "../debugInfo.js";
2
+ import { safeSendJson } from "../requestLifecycle.js";
3
+ const DAEMON_STARTED_AT_ISO = new Date().toISOString();
4
+ export async function handleDebug(res, input) {
5
+ const bearerTtlSeconds = input.tokenManager.expiresInSeconds();
6
+ const uptimeSeconds = Math.max(0, Math.floor((Date.now() - Date.parse(DAEMON_STARTED_AT_ISO)) / 1_000));
7
+ let user = null;
8
+ let userError = null;
9
+ if (input.githubToken) {
10
+ try {
11
+ const summary = await getGithubUserSummary(input.githubToken);
12
+ user = {
13
+ login: summary.login,
14
+ id: summary.id,
15
+ type: summary.type
16
+ };
17
+ }
18
+ catch (error) {
19
+ if (error instanceof GithubUserFetchError) {
20
+ userError = `github_user_lookup_failed_${error.status}`;
21
+ }
22
+ else {
23
+ userError = error instanceof Error ? error.message : "unknown_error";
24
+ }
25
+ }
26
+ }
27
+ else {
28
+ userError = "github_token_unavailable_in_proxy";
29
+ }
30
+ safeSendJson(res, 200, {
31
+ server: {
32
+ port: input.port,
33
+ pid: process.pid,
34
+ node_version: process.version,
35
+ started_at_iso: DAEMON_STARTED_AT_ISO,
36
+ uptime_seconds: uptimeSeconds,
37
+ account_type: input.config.accountType,
38
+ selected_models: input.config.selectedModels,
39
+ require_caller_secret: input.config.requireCallerSecret,
40
+ log_level: input.logger.level,
41
+ log_file: process.env.COPILLM_LOG_FILE ?? null
42
+ },
43
+ auth: {
44
+ bearer_ttl_seconds: bearerTtlSeconds,
45
+ bearer_present: input.tokenManager.current !== null,
46
+ bearer_expires_at_unix: input.tokenManager.current?.expiresAtUnix ?? null
47
+ },
48
+ user,
49
+ user_error: userError,
50
+ routes: [
51
+ "GET /livez",
52
+ "GET /healthz",
53
+ "GET /models",
54
+ "GET /v1/models",
55
+ "GET /codex/v1/models",
56
+ "GET /anthropic/v1/models",
57
+ "POST /codex/v1/responses",
58
+ "POST /v1/chat/completions",
59
+ "POST /v1/messages",
60
+ "POST /anthropic/v1/messages",
61
+ "GET /_debug"
62
+ ],
63
+ debug_enabled: true
64
+ });
65
+ }
@@ -0,0 +1,32 @@
1
+ import { healthFailure } from "../errors.js";
2
+ import { safeSendJson } from "../requestLifecycle.js";
3
+ const HEALTH_REFRESH_THRESHOLD_SECONDS = 60;
4
+ export function handleLivez(res) {
5
+ safeSendJson(res, 200, { status: "ok", uptime_seconds: Math.floor(process.uptime()) });
6
+ }
7
+ export async function handleHealthz(res, tokenManager) {
8
+ const ttl = tokenManager.expiresInSeconds();
9
+ if (ttl !== null && ttl > HEALTH_REFRESH_THRESHOLD_SECONDS) {
10
+ safeSendJson(res, 200, {
11
+ status: "ok",
12
+ token_state: "fresh",
13
+ refresh_threshold_seconds: HEALTH_REFRESH_THRESHOLD_SECONDS,
14
+ bearer_ttl_seconds: ttl
15
+ });
16
+ return;
17
+ }
18
+ try {
19
+ await tokenManager.ensureToken({ refreshThresholdSeconds: HEALTH_REFRESH_THRESHOLD_SECONDS });
20
+ const refreshedTtl = tokenManager.expiresInSeconds() ?? 0;
21
+ safeSendJson(res, 200, {
22
+ status: "ok",
23
+ token_state: "refreshed",
24
+ refresh_threshold_seconds: HEALTH_REFRESH_THRESHOLD_SECONDS,
25
+ bearer_ttl_seconds: refreshedTtl
26
+ });
27
+ }
28
+ catch (error) {
29
+ const failed = healthFailure(error);
30
+ safeSendJson(res, failed.httpStatus, failed.payload);
31
+ }
32
+ }
@@ -0,0 +1,41 @@
1
+ import { CopilotTokenManagerError } from "../../auth/copilotToken.js";
2
+ import { listModels, listModelsUnion } from "../../models/discovery.js";
3
+ import { buildCodexCatalog } from "../codexSchema.js";
4
+ import { buildAnthropicModelsResponse } from "../anthropicModelsResponse.js";
5
+ import { safeSendJson } from "../requestLifecycle.js";
6
+ export async function handleModels(res, routeKind, config, tokenManager, githubToken) {
7
+ try {
8
+ await tokenManager.ensureToken(false);
9
+ if (!githubToken) {
10
+ safeSendJson(res, 503, { error: "github_token_unavailable" });
11
+ return;
12
+ }
13
+ const result = routeKind === "codex_models" || routeKind === "anthropic_models"
14
+ ? await listModelsUnion(config.accountType, githubToken, 3)
15
+ : await listModels(config.accountType, githubToken);
16
+ if (routeKind === "codex_models") {
17
+ safeSendJson(res, 200, buildCodexCatalog(result.models));
18
+ return;
19
+ }
20
+ if (routeKind === "anthropic_models") {
21
+ safeSendJson(res, 200, buildAnthropicModelsResponse(result.models));
22
+ return;
23
+ }
24
+ safeSendJson(res, 200, {
25
+ models: result.models,
26
+ discovery: {
27
+ source: result.source,
28
+ stale: result.stale,
29
+ cache_age_seconds: result.cacheAgeSeconds,
30
+ warning: result.warning
31
+ }
32
+ });
33
+ }
34
+ catch (error) {
35
+ if (error instanceof CopilotTokenManagerError) {
36
+ safeSendJson(res, 503, { error: "token_refresh_failed" });
37
+ return;
38
+ }
39
+ throw error;
40
+ }
41
+ }
@@ -0,0 +1,108 @@
1
+ import { resolveModelId } from "../../models/discovery.js";
2
+ import { CopilotTokenManagerError } from "../../auth/copilotToken.js";
3
+ import { anthropicToOpenAI } from "../../translation/openaiAnthropic.js";
4
+ import { writeAnthropicPrelude } from "../../translation/streamingOpenAIToAnthropic.js";
5
+ import { isBenignSocketError, safeEnd, safeSendJson } from "../requestLifecycle.js";
6
+ import { InvalidRequestShapeError, writeAnthropicSseError } from "../errors.js";
7
+ import { postToCopilot } from "../upstream/copilotClient.js";
8
+ import { beginAnthropicSseResponse, forwardResponse, isStreamingRequestBody } from "../upstream/streaming.js";
9
+ import { normaliseAliasedModelInPlace, readJson, readRequestedModel, rewriteRequestedModel, summarizeUpstreamPayload } from "./shared.js";
10
+ function translateRequestBody(routeKind, body) {
11
+ if (routeKind !== "anthropic") {
12
+ return normaliseAliasedModelInPlace(body);
13
+ }
14
+ try {
15
+ return anthropicToOpenAI(body);
16
+ }
17
+ catch (error) {
18
+ if (error instanceof Error) {
19
+ throw new InvalidRequestShapeError(error.message);
20
+ }
21
+ throw new InvalidRequestShapeError("Invalid Anthropic request body.");
22
+ }
23
+ }
24
+ export async function handleProxyForward(input) {
25
+ const { req, res, route, config, tokenManager, logger, requestId, signal } = input;
26
+ const requestBody = await readJson(req);
27
+ const translatedBody = translateRequestBody(route.kind, requestBody);
28
+ const requestedModel = readRequestedModel(translatedBody);
29
+ if (config.selectedModels.length > 0 && !requestedModel) {
30
+ safeSendJson(res, 400, {
31
+ error: "model_not_selected",
32
+ detail: "Requested model is not enabled in local selection."
33
+ });
34
+ return;
35
+ }
36
+ let resolvedModel = null;
37
+ try {
38
+ resolvedModel = requestedModel ? resolveModelId(requestedModel, config.selectedModels) : null;
39
+ }
40
+ catch (error) {
41
+ const detail = error instanceof Error ? error.message : "Model resolution failed.";
42
+ safeSendJson(res, 400, { error: "ambiguous_model_selection", detail });
43
+ return;
44
+ }
45
+ if (config.selectedModels.length > 0 && !resolvedModel) {
46
+ safeSendJson(res, 400, {
47
+ error: "model_not_selected",
48
+ detail: "Requested model is not enabled in local selection."
49
+ });
50
+ return;
51
+ }
52
+ const upstreamBody = resolvedModel ? rewriteRequestedModel(translatedBody, resolvedModel.id) : translatedBody;
53
+ const upstreamPath = route.kind === "codex_responses" ? "/responses" : "/chat/completions";
54
+ const isAnthropicStreaming = route.anthroShape && isStreamingRequestBody(translatedBody);
55
+ let prelude = null;
56
+ if (isAnthropicStreaming) {
57
+ beginAnthropicSseResponse(res, req);
58
+ prelude = writeAnthropicPrelude(res, requestedModel ?? "");
59
+ }
60
+ logger.debug({
61
+ event: "request_prepared",
62
+ request_id: requestId,
63
+ route: route.kind,
64
+ anthro_shape: route.anthroShape,
65
+ requested_model: requestedModel,
66
+ upstream_model: readRequestedModel(upstreamBody),
67
+ model_resolution_rule: resolvedModel?.rule ?? null,
68
+ upstream_path: upstreamPath,
69
+ ...summarizeUpstreamPayload(upstreamBody)
70
+ }, "prepared upstream request");
71
+ try {
72
+ const upstream = await postToCopilot({
73
+ tokenManager,
74
+ accountType: config.accountType,
75
+ body: upstreamBody,
76
+ requestId,
77
+ logger,
78
+ upstreamPath,
79
+ signal
80
+ });
81
+ await forwardResponse(upstream, route.anthroShape, res, {
82
+ requestedModel: requestedModel ?? undefined,
83
+ prelude,
84
+ logger,
85
+ requestId
86
+ });
87
+ }
88
+ catch (error) {
89
+ if (isBenignSocketError(error)) {
90
+ logger.debug({ event: "upstream_aborted", request_id: requestId, err: error }, "upstream request aborted (client disconnected)");
91
+ safeEnd(res);
92
+ return;
93
+ }
94
+ if (error instanceof CopilotTokenManagerError) {
95
+ if (prelude) {
96
+ writeAnthropicSseError(res, prelude, "token_refresh_failed");
97
+ return;
98
+ }
99
+ safeSendJson(res, 503, { error: "token_refresh_failed" });
100
+ return;
101
+ }
102
+ if (prelude) {
103
+ writeAnthropicSseError(res, prelude, "internal_error");
104
+ return;
105
+ }
106
+ throw error;
107
+ }
108
+ }
@@ -0,0 +1,161 @@
1
+ import { stripOneMillionAlias } from "../../translation/openaiAnthropic.js";
2
+ import { JsonRequestParseError } from "../errors.js";
3
+ export async function readJson(req) {
4
+ const chunks = [];
5
+ for await (const chunk of req) {
6
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
7
+ }
8
+ if (chunks.length === 0) {
9
+ return {};
10
+ }
11
+ const text = Buffer.concat(chunks).toString("utf8");
12
+ try {
13
+ return JSON.parse(text);
14
+ }
15
+ catch (error) {
16
+ if (error instanceof SyntaxError) {
17
+ throw new JsonRequestParseError("Failed to parse JSON body.");
18
+ }
19
+ throw error;
20
+ }
21
+ }
22
+ export function readRequestedModel(payload) {
23
+ if (!payload || typeof payload !== "object") {
24
+ return null;
25
+ }
26
+ const maybeModel = payload.model;
27
+ return typeof maybeModel === "string" ? maybeModel : null;
28
+ }
29
+ export function rewriteRequestedModel(payload, model) {
30
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
31
+ return payload;
32
+ }
33
+ return {
34
+ ...payload,
35
+ model
36
+ };
37
+ }
38
+ /**
39
+ * Defensive strip of the `[1m]` alias for pass-through routes (OpenAI chat
40
+ * completions, Codex responses). The Anthropic route already strips inside
41
+ * `anthropicToOpenAI`; this catches anything that might land on the
42
+ * pass-through paths with a hand-pasted aliased id so upstream Copilot
43
+ * always receives the canonical model id.
44
+ */
45
+ export function normaliseAliasedModelInPlace(body) {
46
+ if (body && typeof body === "object" && !Array.isArray(body)) {
47
+ const record = body;
48
+ if (typeof record.model === "string") {
49
+ const stripped = stripOneMillionAlias(record.model);
50
+ if (stripped !== record.model) {
51
+ record.model = stripped;
52
+ }
53
+ }
54
+ }
55
+ return body;
56
+ }
57
+ export function summarizeUpstreamPayload(payload) {
58
+ let requestBytes = null;
59
+ try {
60
+ requestBytes = Buffer.byteLength(JSON.stringify(payload), "utf8");
61
+ }
62
+ catch {
63
+ requestBytes = null;
64
+ }
65
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
66
+ return { upstream_request_bytes: requestBytes };
67
+ }
68
+ const record = payload;
69
+ const messages = Array.isArray(record.messages) ? record.messages : null;
70
+ const input = Array.isArray(record.input) ? record.input : null;
71
+ return {
72
+ upstream_request_bytes: requestBytes,
73
+ stream: record.stream === true,
74
+ max_tokens: typeof record.max_tokens === "number" ? record.max_tokens : null,
75
+ message_count: messages?.length ?? null,
76
+ input_item_count: input?.length ?? null,
77
+ tool_count: Array.isArray(record.tools) ? record.tools.length : 0,
78
+ text_characters: sumTextCharacters(payload)
79
+ };
80
+ }
81
+ function sumTextCharacters(value) {
82
+ if (typeof value === "string") {
83
+ return value.length;
84
+ }
85
+ if (!value || typeof value !== "object") {
86
+ return 0;
87
+ }
88
+ if (Array.isArray(value)) {
89
+ return value.reduce((total, item) => total + sumTextCharacters(item), 0);
90
+ }
91
+ let total = 0;
92
+ const record = value;
93
+ for (const [key, nested] of Object.entries(record)) {
94
+ if (key === "text" || key === "content" || key === "arguments" || key === "input") {
95
+ total += sumTextCharacters(nested);
96
+ }
97
+ else if (nested && typeof nested === "object" && key !== "data" && key !== "image_url" && key !== "source") {
98
+ total += sumTextCharacters(nested);
99
+ }
100
+ }
101
+ return total;
102
+ }
103
+ export function isLocalRequest(req) {
104
+ const remote = req.socket.remoteAddress ?? "";
105
+ const local = req.socket.localAddress ?? "";
106
+ return isLoopbackAddress(remote) && (local.length === 0 || isLoopbackAddress(local));
107
+ }
108
+ function isLoopbackAddress(value) {
109
+ return value === "127.0.0.1" || value === "::1" || value === "::ffff:127.0.0.1";
110
+ }
111
+ export function safePathname(rawUrl) {
112
+ if (!rawUrl) {
113
+ return "/";
114
+ }
115
+ try {
116
+ return new URL(rawUrl, "http://127.0.0.1").pathname;
117
+ }
118
+ catch {
119
+ return "/";
120
+ }
121
+ }
122
+ export function resolveRoute(method, rawUrl) {
123
+ if (!method || !rawUrl) {
124
+ return { kind: "not_found", anthroShape: false };
125
+ }
126
+ let pathname;
127
+ try {
128
+ pathname = new URL(rawUrl, "http://127.0.0.1").pathname;
129
+ }
130
+ catch {
131
+ return { kind: "not_found", anthroShape: false };
132
+ }
133
+ if (method === "GET" && pathname === "/livez") {
134
+ return { kind: "livez", anthroShape: false };
135
+ }
136
+ if (method === "GET" && pathname === "/healthz") {
137
+ return { kind: "healthz", anthroShape: false };
138
+ }
139
+ if (method === "GET" && (pathname === "/models" || pathname === "/v1/models")) {
140
+ return { kind: "models", anthroShape: false };
141
+ }
142
+ if (method === "GET" && pathname === "/codex/v1/models") {
143
+ return { kind: "codex_models", anthroShape: false };
144
+ }
145
+ if (method === "GET" && pathname === "/anthropic/v1/models") {
146
+ return { kind: "anthropic_models", anthroShape: false };
147
+ }
148
+ if (method === "POST" && pathname === "/codex/v1/responses") {
149
+ return { kind: "codex_responses", anthroShape: false };
150
+ }
151
+ if (method === "GET" && pathname === "/_debug") {
152
+ return { kind: "debug", anthroShape: false };
153
+ }
154
+ if (method === "POST" && pathname === "/v1/chat/completions") {
155
+ return { kind: "openai", anthroShape: false };
156
+ }
157
+ if (method === "POST" && (pathname === "/anthropic/v1/messages" || pathname === "/v1/messages")) {
158
+ return { kind: "anthropic", anthroShape: true };
159
+ }
160
+ return { kind: "not_found", anthroShape: false };
161
+ }
@@ -0,0 +1,137 @@
1
+ import { setTimeout as sleep } from "node:timers/promises";
2
+ import { accountBaseUrl } from "../../models/discovery.js";
3
+ import { isBenignSocketError } from "../requestLifecycle.js";
4
+ const COPILOT_HEADERS = {
5
+ "Content-Type": "application/json",
6
+ "Copilot-Integration-Id": "vscode-chat",
7
+ "Editor-Version": "vscode/1.95.0",
8
+ "Editor-Plugin-Version": "copilot-chat/0.26.7",
9
+ "User-Agent": "GitHubCopilotChat/0.26.7",
10
+ "Openai-Intent": "conversation-panel",
11
+ "X-GitHub-Api-Version": "2025-04-01",
12
+ "X-VScode-User-Agent-Library-Version": "electron-fetch",
13
+ // Disable gzip/br on the upstream response. Compressed SSE streams get
14
+ // buffered at gzip flush boundaries by undici's decoder, which causes
15
+ // visible "freeze then dump a paragraph" behaviour in Claude Code and
16
+ // other Anthropic-shape clients. Identity encoding lets each SSE event
17
+ // flow through immediately.
18
+ "Accept-Encoding": "identity"
19
+ };
20
+ const RETRYABLE_UPSTREAM_STATUSES = new Set([408, 409, 425, 429, 500, 502, 503, 504]);
21
+ const MAX_UPSTREAM_ATTEMPTS = 3;
22
+ const BASE_BACKOFF_MS = 200;
23
+ export async function postToCopilot(input) {
24
+ let forceRefresh = false;
25
+ let authRefreshRetried = false;
26
+ for (let attempt = 1; attempt <= MAX_UPSTREAM_ATTEMPTS; attempt += 1) {
27
+ if (input.signal?.aborted) {
28
+ throw abortErrorFromSignal(input.signal);
29
+ }
30
+ try {
31
+ const attemptStartedAt = Date.now();
32
+ input.logger.debug({
33
+ event: "upstream_request",
34
+ request_id: input.requestId,
35
+ attempt,
36
+ upstream_path: input.upstreamPath,
37
+ force_refresh: forceRefresh
38
+ }, "posting upstream request");
39
+ const response = await postWithCurrentBearer(input.tokenManager, input.accountType, input.body, forceRefresh, input.requestId, input.upstreamPath, input.signal);
40
+ input.logger.debug({
41
+ event: "upstream_response",
42
+ request_id: input.requestId,
43
+ attempt,
44
+ upstream_path: input.upstreamPath,
45
+ status_code: response.status,
46
+ duration_ms: Date.now() - attemptStartedAt,
47
+ content_type: response.headers.get("content-type"),
48
+ retry_after: response.headers.get("retry-after")
49
+ }, "received upstream response");
50
+ forceRefresh = false;
51
+ if (response.status === 401 && !authRefreshRetried && attempt < MAX_UPSTREAM_ATTEMPTS) {
52
+ authRefreshRetried = true;
53
+ forceRefresh = true;
54
+ input.logger.warn({ event: "upstream_retry", request_id: input.requestId, attempt, reason: "upstream_auth_401" }, "retrying upstream request after forced token refresh");
55
+ await discardUpstreamBody(response);
56
+ continue;
57
+ }
58
+ if (isRetryableStatus(response.status) && attempt < MAX_UPSTREAM_ATTEMPTS) {
59
+ input.logger.warn({ event: "upstream_retry", request_id: input.requestId, attempt, status_code: response.status }, "retrying upstream request");
60
+ await discardUpstreamBody(response);
61
+ await sleep(retryDelayMs(attempt));
62
+ continue;
63
+ }
64
+ return response;
65
+ }
66
+ catch (error) {
67
+ if (isBenignSocketError(error)) {
68
+ // Client disconnected — propagate so the request handler can clean up.
69
+ throw error;
70
+ }
71
+ if (!isRetryableTransportError(error) || attempt >= MAX_UPSTREAM_ATTEMPTS) {
72
+ throw error;
73
+ }
74
+ input.logger.warn({ event: "upstream_retry", request_id: input.requestId, attempt, reason: "transport_error" }, "retrying upstream request after transport error");
75
+ await sleep(retryDelayMs(attempt));
76
+ }
77
+ }
78
+ throw new Error("Upstream retry budget exhausted unexpectedly.");
79
+ }
80
+ async function postWithCurrentBearer(tokenManager, accountType, body, forceRefresh, requestId, upstreamPath, signal) {
81
+ const bearer = await tokenManager.ensureToken({ forceRefresh });
82
+ return fetch(`${accountBaseUrl(accountType)}${upstreamPath}`, {
83
+ method: "POST",
84
+ headers: {
85
+ ...COPILOT_HEADERS,
86
+ Authorization: `Bearer ${bearer}`,
87
+ "X-Request-Id": requestId
88
+ },
89
+ body: JSON.stringify(body),
90
+ signal
91
+ });
92
+ }
93
+ function abortErrorFromSignal(signal) {
94
+ const reason = signal.reason;
95
+ if (reason instanceof Error) {
96
+ return reason;
97
+ }
98
+ const err = new Error("Request aborted by client");
99
+ err.name = "AbortError";
100
+ return err;
101
+ }
102
+ function isRetryableStatus(status) {
103
+ return RETRYABLE_UPSTREAM_STATUSES.has(status);
104
+ }
105
+ function retryDelayMs(attempt) {
106
+ return BASE_BACKOFF_MS * Math.pow(2, Math.max(0, attempt - 1));
107
+ }
108
+ function isRetryableTransportError(error) {
109
+ if (!error || typeof error !== "object") {
110
+ return false;
111
+ }
112
+ const typedError = error;
113
+ const directCode = typedError.code?.toUpperCase();
114
+ const causeCode = typedError.cause?.code?.toUpperCase();
115
+ if (directCode === "ECONNRESET" || directCode === "ECONNREFUSED" || directCode === "ETIMEDOUT") {
116
+ return true;
117
+ }
118
+ if (causeCode === "ECONNRESET" || causeCode === "ECONNREFUSED" || causeCode === "ETIMEDOUT") {
119
+ return true;
120
+ }
121
+ if (!(typedError instanceof Error)) {
122
+ return false;
123
+ }
124
+ const message = typedError.message.toLowerCase();
125
+ if (message.includes("timed out") || message.includes("timeout")) {
126
+ return true;
127
+ }
128
+ return message.includes("econnreset") || message.includes("econnrefused") || message.includes("enotfound");
129
+ }
130
+ async function discardUpstreamBody(response) {
131
+ try {
132
+ await response.arrayBuffer();
133
+ }
134
+ catch {
135
+ // ignore body drain failures
136
+ }
137
+ }