github-router 0.3.258 → 0.3.263

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 (78) hide show
  1. package/dist/{attribution-settings-CYBfjHzd.js → attribution-settings-DFIVIRG8.js} +4 -4
  2. package/dist/{attribution-settings-CYBfjHzd.js.map → attribution-settings-DFIVIRG8.js.map} +1 -1
  3. package/dist/{auth-CP4q2CjO.js → auth-WA88cE9q.js} +3 -3
  4. package/dist/{auth-CP4q2CjO.js.map → auth-WA88cE9q.js.map} +1 -1
  5. package/dist/browser-ext/manifest.json +1 -1
  6. package/dist/{check-usage-6PB5IUFb.js → check-usage-v2apGKz7.js} +4 -4
  7. package/dist/{check-usage-6PB5IUFb.js.map → check-usage-v2apGKz7.js.map} +1 -1
  8. package/dist/{claude-DZ1VmE7I.js → claude-BCI3Sx6_.js} +16 -14
  9. package/dist/claude-BCI3Sx6_.js.map +1 -0
  10. package/dist/{codex-DwabDTOC.js → codex-DyJIrbJt.js} +8 -6
  11. package/dist/codex-DyJIrbJt.js.map +1 -0
  12. package/dist/{debug-CjmE9sgG.js → debug-vshv4nIv.js} +2 -2
  13. package/dist/{debug-CjmE9sgG.js.map → debug-vshv4nIv.js.map} +1 -1
  14. package/dist/engine-D8dlcSW6.js +2 -0
  15. package/dist/{gate-discovery-BpHK5pIq.js → gate-discovery-cMbt8Ct2.js} +5 -5
  16. package/dist/{gate-discovery-BpHK5pIq.js.map → gate-discovery-cMbt8Ct2.js.map} +1 -1
  17. package/dist/{get-copilot-usage-DD1N0Xlh.js → get-copilot-usage-DOFJWJaQ.js} +2 -2
  18. package/dist/{get-copilot-usage-DD1N0Xlh.js.map → get-copilot-usage-DOFJWJaQ.js.map} +1 -1
  19. package/dist/{internal-artifact-open-D2cQ2QCw.js → internal-artifact-open-CZGO831M.js} +2 -2
  20. package/dist/{internal-artifact-open-D2cQ2QCw.js.map → internal-artifact-open-CZGO831M.js.map} +1 -1
  21. package/dist/{internal-plan-review-ipnT3bhj.js → internal-plan-review-6gkcOAjT.js} +3 -3
  22. package/dist/{internal-plan-review-ipnT3bhj.js.map → internal-plan-review-6gkcOAjT.js.map} +1 -1
  23. package/dist/{internal-prompt-submit-DOj75_hh.js → internal-prompt-submit-C0XOPgcP.js} +4 -4
  24. package/dist/{internal-prompt-submit-DOj75_hh.js.map → internal-prompt-submit-C0XOPgcP.js.map} +1 -1
  25. package/dist/{internal-session-bind-DwK2iGW0.js → internal-session-bind-DtSBTkQe.js} +2 -2
  26. package/dist/{internal-session-bind-DwK2iGW0.js.map → internal-session-bind-DtSBTkQe.js.map} +1 -1
  27. package/dist/{internal-stop-hook-DiqatTCG.js → internal-stop-hook-CBcE2Js8.js} +5 -5
  28. package/dist/{internal-stop-hook-DiqatTCG.js.map → internal-stop-hook-CBcE2Js8.js.map} +1 -1
  29. package/dist/{internal-stop-review-AUcFssrW.js → internal-stop-review-_1Mqz7eT.js} +2 -2
  30. package/dist/{internal-stop-review-AUcFssrW.js.map → internal-stop-review-_1Mqz7eT.js.map} +1 -1
  31. package/dist/{lifecycle-Byw5s1G8.js → lifecycle-Bd1Fyf4B.js} +100 -3
  32. package/dist/lifecycle-Bd1Fyf4B.js.map +1 -0
  33. package/dist/lifecycle-CRTWEbiP.js +2 -0
  34. package/dist/lifecycle-DUPXAOQS.js +2 -0
  35. package/dist/{lifecycle-DD88ka3f.js → lifecycle-DjC9tC1A.js} +2 -2
  36. package/dist/{lifecycle-DD88ka3f.js.map → lifecycle-DjC9tC1A.js.map} +1 -1
  37. package/dist/main.js +14 -14
  38. package/dist/{models-B6UnGyRo.js → models-Cv6gprpy.js} +3 -3
  39. package/dist/{models-B6UnGyRo.js.map → models-Cv6gprpy.js.map} +1 -1
  40. package/dist/{orchestration-hRU4TdWr.js → orchestration-B6qidQAd.js} +2 -2
  41. package/dist/{orchestration-hRU4TdWr.js.map → orchestration-B6qidQAd.js.map} +1 -1
  42. package/dist/{paths-C1cLZ7Ga.js → paths-BXrbboAS.js} +3 -3
  43. package/dist/{paths-C1cLZ7Ga.js.map → paths-BXrbboAS.js.map} +1 -1
  44. package/dist/paths-Dz1WyRJG.js +2 -0
  45. package/dist/{peer-mcp-personas-DYT9RZBk.js → peer-mcp-personas-B22ncnbQ.js} +380 -77
  46. package/dist/peer-mcp-personas-B22ncnbQ.js.map +1 -0
  47. package/dist/{plan-review-hook-C8x0uKPf.js → plan-review-hook-Bo4y1r3j.js} +2 -2
  48. package/dist/{plan-review-hook-C8x0uKPf.js.map → plan-review-hook-Bo4y1r3j.js.map} +1 -1
  49. package/dist/{prompt-submit-hook-BrkkLAAB.js → prompt-submit-hook-BFRFLSk5.js} +2 -2
  50. package/dist/{prompt-submit-hook-BrkkLAAB.js.map → prompt-submit-hook-BFRFLSk5.js.map} +1 -1
  51. package/dist/{provision-C2wCxRK_.js → provision-DSpLQHC3.js} +4 -4
  52. package/dist/{provision-C2wCxRK_.js.map → provision-DSpLQHC3.js.map} +1 -1
  53. package/dist/{serve-DHTgMHKU.js → serve-D9tBVVoK.js} +19 -14
  54. package/dist/{serve-DHTgMHKU.js.map → serve-D9tBVVoK.js.map} +1 -1
  55. package/dist/{server-setup-CJzxzYks.js → server-setup-BM9Smfek.js} +318 -21
  56. package/dist/server-setup-BM9Smfek.js.map +1 -0
  57. package/dist/{start-DePkmXJt.js → start-D7Hx2T_T.js} +8 -5
  58. package/dist/start-D7Hx2T_T.js.map +1 -0
  59. package/dist/{stop-gate-hook-DQc2KV7B.js → stop-gate-hook-Bmwla_2u.js} +11 -6
  60. package/dist/{stop-gate-hook-DQc2KV7B.js.map → stop-gate-hook-Bmwla_2u.js.map} +1 -1
  61. package/dist/{stop-gate-policy-CHwWbuF_.js → stop-gate-policy-CoMKWSsS.js} +2 -2
  62. package/dist/{stop-gate-policy-CHwWbuF_.js.map → stop-gate-policy-CoMKWSsS.js.map} +1 -1
  63. package/dist/{token-R2ocFC3M.js → token-C8Uh7aUe.js} +95 -13
  64. package/dist/token-C8Uh7aUe.js.map +1 -0
  65. package/package.json +1 -1
  66. package/dist/claude-DZ1VmE7I.js.map +0 -1
  67. package/dist/codex-DwabDTOC.js.map +0 -1
  68. package/dist/engine-Cpu-wQ6p.js +0 -2
  69. package/dist/file-log-reporter-IFQcL5IK.js +0 -158
  70. package/dist/file-log-reporter-IFQcL5IK.js.map +0 -1
  71. package/dist/lifecycle-Byw5s1G8.js.map +0 -1
  72. package/dist/lifecycle-D80CaWie.js +0 -2
  73. package/dist/lifecycle-DzqCqDlM.js +0 -2
  74. package/dist/paths-CwKwc2pi.js +0 -2
  75. package/dist/peer-mcp-personas-DYT9RZBk.js.map +0 -1
  76. package/dist/server-setup-CJzxzYks.js.map +0 -1
  77. package/dist/start-DePkmXJt.js.map +0 -1
  78. package/dist/token-R2ocFC3M.js.map +0 -1
