copillm 0.1.0

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 (38) hide show
  1. package/README.md +52 -0
  2. package/dist/agentconfig/apply.js +53 -0
  3. package/dist/agentconfig/load.js +163 -0
  4. package/dist/agentconfig/markerBlock.js +76 -0
  5. package/dist/agentconfig/render.js +317 -0
  6. package/dist/agentconfig/schema.js +65 -0
  7. package/dist/auth/copilotToken.js +122 -0
  8. package/dist/auth/credentials.js +221 -0
  9. package/dist/auth/deviceFlow.js +89 -0
  10. package/dist/auth/ensureAuthenticated.js +55 -0
  11. package/dist/auth/githubIdentity.js +42 -0
  12. package/dist/auth/interactivePrompt.js +135 -0
  13. package/dist/claude/cache.js +20 -0
  14. package/dist/claude/settingsConflict.js +85 -0
  15. package/dist/cli/agentEnv.js +56 -0
  16. package/dist/cli/configCommands.js +149 -0
  17. package/dist/cli/envBlock.js +43 -0
  18. package/dist/cli/launchAgent.js +59 -0
  19. package/dist/cli/resolveAgent.js +361 -0
  20. package/dist/cli.js +1178 -0
  21. package/dist/codex/init.js +93 -0
  22. package/dist/config/config.js +51 -0
  23. package/dist/config/fsSecurity.js +39 -0
  24. package/dist/config/home.js +62 -0
  25. package/dist/config/logging.js +33 -0
  26. package/dist/config/upstream.js +38 -0
  27. package/dist/models/anthropicDefaults.js +138 -0
  28. package/dist/models/discovery.js +208 -0
  29. package/dist/pi/init.js +174 -0
  30. package/dist/server/anthropicModelsResponse.js +151 -0
  31. package/dist/server/codexSchema.js +100 -0
  32. package/dist/server/debugInfo.js +48 -0
  33. package/dist/server/lock.js +150 -0
  34. package/dist/server/proxy.js +715 -0
  35. package/dist/translation/openaiAnthropic.js +391 -0
  36. package/dist/translation/streamingOpenAIToAnthropic.js +290 -0
  37. package/dist/types/index.js +1 -0
  38. package/package.json +50 -0
