github-router 0.3.261 → 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.
- package/dist/{attribution-settings-Bl0HHJQF.js → attribution-settings-DFIVIRG8.js} +3 -3
- package/dist/{attribution-settings-Bl0HHJQF.js.map → attribution-settings-DFIVIRG8.js.map} +1 -1
- package/dist/{auth-I8k2OOvA.js → auth-WA88cE9q.js} +2 -2
- package/dist/{auth-I8k2OOvA.js.map → auth-WA88cE9q.js.map} +1 -1
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/{check-usage-SOn7-cWc.js → check-usage-v2apGKz7.js} +3 -3
- package/dist/{check-usage-SOn7-cWc.js.map → check-usage-v2apGKz7.js.map} +1 -1
- package/dist/{claude-CEaI47j7.js → claude-BCI3Sx6_.js} +9 -10
- package/dist/{claude-CEaI47j7.js.map → claude-BCI3Sx6_.js.map} +1 -1
- package/dist/{codex-C6Eyz3EV.js → codex-DyJIrbJt.js} +5 -6
- package/dist/{codex-C6Eyz3EV.js.map → codex-DyJIrbJt.js.map} +1 -1
- package/dist/engine-D8dlcSW6.js +2 -0
- package/dist/{gate-discovery-BnPhH_AC.js → gate-discovery-cMbt8Ct2.js} +3 -3
- package/dist/{gate-discovery-BnPhH_AC.js.map → gate-discovery-cMbt8Ct2.js.map} +1 -1
- package/dist/{get-copilot-usage-CYpAsYrM.js → get-copilot-usage-DOFJWJaQ.js} +2 -2
- package/dist/{get-copilot-usage-CYpAsYrM.js.map → get-copilot-usage-DOFJWJaQ.js.map} +1 -1
- package/dist/{internal-prompt-submit-CSJR24-y.js → internal-prompt-submit-C0XOPgcP.js} +2 -2
- package/dist/{internal-prompt-submit-CSJR24-y.js.map → internal-prompt-submit-C0XOPgcP.js.map} +1 -1
- package/dist/{internal-stop-hook-CiflCJO-.js → internal-stop-hook-CBcE2Js8.js} +4 -4
- package/dist/{internal-stop-hook-CiflCJO-.js.map → internal-stop-hook-CBcE2Js8.js.map} +1 -1
- package/dist/main.js +9 -9
- package/dist/{models-uQgsKNNC.js → models-Cv6gprpy.js} +2 -2
- package/dist/{models-uQgsKNNC.js.map → models-Cv6gprpy.js.map} +1 -1
- package/dist/{orchestration-BiZviyam.js → orchestration-B6qidQAd.js} +2 -2
- package/dist/{orchestration-BiZviyam.js.map → orchestration-B6qidQAd.js.map} +1 -1
- package/dist/{peer-mcp-personas-V4AXhm9B.js → peer-mcp-personas-B22ncnbQ.js} +38 -23
- package/dist/peer-mcp-personas-B22ncnbQ.js.map +1 -0
- package/dist/{provision-BLwsOFCI.js → provision-DSpLQHC3.js} +2 -2
- package/dist/{provision-BLwsOFCI.js.map → provision-DSpLQHC3.js.map} +1 -1
- package/dist/{serve-De-kqWXM.js → serve-D9tBVVoK.js} +16 -11
- package/dist/{serve-De-kqWXM.js.map → serve-D9tBVVoK.js.map} +1 -1
- package/dist/{server-setup-BeeT8m-w.js → server-setup-BM9Smfek.js} +317 -20
- package/dist/server-setup-BM9Smfek.js.map +1 -0
- package/dist/{start-DEC0M8UH.js → start-D7Hx2T_T.js} +3 -3
- package/dist/{start-DEC0M8UH.js.map → start-D7Hx2T_T.js.map} +1 -1
- package/dist/{stop-gate-hook-DoYD9Wt8.js → stop-gate-hook-Bmwla_2u.js} +10 -5
- package/dist/{stop-gate-hook-DoYD9Wt8.js.map → stop-gate-hook-Bmwla_2u.js.map} +1 -1
- package/dist/{token-DWknHe9M.js → token-C8Uh7aUe.js} +73 -12
- package/dist/{token-DWknHe9M.js.map → token-C8Uh7aUe.js.map} +1 -1
- package/package.json +1 -1
- package/dist/engine-B4Ohhv5i.js +0 -2
- package/dist/file-log-reporter-vI8qCFNM.js +0 -158
- package/dist/file-log-reporter-vI8qCFNM.js.map +0 -1
- 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-B22ncnbQ.js";
|
|
2
2
|
import { t as getPackageVersion } from "./version-_Q1WpsQp.js";
|
|
3
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-
|
|
5
|
-
import { t as getCopilotUsage } from "./get-copilot-usage-
|
|
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.
|
|
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
|
|
978
|
-
parts.push(info.
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|
|
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)
|
|
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,
|
|
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-
|
|
5159
|
+
//# sourceMappingURL=server-setup-BM9Smfek.js.map
|