@@ -1,12 +1,12 @@
1
- import { A as searchWeb, B as readIteratorWithTimeout, Et as toolbeltPathOverride, F as isAdvisorRequested, Ft as withOneMSuffix, H as handleMcpDelete, I as buildAnthropicErrorEvent, It as withInstallLock, L as buildOpenAIErrorEvent, M as ADVISOR_TOOL_INSTRUCTIONS, Mt as UPSTREAM_INACTIVITY_TIMEOUT_MS, N as buildAdvisorStream, Nt as generateRandomPort, P as injectAdvisorTool, R as isControllerClosedError, U as handleMcpPost, V as relayAnthropicStream, W as agentToolsEnabled, at as countTokens, ct as assembleResponsesPayload, dt as createResponses, ft as createChatCompletions, ht as parseJsonOrDiagnose, it as shimDefaultsToXhigh, j as ADVISOR_INTERNAL_TOOL_NAME, jt as UPSTREAM_FETCH_TIMEOUT_MS, lt as resolveMcpToolTimeoutMs, mt as readResponseBodyCapped, ot as createMessages, pt as MAX_RESPONSE_BODY_BYTES, r as assertMcpToolSurfaceConsistent, st as getTokenCount, ut as pickEndpoint, w as toolbeltEnabled, z as logStreamError } from "./peer-mcp-personas-DYT9RZBk.js";
1
+ import { A as searchWeb, B as readIteratorWithTimeout, Dt as toolbeltPathOverride, F as isAdvisorRequested, H as handleMcpDelete, I as buildAnthropicErrorEvent, It as withOneMSuffix, L as buildOpenAIErrorEvent, Lt as withInstallLock, M as ADVISOR_TOOL_INSTRUCTIONS, Mt as UPSTREAM_FETCH_TIMEOUT_MS, N as buildAdvisorStream, Nt as UPSTREAM_INACTIVITY_TIMEOUT_MS, P as injectAdvisorTool, Pt as generateRandomPort, R as isControllerClosedError, U as handleMcpPost, V as relayAnthropicStream, W as agentToolsEnabled, at as countTokens, ct as assembleResponsesPayload, dt as createResponses, ft as createChatCompletions, ht as parseJsonOrDiagnose, it as shimDefaultsToXhigh, j as ADVISOR_INTERNAL_TOOL_NAME, lt as resolveMcpToolTimeoutMs, mt as readResponseBodyCapped, ot as createMessages, pt as MAX_RESPONSE_BODY_BYTES, r as assertMcpToolSurfaceConsistent, st as getTokenCount, ut as pickEndpoint, w as toolbeltEnabled, z as logStreamError } from "./peer-mcp-personas-B22ncnbQ.js";
2
2
  import { t as getPackageVersion } from "./version-_Q1WpsQp.js";