@@ -0,0 +1,715 @@
1
+ import { createServer } from "node:http";
2
+ import { randomUUID } from "node:crypto";
3
+ import { setTimeout as sleep } from "node:timers/promises";
4
+ import { Readable } from "node:stream";
5
+ import { pipeline } from "node:stream/promises";
6
+ import { accountBaseUrl, listModels, listModelsUnion, resolveModelId } from "../models/discovery.js";
7
+ import { CopilotTokenExchangeError, CopilotTokenManagerError } from "../auth/copilotToken.js";
8
+ import { anthropicToOpenAI, openAIToAnthropic, ProtocolTranslationError, stripOneMillionAlias } from "../translation/openaiAnthropic.js";
9
+ import { translateOpenAIStreamToAnthropic, writeAnthropicPrelude } from "../translation/streamingOpenAIToAnthropic.js";
10
+ import { buildCodexCatalog } from "./codexSchema.js";
11
+ import { getGithubUserSummary, GithubUserFetchError } from "./debugInfo.js";
12
+ import { buildAnthropicModelsResponse } from "./anthropicModelsResponse.js";
13
+ const COPILOT_HEADERS = {
14
+ "Content-Type": "application/json",
15
+ "Copilot-Integration-Id": "vscode-chat",
16
+ "Editor-Version": "vscode/1.95.0",
17
+ "Editor-Plugin-Version": "copilot-chat/0.26.7",
18
+ "User-Agent": "GitHubCopilotChat/0.26.7",
19
+ "Openai-Intent": "conversation-panel",
20
+ "X-GitHub-Api-Version": "2025-04-01",
21
+ "X-VScode-User-Agent-Library-Version": "electron-fetch",
22
+ // Disable gzip/br on the upstream response. Compressed SSE streams get
23
+ // buffered at gzip flush boundaries by undici's decoder, which causes
24
+ // visible "freeze then dump a paragraph" behaviour in Claude Code and
25
+ // other Anthropic-shape clients. Identity encoding lets each SSE event
26
+ // flow through immediately.
27
+ "Accept-Encoding": "identity"
28
+ };
29
+ const HEALTH_REFRESH_THRESHOLD_SECONDS = 60;
30
+ const RETRYABLE_UPSTREAM_STATUSES = new Set([408, 409, 425, 429, 500, 502, 503, 504]);
31
+ const MAX_UPSTREAM_ATTEMPTS = 3;
32
+ const BASE_BACKOFF_MS = 200;
33
+ const DAEMON_STARTED_AT_ISO = new Date().toISOString();
34
+ class JsonRequestParseError extends Error {
35
+ constructor(message) {
36
+ super(message);
37
+ this.name = "JsonRequestParseError";
38
+ }
39
+ }
40
+ class InvalidRequestShapeError extends Error {
41
+ constructor(message) {
42
+ super(message);
43
+ this.name = "InvalidRequestShapeError";
44
+ }
45
+ }
46
+ export async function startProxyServer(input) {
47
+ const debugEnabled = input.debug === true;
48
+ const server = createServer(async (req, res) => {
49
+ const requestId = randomUUID();
50
+ const startedAt = Date.now();
51
+ const pathname = safePathname(req.url);
52
+ res.on("finish", () => {
53
+ input.logger.info({
54
+ event: "http_request",
55
+ request_id: requestId,
56
+ method: req.method ?? "UNKNOWN",
57
+ path: pathname,
58
+ status_code: res.statusCode,
59
+ duration_ms: Date.now() - startedAt
60
+ }, "request completed");
61
+ });
62
+ try {
63
+ if (!isLocalRequest(req)) {
64
+ sendJson(res, 403, { error: "non_loopback_request_rejected" });
65
+ return;
66
+ }
67
+ const route = resolveRoute(req.method, req.url);
68
+ if (input.config.requireCallerSecret && route.kind !== "livez" && route.kind !== "healthz") {
69
+ const auth = req.headers.authorization;
70
+ if (!input.callerSecret || auth !== `Bearer ${input.callerSecret}`) {
71
+ sendJson(res, 401, { error: "invalid_caller_secret" });
72
+ return;
73
+ }
74
+ }
75
+ if (route.kind === "livez") {
76
+ sendJson(res, 200, { status: "ok", uptime_seconds: Math.floor(process.uptime()) });
77
+ return;
78
+ }
79
+ if (route.kind === "healthz") {
80
+ const ttl = input.tokenManager.expiresInSeconds();
81
+ if (ttl !== null && ttl > HEALTH_REFRESH_THRESHOLD_SECONDS) {
82
+ sendJson(res, 200, {
83
+ status: "ok",
84
+ token_state: "fresh",
85
+ refresh_threshold_seconds: HEALTH_REFRESH_THRESHOLD_SECONDS,
86
+ bearer_ttl_seconds: ttl
87
+ });
88
+ return;
89
+ }
90
+ try {
91
+ await input.tokenManager.ensureToken({ refreshThresholdSeconds: HEALTH_REFRESH_THRESHOLD_SECONDS });
92
+ const refreshedTtl = input.tokenManager.expiresInSeconds() ?? 0;
93
+ sendJson(res, 200, {
94
+ status: "ok",
95
+ token_state: "refreshed",
96
+ refresh_threshold_seconds: HEALTH_REFRESH_THRESHOLD_SECONDS,
97
+ bearer_ttl_seconds: refreshedTtl
98
+ });
99
+ }
100
+ catch (error) {
101
+ const failed = healthFailure(error);
102
+ sendJson(res, failed.httpStatus, failed.payload);
103
+ }
104
+ return;
105
+ }
106
+ if (route.kind === "models" || route.kind === "codex_models" || route.kind === "anthropic_models") {
107
+ try {
108
+ await input.tokenManager.ensureToken(false);
109
+ const githubToken = input.githubToken;
110
+ if (!githubToken) {
111
+ sendJson(res, 503, { error: "github_token_unavailable" });
112
+ return;
113
+ }
114
+ const result = route.kind === "codex_models" || route.kind === "anthropic_models"
115
+ ? await listModelsUnion(input.config.accountType, githubToken, 3)
116
+ : await listModels(input.config.accountType, githubToken);
117
+ if (route.kind === "codex_models") {
118
+ sendJson(res, 200, buildCodexCatalog(result.models));
119
+ return;
120
+ }
121
+ if (route.kind === "anthropic_models") {
122
+ sendJson(res, 200, buildAnthropicModelsResponse(result.models));
123
+ return;
124
+ }
125
+ sendJson(res, 200, {
126
+ models: result.models,
127
+ discovery: {
128
+ source: result.source,
129
+ stale: result.stale,
130
+ cache_age_seconds: result.cacheAgeSeconds,
131
+ warning: result.warning
132
+ }
133
+ });
134
+ }
135
+ catch (error) {
136
+ if (error instanceof CopilotTokenManagerError) {
137
+ sendJson(res, 503, { error: "token_refresh_failed" });
138
+ return;
139
+ }
140
+ throw error;
141
+ }
142
+ return;
143
+ }
144
+ if (route.kind === "debug") {
145
+ if (!debugEnabled) {
146
+ sendJson(res, 404, { error: "not_found" });
147
+ return;
148
+ }
149
+ await handleDebug(res, {
150
+ config: input.config,
151
+ tokenManager: input.tokenManager,
152
+ githubToken: input.githubToken,
153
+ port: input.port
154
+ });
155
+ return;
156
+ }
157
+ if (route.kind === "not_found") {
158
+ sendJson(res, 404, { error: "not_found" });
159
+ return;
160
+ }
161
+ const requestBody = await readJson(req);
162
+ const translatedBody = translateRequestBody(route.kind, requestBody);
163
+ const requestedModel = readRequestedModel(translatedBody);
164
+ if (input.config.selectedModels.length > 0 && !requestedModel) {
165
+ sendJson(res, 400, {
166
+ error: "model_not_selected",
167
+ detail: "Requested model is not enabled in local selection."
168
+ });
169
+ return;
170
+ }
171
+ let resolvedModel = null;
172
+ try {
173
+ resolvedModel = requestedModel ? resolveModelId(requestedModel, input.config.selectedModels) : null;
174
+ }
175
+ catch (error) {
176
+ const detail = error instanceof Error ? error.message : "Model resolution failed.";
177
+ sendJson(res, 400, { error: "ambiguous_model_selection", detail });
178
+ return;
179
+ }
180
+ if (input.config.selectedModels.length > 0 && !resolvedModel) {
181
+ sendJson(res, 400, {
182
+ error: "model_not_selected",
183
+ detail: "Requested model is not enabled in local selection."
184
+ });
185
+ return;
186
+ }
187
+ const upstreamBody = resolvedModel ? rewriteRequestedModel(translatedBody, resolvedModel.id) : translatedBody;
188
+ const upstreamPath = route.kind === "codex_responses" ? "/responses" : "/chat/completions";
189
+ const isAnthropicStreaming = route.anthroShape && isStreamingRequestBody(translatedBody);
190
+ let prelude = null;
191
+ if (isAnthropicStreaming) {
192
+ beginAnthropicSseResponse(res, req);
193
+ prelude = writeAnthropicPrelude(res, requestedModel ?? "");
194
+ }
195
+ try {
196
+ const upstream = await postToCopilot({
197
+ tokenManager: input.tokenManager,
198
+ accountType: input.config.accountType,
199
+ body: upstreamBody,
200
+ requestId,
201
+ logger: input.logger,
202
+ upstreamPath
203
+ });
204
+ await forwardResponse(upstream, route.anthroShape, res, requestedModel ?? undefined, prelude);
205
+ }
206
+ catch (error) {
207
+ if (error instanceof CopilotTokenManagerError) {
208
+ if (prelude) {
209
+ writeAnthropicSseError(res, prelude, "token_refresh_failed");
210
+ return;
211
+ }
212
+ sendJson(res, 503, { error: "token_refresh_failed" });
213
+ return;
214
+ }
215
+ if (prelude) {
216
+ writeAnthropicSseError(res, prelude, "internal_error");
217
+ return;
218
+ }
219
+ throw error;
220
+ }
221
+ }
222
+ catch (error) {
223
+ if (error instanceof JsonRequestParseError) {
224
+ sendJson(res, 400, { error: "invalid_request_json", detail: error.message });
225
+ return;
226
+ }
227
+ if (error instanceof InvalidRequestShapeError) {
228
+ sendJson(res, 400, { error: "invalid_request_shape", detail: error.message });
229
+ return;
230
+ }
231
+ if (error instanceof ProtocolTranslationError) {
232
+ sendJson(res, 400, { error: error.code, detail: error.message });
233
+ return;
234
+ }
235
+ input.logger.error({ err: error }, "request failed");
236
+ sendJson(res, 500, { error: "internal_error" });
237
+ }
238
+ });
239
+ await new Promise((resolve, reject) => {
240
+ server.listen(input.port, "127.0.0.1", () => resolve());
241
+ server.on("error", reject);
242
+ });
243
+ return {
244
+ close: async () => new Promise((resolve, reject) => {
245
+ server.close((error) => {
246
+ if (error) {
247
+ reject(error);
248
+ return;
249
+ }
250
+ resolve();
251
+ });
252
+ })
253
+ };
254
+ }
255
+ async function postToCopilot(input) {
256
+ let forceRefresh = false;
257
+ let authRefreshRetried = false;
258
+ for (let attempt = 1; attempt <= MAX_UPSTREAM_ATTEMPTS; attempt += 1) {
259
+ try {
260
+ const response = await postWithCurrentBearer(input.tokenManager, input.accountType, input.body, forceRefresh, input.requestId, input.upstreamPath);
261
+ forceRefresh = false;
262
+ if (response.status === 401 && !authRefreshRetried && attempt < MAX_UPSTREAM_ATTEMPTS) {
263
+ authRefreshRetried = true;
264
+ forceRefresh = true;
265
+ input.logger.warn({ event: "upstream_retry", request_id: input.requestId, attempt, reason: "upstream_auth_401" }, "retrying upstream request after forced token refresh");
266
+ await discardUpstreamBody(response);
267
+ continue;
268
+ }
269
+ if (isRetryableStatus(response.status) && attempt < MAX_UPSTREAM_ATTEMPTS) {
270
+ input.logger.warn({ event: "upstream_retry", request_id: input.requestId, attempt, status_code: response.status }, "retrying upstream request");
271
+ await discardUpstreamBody(response);
272
+ await sleep(retryDelayMs(attempt));
273
+ continue;
274
+ }
275
+ return response;
276
+ }
277
+ catch (error) {
278
+ if (!isRetryableTransportError(error) || attempt >= MAX_UPSTREAM_ATTEMPTS) {
279
+ throw error;
280
+ }
281
+ input.logger.warn({ event: "upstream_retry", request_id: input.requestId, attempt, reason: "transport_error" }, "retrying upstream request after transport error");
282
+ await sleep(retryDelayMs(attempt));
283
+ }
284
+ }
285
+ throw new Error("Upstream retry budget exhausted unexpectedly.");
286
+ }
287
+ async function postWithCurrentBearer(tokenManager, accountType, body, forceRefresh, requestId, upstreamPath) {
288
+ const bearer = await tokenManager.ensureToken({ forceRefresh });
289
+ return fetch(`${accountBaseUrl(accountType)}${upstreamPath}`, {
290
+ method: "POST",
291
+ headers: {
292
+ ...COPILOT_HEADERS,
293
+ Authorization: `Bearer ${bearer}`,
294
+ "X-Request-Id": requestId
295
+ },
296
+ body: JSON.stringify(body)
297
+ });
298
+ }
299
+ async function forwardResponse(upstream, anthroShape, res, requestedModel, prelude) {
300
+ if (!upstream.ok) {
301
+ await discardUpstreamBody(upstream);
302
+ if (prelude) {
303
+ writeAnthropicSseError(res, prelude, upstreamStatusCategory(upstream.status));
304
+ return;
305
+ }
306
+ sendJson(res, upstream.status, { error: upstreamStatusCategory(upstream.status) });
307
+ return;
308
+ }
309
+ if (isEventStream(upstream)) {
310
+ if (anthroShape) {
311
+ if (!upstream.body) {
312
+ if (prelude) {
313
+ writeAnthropicSseError(res, prelude, "invalid_upstream_response");
314
+ return;
315
+ }
316
+ sendJson(res, 502, { error: "invalid_upstream_response", detail: "Upstream stream body is missing." });
317
+ return;
318
+ }
319
+ if (!prelude) {
320
+ beginAnthropicSseResponse(res);
321
+ }
322
+ const upstreamReadable = Readable.fromWeb(upstream.body);
323
+ await translateOpenAIStreamToAnthropic({
324
+ upstream: upstreamReadable,
325
+ downstream: res,
326
+ fallbackModel: requestedModel,
327
+ preEmittedMessageId: prelude?.messageId
328
+ });
329
+ return;
330
+ }
331
+ await pipeEventStream(upstream, res);
332
+ return;
333
+ }
334
+ if (prelude) {
335
+ writeAnthropicSseError(res, prelude, "invalid_upstream_response");
336
+ return;
337
+ }
338
+ let json;
339
+ try {
340
+ json = (await upstream.json());
341
+ }
342
+ catch {
343
+ sendJson(res, 502, { error: "invalid_upstream_response" });
344
+ return;
345
+ }
346
+ let payload = json;
347
+ if (anthroShape) {
348
+ try {
349
+ payload = openAIToAnthropic(json);
350
+ }
351
+ catch (error) {
352
+ if (error instanceof ProtocolTranslationError) {
353
+ sendJson(res, 502, { error: error.code, detail: error.message });
354
+ return;
355
+ }
356
+ throw error;
357
+ }
358
+ }
359
+ sendJson(res, 200, payload);
360
+ }
361
+ async function pipeEventStream(upstream, res) {
362
+ if (!upstream.body) {
363
+ sendJson(res, 502, { error: "invalid_upstream_response", detail: "Upstream stream body is missing." });
364
+ return;
365
+ }
366
+ res.statusCode = upstream.status;
367
+ res.setHeader("Content-Type", "text/event-stream");
368
+ const cacheControl = upstream.headers.get("cache-control");
369
+ if (cacheControl) {
370
+ res.setHeader("Cache-Control", cacheControl);
371
+ }
372
+ else {
373
+ res.setHeader("Cache-Control", "no-cache");
374
+ }
375
+ const connection = upstream.headers.get("connection");
376
+ if (connection) {
377
+ res.setHeader("Connection", connection);
378
+ }
379
+ else {
380
+ res.setHeader("Connection", "keep-alive");
381
+ }
382
+ res.setHeader("X-Accel-Buffering", "no");
383
+ if (typeof res.flushHeaders === "function") {
384
+ res.flushHeaders();
385
+ }
386
+ if (res.socket && typeof res.socket.setNoDelay === "function") {
387
+ res.socket.setNoDelay(true);
388
+ }
389
+ await pipeline(Readable.fromWeb(upstream.body), res);
390
+ }
391
+ function isEventStream(upstream) {
392
+ const contentType = upstream.headers.get("content-type");
393
+ return typeof contentType === "string" && contentType.toLowerCase().includes("text/event-stream");
394
+ }
395
+ function isStreamingRequestBody(body) {
396
+ return typeof body === "object" && body !== null && body.stream === true;
397
+ }
398
+ function beginAnthropicSseResponse(res, req) {
399
+ if (res.headersSent) {
400
+ return;
401
+ }
402
+ res.statusCode = 200;
403
+ res.setHeader("Content-Type", "text/event-stream");
404
+ res.setHeader("Cache-Control", "no-cache");
405
+ res.setHeader("Connection", "keep-alive");
406
+ res.setHeader("X-Accel-Buffering", "no");
407
+ if (typeof res.flushHeaders === "function") {
408
+ res.flushHeaders();
409
+ }
410
+ const socket = res.socket ?? req?.socket;
411
+ if (socket && typeof socket.setNoDelay === "function") {
412
+ socket.setNoDelay(true);
413
+ }
414
+ }
415
+ export function writeAnthropicSseError(res, prelude, code) {
416
+ void prelude;
417
+ try {
418
+ res.write(`event: message_delta\ndata: ${JSON.stringify({
419
+ type: "message_delta",
420
+ delta: { stop_reason: "end_turn", stop_sequence: null },
421
+ usage: { input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0 }
422
+ })}\n\n`);
423
+ res.write(`event: error\ndata: ${JSON.stringify({ type: "error", error: { type: "api_error", message: code } })}\n\n`);
424
+ res.write(`event: message_stop\ndata: ${JSON.stringify({ type: "message_stop" })}\n\n`);
425
+ }
426
+ finally {
427
+ res.end();
428
+ }
429
+ }
430
+ function isLocalRequest(req) {
431
+ const remote = req.socket.remoteAddress ?? "";
432
+ const local = req.socket.localAddress ?? "";
433
+ return isLoopbackAddress(remote) && (local.length === 0 || isLoopbackAddress(local));
434
+ }
435
+ function isLoopbackAddress(value) {
436
+ return value === "127.0.0.1" || value === "::1" || value === "::ffff:127.0.0.1";
437
+ }
438
+ async function readJson(req) {
439
+ const chunks = [];
440
+ for await (const chunk of req) {
441
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
442
+ }
443
+ if (chunks.length === 0) {
444
+ return {};
445
+ }
446
+ const text = Buffer.concat(chunks).toString("utf8");
447
+ try {
448
+ return JSON.parse(text);
449
+ }
450
+ catch (error) {
451
+ if (error instanceof SyntaxError) {
452
+ throw new JsonRequestParseError("Failed to parse JSON body.");
453
+ }
454
+ throw error;
455
+ }
456
+ }
457
+ function sendJson(res, status, payload) {
458
+ res.statusCode = status;
459
+ res.setHeader("Content-Type", "application/json");
460
+ res.end(JSON.stringify(payload));
461
+ }
462
+ function readRequestedModel(payload) {
463
+ if (!payload || typeof payload !== "object") {
464
+ return null;
465
+ }
466
+ const maybeModel = payload.model;
467
+ return typeof maybeModel === "string" ? maybeModel : null;
468
+ }
469
+ function rewriteRequestedModel(payload, model) {
470
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
471
+ return payload;
472
+ }
473
+ return {
474
+ ...payload,
475
+ model
476
+ };
477
+ }
478
+ function translateRequestBody(routeKind, body) {
479
+ if (routeKind !== "anthropic") {
480
+ return normaliseAliasedModelInPlace(body);
481
+ }
482
+ try {
483
+ return anthropicToOpenAI(body);
484
+ }
485
+ catch (error) {
486
+ if (error instanceof Error) {
487
+ throw new InvalidRequestShapeError(error.message);
488
+ }
489
+ throw new InvalidRequestShapeError("Invalid Anthropic request body.");
490
+ }
491
+ }
492
+ /**
493
+ * Defensive strip of the `[1m]` alias for pass-through routes (OpenAI chat
494
+ * completions, Codex responses). The Anthropic route already strips inside
495
+ * `anthropicToOpenAI`; this catches anything that might land on the
496
+ * pass-through paths with a hand-pasted aliased id so upstream Copilot
497
+ * always receives the canonical model id.
498
+ */
499
+ function normaliseAliasedModelInPlace(body) {
500
+ if (body && typeof body === "object" && !Array.isArray(body)) {
501
+ const record = body;
502
+ if (typeof record.model === "string") {
503
+ const stripped = stripOneMillionAlias(record.model);
504
+ if (stripped !== record.model) {
505
+ record.model = stripped;
506
+ }
507
+ }
508
+ }
509
+ return body;
510
+ }
511
+ async function handleDebug(res, input) {
512
+ const bearerTtlSeconds = input.tokenManager.expiresInSeconds();
513
+ const uptimeSeconds = Math.max(0, Math.floor((Date.now() - Date.parse(DAEMON_STARTED_AT_ISO)) / 1_000));
514
+ let user = null;
515
+ let userError = null;
516
+ if (input.githubToken) {
517
+ try {
518
+ const summary = await getGithubUserSummary(input.githubToken);
519
+ user = {
520
+ login: summary.login,
521
+ id: summary.id,
522
+ name: summary.name,
523
+ email: summary.email,
524
+ type: summary.type,
525
+ avatar_url: summary.avatar_url,
526
+ html_url: summary.html_url,
527
+ plan_name: summary.plan_name
528
+ };
529
+ }
530
+ catch (error) {
531
+ if (error instanceof GithubUserFetchError) {
532
+ userError = `github_user_lookup_failed_${error.status}`;
533
+ }
534
+ else {
535
+ userError = error instanceof Error ? error.message : "unknown_error";
536
+ }
537
+ }
538
+ }
539
+ else {
540
+ userError = "github_token_unavailable_in_proxy";
541
+ }
542
+ sendJson(res, 200, {
543
+ server: {
544
+ port: input.port,
545
+ pid: process.pid,
546
+ node_version: process.version,
547
+ started_at_iso: DAEMON_STARTED_AT_ISO,
548
+ uptime_seconds: uptimeSeconds,
549
+ account_type: input.config.accountType,
550
+ selected_models: input.config.selectedModels,
551
+ require_caller_secret: input.config.requireCallerSecret
552
+ },
553
+ auth: {
554
+ bearer_ttl_seconds: bearerTtlSeconds,
555
+ bearer_present: input.tokenManager.current !== null,
556
+ bearer_expires_at_unix: input.tokenManager.current?.expiresAtUnix ?? null
557
+ },
558
+ user,
559
+ user_error: userError,
560
+ routes: [
561
+ "GET /livez",
562
+ "GET /healthz",
563
+ "GET /models",
564
+ "GET /v1/models",
565
+ "GET /codex/v1/models",
566
+ "GET /anthropic/v1/models",
567
+ "POST /codex/v1/responses",
568
+ "POST /v1/chat/completions",
569
+ "POST /v1/messages",
570
+ "POST /anthropic/v1/messages",
571
+ "GET /_debug"
572
+ ],
573
+ debug_enabled: true
574
+ });
575
+ }
576
+ function resolveRoute(method, rawUrl) {
577
+ if (!method || !rawUrl) {
578
+ return { kind: "not_found", anthroShape: false };
579
+ }
580
+ let pathname;
581
+ try {
582
+ pathname = new URL(rawUrl, "http://127.0.0.1").pathname;
583
+ }
584
+ catch {
585
+ return { kind: "not_found", anthroShape: false };
586
+ }
587
+ if (method === "GET" && pathname === "/livez") {
588
+ return { kind: "livez", anthroShape: false };
589
+ }
590
+ if (method === "GET" && pathname === "/healthz") {
591
+ return { kind: "healthz", anthroShape: false };
592
+ }
593
+ if (method === "GET" && (pathname === "/models" || pathname === "/v1/models")) {
594
+ return { kind: "models", anthroShape: false };
595
+ }
596
+ if (method === "GET" && pathname === "/codex/v1/models") {
597
+ return { kind: "codex_models", anthroShape: false };
598
+ }
599
+ if (method === "GET" && pathname === "/anthropic/v1/models") {
600
+ return { kind: "anthropic_models", anthroShape: false };
601
+ }
602
+ if (method === "POST" && pathname === "/codex/v1/responses") {
603
+ return { kind: "codex_responses", anthroShape: false };
604
+ }
605
+ if (method === "GET" && pathname === "/_debug") {
606
+ return { kind: "debug", anthroShape: false };
607
+ }
608
+ if (method === "POST" && pathname === "/v1/chat/completions") {
609
+ return { kind: "openai", anthroShape: false };
610
+ }
611
+ if (method === "POST" && (pathname === "/anthropic/v1/messages" || pathname === "/v1/messages")) {
612
+ return { kind: "anthropic", anthroShape: true };
613
+ }
614
+ return { kind: "not_found", anthroShape: false };
615
+ }
616
+ function isRetryableStatus(status) {
617
+ return RETRYABLE_UPSTREAM_STATUSES.has(status);
618
+ }
619
+ function retryDelayMs(attempt) {
620
+ return BASE_BACKOFF_MS * Math.pow(2, Math.max(0, attempt - 1));
621
+ }
622
+ function isRetryableTransportError(error) {
623
+ if (!error || typeof error !== "object") {
624
+ return false;
625
+ }
626
+ const typedError = error;
627
+ const directCode = typedError.code?.toUpperCase();
628
+ const causeCode = typedError.cause?.code?.toUpperCase();
629
+ if (directCode === "ECONNRESET" || directCode === "ECONNREFUSED" || directCode === "ETIMEDOUT") {
630
+ return true;
631
+ }
632
+ if (causeCode === "ECONNRESET" || causeCode === "ECONNREFUSED" || causeCode === "ETIMEDOUT") {
633
+ return true;
634
+ }
635
+ if (!(typedError instanceof Error)) {
636
+ return false;
637
+ }
638
+ const message = typedError.message.toLowerCase();
639
+ if (message.includes("timed out") || message.includes("timeout")) {
640
+ return true;
641
+ }
642
+ return message.includes("econnreset") || message.includes("econnrefused") || message.includes("enotfound");
643
+ }
644
+ function upstreamStatusCategory(status) {
645
+ if (status === 401 || status === 403) {
646
+ return "upstream_auth_error";
647
+ }
648
+ if (status === 429) {
649
+ return "upstream_rate_limited";
650
+ }
651
+ if (status >= 500) {
652
+ return "upstream_server_error";
653
+ }
654
+ if (status >= 400) {
655
+ return "upstream_request_error";
656
+ }
657
+ return "upstream_error";
658
+ }
659
+ function healthFailure(error) {
660
+ if (error instanceof CopilotTokenExchangeError) {
661
+ if (error.statusCode === 401 || error.statusCode === 403) {
662
+ return {
663
+ httpStatus: 401,
664
+ payload: {
665
+ status: "unauthenticated",
666
+ error: "github_auth_invalid",
667
+ upstream_status_code: error.statusCode
668
+ }
669
+ };
670
+ }
671
+ return {
672
+ httpStatus: 503,
673
+ payload: {
674
+ status: "upstream_unreachable",
675
+ error: "token_exchange_failed",
676
+ upstream_status_code: error.statusCode
677
+ }
678
+ };
679
+ }
680
+ if (error instanceof CopilotTokenManagerError) {
681
+ return {
682
+ httpStatus: 401,
683
+ payload: {
684
+ status: "unauthenticated",
685
+ error: "token_refresh_failed"
686
+ }
687
+ };
688
+ }
689
+ return {
690
+ httpStatus: 503,
691
+ payload: {
692
+ status: "upstream_unreachable",
693
+ error: "token_refresh_unavailable"
694
+ }
695
+ };
696
+ }
697
+ function safePathname(rawUrl) {
698
+ if (!rawUrl) {
699
+ return "/";
700
+ }
701
+ try {
702
+ return new URL(rawUrl, "http://127.0.0.1").pathname;
703
+ }
704
+ catch {
705
+ return "/";
706
+ }
707
+ }
708
+ async function discardUpstreamBody(response) {
709
+ try {
710
+ await response.arrayBuffer();
711
+ }
712
+ catch {
713
+ // ignore body drain failures
714
+ }
715
+ }