github-router 0.3.261 → 0.3.269
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.
- package/dist/{attribution-settings-Bl0HHJQF.js → attribution-settings-B1txv_Lk.js} +4 -4
- package/dist/{attribution-settings-Bl0HHJQF.js.map → attribution-settings-B1txv_Lk.js.map} +1 -1
- package/dist/{auth-I8k2OOvA.js → auth-r-my7efo.js} +3 -3
- package/dist/{auth-I8k2OOvA.js.map → auth-r-my7efo.js.map} +1 -1
- package/dist/browser-bridge/index.js +67 -27
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/{check-usage-SOn7-cWc.js → check-usage-CZr2ZkG0.js} +4 -4
- package/dist/{check-usage-SOn7-cWc.js.map → check-usage-CZr2ZkG0.js.map} +1 -1
- package/dist/{claude-CEaI47j7.js → claude-BjNY0J6e.js} +15 -15
- package/dist/{claude-CEaI47j7.js.map → claude-BjNY0J6e.js.map} +1 -1
- package/dist/{codex-C6Eyz3EV.js → codex-CoRWpkHL.js} +5 -6
- package/dist/{codex-C6Eyz3EV.js.map → codex-CoRWpkHL.js.map} +1 -1
- package/dist/{debug-vshv4nIv.js → debug-CuuxCk_J.js} +2 -2
- package/dist/{debug-vshv4nIv.js.map → debug-CuuxCk_J.js.map} +1 -1
- package/dist/engine-DpVC7HRL.js +2 -0
- package/dist/{exec-DTO2oin0.js → exec-y8C_MU8A.js} +89 -18
- package/dist/exec-y8C_MU8A.js.map +1 -0
- package/dist/{gate-discovery-BnPhH_AC.js → gate-discovery-D-7oua8b.js} +6 -6
- package/dist/{gate-discovery-BnPhH_AC.js.map → gate-discovery-D-7oua8b.js.map} +1 -1
- package/dist/{get-copilot-usage-CYpAsYrM.js → get-copilot-usage-CFEpeA7E.js} +2 -2
- package/dist/{get-copilot-usage-CYpAsYrM.js.map → get-copilot-usage-CFEpeA7E.js.map} +1 -1
- package/dist/{internal-artifact-open-CZGO831M.js → internal-artifact-open-CDqPfegE.js} +2 -2
- package/dist/{internal-artifact-open-CZGO831M.js.map → internal-artifact-open-CDqPfegE.js.map} +1 -1
- package/dist/{internal-plan-review-6gkcOAjT.js → internal-plan-review-CH9qTHoZ.js} +3 -3
- package/dist/{internal-plan-review-6gkcOAjT.js.map → internal-plan-review-CH9qTHoZ.js.map} +1 -1
- package/dist/{internal-prompt-submit-CSJR24-y.js → internal-prompt-submit-Bve1s4z_.js} +5 -5
- package/dist/{internal-prompt-submit-CSJR24-y.js.map → internal-prompt-submit-Bve1s4z_.js.map} +1 -1
- package/dist/{internal-session-bind-DtSBTkQe.js → internal-session-bind-Cc8bMXHK.js} +2 -2
- package/dist/{internal-session-bind-DtSBTkQe.js.map → internal-session-bind-Cc8bMXHK.js.map} +1 -1
- package/dist/{internal-stop-hook-CiflCJO-.js → internal-stop-hook-Db5o9RcZ.js} +6 -6
- package/dist/{internal-stop-hook-CiflCJO-.js.map → internal-stop-hook-Db5o9RcZ.js.map} +1 -1
- package/dist/{internal-stop-review-_1Mqz7eT.js → internal-stop-review-CWE7_QVv.js} +2 -2
- package/dist/{internal-stop-review-_1Mqz7eT.js.map → internal-stop-review-CWE7_QVv.js.map} +1 -1
- package/dist/{lifecycle-DjC9tC1A.js → lifecycle-0XrazT05.js} +2 -2
- package/dist/{lifecycle-DjC9tC1A.js.map → lifecycle-0XrazT05.js.map} +1 -1
- package/dist/lifecycle-2M0IThRr.js +2 -0
- package/dist/{lifecycle-Bd1Fyf4B.js → lifecycle-AgNOtFRv.js} +3 -3
- package/dist/{lifecycle-Bd1Fyf4B.js.map → lifecycle-AgNOtFRv.js.map} +1 -1
- package/dist/lifecycle-C8kFsAj-.js +2 -0
- package/dist/main.js +14 -14
- package/dist/{models-uQgsKNNC.js → models-CJLt4XjP.js} +35 -30
- package/dist/models-CJLt4XjP.js.map +1 -0
- package/dist/{orchestration-BiZviyam.js → orchestration-B_Q3ncXI.js} +3 -3
- package/dist/{orchestration-BiZviyam.js.map → orchestration-B_Q3ncXI.js.map} +1 -1
- package/dist/paths-BbHtQAOs.js +2 -0
- package/dist/{paths-BXrbboAS.js → paths-Bg1_DWhi.js} +3 -3
- package/dist/{paths-BXrbboAS.js.map → paths-Bg1_DWhi.js.map} +1 -1
- package/dist/{peer-mcp-personas-V4AXhm9B.js → peer-mcp-personas-DSbBlgFt.js} +119 -45
- package/dist/peer-mcp-personas-DSbBlgFt.js.map +1 -0
- package/dist/{plan-review-hook-Bo4y1r3j.js → plan-review-hook-BKOcW5s4.js} +3 -3
- package/dist/{plan-review-hook-Bo4y1r3j.js.map → plan-review-hook-BKOcW5s4.js.map} +1 -1
- package/dist/{prompt-submit-hook-BFRFLSk5.js → prompt-submit-hook-F7Zih_au.js} +2 -2
- package/dist/{prompt-submit-hook-BFRFLSk5.js.map → prompt-submit-hook-F7Zih_au.js.map} +1 -1
- package/dist/{provision-BLwsOFCI.js → provision-CA-xwnFu.js} +5 -5
- package/dist/{provision-BLwsOFCI.js.map → provision-CA-xwnFu.js.map} +1 -1
- package/dist/{serve-De-kqWXM.js → serve-DHny4ewF.js} +20 -15
- package/dist/{serve-De-kqWXM.js.map → serve-DHny4ewF.js.map} +1 -1
- package/dist/{server-setup-BeeT8m-w.js → server-setup-Dd293T1O.js} +479 -75
- package/dist/server-setup-Dd293T1O.js.map +1 -0
- package/dist/{start-DEC0M8UH.js → start-P2rmHBkL.js} +3 -3
- package/dist/{start-DEC0M8UH.js.map → start-P2rmHBkL.js.map} +1 -1
- package/dist/{stop-gate-hook-DoYD9Wt8.js → stop-gate-hook-BqpumJtl.js} +12 -7
- package/dist/{stop-gate-hook-DoYD9Wt8.js.map → stop-gate-hook-BqpumJtl.js.map} +1 -1
- package/dist/{stop-gate-policy-CoMKWSsS.js → stop-gate-policy-C0XOBZhB.js} +3 -3
- package/dist/{stop-gate-policy-CoMKWSsS.js.map → stop-gate-policy-C0XOBZhB.js.map} +1 -1
- package/dist/{token-DWknHe9M.js → token-C_q95GF7.js} +124 -14
- package/dist/{token-DWknHe9M.js.map → token-C_q95GF7.js.map} +1 -1
- package/package.json +2 -2
- package/dist/engine-B4Ohhv5i.js +0 -2
- package/dist/exec-DTO2oin0.js.map +0 -1
- package/dist/file-log-reporter-vI8qCFNM.js +0 -158
- package/dist/file-log-reporter-vI8qCFNM.js.map +0 -1
- package/dist/lifecycle-CRTWEbiP.js +0 -2
- package/dist/lifecycle-DUPXAOQS.js +0 -2
- package/dist/models-uQgsKNNC.js.map +0 -1
- package/dist/paths-Dz1WyRJG.js +0 -2
- package/dist/peer-mcp-personas-V4AXhm9B.js.map +0 -1
- package/dist/server-setup-BeeT8m-w.js.map +0 -1
|
@@ -1,12 +1,12 @@
|
|
|
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-
|
|
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-DSbBlgFt.js";
|
|
2
2
|
import { t as getPackageVersion } from "./version-_Q1WpsQp.js";
|
|
3
|
-
import { i as ensurePaths, t as PATHS } from "./paths-
|
|
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-
|
|
5
|
-
import { t as getCopilotUsage } from "./get-copilot-usage-
|
|
6
|
-
import { a as resolveExecutable, n as killManagedTree, o as runCommandCapture, r as parseBoolEnv, s as runCommandVoid } from "./exec-
|
|
3
|
+
import { i as ensurePaths, t as PATHS } from "./paths-Bg1_DWhi.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-C_q95GF7.js";
|
|
5
|
+
import { t as getCopilotUsage } from "./get-copilot-usage-CFEpeA7E.js";
|
|
6
|
+
import { a as resolveExecutable, n as killManagedTree, o as runCommandCapture, r as parseBoolEnv, s as runCommandVoid } from "./exec-y8C_MU8A.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",
|
|
@@ -880,61 +1045,108 @@ function initProxyFromEnv() {
|
|
|
880
1045
|
}
|
|
881
1046
|
}
|
|
882
1047
|
//#endregion
|
|
883
|
-
//#region
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1048
|
+
//#region src/lib/request-log.ts
|
|
1049
|
+
/**
|
|
1050
|
+
* Rolling request-body size distribution.
|
|
1051
|
+
*
|
|
1052
|
+
* Motivation: the cost of the `/v1/messages` prologue (substring guards, JSON
|
|
1053
|
+
* parses, re-serialization) scales with body size, but the proxy read
|
|
1054
|
+
* `content-length` and never recorded it — so there was no way to tell whether
|
|
1055
|
+
* a prologue optimization was worth anything. Benchmarks at 4.5 MiB mean
|
|
1056
|
+
* nothing if the real p50 is 40 KiB.
|
|
1057
|
+
*
|
|
1058
|
+
* Bounded by construction: a fixed-size ring, not a growing array.
|
|
1059
|
+
*/
|
|
1060
|
+
const BODY_SIZE_RING = 512;
|
|
1061
|
+
const bodySizes = new Float64Array(BODY_SIZE_RING);
|
|
1062
|
+
let bodySizeCount = 0;
|
|
1063
|
+
let bodySizeIdx = 0;
|
|
1064
|
+
/** Record one request body size (bytes). Cheap; safe to call per request. */
|
|
1065
|
+
function recordBodySize(bytes) {
|
|
1066
|
+
if (!Number.isFinite(bytes) || bytes < 0) return;
|
|
1067
|
+
bodySizes[bodySizeIdx] = bytes;
|
|
1068
|
+
bodySizeIdx = (bodySizeIdx + 1) % BODY_SIZE_RING;
|
|
1069
|
+
if (bodySizeCount < BODY_SIZE_RING) bodySizeCount++;
|
|
1070
|
+
}
|
|
1071
|
+
/** Percentile snapshot of observed body sizes, or undefined if none seen. */
|
|
1072
|
+
function bodySizeStats() {
|
|
1073
|
+
if (bodySizeCount === 0) return void 0;
|
|
1074
|
+
const s = Array.from(bodySizes.slice(0, bodySizeCount)).sort((a, b) => a - b);
|
|
1075
|
+
const at = (q) => s[Math.min(s.length - 1, Math.floor(s.length * q))];
|
|
1076
|
+
return {
|
|
1077
|
+
count: bodySizeCount,
|
|
1078
|
+
p50: at(.5),
|
|
1079
|
+
p95: at(.95),
|
|
1080
|
+
p99: at(.99),
|
|
1081
|
+
max: s[s.length - 1]
|
|
1082
|
+
};
|
|
910
1083
|
}
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
const
|
|
925
|
-
if (!
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1084
|
+
/**
|
|
1085
|
+
* Print the observed body-size distribution, if any requests were seen.
|
|
1086
|
+
*
|
|
1087
|
+
* Without this the ring was WRITE-ONLY: `bodySizeStats()` had no caller
|
|
1088
|
+
* anywhere in `src/`, so the one number its own doc comment says the decision
|
|
1089
|
+
* needs ("benchmarks at 4.5 MiB mean nothing if the real p50 is 40 KiB") was
|
|
1090
|
+
* collected every request and readable by nobody.
|
|
1091
|
+
*
|
|
1092
|
+
* At `consola.info` because it is a once-per-session line answering a question
|
|
1093
|
+
* an operator or contributor actually asks; anything noisier would be a reason
|
|
1094
|
+
* to switch it off.
|
|
1095
|
+
*/
|
|
1096
|
+
function logBodySizeStats() {
|
|
1097
|
+
const stats = bodySizeStats();
|
|
1098
|
+
if (!stats) return void 0;
|
|
1099
|
+
const line = `request body sizes (n=${stats.count}): p50 ${formatBytes(stats.p50)}, p95 ${formatBytes(stats.p95)}, p99 ${formatBytes(stats.p99)}, max ${formatBytes(stats.max)}`;
|
|
1100
|
+
consola.info(line);
|
|
1101
|
+
return line;
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Report the distribution once, on the way out.
|
|
1105
|
+
*
|
|
1106
|
+
* Self-registered rather than wired into each subcommand's shutdown chain
|
|
1107
|
+
* because `start` has NO `launchChild`/`onShutdown` to hang it on (see the
|
|
1108
|
+
* comment at its `startKeepAwake()` call), and a stat that only appeared under
|
|
1109
|
+
* `claude` would answer the question for one of three entry points. Same
|
|
1110
|
+
* pattern, and same reasoning, as `~/lib/keep-awake`'s own reaper.
|
|
1111
|
+
*
|
|
1112
|
+
* **Windows caveat, measured rather than assumed.** This reports on a clean
|
|
1113
|
+
* exit and on a signal the runtime actually delivers to JS (Ctrl-C in an
|
|
1114
|
+
* interactive console). It does NOT report when the process is killed by
|
|
1115
|
+
* another process on Windows: `child.kill(...)` and `taskkill` terminate
|
|
1116
|
+
* abruptly via `TerminateProcess`, dispatching no JS signal event, so NO
|
|
1117
|
+
* handler runs — not this one, and not `process.on("exit")` either. Verified
|
|
1118
|
+
* on a real win32 host: a child registering both handlers and killed by its
|
|
1119
|
+
* parent produced no output and exited with a null code.
|
|
1120
|
+
*
|
|
1121
|
+
* That is acceptable for a diagnostic — it is a measurement aid, not a
|
|
1122
|
+
* durability guarantee, and the interactive case it exists to serve (run a
|
|
1123
|
+
* session, Ctrl-C, read the distribution) does work. It is NOT acceptable for
|
|
1124
|
+
* anything durable: do not move credential, ledger, or cleanup work onto this
|
|
1125
|
+
* hook. The existing teardown paths that must survive a hard kill are built
|
|
1126
|
+
* differently on purpose (see `~/lib/keep-awake`, which relies on the OS
|
|
1127
|
+
* releasing a thread-scoped assertion rather than on a handler running, and
|
|
1128
|
+
* `~/lib/process-guard`, which uses a separate reaper process).
|
|
1129
|
+
*
|
|
1130
|
+
* Idempotent: `exit` can follow a signal handler, and both call this.
|
|
1131
|
+
*/
|
|
1132
|
+
let statsReported = false;
|
|
1133
|
+
function reportOnce() {
|
|
1134
|
+
if (statsReported) return;
|
|
1135
|
+
statsReported = true;
|
|
1136
|
+
logBodySizeStats();
|
|
1137
|
+
}
|
|
1138
|
+
let exitHooksInstalled = false;
|
|
1139
|
+
function installBodySizeStatsExitHook() {
|
|
1140
|
+
if (exitHooksInstalled) return;
|
|
1141
|
+
exitHooksInstalled = true;
|
|
1142
|
+
process.once("exit", reportOnce);
|
|
1143
|
+
}
|
|
1144
|
+
/** Format a byte count compactly (1.2M / 15.3K / 900B). */
|
|
1145
|
+
function formatBytes(n) {
|
|
1146
|
+
if (n >= 1048576) return `${(n / 1048576).toFixed(1)}M`;
|
|
1147
|
+
if (n >= 1024) return `${(n / 1024).toFixed(1)}K`;
|
|
1148
|
+
return `${n}B`;
|
|
935
1149
|
}
|
|
936
|
-
//#endregion
|
|
937
|
-
//#region src/lib/request-log.ts
|
|
938
1150
|
/**
|
|
939
1151
|
* Format a number with K/M suffix for compact display.
|
|
940
1152
|
*/
|
|
@@ -958,6 +1170,26 @@ function formatTokenInfo(inputTokens, outputTokens, model) {
|
|
|
958
1170
|
return parts.join(" ");
|
|
959
1171
|
}
|
|
960
1172
|
/**
|
|
1173
|
+
* Will a per-request summary line actually reach a consumer?
|
|
1174
|
+
*
|
|
1175
|
+
* Two ways it will not:
|
|
1176
|
+
* - consola's level is below `info` (3), so the line is filtered out; or
|
|
1177
|
+
* - file-logging mode is active, whose reporter accepts only
|
|
1178
|
+
* fatal/error/warn and drops `info` entirely.
|
|
1179
|
+
*
|
|
1180
|
+
* Callers use this to skip work that exists ONLY to populate that line — most
|
|
1181
|
+
* notably the BPE tokenization on the `/chat/completions` path, which is
|
|
1182
|
+
* synchronous, scales with prompt size, and sits ahead of the upstream call.
|
|
1183
|
+
*
|
|
1184
|
+
* Deliberately conservative: when in doubt this returns true, so the failure
|
|
1185
|
+
* mode is "did unnecessary work", never "silently dropped a field the operator
|
|
1186
|
+
* was looking at".
|
|
1187
|
+
*/
|
|
1188
|
+
function requestLogVisible() {
|
|
1189
|
+
if (isFileLoggingEnabled()) return false;
|
|
1190
|
+
return consola.level >= 3;
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
961
1193
|
* Print a single summary line for a completed request.
|
|
962
1194
|
*
|
|
963
1195
|
* Examples:
|
|
@@ -972,10 +1204,12 @@ function logRequest(info, model, startTime) {
|
|
|
972
1204
|
else if (info.resolvedModel ?? info.model) parts.push(info.resolvedModel ?? info.model);
|
|
973
1205
|
const tokenInfo = formatTokenInfo(info.inputTokens, info.outputTokens, model);
|
|
974
1206
|
if (tokenInfo) parts.push(tokenInfo);
|
|
1207
|
+
if (info.bodyBytes !== void 0) parts.push(`body:${formatBytes(info.bodyBytes)}`);
|
|
975
1208
|
if (info.status !== void 0) parts.push(String(info.status));
|
|
976
|
-
const elapsed = Date.now() - startTime;
|
|
977
|
-
const
|
|
978
|
-
parts.push(info.
|
|
1209
|
+
const elapsed = info.streamMs ?? Date.now() - startTime;
|
|
1210
|
+
const fmt = (ms) => ms >= 1e3 ? `${(ms / 1e3).toFixed(1)}s` : `${Math.round(ms)}ms`;
|
|
1211
|
+
if (info.ttfbMs !== void 0) parts.push(`ttfb:${fmt(info.ttfbMs)}`);
|
|
1212
|
+
parts.push(info.streaming ? `${fmt(elapsed)} stream` : fmt(elapsed));
|
|
979
1213
|
const line = parts.join(" ");
|
|
980
1214
|
if (detectCapabilityMismatch(info, model)) consola.error(`[MISMATCH] ${line}`);
|
|
981
1215
|
else consola.info(line);
|
|
@@ -1041,6 +1275,60 @@ function collectToolFieldKeys(body) {
|
|
|
1041
1275
|
return [...seen].sort();
|
|
1042
1276
|
}
|
|
1043
1277
|
//#endregion
|
|
1278
|
+
//#region package.json
|
|
1279
|
+
var name = "github-router";
|
|
1280
|
+
var version = "0.3.269";
|
|
1281
|
+
//#endregion
|
|
1282
|
+
//#region src/lib/approval.ts
|
|
1283
|
+
const awaitApproval = async () => {
|
|
1284
|
+
if (!await consola.prompt(`Accept incoming request?`, { type: "confirm" })) throw new HTTPError("Request rejected by user", Response.json({ message: "Request rejected by user" }, { status: 403 }));
|
|
1285
|
+
};
|
|
1286
|
+
//#endregion
|
|
1287
|
+
//#region src/lib/rate-limit.ts
|
|
1288
|
+
const RATE_LIMIT_QUEUE_TIMEOUT_MS = 5e3;
|
|
1289
|
+
let rateLimitChain = Promise.resolve();
|
|
1290
|
+
async function checkRateLimit(state) {
|
|
1291
|
+
if (state.rateLimitSeconds === void 0) return;
|
|
1292
|
+
const ticket = { aborted: false };
|
|
1293
|
+
const myTurn = rateLimitChain.then(() => doCheck(state, ticket));
|
|
1294
|
+
rateLimitChain = myTurn.catch(() => {});
|
|
1295
|
+
return Promise.race([myTurn, sleep(RATE_LIMIT_QUEUE_TIMEOUT_MS).then(() => {
|
|
1296
|
+
ticket.aborted = true;
|
|
1297
|
+
throw new HTTPError("Rate limit queue wait exceeded", Response.json({
|
|
1298
|
+
type: "error",
|
|
1299
|
+
error: {
|
|
1300
|
+
type: "rate_limit_error",
|
|
1301
|
+
message: `Rate limit queue exceeded ${RATE_LIMIT_QUEUE_TIMEOUT_MS}ms; try again`
|
|
1302
|
+
}
|
|
1303
|
+
}, { status: 429 }));
|
|
1304
|
+
})]);
|
|
1305
|
+
}
|
|
1306
|
+
async function doCheck(state, ticket) {
|
|
1307
|
+
if (state.rateLimitSeconds === void 0) return;
|
|
1308
|
+
if (ticket.aborted) return;
|
|
1309
|
+
const now = Date.now();
|
|
1310
|
+
if (!state.lastRequestTimestamp) {
|
|
1311
|
+
state.lastRequestTimestamp = now;
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
const elapsedSeconds = (now - state.lastRequestTimestamp) / 1e3;
|
|
1315
|
+
if (elapsedSeconds > state.rateLimitSeconds) {
|
|
1316
|
+
state.lastRequestTimestamp = now;
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
const waitTimeSeconds = Math.ceil(state.rateLimitSeconds - elapsedSeconds);
|
|
1320
|
+
if (!state.rateLimitWait) {
|
|
1321
|
+
consola.warn(`Rate limit exceeded. Need to wait ${waitTimeSeconds} more seconds.`);
|
|
1322
|
+
throw new HTTPError("Rate limit exceeded", Response.json({ message: "Rate limit exceeded" }, { status: 429 }));
|
|
1323
|
+
}
|
|
1324
|
+
const waitTimeMs = waitTimeSeconds * 1e3;
|
|
1325
|
+
consola.warn(`Rate limit reached. Waiting ${waitTimeSeconds} seconds before proceeding...`);
|
|
1326
|
+
await sleep(waitTimeMs);
|
|
1327
|
+
if (ticket.aborted) return;
|
|
1328
|
+
state.lastRequestTimestamp = Date.now();
|
|
1329
|
+
consola.info("Rate limit wait completed, proceeding with request");
|
|
1330
|
+
}
|
|
1331
|
+
//#endregion
|
|
1044
1332
|
//#region src/routes/chat-completions/handler.ts
|
|
1045
1333
|
const ENCODER$1 = new TextEncoder();
|
|
1046
1334
|
function formatSSE$1(chunk) {
|
|
@@ -1065,7 +1353,7 @@ async function handleCompletion$1(c) {
|
|
|
1065
1353
|
logEndpointMismatch(payload.model, "/chat/completions");
|
|
1066
1354
|
let inputTokens;
|
|
1067
1355
|
try {
|
|
1068
|
-
if (selectedModel) inputTokens = (await getTokenCount(payload, selectedModel)).input;
|
|
1356
|
+
if (selectedModel && requestLogVisible()) inputTokens = (await getTokenCount(payload, selectedModel)).input;
|
|
1069
1357
|
} catch {}
|
|
1070
1358
|
if (isNullish(payload.max_tokens)) {
|
|
1071
1359
|
payload = {
|
|
@@ -1242,11 +1530,13 @@ completionRoutes.post("/", async (c) => {
|
|
|
1242
1530
|
//#region src/services/copilot/create-embeddings.ts
|
|
1243
1531
|
const createEmbeddings = async (payload) => {
|
|
1244
1532
|
if (!state.copilotToken) throw new Error("Copilot token not found");
|
|
1245
|
-
const
|
|
1533
|
+
const body = JSON.stringify(payload);
|
|
1534
|
+
const response = await fetchWithTransientRetry(() => tryRefreshAndRetry(() => fetch(`${copilotBaseUrl(state)}/embeddings`, {
|
|
1246
1535
|
method: "POST",
|
|
1247
1536
|
headers: copilotHeaders(state),
|
|
1248
|
-
body
|
|
1249
|
-
|
|
1537
|
+
body,
|
|
1538
|
+
...UPSTREAM_FETCH_TIMEOUT_MS > 0 ? { signal: AbortSignal.timeout(UPSTREAM_FETCH_TIMEOUT_MS) } : {}
|
|
1539
|
+
}), "/embeddings"), { label: "/embeddings" });
|
|
1250
1540
|
if (!response.ok) throw new HTTPError("Failed to create embeddings", response);
|
|
1251
1541
|
return await response.json();
|
|
1252
1542
|
};
|
|
@@ -1631,16 +1921,92 @@ function serializeAnthropicEvent(ev) {
|
|
|
1631
1921
|
* converts a mid-stream generator throw into a terminal `event: error` frame.
|
|
1632
1922
|
* On consumer cancel it invokes `onCancel` (abort the upstream fetch) and
|
|
1633
1923
|
* `return()`s the generator so its `finally` tears down the upstream reader.
|
|
1924
|
+
*
|
|
1925
|
+
* `inactivityTimeoutMs` bounds how long a single `events.next()` may stall.
|
|
1926
|
+
* Without it a Copilot upstream that holds the socket open but stops emitting
|
|
1927
|
+
* hangs the caller FOREVER: `UPSTREAM_FETCH_TIMEOUT_MS` defaults to 0 (see
|
|
1928
|
+
* `~/lib/port`), so inactivity detection is the only stall defense, and every
|
|
1929
|
+
* sibling streaming path already has it (`readIteratorWithTimeout`). The clock
|
|
1930
|
+
* is per-read, NOT an absolute budget — a long-reasoning model legitimately
|
|
1931
|
+
* streams for many minutes, and only a gap between events is a stall.
|
|
1932
|
+
*
|
|
1933
|
+
* Three properties this must preserve, each pinned by a test in
|
|
1934
|
+
* `tests/anthropic-translate-inactivity.test.ts`:
|
|
1935
|
+
*
|
|
1936
|
+
* - NO LEAK: on timeout we take the SAME teardown path as consumer cancel
|
|
1937
|
+
* (`onCancel` aborts the upstream fetch, `events.return()` drives the
|
|
1938
|
+
* generator's `finally`). Timing out without that leaves the fetch alive
|
|
1939
|
+
* for the life of the process.
|
|
1940
|
+
* - NO CRASH: the abandoned `events.next()` promise keeps running. If the
|
|
1941
|
+
* upstream later errors, that rejection has no handler and would kill the
|
|
1942
|
+
* process under Node's `--unhandled-rejections=throw`, so it is swallowed
|
|
1943
|
+
* explicitly. (Same reasoning as the noop `.catch` in `stream-relay.ts`.)
|
|
1944
|
+
* - NO TRUNCATION: a stream that keeps producing inside the deadline is
|
|
1945
|
+
* never cut short, however long it runs in total.
|
|
1634
1946
|
*/
|
|
1635
1947
|
function anthropicSseStreamFromEvents(events, opts) {
|
|
1636
1948
|
const enc = new TextEncoder();
|
|
1637
1949
|
let consumerCancelled = false;
|
|
1638
1950
|
let finished = false;
|
|
1951
|
+
/** Sentinel identity — never a legitimate iterator result. */
|
|
1952
|
+
const STALLED = Symbol("stalled");
|
|
1953
|
+
/**
|
|
1954
|
+
* `events.next()` bounded by an inactivity deadline. Resolves to `STALLED`
|
|
1955
|
+
* rather than throwing so the caller can distinguish a stall (terminate with
|
|
1956
|
+
* a stall-specific message) from a genuine upstream error.
|
|
1957
|
+
*/
|
|
1958
|
+
const nextWithTimeout = async (timeoutMs) => {
|
|
1959
|
+
const pending = events.next();
|
|
1960
|
+
let timer;
|
|
1961
|
+
try {
|
|
1962
|
+
return await Promise.race([pending, new Promise((resolve) => {
|
|
1963
|
+
timer = setTimeout(() => resolve(STALLED), timeoutMs);
|
|
1964
|
+
timer.unref?.();
|
|
1965
|
+
})]);
|
|
1966
|
+
} finally {
|
|
1967
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
1968
|
+
pending.catch(() => void 0);
|
|
1969
|
+
}
|
|
1970
|
+
};
|
|
1639
1971
|
const safeClose = (controller) => {
|
|
1640
1972
|
try {
|
|
1641
1973
|
controller.close();
|
|
1642
1974
|
} catch {}
|
|
1643
1975
|
};
|
|
1976
|
+
/**
|
|
1977
|
+
* Tear down the upstream, exactly once, on a PREMATURE end of stream.
|
|
1978
|
+
*
|
|
1979
|
+
* Called from the three places a stream can end early — the stall branch,
|
|
1980
|
+
* the error branch, and consumer `cancel()` — because all three leave the
|
|
1981
|
+
* upstream fetch running and the generator suspended. Both halves matter:
|
|
1982
|
+
* `onCancel` aborts the fetch, and `return()` drives the generator's
|
|
1983
|
+
* `finally`; `return()` alone cannot release a generator parked on a
|
|
1984
|
+
* never-settling await, since it queues behind that await.
|
|
1985
|
+
*
|
|
1986
|
+
* Two things this is deliberately NOT:
|
|
1987
|
+
*
|
|
1988
|
+
* - **Not a `finally` around `pull`.** `pull` re-enters per chunk, so a
|
|
1989
|
+
* `finally` there would fire on every successful chunk.
|
|
1990
|
+
* - **Not called on normal completion** (`res.done`). Aborting an
|
|
1991
|
+
* AbortController whose fetch has already completed DESTROYS the socket
|
|
1992
|
+
* instead of returning it to the keep-alive pool, so every successful
|
|
1993
|
+
* turn would pay a fresh TLS handshake. Premature-only is the whole
|
|
1994
|
+
* correctness condition, and `tornDown` only makes it idempotent — it
|
|
1995
|
+
* does not make an unconditional call safe.
|
|
1996
|
+
*/
|
|
1997
|
+
let tornDown = false;
|
|
1998
|
+
const teardownUpstream = () => {
|
|
1999
|
+
if (tornDown) return;
|
|
2000
|
+
tornDown = true;
|
|
2001
|
+
try {
|
|
2002
|
+
opts.onCancel?.();
|
|
2003
|
+
} catch (err) {
|
|
2004
|
+
consola.warn(`Anthropic-translate onCancel threw during teardown at ${opts.routePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2005
|
+
}
|
|
2006
|
+
try {
|
|
2007
|
+
events.return?.(void 0)?.catch?.(() => void 0);
|
|
2008
|
+
} catch {}
|
|
2009
|
+
};
|
|
1644
2010
|
return new ReadableStream({
|
|
1645
2011
|
async pull(controller) {
|
|
1646
2012
|
if (consumerCancelled || finished) {
|
|
@@ -1649,7 +2015,26 @@ function anthropicSseStreamFromEvents(events, opts) {
|
|
|
1649
2015
|
}
|
|
1650
2016
|
let res;
|
|
1651
2017
|
try {
|
|
1652
|
-
|
|
2018
|
+
const timeoutMs = opts.inactivityTimeoutMs;
|
|
2019
|
+
const settled = timeoutMs !== void 0 && timeoutMs > 0 ? await nextWithTimeout(timeoutMs) : await events.next();
|
|
2020
|
+
if (settled === STALLED) {
|
|
2021
|
+
finished = true;
|
|
2022
|
+
if (consumerCancelled) {
|
|
2023
|
+
safeClose(controller);
|
|
2024
|
+
return;
|
|
2025
|
+
}
|
|
2026
|
+
const message = `Upstream stalled: no stream activity for ${timeoutMs}ms`;
|
|
2027
|
+
consola.error(`Anthropic-translate stream inactivity timeout at ${opts.routePath}: ${message}`);
|
|
2028
|
+
teardownUpstream();
|
|
2029
|
+
try {
|
|
2030
|
+
controller.enqueue(enc.encode(buildAnthropicErrorEvent("timeout_error", message)));
|
|
2031
|
+
} catch (enqueueError) {
|
|
2032
|
+
if (!isControllerClosedError(enqueueError)) consola.warn(`Could not deliver error event to consumer at ${opts.routePath}: ${enqueueError instanceof Error ? enqueueError.message : String(enqueueError)}`);
|
|
2033
|
+
}
|
|
2034
|
+
safeClose(controller);
|
|
2035
|
+
return;
|
|
2036
|
+
}
|
|
2037
|
+
res = settled;
|
|
1653
2038
|
} catch (err) {
|
|
1654
2039
|
finished = true;
|
|
1655
2040
|
if (consumerCancelled) {
|
|
@@ -1659,6 +2044,7 @@ function anthropicSseStreamFromEvents(events, opts) {
|
|
|
1659
2044
|
const name = err instanceof Error ? err.name : "Error";
|
|
1660
2045
|
const message = err instanceof Error ? err.message : String(err);
|
|
1661
2046
|
consola.error(`Anthropic-translate stream interrupted at ${opts.routePath}: ${name}: ${JSON.stringify(message)}`);
|
|
2047
|
+
teardownUpstream();
|
|
1662
2048
|
try {
|
|
1663
2049
|
controller.enqueue(enc.encode(buildAnthropicErrorEvent(name, message)));
|
|
1664
2050
|
} catch (enqueueError) {
|
|
@@ -1689,8 +2075,7 @@ function anthropicSseStreamFromEvents(events, opts) {
|
|
|
1689
2075
|
cancel() {
|
|
1690
2076
|
consumerCancelled = true;
|
|
1691
2077
|
finished = true;
|
|
1692
|
-
|
|
1693
|
-
events.return?.(void 0);
|
|
2078
|
+
teardownUpstream();
|
|
1694
2079
|
}
|
|
1695
2080
|
});
|
|
1696
2081
|
}
|
|
@@ -3038,7 +3423,8 @@ async function handleNonClaudeResponses(c, opts) {
|
|
|
3038
3423
|
}, opts.model, opts.startTime);
|
|
3039
3424
|
const stream = anthropicSseStreamFromEvents(synthAnthropicFromResponses(result, { modelId: opts.modelId }), {
|
|
3040
3425
|
routePath,
|
|
3041
|
-
onCancel: () => aborter.abort()
|
|
3426
|
+
onCancel: () => aborter.abort(),
|
|
3427
|
+
inactivityTimeoutMs: UPSTREAM_INACTIVITY_TIMEOUT_MS
|
|
3042
3428
|
});
|
|
3043
3429
|
return new Response(stream, {
|
|
3044
3430
|
status: 200,
|
|
@@ -3094,7 +3480,8 @@ async function handleNonClaudeChat(c, opts) {
|
|
|
3094
3480
|
}, opts.model, opts.startTime);
|
|
3095
3481
|
const stream = anthropicSseStreamFromEvents(synthAnthropicFromChat(result, { modelId: opts.modelId }), {
|
|
3096
3482
|
routePath,
|
|
3097
|
-
onCancel: () => aborter.abort()
|
|
3483
|
+
onCancel: () => aborter.abort(),
|
|
3484
|
+
inactivityTimeoutMs: UPSTREAM_INACTIVITY_TIMEOUT_MS
|
|
3098
3485
|
});
|
|
3099
3486
|
return new Response(stream, {
|
|
3100
3487
|
status: 200,
|
|
@@ -3258,6 +3645,7 @@ async function handleCompletion(c) {
|
|
|
3258
3645
|
const startTime = Date.now();
|
|
3259
3646
|
await checkRateLimit(state);
|
|
3260
3647
|
const rawBody = await c.req.text();
|
|
3648
|
+
recordBodySize(rawBody.length);
|
|
3261
3649
|
const debugEnabled = consola.level >= 4;
|
|
3262
3650
|
if (debugEnabled) consola.debug("Anthropic request body:", rawBody.slice(0, 2e3));
|
|
3263
3651
|
if (process.env.GH_ROUTER_LOG_FIELDS === "1") {
|
|
@@ -3860,6 +4248,14 @@ modelRoutes.get("/", async (c) => {
|
|
|
3860
4248
|
//#endregion
|
|
3861
4249
|
//#region src/routes/responses/handler.ts
|
|
3862
4250
|
const ENCODER = new TextEncoder();
|
|
4251
|
+
/**
|
|
4252
|
+
* Cap on CONSECUTIVE empty frames tolerated while discovering the first real
|
|
4253
|
+
* SSE event. Reset by any meaningful frame, so a legitimately slow upstream
|
|
4254
|
+
* that interleaves keepalives with progress is never cut off — only a source
|
|
4255
|
+
* that emits nothing BUT empty frames trips it. Generous by design: this is a
|
|
4256
|
+
* runaway backstop, not a policy knob.
|
|
4257
|
+
*/
|
|
4258
|
+
const MAX_CONSECUTIVE_EMPTY_DISCOVERY_FRAMES = 1e4;
|
|
3863
4259
|
function formatSSE(chunk) {
|
|
3864
4260
|
const parts = [];
|
|
3865
4261
|
if (chunk.event) parts.push(`event: ${chunk.event}`);
|
|
@@ -3910,18 +4306,25 @@ async function handleResponses(c) {
|
|
|
3910
4306
|
const iterator = response[Symbol.asyncIterator]();
|
|
3911
4307
|
let firstChunk;
|
|
3912
4308
|
let upstreamFinished = false;
|
|
4309
|
+
let consecutiveEmpty = 0;
|
|
3913
4310
|
while (true) {
|
|
3914
4311
|
const r = await readIteratorWithTimeout(iterator, UPSTREAM_INACTIVITY_TIMEOUT_MS);
|
|
3915
4312
|
if (r.done) {
|
|
3916
4313
|
upstreamFinished = true;
|
|
3917
4314
|
break;
|
|
3918
4315
|
}
|
|
3919
|
-
if (r.value === void 0 || r.value === null)
|
|
4316
|
+
if (r.value === void 0 || r.value === null) {
|
|
4317
|
+
if (++consecutiveEmpty > MAX_CONSECUTIVE_EMPTY_DISCOVERY_FRAMES) break;
|
|
4318
|
+
continue;
|
|
4319
|
+
}
|
|
3920
4320
|
if (r.value.data === "[DONE]") {
|
|
3921
4321
|
upstreamFinished = true;
|
|
3922
4322
|
break;
|
|
3923
4323
|
}
|
|
3924
|
-
if (!r.value.data)
|
|
4324
|
+
if (!r.value.data) {
|
|
4325
|
+
if (++consecutiveEmpty > MAX_CONSECUTIVE_EMPTY_DISCOVERY_FRAMES) break;
|
|
4326
|
+
continue;
|
|
4327
|
+
}
|
|
3925
4328
|
firstChunk = r.value;
|
|
3926
4329
|
break;
|
|
3927
4330
|
}
|
|
@@ -4454,6 +4857,7 @@ async function setupAndServe(options) {
|
|
|
4454
4857
|
await cacheModels();
|
|
4455
4858
|
consola.debug(`Available models: \n${state.models?.data.map((model) => `- ${model.id}`).join("\n")}`);
|
|
4456
4859
|
const serveOptions = buildServeOptions(server.fetch, options.silent);
|
|
4860
|
+
installBodySizeStatsExitHook();
|
|
4457
4861
|
let srvxServer;
|
|
4458
4862
|
if (options.port !== void 0) srvxServer = serve({
|
|
4459
4863
|
...serveOptions,
|
|
@@ -4857,6 +5261,6 @@ function getCodexEnvVars(serverUrl) {
|
|
|
4857
5261
|
return vars;
|
|
4858
5262
|
}
|
|
4859
5263
|
//#endregion
|
|
4860
|
-
export { sharedServerArgs as a, listModelsForEndpoint as c,
|
|
5264
|
+
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
5265
|
|
|
4862
|
-
//# sourceMappingURL=server-setup-
|
|
5266
|
+
//# sourceMappingURL=server-setup-Dd293T1O.js.map
|