3
- import { i as ensurePaths, t as PATHS } from "./paths-C1cLZ7Ga.js";
4
- import { C as copilotHeaders, O as state, S as copilotBaseUrl, a as cacheCopilotVersion, c as filterBetaHeader, d as resolveModel, f as sleep, g as fetchWithTransientRetry, i as tryRefreshAndRetry, l as isNullish, n as setupGitHubAgentToken, o as cacheModels, r as setupGitHubToken, s as cacheVSCodeVersion, t as setupCopilotToken, v as HTTPError, y as forwardError } from "./token-R2ocFC3M.js";
5
- import { t as getCopilotUsage } from "./get-copilot-usage-DD1N0Xlh.js";
3
+ import { i as ensurePaths, t as PATHS } from "./paths-BXrbboAS.js";
4
+ import { C as copilotHeaders, O as state, S as copilotBaseUrl, a as cacheCopilotVersion, c as filterBetaHeader, d as resolveModel, f as sleep, g as fetchWithTransientRetry, i as tryRefreshAndRetry, l as isNullish, n as setupGitHubAgentToken, o as cacheModels, r as setupGitHubToken, s as cacheVSCodeVersion, t as setupCopilotToken, v as HTTPError, y as forwardError } from "./token-C8Uh7aUe.js";
5
+ import { t as getCopilotUsage } from "./get-copilot-usage-DOFJWJaQ.js";
6
6
  import { a as resolveExecutable, n as killManagedTree, o as runCommandCapture, r as parseBoolEnv, s as runCommandVoid } from "./exec-DTO2oin0.js";
7
7
  import consola from "consola";
8
8
  import * as fs$2 from "node:fs";
9
- import { existsSync } from "node:fs";
9
+ import fs, { existsSync } from "node:fs";
10
10
  import * as nodePath from "node:path";
11
11
  import path from "node:path";
12
12
  import { randomUUID } from "node:crypto";
@@ -15,6 +15,7 @@ import os from "node:os";
15
15
  import process$1 from "node:process";
16
16
  import { spawn } from "node:child_process";
17
17
  import { Agent, ProxyAgent, setGlobalDispatcher } from "undici";
18
+ import { Writable } from "node:stream";
18
19
  import { serve } from "srvx";
19
20
  import { createBodyTooLargeError, limitRequestBody } from "srvx/body-limit";
20
21
  import { getProxyForUrl } from "proxy-from-env";
@@ -353,6 +354,170 @@ async function runSelfUpdate(opts) {
353
354
  }
354
355
  }
355
356
  //#endregion
357
+ //#region src/lib/file-log-reporter.ts
358
+ const MAX_LOG_BYTES = 1048576;
359
+ const DEDUP_MAX = 1e3;
360
+ const ARG_MAX_LEN = 2048;
361
+ const DEDUP_KEY_MAX_LEN = 200;
362
+ const CREDENTIAL_RE = /\b(eyJ[A-Za-z0-9_-]{20,}(?:\.[A-Za-z0-9_-]+){0,2}|gh[opsu]_[A-Za-z0-9_]{20,}|Bearer\s+\S{20,})\b/g;
363
+ const ALLOWED_TYPES = /* @__PURE__ */ new Set([
364
+ "fatal",
365
+ "error",
366
+ "warn"
367
+ ]);
368
+ function sanitize(line) {
369
+ return line.replace(CREDENTIAL_RE, "[REDACTED]");
370
+ }
371
+ function serializeArg(arg) {
372
+ if (typeof arg === "string") return arg;
373
+ if (arg instanceof Error) {
374
+ const parts = [arg.message];
375
+ if (arg.stack) parts.push(arg.stack);
376
+ return parts.join("\n");
377
+ }
378
+ return String(arg);
379
+ }
380
+ function formatLogLine(logObj) {
381
+ return sanitize(`${logObj.date.toISOString()} [${(logObj.type ?? "error").toUpperCase()}] ${logObj.args.map((a) => {
382
+ const s = serializeArg(a);
383
+ return s.length > ARG_MAX_LEN ? s.slice(0, ARG_MAX_LEN) + "…" : s;
384
+ }).join(" ").replace(/\r\n|\r|\n/g, "\\n")}\n`);
385
+ }
386
+ function makeDedupeKey(logObj) {
387
+ const firstArg = logObj.args.length > 0 ? serializeArg(logObj.args[0]) : "";
388
+ const key = `${logObj.type}:${firstArg}`;
389
+ return key.length > DEDUP_KEY_MAX_LEN ? key.slice(0, DEDUP_KEY_MAX_LEN) : key;
390
+ }
391
+ /**
392
+ * Construction-time rotation: rename the log aside if it's already over the
393
+ * cap before we start appending. Runs with no descriptor held (the instance
394
+ * fd is opened lazily on the first `log()`), so a plain path stat + rename is
395
+ * correct here. The per-`log()` ceiling check lives in `rotateIfNeeded()`.
396
+ */
397
+ function rotateAtStartup(filePath) {
398
+ let size;
399
+ try {
400
+ size = fs.statSync(filePath).size;
401
+ } catch {
402
+ return;
403
+ }
404
+ if (size <= MAX_LOG_BYTES) return;
405
+ try {
406
+ fs.renameSync(filePath, filePath + ".1");
407
+ } catch {}
408
+ }
409
+ var FileLogReporter = class FileLogReporter {
410
+ filePath;
411
+ seen = /* @__PURE__ */ new Set();
412
+ bytesSinceCheck = 0;
413
+ static ROTATE_CHECK_BYTES = MAX_LOG_BYTES / 2;
414
+ fd;
415
+ constructor(filePath) {
416
+ this.filePath = filePath;
417
+ rotateAtStartup(filePath);
418
+ }
419
+ ensureFd() {
420
+ if (this.fd !== void 0) return this.fd;
421
+ try {
422
+ this.fd = fs.openSync(this.filePath, "a", 384);
423
+ } catch {
424
+ this.fd = void 0;
425
+ }
426
+ return this.fd;
427
+ }
428
+ closeFd() {
429
+ if (this.fd === void 0) return;
430
+ try {
431
+ fs.closeSync(this.fd);
432
+ } catch {}
433
+ this.fd = void 0;
434
+ }
435
+ /**
436
+ * Enforce the MAX_LOG_BYTES ceiling. Sizes the LIVE file (fstat on the open
437
+ * fd when we hold one — no path race — else a path stat), and on overflow
438
+ * CLOSES the fd before renaming. Closing first is load-bearing on Windows
439
+ * (renaming a file with an open handle fails with EBUSY/EPERM) and correct
440
+ * on POSIX too (a held append-fd would otherwise keep writing into the
441
+ * renamed `.1` inode). The fd is left closed so the next write reopens the
442
+ * freshly-created file.
443
+ */
444
+ rotateIfNeeded() {
445
+ let size;
446
+ try {
447
+ size = this.fd !== void 0 ? fs.fstatSync(this.fd).size : fs.statSync(this.filePath).size;
448
+ } catch {
449
+ return;
450
+ }
451
+ if (size <= MAX_LOG_BYTES) return;
452
+ this.closeFd();
453
+ try {
454
+ fs.renameSync(this.filePath, this.filePath + ".1");
455
+ } catch {}
456
+ }
457
+ /**
458
+ * Close the held descriptor. Safe to call repeatedly and after a write
459
+ * failure. Optional for correctness (writeSync flushes immediately and the
460
+ * OS closes fds at process exit), but lets a long-lived host release the
461
+ * handle deterministically on shutdown.
462
+ */
463
+ close() {
464
+ this.closeFd();
465
+ }
466
+ log(logObj, _ctx) {
467
+ if (!ALLOWED_TYPES.has(logObj.type)) return;
468
+ const key = makeDedupeKey(logObj);
469
+ if (this.seen.has(key)) return;
470
+ if (this.seen.size >= DEDUP_MAX) this.seen.clear();
471
+ this.seen.add(key);
472
+ const line = formatLogLine(logObj);
473
+ this.bytesSinceCheck += line.length;
474
+ if (this.bytesSinceCheck >= FileLogReporter.ROTATE_CHECK_BYTES) {
475
+ this.rotateIfNeeded();
476
+ this.bytesSinceCheck = 0;
477
+ }
478
+ const fd = this.ensureFd();
479
+ if (fd === void 0) return;
480
+ try {
481
+ fs.writeSync(fd, line);
482
+ } catch {
483
+ this.closeFd();
484
+ }
485
+ }
486
+ };
487
+ const nullStream = new Writable({ write(_chunk, _encoding, cb) {
488
+ cb();
489
+ } });
490
+ /**
491
+ * Switch consola to file-only mode for TUI sessions.
492
+ * Removes the terminal reporter and installs a file reporter that
493
+ * persists errors and warnings to disk with dedup and credential scrubbing.
494
+ *
495
+ * Also sinks consola's stdout/stderr streams as belt-and-suspenders:
496
+ * even if a terminal reporter is re-added, it cannot write to the terminal.
497
+ * Crash handlers that call process.stderr.write() directly are unaffected.
498
+ * FileLogReporter uses fs.writeSync() directly and is also unaffected.
499
+ */
500
+ function enableFileLogging() {
501
+ const reporter = new FileLogReporter(PATHS.ERROR_LOG_PATH);
502
+ consola.options.throttle = 0;
503
+ consola.setReporters([reporter]);
504
+ consola.options.stdout = nullStream;
505
+ consola.options.stderr = nullStream;
506
+ fileLoggingActive = true;
507
+ }
508
+ /**
509
+ * True once `enableFileLogging` has run. The file reporter accepts only
510
+ * fatal/error/warn (`ALLOWED_TYPES`), so an `info`-level line — which is what
511
+ * the per-request summary is — reaches no consumer at all in this mode.
512
+ *
513
+ * Exposed so callers can skip work whose ONLY purpose is to populate that
514
+ * line. See `requestLogVisible` in `~/lib/request-log`.
515
+ */
516
+ let fileLoggingActive = false;
517
+ function isFileLoggingEnabled() {
518
+ return fileLoggingActive;
519
+ }
520
+ //#endregion
356
521
  //#region src/lib/model-validation.ts
357
522
  const ENDPOINT_ALIASES = {
358
523
  "/chat/completions": "/chat/completions",
@@ -882,7 +1047,7 @@ function initProxyFromEnv() {
882
1047
  //#endregion
883
1048
  //#region package.json
884
1049
  var name = "github-router";
885
- var version = "0.3.258";
1050
+ var version = "0.3.263";
886
1051
  //#endregion
887
1052
  //#region src/lib/approval.ts
888
1053
  const awaitApproval = async () => {
@@ -936,6 +1101,34 @@ async function doCheck(state, ticket) {
936
1101
  //#endregion
937
1102
  //#region src/lib/request-log.ts
938
1103
  /**
1104
+ * Rolling request-body size distribution.
1105
+ *
1106
+ * Motivation: the cost of the `/v1/messages` prologue (substring guards, JSON
1107
+ * parses, re-serialization) scales with body size, but the proxy read
1108
+ * `content-length` and never recorded it — so there was no way to tell whether
1109
+ * a prologue optimization was worth anything. Benchmarks at 4.5 MiB mean
1110
+ * nothing if the real p50 is 40 KiB.
1111
+ *
1112
+ * Bounded by construction: a fixed-size ring, not a growing array.
1113
+ */
1114
+ const BODY_SIZE_RING = 512;
1115
+ const bodySizes = new Float64Array(BODY_SIZE_RING);
1116
+ let bodySizeCount = 0;
1117
+ let bodySizeIdx = 0;
1118
+ /** Record one request body size (bytes). Cheap; safe to call per request. */
1119
+ function recordBodySize(bytes) {
1120
+ if (!Number.isFinite(bytes) || bytes < 0) return;
1121
+ bodySizes[bodySizeIdx] = bytes;
1122
+ bodySizeIdx = (bodySizeIdx + 1) % BODY_SIZE_RING;
1123
+ if (bodySizeCount < BODY_SIZE_RING) bodySizeCount++;
1124
+ }
1125
+ /** Format a byte count compactly (1.2M / 15.3K / 900B). */
1126
+ function formatBytes(n) {
1127
+ if (n >= 1048576) return `${(n / 1048576).toFixed(1)}M`;
1128
+ if (n >= 1024) return `${(n / 1024).toFixed(1)}K`;
1129
+ return `${n}B`;
1130
+ }
1131
+ /**
939
1132
  * Format a number with K/M suffix for compact display.
940
1133
  */
941
1134
  function formatTokens(n) {
@@ -958,6 +1151,26 @@ function formatTokenInfo(inputTokens, outputTokens, model) {
958
1151
  return parts.join(" ");
959
1152
  }
960
1153
  /**
1154
+ * Will a per-request summary line actually reach a consumer?
1155
+ *
1156
+ * Two ways it will not:
1157
+ * - consola's level is below `info` (3), so the line is filtered out; or
1158
+ * - file-logging mode is active, whose reporter accepts only
1159
+ * fatal/error/warn and drops `info` entirely.
1160
+ *
1161
+ * Callers use this to skip work that exists ONLY to populate that line — most
1162
+ * notably the BPE tokenization on the `/chat/completions` path, which is
1163
+ * synchronous, scales with prompt size, and sits ahead of the upstream call.
1164
+ *
1165
+ * Deliberately conservative: when in doubt this returns true, so the failure
1166
+ * mode is "did unnecessary work", never "silently dropped a field the operator
1167
+ * was looking at".
1168
+ */
1169
+ function requestLogVisible() {
1170
+ if (isFileLoggingEnabled()) return false;
1171
+ return consola.level >= 3;
1172
+ }
1173
+ /**
961
1174
  * Print a single summary line for a completed request.
962
1175
  *
963
1176
  * Examples:
@@ -972,10 +1185,12 @@ function logRequest(info, model, startTime) {
972
1185
  else if (info.resolvedModel ?? info.model) parts.push(info.resolvedModel ?? info.model);
973
1186
  const tokenInfo = formatTokenInfo(info.inputTokens, info.outputTokens, model);
974
1187
  if (tokenInfo) parts.push(tokenInfo);
1188
+ if (info.bodyBytes !== void 0) parts.push(`body:${formatBytes(info.bodyBytes)}`);
975
1189
  if (info.status !== void 0) parts.push(String(info.status));
976
- const elapsed = Date.now() - startTime;
977
- const duration = elapsed >= 1e3 ? `${(elapsed / 1e3).toFixed(1)}s` : `${elapsed}ms`;
978
- parts.push(info.streaming ? `${duration} stream` : duration);
1190
+ const elapsed = info.streamMs ?? Date.now() - startTime;
1191
+ const fmt = (ms) => ms >= 1e3 ? `${(ms / 1e3).toFixed(1)}s` : `${Math.round(ms)}ms`;
1192
+ if (info.ttfbMs !== void 0) parts.push(`ttfb:${fmt(info.ttfbMs)}`);
1193
+ parts.push(info.streaming ? `${fmt(elapsed)} stream` : fmt(elapsed));
979
1194
  const line = parts.join(" ");
980
1195
  if (detectCapabilityMismatch(info, model)) consola.error(`[MISMATCH] ${line}`);
981
1196
  else consola.info(line);
@@ -1065,7 +1280,7 @@ async function handleCompletion$1(c) {
1065
1280
  logEndpointMismatch(payload.model, "/chat/completions");
1066
1281
  let inputTokens;
1067
1282
  try {
1068
- if (selectedModel) inputTokens = (await getTokenCount(payload, selectedModel)).input;
1283
+ if (selectedModel && requestLogVisible()) inputTokens = (await getTokenCount(payload, selectedModel)).input;
1069
1284
  } catch {}
1070
1285
  if (isNullish(payload.max_tokens)) {
1071
1286
  payload = {
@@ -1242,11 +1457,13 @@ completionRoutes.post("/", async (c) => {
1242
1457
  //#region src/services/copilot/create-embeddings.ts
1243
1458
  const createEmbeddings = async (payload) => {
1244
1459
  if (!state.copilotToken) throw new Error("Copilot token not found");
1245
- const response = await fetch(`${copilotBaseUrl(state)}/embeddings`, {
1460
+ const body = JSON.stringify(payload);
1461
+ const response = await fetchWithTransientRetry(() => tryRefreshAndRetry(() => fetch(`${copilotBaseUrl(state)}/embeddings`, {
1246
1462
  method: "POST",
1247
1463
  headers: copilotHeaders(state),
1248
- body: JSON.stringify(payload)
1249
- });
1464
+ body,
1465
+ ...UPSTREAM_FETCH_TIMEOUT_MS > 0 ? { signal: AbortSignal.timeout(UPSTREAM_FETCH_TIMEOUT_MS) } : {}
1466
+ }), "/embeddings"), { label: "/embeddings" });
1250
1467
  if (!response.ok) throw new HTTPError("Failed to create embeddings", response);
1251
1468
  return await response.json();
1252
1469
  };
@@ -1631,11 +1848,53 @@ function serializeAnthropicEvent(ev) {
1631
1848
  * converts a mid-stream generator throw into a terminal `event: error` frame.
1632
1849
  * On consumer cancel it invokes `onCancel` (abort the upstream fetch) and
1633
1850
  * `return()`s the generator so its `finally` tears down the upstream reader.
1851
+ *
1852
+ * `inactivityTimeoutMs` bounds how long a single `events.next()` may stall.
1853
+ * Without it a Copilot upstream that holds the socket open but stops emitting
1854
+ * hangs the caller FOREVER: `UPSTREAM_FETCH_TIMEOUT_MS` defaults to 0 (see
1855
+ * `~/lib/port`), so inactivity detection is the only stall defense, and every
1856
+ * sibling streaming path already has it (`readIteratorWithTimeout`). The clock
1857
+ * is per-read, NOT an absolute budget — a long-reasoning model legitimately
1858
+ * streams for many minutes, and only a gap between events is a stall.
1859
+ *
1860
+ * Three properties this must preserve, each pinned by a test in
1861
+ * `tests/anthropic-translate-inactivity.test.ts`:
1862
+ *
1863
+ * - NO LEAK: on timeout we take the SAME teardown path as consumer cancel
1864
+ * (`onCancel` aborts the upstream fetch, `events.return()` drives the
1865
+ * generator's `finally`). Timing out without that leaves the fetch alive
1866
+ * for the life of the process.
1867
+ * - NO CRASH: the abandoned `events.next()` promise keeps running. If the
1868
+ * upstream later errors, that rejection has no handler and would kill the
1869
+ * process under Node's `--unhandled-rejections=throw`, so it is swallowed
1870
+ * explicitly. (Same reasoning as the noop `.catch` in `stream-relay.ts`.)
1871
+ * - NO TRUNCATION: a stream that keeps producing inside the deadline is
1872
+ * never cut short, however long it runs in total.
1634
1873
  */
1635
1874
  function anthropicSseStreamFromEvents(events, opts) {
1636
1875
  const enc = new TextEncoder();
1637
1876
  let consumerCancelled = false;
1638
1877
  let finished = false;
1878
+ /** Sentinel identity — never a legitimate iterator result. */
1879
+ const STALLED = Symbol("stalled");
1880
+ /**
1881
+ * `events.next()` bounded by an inactivity deadline. Resolves to `STALLED`
1882
+ * rather than throwing so the caller can distinguish a stall (terminate with
1883
+ * a stall-specific message) from a genuine upstream error.
1884
+ */
1885
+ const nextWithTimeout = async (timeoutMs) => {
1886
+ const pending = events.next();
1887
+ let timer;
1888
+ try {
1889
+ return await Promise.race([pending, new Promise((resolve) => {
1890
+ timer = setTimeout(() => resolve(STALLED), timeoutMs);
1891
+ timer.unref?.();
1892
+ })]);
1893
+ } finally {
1894
+ if (timer !== void 0) clearTimeout(timer);
1895
+ pending.catch(() => void 0);
1896
+ }
1897
+ };
1639
1898
  const safeClose = (controller) => {
1640
1899
  try {
1641
1900
  controller.close();
@@ -1649,7 +1908,27 @@ function anthropicSseStreamFromEvents(events, opts) {
1649
1908
  }
1650
1909
  let res;
1651
1910
  try {
1652
- res = await events.next();
1911
+ const timeoutMs = opts.inactivityTimeoutMs;
1912
+ const settled = timeoutMs !== void 0 && timeoutMs > 0 ? await nextWithTimeout(timeoutMs) : await events.next();
1913
+ if (settled === STALLED) {
1914
+ finished = true;
1915
+ if (consumerCancelled) {
1916
+ safeClose(controller);
1917
+ return;
1918
+ }
1919
+ const message = `Upstream stalled: no stream activity for ${timeoutMs}ms`;
1920
+ consola.error(`Anthropic-translate stream inactivity timeout at ${opts.routePath}: ${message}`);
1921
+ opts.onCancel?.();
1922
+ events.return?.(void 0)?.catch?.(() => void 0);
1923
+ try {
1924
+ controller.enqueue(enc.encode(buildAnthropicErrorEvent("timeout_error", message)));
1925
+ } catch (enqueueError) {
1926
+ if (!isControllerClosedError(enqueueError)) consola.warn(`Could not deliver error event to consumer at ${opts.routePath}: ${enqueueError instanceof Error ? enqueueError.message : String(enqueueError)}`);
1927
+ }
1928
+ safeClose(controller);
1929
+ return;
1930
+ }
1931
+ res = settled;
1653
1932
  } catch (err) {
1654
1933
  finished = true;
1655
1934
  if (consumerCancelled) {
@@ -1690,7 +1969,7 @@ function anthropicSseStreamFromEvents(events, opts) {
1690
1969
  consumerCancelled = true;
1691
1970
  finished = true;
1692
1971
  opts.onCancel?.();
1693
- events.return?.(void 0);
1972
+ events.return?.(void 0)?.catch?.(() => void 0);
1694
1973
  }
1695
1974
  });
1696
1975
  }
@@ -3038,7 +3317,8 @@ async function handleNonClaudeResponses(c, opts) {
3038
3317
  }, opts.model, opts.startTime);
3039
3318
  const stream = anthropicSseStreamFromEvents(synthAnthropicFromResponses(result, { modelId: opts.modelId }), {
3040
3319
  routePath,
3041
- onCancel: () => aborter.abort()
3320
+ onCancel: () => aborter.abort(),
3321
+ inactivityTimeoutMs: UPSTREAM_INACTIVITY_TIMEOUT_MS
3042
3322
  });
3043
3323
  return new Response(stream, {
3044
3324
  status: 200,
@@ -3094,7 +3374,8 @@ async function handleNonClaudeChat(c, opts) {
3094
3374
  }, opts.model, opts.startTime);
3095
3375
  const stream = anthropicSseStreamFromEvents(synthAnthropicFromChat(result, { modelId: opts.modelId }), {
3096
3376
  routePath,
3097
- onCancel: () => aborter.abort()
3377
+ onCancel: () => aborter.abort(),
3378
+ inactivityTimeoutMs: UPSTREAM_INACTIVITY_TIMEOUT_MS
3098
3379
  });
3099
3380
  return new Response(stream, {
3100
3381
  status: 200,
@@ -3258,6 +3539,7 @@ async function handleCompletion(c) {
3258
3539
  const startTime = Date.now();
3259
3540
  await checkRateLimit(state);
3260
3541
  const rawBody = await c.req.text();
3542
+ recordBodySize(rawBody.length);
3261
3543
  const debugEnabled = consola.level >= 4;
3262
3544
  if (debugEnabled) consola.debug("Anthropic request body:", rawBody.slice(0, 2e3));
3263
3545
  if (process.env.GH_ROUTER_LOG_FIELDS === "1") {
@@ -3860,6 +4142,14 @@ modelRoutes.get("/", async (c) => {
3860
4142
  //#endregion
3861
4143
  //#region src/routes/responses/handler.ts
3862
4144
  const ENCODER = new TextEncoder();
4145
+ /**
4146
+ * Cap on CONSECUTIVE empty frames tolerated while discovering the first real
4147
+ * SSE event. Reset by any meaningful frame, so a legitimately slow upstream
4148
+ * that interleaves keepalives with progress is never cut off — only a source
4149
+ * that emits nothing BUT empty frames trips it. Generous by design: this is a
4150
+ * runaway backstop, not a policy knob.
4151
+ */
4152
+ const MAX_CONSECUTIVE_EMPTY_DISCOVERY_FRAMES = 1e4;
3863
4153
  function formatSSE(chunk) {
3864
4154
  const parts = [];
3865
4155
  if (chunk.event) parts.push(`event: ${chunk.event}`);
@@ -3910,18 +4200,25 @@ async function handleResponses(c) {
3910
4200
  const iterator = response[Symbol.asyncIterator]();
3911
4201
  let firstChunk;
3912
4202
  let upstreamFinished = false;
4203
+ let consecutiveEmpty = 0;
3913
4204
  while (true) {
3914
4205
  const r = await readIteratorWithTimeout(iterator, UPSTREAM_INACTIVITY_TIMEOUT_MS);
3915
4206
  if (r.done) {
3916
4207
  upstreamFinished = true;
3917
4208
  break;
3918
4209
  }
3919
- if (r.value === void 0 || r.value === null) continue;
4210
+ if (r.value === void 0 || r.value === null) {
4211
+ if (++consecutiveEmpty > MAX_CONSECUTIVE_EMPTY_DISCOVERY_FRAMES) break;
4212
+ continue;
4213
+ }
3920
4214
  if (r.value.data === "[DONE]") {
3921
4215
  upstreamFinished = true;
3922
4216
  break;
3923
4217
  }
3924
- if (!r.value.data) continue;
4218
+ if (!r.value.data) {
4219
+ if (++consecutiveEmpty > MAX_CONSECUTIVE_EMPTY_DISCOVERY_FRAMES) break;
4220
+ continue;
4221
+ }
3925
4222
  firstChunk = r.value;
3926
4223
  break;
3927
4224
  }
@@ -4857,6 +5154,6 @@ function getCodexEnvVars(serverUrl) {
4857
5154
  return vars;
4858
5155
  }
4859
5156
  //#endregion
4860
- export { sharedServerArgs as a, listModelsForEndpoint as c, updateClaude as d, setupAndServe as i, runSelfUpdate as l, getCodexEnvVars as n, startKeepAwake as o, parseSharedArgs as r, stopKeepAwake as s, getClaudeCodeEnvVars as t, checkClaudeVersion as u };
5157
+ export { sharedServerArgs as a, listModelsForEndpoint as c, checkClaudeVersion as d, updateClaude as f, setupAndServe as i, enableFileLogging as l, getCodexEnvVars as n, startKeepAwake as o, parseSharedArgs as r, stopKeepAwake as s, getClaudeCodeEnvVars as t, runSelfUpdate as u };
4861
5158
 
4862
- //# sourceMappingURL=server-setup-CJzxzYks.js.map
5159
+ //# sourceMappingURL=server-setup-BM9Smfek.js.map