copilot-relay 0.4.0 → 0.4.2
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/README.md +2 -2
- package/dist/main.js +568 -124
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -264,7 +264,9 @@ const fileLevelByMethod = {
|
|
|
264
264
|
debug: consolaLevelByName.debug
|
|
265
265
|
};
|
|
266
266
|
const loggingSuppressed = new AsyncLocalStorage();
|
|
267
|
+
const consoleSuppressed = new AsyncLocalStorage();
|
|
267
268
|
const withoutLogging = (run) => loggingSuppressed.run(true, run);
|
|
269
|
+
const withoutConsoleLogging = (run) => consoleSuppressed.run(true, run);
|
|
268
270
|
let currentLogLevel = consolaLevelByName.info;
|
|
269
271
|
const pendingLogWrites = /* @__PURE__ */ new Set();
|
|
270
272
|
const registeredLogSecrets = /* @__PURE__ */ new Set();
|
|
@@ -423,15 +425,16 @@ const wrapFileLog = (level, fn) => ((...args) => {
|
|
|
423
425
|
if (loggingSuppressed.getStore()) return;
|
|
424
426
|
const methodLevel = fileLevelByMethod[level] ?? consolaLevelByName.info;
|
|
425
427
|
const writesToFile = methodLevel <= currentLogLevel;
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
+
const quiet = consoleSuppressed.getStore() === true;
|
|
429
|
+
const writesToConsole = !quiet && methodLevel <= consola.level;
|
|
430
|
+
if (!writesToFile && !writesToConsole) return quiet ? void 0 : fn(...args);
|
|
428
431
|
const rendered = [boundLogText(args.map((value) => boundLogText(escapeLogLineSeparators(scrubLogSecrets(scrubSensitiveUrls(formatLogValue(value)))), maxLogArgumentBytes)).join(" "), maxLogPayloadBytes)];
|
|
429
432
|
if (writesToFile) {
|
|
430
433
|
const pending = writeLogFile(level, rendered).catch(() => void 0);
|
|
431
434
|
pendingLogWrites.add(pending);
|
|
432
435
|
pending.then(() => pendingLogWrites.delete(pending));
|
|
433
436
|
}
|
|
434
|
-
return fn(...rendered);
|
|
437
|
+
return quiet ? void 0 : fn(...rendered);
|
|
435
438
|
});
|
|
436
439
|
consola.error = wrapFileLog("error", consola.error.bind(consola));
|
|
437
440
|
consola.info = wrapFileLog("info", consola.info.bind(consola));
|
|
@@ -609,7 +612,8 @@ const ensureGitHubToken = async (options = {}) => {
|
|
|
609
612
|
return existingToken;
|
|
610
613
|
}
|
|
611
614
|
const deviceCode = await getDeviceCode();
|
|
612
|
-
|
|
615
|
+
if (options.onDeviceCode) options.onDeviceCode(deviceCode.verification_uri, deviceCode.user_code);
|
|
616
|
+
else log.info(`Open ${deviceCode.verification_uri} and enter code ${deviceCode.user_code}`);
|
|
613
617
|
const githubToken = await pollAccessToken(deviceCode);
|
|
614
618
|
await writeGitHubToken(githubToken);
|
|
615
619
|
log.info(`GitHub token synced to ${paths.githubTokenPath}`);
|
|
@@ -807,7 +811,7 @@ const resolveTarget = async (filePath, remainingLinks = 40) => {
|
|
|
807
811
|
return path.join(await resolveTarget(parent, remainingLinks), path.basename(filePath));
|
|
808
812
|
}
|
|
809
813
|
};
|
|
810
|
-
const
|
|
814
|
+
const readStableSnapshot = async (filePath) => {
|
|
811
815
|
const requestedPath = path.resolve(filePath);
|
|
812
816
|
const resolvedPath = await resolveTarget(requestedPath);
|
|
813
817
|
const before = await fs.lstat(resolvedPath).catch((error) => {
|
|
@@ -830,14 +834,46 @@ const readFileSnapshot = async (filePath) => {
|
|
|
830
834
|
if (isMissing(error)) throw new FileConflictError();
|
|
831
835
|
throw error;
|
|
832
836
|
}
|
|
833
|
-
|
|
834
|
-
return {
|
|
837
|
+
const snapshot = {
|
|
835
838
|
requestedPath,
|
|
836
839
|
resolvedPath,
|
|
837
840
|
raw,
|
|
838
841
|
mode: before.mode & 511,
|
|
839
842
|
identity: identityOf(before)
|
|
840
843
|
};
|
|
844
|
+
if (!after.isFile() || before.mode !== after.mode || await resolveTarget(requestedPath) !== resolvedPath) throw new FileConflictError();
|
|
845
|
+
if (!sameIdentity(snapshot.identity, identityOf(after))) {
|
|
846
|
+
if (sameReadContent(snapshot, {
|
|
847
|
+
...snapshot,
|
|
848
|
+
identity: identityOf(after)
|
|
849
|
+
})) throw new MetadataReadConflictError(snapshot);
|
|
850
|
+
throw new FileConflictError();
|
|
851
|
+
}
|
|
852
|
+
return snapshot;
|
|
853
|
+
};
|
|
854
|
+
const sameReadContent = (left, right) => left.resolvedPath === right.resolvedPath && left.raw === right.raw && left.mode === right.mode && left.identity !== null && right.identity !== null && sameIdentity(left.identity, {
|
|
855
|
+
...right.identity,
|
|
856
|
+
ctimeMs: left.identity.ctimeMs
|
|
857
|
+
});
|
|
858
|
+
var MetadataReadConflictError = class extends FileConflictError {
|
|
859
|
+
snapshot;
|
|
860
|
+
constructor(snapshot) {
|
|
861
|
+
super();
|
|
862
|
+
this.snapshot = snapshot;
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
const readFileSnapshot = async (filePath) => {
|
|
866
|
+
let firstRead;
|
|
867
|
+
for (let attempt = 1;; attempt++) try {
|
|
868
|
+
const snapshot = await readStableSnapshot(filePath);
|
|
869
|
+
if (firstRead && !sameReadContent(firstRead, snapshot)) throw new FileConflictError();
|
|
870
|
+
return snapshot;
|
|
871
|
+
} catch (error) {
|
|
872
|
+
if (!(error instanceof MetadataReadConflictError)) throw error;
|
|
873
|
+
if (attempt >= 3) throw new FileConflictError();
|
|
874
|
+
if (firstRead && !sameReadContent(firstRead, error.snapshot)) throw new FileConflictError();
|
|
875
|
+
firstRead ??= error.snapshot;
|
|
876
|
+
}
|
|
841
877
|
};
|
|
842
878
|
const assertUnchanged = async (expected) => {
|
|
843
879
|
const current = await readFileSnapshot(expected.requestedPath);
|
|
@@ -1306,6 +1342,7 @@ const maxQueuedBytes = 8 * 1024 * 1024;
|
|
|
1306
1342
|
const activeCaptures = /* @__PURE__ */ new Set();
|
|
1307
1343
|
const traceContext = new AsyncLocalStorage();
|
|
1308
1344
|
const transportContext = new AsyncLocalStorage();
|
|
1345
|
+
const traceObserver = new AsyncLocalStorage();
|
|
1309
1346
|
const responseExchanges = /* @__PURE__ */ new WeakMap();
|
|
1310
1347
|
const pendingCaptures = /* @__PURE__ */ new Set();
|
|
1311
1348
|
let captureRetentionDays = 3;
|
|
@@ -1331,6 +1368,11 @@ const errorName = (error) => error instanceof Error && [
|
|
|
1331
1368
|
"SyntaxError"
|
|
1332
1369
|
].includes(error.name) ? error.name : "Error";
|
|
1333
1370
|
const record$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1371
|
+
const withTraceObserver = (observe, run) => traceObserver.run(observe, run);
|
|
1372
|
+
const getReplayRequestId = () => {
|
|
1373
|
+
const id = transportContext.getStore()?.requestId;
|
|
1374
|
+
return id && uuid$1.test(id) ? id : void 0;
|
|
1375
|
+
};
|
|
1334
1376
|
const withRecordedTransport = (transport, run) => transportContext.run(transport, run);
|
|
1335
1377
|
const withRequestTrace = (trace, run) => traceContext.run(trace, run);
|
|
1336
1378
|
const getRequestTrace = () => traceContext.getStore();
|
|
@@ -1495,6 +1537,109 @@ var RequestTrace = class RequestTrace {
|
|
|
1495
1537
|
deferredHandler = false;
|
|
1496
1538
|
removeAbort;
|
|
1497
1539
|
credentials = /* @__PURE__ */ new Set();
|
|
1540
|
+
captureRequested = false;
|
|
1541
|
+
finishedSettling = false;
|
|
1542
|
+
failure;
|
|
1543
|
+
recordFailure(failure) {
|
|
1544
|
+
this.failure = failure;
|
|
1545
|
+
}
|
|
1546
|
+
diagnosticCaptureState() {
|
|
1547
|
+
if (!this.captureRequested) return "off";
|
|
1548
|
+
if (!this.captureDirectory) return "failed";
|
|
1549
|
+
if (!this.finishedSettling) return "pending";
|
|
1550
|
+
return this.manifest.captureState === "complete" ? "complete" : "incomplete";
|
|
1551
|
+
}
|
|
1552
|
+
diagnosticSnapshot() {
|
|
1553
|
+
const known = (value, choices) => typeof value === "string" && choices.includes(value) ? value : void 0;
|
|
1554
|
+
const id = (value) => {
|
|
1555
|
+
const text$1 = this.metadataValue(value);
|
|
1556
|
+
return text$1 && /^[A-Za-z0-9_.:-]{1,160}$/.test(text$1) ? text$1 : void 0;
|
|
1557
|
+
};
|
|
1558
|
+
const modelId = (value) => {
|
|
1559
|
+
const text$1 = this.metadataValue(value);
|
|
1560
|
+
return text$1 && /^[A-Za-z0-9_.\[\]-]{1,128}$/.test(text$1) ? text$1 : void 0;
|
|
1561
|
+
};
|
|
1562
|
+
const bodyState = (value) => known(value, [
|
|
1563
|
+
"pending",
|
|
1564
|
+
"complete",
|
|
1565
|
+
"cancelled",
|
|
1566
|
+
"error",
|
|
1567
|
+
"absent"
|
|
1568
|
+
]);
|
|
1569
|
+
const status$1 = (value) => typeof value === "number" && Number.isInteger(value) && value >= 100 && value <= 599 ? value : void 0;
|
|
1570
|
+
const reasons$1 = [
|
|
1571
|
+
"end_turn",
|
|
1572
|
+
"stop_sequence",
|
|
1573
|
+
"tool_use",
|
|
1574
|
+
"pause_turn",
|
|
1575
|
+
"refusal",
|
|
1576
|
+
"max_tokens"
|
|
1577
|
+
];
|
|
1578
|
+
return {
|
|
1579
|
+
requestId: this.requestId,
|
|
1580
|
+
status: status$1(this.manifest.status),
|
|
1581
|
+
responseState: bodyState(this.manifest.response?.state),
|
|
1582
|
+
stopReason: known(this.manifest.outcome?.stop_reason, reasons$1),
|
|
1583
|
+
reportedModel: modelId(this.manifest.outcome?.model),
|
|
1584
|
+
terminal: this.manifest.outcome?.terminal === true,
|
|
1585
|
+
failure: this.failure,
|
|
1586
|
+
capture: { state: this.diagnosticCaptureState() },
|
|
1587
|
+
exchanges: this.manifest.exchanges.map((exchange) => ({
|
|
1588
|
+
order: exchange.order,
|
|
1589
|
+
path: known(exchange.path, [
|
|
1590
|
+
"/models",
|
|
1591
|
+
"/chat/completions",
|
|
1592
|
+
"/responses",
|
|
1593
|
+
"/v1/messages"
|
|
1594
|
+
]) ?? "unknown",
|
|
1595
|
+
upstreamRequestId: id(exchange.upstreamRequestId),
|
|
1596
|
+
providerRequestId: [
|
|
1597
|
+
"x-copilot-service-request-id",
|
|
1598
|
+
"x-github-request-id",
|
|
1599
|
+
"x-request-id"
|
|
1600
|
+
].map((key) => id(exchange.responseHeaders?.[key])).find(Boolean),
|
|
1601
|
+
messageId: id(exchange.outcome?.message_id),
|
|
1602
|
+
model: modelId(exchange.outcome?.model),
|
|
1603
|
+
status: status$1(exchange.status),
|
|
1604
|
+
responseState: bodyState(exchange.response?.state),
|
|
1605
|
+
finishReason: known(exchange.outcome?.finish_reason, [
|
|
1606
|
+
"stop",
|
|
1607
|
+
"length",
|
|
1608
|
+
"tool_calls",
|
|
1609
|
+
"content_filter"
|
|
1610
|
+
]),
|
|
1611
|
+
stopReason: known(exchange.outcome?.stop_reason, reasons$1),
|
|
1612
|
+
responseStatus: known(exchange.outcome?.response_status, [
|
|
1613
|
+
"completed",
|
|
1614
|
+
"failed",
|
|
1615
|
+
"cancelled",
|
|
1616
|
+
"incomplete"
|
|
1617
|
+
]),
|
|
1618
|
+
refusalCategory: known(exchange.outcome?.refusal_category, [
|
|
1619
|
+
"cyber",
|
|
1620
|
+
"bio",
|
|
1621
|
+
"reasoning_extraction",
|
|
1622
|
+
"general_harms",
|
|
1623
|
+
"frontier_llm",
|
|
1624
|
+
"unknown"
|
|
1625
|
+
]),
|
|
1626
|
+
incompleteReason: known(exchange.outcome?.incomplete_reason, [
|
|
1627
|
+
"max_output_tokens",
|
|
1628
|
+
"content_filter",
|
|
1629
|
+
"unknown"
|
|
1630
|
+
]),
|
|
1631
|
+
error: known(exchange.error ?? exchange.response?.error, [
|
|
1632
|
+
"AbortError",
|
|
1633
|
+
"TimeoutError",
|
|
1634
|
+
"TypeError",
|
|
1635
|
+
"SyntaxError",
|
|
1636
|
+
"Error"
|
|
1637
|
+
]),
|
|
1638
|
+
discarded: exchange.discarded === true
|
|
1639
|
+
})),
|
|
1640
|
+
refreshes: this.manifest.refreshes.map((refresh) => refresh.outcome)
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1498
1643
|
protectCredential(value) {
|
|
1499
1644
|
if (value) this.credentials.add(value);
|
|
1500
1645
|
registerLogSecret(value);
|
|
@@ -1599,6 +1744,8 @@ var RequestTrace = class RequestTrace {
|
|
|
1599
1744
|
}
|
|
1600
1745
|
static async create(id, request, config, runtime, capture) {
|
|
1601
1746
|
const trace = new RequestTrace(id, request, config, runtime);
|
|
1747
|
+
trace.captureRequested = capture;
|
|
1748
|
+
traceObserver.getStore()?.(trace);
|
|
1602
1749
|
if (!capture) return trace;
|
|
1603
1750
|
try {
|
|
1604
1751
|
if (!uuid$1.test(id)) throw new Error("Invalid capture identifier.");
|
|
@@ -1881,6 +2028,7 @@ var RequestTrace = class RequestTrace {
|
|
|
1881
2028
|
await Promise.all([...this.openFiles.values()].map((handle) => handle.close().catch(() => {})));
|
|
1882
2029
|
this.openFiles.clear();
|
|
1883
2030
|
if (this.captureDirectory) activeCaptures.delete(this.captureDirectory);
|
|
2031
|
+
this.finishedSettling = true;
|
|
1884
2032
|
this.resolveFinished();
|
|
1885
2033
|
});
|
|
1886
2034
|
}
|
|
@@ -2297,8 +2445,163 @@ async function boundModelOutputTokens(config, model, requested) {
|
|
|
2297
2445
|
return bounded;
|
|
2298
2446
|
}
|
|
2299
2447
|
|
|
2448
|
+
//#endregion
|
|
2449
|
+
//#region src/lib/terminal.ts
|
|
2450
|
+
const colorEnabled = (env = process.env, isTTY = Boolean(process.stdout.isTTY)) => {
|
|
2451
|
+
if (env.NO_COLOR !== void 0 || env.FORCE_COLOR === "0") return false;
|
|
2452
|
+
if (env.FORCE_COLOR !== void 0) return env.FORCE_COLOR === "" || [
|
|
2453
|
+
"1",
|
|
2454
|
+
"2",
|
|
2455
|
+
"3",
|
|
2456
|
+
"true"
|
|
2457
|
+
].includes(env.FORCE_COLOR);
|
|
2458
|
+
return isTTY && env.TERM !== "dumb";
|
|
2459
|
+
};
|
|
2460
|
+
const codes = {
|
|
2461
|
+
good: 32,
|
|
2462
|
+
bad: 31,
|
|
2463
|
+
warning: 33,
|
|
2464
|
+
muted: 90
|
|
2465
|
+
};
|
|
2466
|
+
const terminalText = (value) => sanitizeTerminalString(value).replace(/[\x80-\x9f]|\p{Cf}|\p{Zl}|\p{Zp}/gu, "");
|
|
2467
|
+
const colorText = (text$1, tone, enabled) => enabled ? `\u001b[${codes[tone]}m${text$1}\u001b[0m` : text$1;
|
|
2468
|
+
|
|
2469
|
+
//#endregion
|
|
2470
|
+
//#region src/lib/model-probe-output.ts
|
|
2471
|
+
const tones = {
|
|
2472
|
+
PASS: "good",
|
|
2473
|
+
FAIL: "bad",
|
|
2474
|
+
INCOMPLETE: "warning",
|
|
2475
|
+
SKIPPED: "muted",
|
|
2476
|
+
NOT_TESTED: "muted"
|
|
2477
|
+
};
|
|
2478
|
+
const reasons = {
|
|
2479
|
+
"completed-text": "Ready",
|
|
2480
|
+
"probe-timeout": "Timed out",
|
|
2481
|
+
"upstream-timeout-or-HTTP-504": "Upstream timeout (504)",
|
|
2482
|
+
"authentication-rejected": "Auth rejected (401)",
|
|
2483
|
+
"access-denied": "Access denied (403)",
|
|
2484
|
+
"rate-limited": "Rate limited (429)",
|
|
2485
|
+
"cancelled": "Cancelled",
|
|
2486
|
+
"total-deadline": "Time budget reached",
|
|
2487
|
+
"unsupported-model-type": "Not a chat model",
|
|
2488
|
+
"unsupported-relay-endpoint": "Unsupported route",
|
|
2489
|
+
"unsupported-api": "Unsupported API",
|
|
2490
|
+
"unsupported-effort": "Unsupported effort",
|
|
2491
|
+
"unsafe-or-noncanonical-id": "Unsupported model ID",
|
|
2492
|
+
"upstream-response-failed": "Upstream failed",
|
|
2493
|
+
"upstream-response-incomplete": "Incomplete response",
|
|
2494
|
+
"malformed-response": "Invalid response",
|
|
2495
|
+
"model-mismatch": "Model mismatch",
|
|
2496
|
+
"reachable-output-budget-exhausted": "Output limit reached",
|
|
2497
|
+
"output-budget-exhausted-usage-unreported": "Output limit reached",
|
|
2498
|
+
"refusal-or-unexpected-completion": "Unexpected completion",
|
|
2499
|
+
"refusal": "Refused",
|
|
2500
|
+
"empty-completed-response": "Empty response",
|
|
2501
|
+
"network-or-invalid-response": "Request failed",
|
|
2502
|
+
"transport-error": "Connection failed",
|
|
2503
|
+
"refresh-failed": "Token refresh failed",
|
|
2504
|
+
"invalid-tool-input": "Invalid tool input",
|
|
2505
|
+
"local-validation": "Request rejected",
|
|
2506
|
+
"unknown-error": "Unknown API error"
|
|
2507
|
+
};
|
|
2508
|
+
const probeReason = (code) => reasons[code] ?? (/^HTTP-\d{3}$/.test(code) ? `HTTP ${code.slice(5)}` : "Unknown result");
|
|
2509
|
+
const probeColumnWidth = (ids, columns) => Math.min(Math.max(14, ...ids.map((id) => terminalText(id).length)), Math.max(14, columns - 42));
|
|
2510
|
+
const renderProbeHeader = (width) => `${"MODEL".padEnd(width)} ${"STATUS".padEnd(10)} ${"TIME".padStart(6)} RESULT`;
|
|
2511
|
+
const wrap = (text$1, width) => {
|
|
2512
|
+
const lines = [];
|
|
2513
|
+
for (let index = 0; index < text$1.length; index += width) lines.push(text$1.slice(index, index + width));
|
|
2514
|
+
return lines;
|
|
2515
|
+
};
|
|
2516
|
+
const probeTime = (row) => {
|
|
2517
|
+
if (!row.sent) return "-";
|
|
2518
|
+
const seconds = row.latency / 1e3;
|
|
2519
|
+
if (seconds < 1e3) return `${seconds.toFixed(1)}s`;
|
|
2520
|
+
if (seconds < 3600) return `${(seconds / 60).toFixed(1)}m`;
|
|
2521
|
+
return `${(seconds / 3600).toFixed(1)}h`;
|
|
2522
|
+
};
|
|
2523
|
+
const renderProbeRow = (row, width, color, columns = 80) => {
|
|
2524
|
+
const models$1 = wrap(terminalText(row.id), width);
|
|
2525
|
+
const time = probeTime(row);
|
|
2526
|
+
const results = wrap(probeReason(row.detail) + (row.unverified ? " *" : ""), Math.max(1, columns - width - 22));
|
|
2527
|
+
const status$1 = colorText(row.status, tones[row.status], color) + " ".repeat(10 - row.status.length);
|
|
2528
|
+
return Array.from({ length: Math.max(models$1.length, results.length) }, (_, index) => {
|
|
2529
|
+
return `${(models$1[index] ?? "").padEnd(width)}${index === 0 ? ` ${status$1} ${time.padStart(6)} ` : " ".repeat(22)}${results[index] ?? ""}`.trimEnd();
|
|
2530
|
+
});
|
|
2531
|
+
};
|
|
2532
|
+
const renderProbeSummary = (rows) => {
|
|
2533
|
+
if (!rows.length) return "Summary: no models to test";
|
|
2534
|
+
const labels = {
|
|
2535
|
+
PASS: "passed",
|
|
2536
|
+
FAIL: "failed",
|
|
2537
|
+
INCOMPLETE: "incomplete",
|
|
2538
|
+
SKIPPED: "skipped",
|
|
2539
|
+
NOT_TESTED: "not tested"
|
|
2540
|
+
};
|
|
2541
|
+
return "Summary: " + Object.keys(labels).flatMap((status$1) => {
|
|
2542
|
+
const count = rows.filter((row) => row.status === status$1).length;
|
|
2543
|
+
return count ? [`${count} ${labels[status$1]}`] : [];
|
|
2544
|
+
}).join(" · ");
|
|
2545
|
+
};
|
|
2546
|
+
const renderProbeDetails = (row, diagnostic) => {
|
|
2547
|
+
const lines = [` effort=${row.effort} max_tokens=${row.maxTokens} planned_route=${row.endpoint}`, ` reported=${row.reported} reason=${row.detail}`];
|
|
2548
|
+
if (!diagnostic) return [...lines, " request=not started"];
|
|
2549
|
+
lines.push(` request_id=${diagnostic.requestId}`);
|
|
2550
|
+
lines.push(` client_http=${diagnostic.status ?? "unknown"} response=${diagnostic.responseState ?? "unknown"} terminal=${diagnostic.terminal ? "observed" : "not observed"}`);
|
|
2551
|
+
for (const exchange of diagnostic.exchanges) {
|
|
2552
|
+
lines.push(` route=${exchange.path} upstream_http=${exchange.status ?? "unknown"} response=${exchange.responseState ?? "unknown"}${exchange.discarded ? " discarded=yes" : ""}`);
|
|
2553
|
+
lines.push(` upstream_request_id=${exchange.upstreamRequestId ?? "unknown"}`);
|
|
2554
|
+
if (exchange.providerRequestId) lines.push(` provider_request_id=${exchange.providerRequestId}`);
|
|
2555
|
+
if (exchange.model && exchange.model !== row.reported) lines.push(` upstream_model=${exchange.model}`);
|
|
2556
|
+
if (exchange.messageId) lines.push(` message_id=${exchange.messageId}`);
|
|
2557
|
+
const outcome = exchange.stopReason ?? exchange.finishReason ?? exchange.responseStatus;
|
|
2558
|
+
if (outcome || exchange.error) lines.push(` outcome=${outcome ?? "unknown"}${exchange.error ? ` transport=${exchange.error}` : ""}`);
|
|
2559
|
+
if (exchange.refusalCategory) lines.push(` refusal_category=${exchange.refusalCategory}`);
|
|
2560
|
+
if (exchange.incompleteReason) lines.push(` incomplete_reason=${exchange.incompleteReason}`);
|
|
2561
|
+
}
|
|
2562
|
+
if (diagnostic.refreshes.length) lines.push(` refresh=${diagnostic.refreshes.join(",")}`);
|
|
2563
|
+
lines.push(` capture=${diagnostic.capture.state}`);
|
|
2564
|
+
if (diagnostic.capture.state === "complete") lines.push(` Offline replay: copilot-relay replay ${diagnostic.requestId}`);
|
|
2565
|
+
else if (diagnostic.capture.state === "off") lines.push(" Replay unavailable: debug was off; capture privately if the error repeats.");
|
|
2566
|
+
else lines.push(" Replay unavailable: capture is not complete.");
|
|
2567
|
+
return lines;
|
|
2568
|
+
};
|
|
2569
|
+
const probeHint = (code) => {
|
|
2570
|
+
if (code === "probe-timeout" || code === "upstream-timeout-or-HTTP-504") return "Timeout: check connection and configured deadline.";
|
|
2571
|
+
if (["authentication-rejected", "refresh-failed"].includes(code)) return "Authentication: run copilot-relay auth before another probe.";
|
|
2572
|
+
if (code === "access-denied") return "Access: check this account's model entitlement and gateway policy.";
|
|
2573
|
+
if (code === "rate-limited") return "Rate limit: wait before retrying; repeated probes consume usage.";
|
|
2574
|
+
if (code === "refusal") return "Refusal: inspect the failing request's evidence; a short probe does not explain its cause.";
|
|
2575
|
+
if (code.includes("budget-exhausted")) return "Output limit: reachability is not completion; review the probe's output budget.";
|
|
2576
|
+
if (code === "model-mismatch") return "Model mismatch: compare selected, sent and reported IDs with --details.";
|
|
2577
|
+
if ([
|
|
2578
|
+
"invalid-tool-input",
|
|
2579
|
+
"malformed-response",
|
|
2580
|
+
"upstream-response-failed",
|
|
2581
|
+
"unknown-error",
|
|
2582
|
+
"network-or-invalid-response",
|
|
2583
|
+
"transport-error"
|
|
2584
|
+
].includes(code) || /^HTTP-5\d\d$/.test(code)) return "API failure: the request ID locates the local summary; --details shows evidence on a new probe.";
|
|
2585
|
+
};
|
|
2586
|
+
|
|
2300
2587
|
//#endregion
|
|
2301
2588
|
//#region src/claude/utils.ts
|
|
2589
|
+
const isEffortOnlyControl = (control) => control !== null && typeof control === "object" && !Array.isArray(control) && Object.keys(control).length === 1 && "effort" in control && isConfiguredReasoningEffort(control.effort);
|
|
2590
|
+
function getClaudeTurnEffort(payload) {
|
|
2591
|
+
let requested = getRequestReasoningEffort(payload);
|
|
2592
|
+
let pending;
|
|
2593
|
+
if (Array.isArray(payload.messages)) {
|
|
2594
|
+
for (const message of payload.messages) if (message?.role === "system" && message.clear_at === void 0 && isEffortOnlyControl(message.output_config)) pending = message.output_config.effort;
|
|
2595
|
+
else if (message?.role === "user" && pending !== void 0) {
|
|
2596
|
+
requested = pending;
|
|
2597
|
+
pending = void 0;
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
return {
|
|
2601
|
+
requested,
|
|
2602
|
+
effective: resolveReasoningEffort(requested)
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2302
2605
|
function mapOpenAIStopReasonToClaude(finishReason) {
|
|
2303
2606
|
if (finishReason === null) return null;
|
|
2304
2607
|
return {
|
|
@@ -2308,6 +2611,43 @@ function mapOpenAIStopReasonToClaude(finishReason) {
|
|
|
2308
2611
|
content_filter: "refusal"
|
|
2309
2612
|
}[finishReason];
|
|
2310
2613
|
}
|
|
2614
|
+
/**
|
|
2615
|
+
* Upstream sends an empty `arguments` string for tools that take no
|
|
2616
|
+
* parameters (#114). Blank text is the empty object; anything else must be a
|
|
2617
|
+
* JSON object, and a failure names the tool rather than surfacing a bare
|
|
2618
|
+
* SyntaxError. Argument text is never echoed: it can carry user data.
|
|
2619
|
+
*/
|
|
2620
|
+
function parseUpstreamToolInput(toolName, argumentsText) {
|
|
2621
|
+
const text$1 = argumentsText ?? "";
|
|
2622
|
+
if (!text$1.trim()) return {};
|
|
2623
|
+
let parsed;
|
|
2624
|
+
try {
|
|
2625
|
+
parsed = JSON.parse(text$1);
|
|
2626
|
+
} catch {
|
|
2627
|
+
throw invalidUpstreamToolInput(toolName, "is not valid JSON");
|
|
2628
|
+
}
|
|
2629
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw invalidUpstreamToolInput(toolName, "is not a JSON object");
|
|
2630
|
+
return parsed;
|
|
2631
|
+
}
|
|
2632
|
+
function invalidUpstreamToolInputMessage(toolName, reason) {
|
|
2633
|
+
return `Upstream returned tool input for "${toolName}" that ${reason}.`;
|
|
2634
|
+
}
|
|
2635
|
+
/** Mapped 502 whose message is client-safe: it names the tool, never the arguments. */
|
|
2636
|
+
var UpstreamToolInputError = class extends HTTPError {
|
|
2637
|
+
constructor(message) {
|
|
2638
|
+
super(message, Response.json({
|
|
2639
|
+
type: "error",
|
|
2640
|
+
error: {
|
|
2641
|
+
type: "api_error",
|
|
2642
|
+
message
|
|
2643
|
+
}
|
|
2644
|
+
}, { status: 502 }), message);
|
|
2645
|
+
this.name = "UpstreamToolInputError";
|
|
2646
|
+
}
|
|
2647
|
+
};
|
|
2648
|
+
function invalidUpstreamToolInput(toolName, reason) {
|
|
2649
|
+
return new UpstreamToolInputError(invalidUpstreamToolInputMessage(toolName, reason));
|
|
2650
|
+
}
|
|
2311
2651
|
|
|
2312
2652
|
//#endregion
|
|
2313
2653
|
//#region src/claude/tool-names.ts
|
|
@@ -2379,8 +2719,8 @@ function translateToOpenAI(payload, _settings, toolNameMapper) {
|
|
|
2379
2719
|
const model = translateModelName(payload.model);
|
|
2380
2720
|
const mapper = toolNameMapper ?? createClaudeToolNameMapper(payload.tools, { ...getToolNameMapperOptionsForModel(model) });
|
|
2381
2721
|
const tools = translateClaudeToolsToOpenAI(payload.tools, mapper);
|
|
2382
|
-
const effort =
|
|
2383
|
-
validateClaudeMessages(payload.messages
|
|
2722
|
+
const { effective: effort } = getClaudeTurnEffort(payload);
|
|
2723
|
+
validateClaudeMessages(payload.messages);
|
|
2384
2724
|
return {
|
|
2385
2725
|
model,
|
|
2386
2726
|
messages: normalizeFinalAssistantPrefill$1(translateClaudeMessagesToOpenAI(payload.messages, payload.system, mapper)),
|
|
@@ -2395,7 +2735,7 @@ function translateToOpenAI(payload, _settings, toolNameMapper) {
|
|
|
2395
2735
|
tool_choice: tools && tools.length > 0 ? translateClaudeToolChoiceToOpenAI(payload.tool_choice, mapper) : void 0
|
|
2396
2736
|
};
|
|
2397
2737
|
}
|
|
2398
|
-
function validateClaudeMessages(messages, native = false
|
|
2738
|
+
function validateClaudeMessages(messages, native = false) {
|
|
2399
2739
|
if (!Array.isArray(messages)) throw invalidMessage("Messages must be an array.");
|
|
2400
2740
|
for (const message of messages) {
|
|
2401
2741
|
if (!message || ![
|
|
@@ -2405,8 +2745,7 @@ function validateClaudeMessages(messages, native = false, effort) {
|
|
|
2405
2745
|
].includes(message.role)) throw invalidMessage("Unsupported message role.");
|
|
2406
2746
|
if (message.role !== "system") continue;
|
|
2407
2747
|
const control = message.output_config;
|
|
2408
|
-
|
|
2409
|
-
if (!native && (message.clear_at !== void 0 || control !== void 0 && !redundantEffort)) throw invalidMessage("Per-message system controls require the native Messages route unless effort exactly matches the request.");
|
|
2748
|
+
if (!native && (message.clear_at !== void 0 || control !== void 0 && !isEffortOnlyControl(control))) throw invalidMessage("Translated system controls support only output_config.effort with low, medium, high, xhigh, or max; other controls require the native Messages route.");
|
|
2410
2749
|
if (typeof message.content !== "string" && (!Array.isArray(message.content) || message.content.some((block) => !block || block.type !== "text" || typeof block.text !== "string"))) throw invalidMessage("System message content must contain only text.");
|
|
2411
2750
|
}
|
|
2412
2751
|
}
|
|
@@ -2416,7 +2755,9 @@ function translateClaudeMessagesToOpenAI(claudeMessages, system, toolNameMapper)
|
|
|
2416
2755
|
switch (message.role) {
|
|
2417
2756
|
case "user": return handleUserMessage(message);
|
|
2418
2757
|
case "assistant": return handleAssistantMessage(message, toolNameMapper);
|
|
2419
|
-
case "system":
|
|
2758
|
+
case "system":
|
|
2759
|
+
if (message.output_config !== void 0 && (typeof message.content === "string" ? message.content.length === 0 : message.content.every((block) => block.text.length === 0))) return [];
|
|
2760
|
+
return handleSystemPrompt(message.content);
|
|
2420
2761
|
default: throw invalidMessage("Unsupported message role.");
|
|
2421
2762
|
}
|
|
2422
2763
|
});
|
|
@@ -2621,17 +2962,15 @@ function getClaudeTextBlocks(messageContent) {
|
|
|
2621
2962
|
}
|
|
2622
2963
|
function getClaudeToolUseBlocks(toolCalls, toolNameMapper) {
|
|
2623
2964
|
if (!toolCalls) return [];
|
|
2624
|
-
return toolCalls.map((toolCall) =>
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Upstream tool input must be a JSON object.");
|
|
2634
|
-
return parsed;
|
|
2965
|
+
return toolCalls.map((toolCall) => {
|
|
2966
|
+
const name = toolNameMapper.toClaude(toolCall.function.name);
|
|
2967
|
+
return {
|
|
2968
|
+
type: "tool_use",
|
|
2969
|
+
id: toolCall.id,
|
|
2970
|
+
name,
|
|
2971
|
+
input: parseUpstreamToolInput(name, toolCall.function.arguments)
|
|
2972
|
+
};
|
|
2973
|
+
});
|
|
2635
2974
|
}
|
|
2636
2975
|
|
|
2637
2976
|
//#endregion
|
|
@@ -2735,8 +3074,8 @@ function translateChunkToClaudeEvents(chunk, state, toolNameMapper = createClaud
|
|
|
2735
3074
|
closeOpenContentBlock(events$1, state);
|
|
2736
3075
|
if (choice.finish_reason === "tool_calls") for (const toolCall of Object.values(state.toolCalls)) {
|
|
2737
3076
|
const argumentsText = toolCall.arguments ?? "";
|
|
2738
|
-
|
|
2739
|
-
|
|
3077
|
+
parseUpstreamToolInput(toolCall.name, argumentsText);
|
|
3078
|
+
const partialJson = argumentsText.trim() ? argumentsText : "{}";
|
|
2740
3079
|
const index = state.contentBlockIndex++;
|
|
2741
3080
|
toolCall.claudeBlockIndex = index;
|
|
2742
3081
|
events$1.push({
|
|
@@ -2753,7 +3092,7 @@ function translateChunkToClaudeEvents(chunk, state, toolNameMapper = createClaud
|
|
|
2753
3092
|
index,
|
|
2754
3093
|
delta: {
|
|
2755
3094
|
type: "input_json_delta",
|
|
2756
|
-
partial_json:
|
|
3095
|
+
partial_json: partialJson
|
|
2757
3096
|
}
|
|
2758
3097
|
}, {
|
|
2759
3098
|
type: "content_block_stop",
|
|
@@ -2775,12 +3114,12 @@ function translateChunkToClaudeEvents(chunk, state, toolNameMapper = createClaud
|
|
|
2775
3114
|
}
|
|
2776
3115
|
return events$1;
|
|
2777
3116
|
}
|
|
2778
|
-
function translateErrorToClaudeErrorEvent() {
|
|
3117
|
+
function translateErrorToClaudeErrorEvent(error) {
|
|
2779
3118
|
return {
|
|
2780
3119
|
type: "error",
|
|
2781
3120
|
error: {
|
|
2782
3121
|
type: "api_error",
|
|
2783
|
-
message: "An unexpected error occurred during streaming."
|
|
3122
|
+
message: error instanceof UpstreamToolInputError ? error.message : "An unexpected error occurred during streaming."
|
|
2784
3123
|
}
|
|
2785
3124
|
};
|
|
2786
3125
|
}
|
|
@@ -2854,7 +3193,11 @@ const getClaudeWebSearchToolCallFromChatResponse = (response, toolNameMapper) =>
|
|
|
2854
3193
|
};
|
|
2855
3194
|
const buildSearchInput = (payload, requestedQuery) => {
|
|
2856
3195
|
const systemText = typeof payload.system === "string" ? payload.system : Array.isArray(payload.system) ? payload.system.map((block) => block.text).join("\n\n") : "";
|
|
2857
|
-
const messages = payload.messages.
|
|
3196
|
+
const messages = payload.messages.flatMap((message) => {
|
|
3197
|
+
const text$1 = textFromMessageContent(message.content);
|
|
3198
|
+
if (message.role === "system" && message.output_config !== void 0 && (typeof message.content === "string" ? message.content.length === 0 : message.content.every((block) => block.text.length === 0))) return [];
|
|
3199
|
+
return [`${message.role}: ${text$1}`];
|
|
3200
|
+
}).join("\n\n");
|
|
2858
3201
|
return [
|
|
2859
3202
|
"You are fulfilling an Anthropic web_search server tool request for Claude Code.",
|
|
2860
3203
|
"Search the web using the provided web_search_preview tool.",
|
|
@@ -2870,7 +3213,7 @@ const buildWebSearchRequestPayload = (payload, requestedQuery, model) => ({
|
|
|
2870
3213
|
model,
|
|
2871
3214
|
input: buildSearchInput(payload, requestedQuery),
|
|
2872
3215
|
tools: [{ type: "web_search_preview" }],
|
|
2873
|
-
reasoning: { effort:
|
|
3216
|
+
reasoning: { effort: getClaudeTurnEffort(payload).effective },
|
|
2874
3217
|
max_output_tokens: Math.max(256, Math.min(payload.max_tokens ?? 1024, 1200)),
|
|
2875
3218
|
temperature: payload.temperature,
|
|
2876
3219
|
top_p: payload.top_p
|
|
@@ -3096,7 +3439,7 @@ const createClaudeWebSearchExecution = async (config, payload, requestedQuery, o
|
|
|
3096
3439
|
} catch (error) {
|
|
3097
3440
|
if (!(error instanceof SyntaxError)) throw error;
|
|
3098
3441
|
}
|
|
3099
|
-
return interpretSearchResponse(upstream, config, requestedQuery, request,
|
|
3442
|
+
return interpretSearchResponse(upstream, config, requestedQuery, request, getClaudeTurnEffort(payload).requested ?? "unset", options.requestId);
|
|
3100
3443
|
};
|
|
3101
3444
|
const buildSearchResultBlock = (toolUseId, results) => ({
|
|
3102
3445
|
type: "web_search_tool_result",
|
|
@@ -4346,7 +4689,7 @@ async function createNativeMessages(config, payload, options) {
|
|
|
4346
4689
|
};
|
|
4347
4690
|
const beta = options.headers?.get("anthropic-beta");
|
|
4348
4691
|
if (beta) headers["anthropic-beta"] = beta;
|
|
4349
|
-
log.info(sanitizeTerminalString(`request_id=${options.requestId} Model request client=claude requested_model=${payload.model} upstream_model=${payload.model} upstream_api=messages effective_think_effort=${body.
|
|
4692
|
+
log.info(sanitizeTerminalString(`request_id=${options.requestId} Model request client=claude requested_model=${payload.model} upstream_model=${payload.model} upstream_api=messages effective_think_effort=${getClaudeTurnEffort(body).effective}`));
|
|
4350
4693
|
const response = await fetchCopilot(getCopilotProviderContext(config), "/v1/messages", {
|
|
4351
4694
|
method: "POST",
|
|
4352
4695
|
headers,
|
|
@@ -4560,7 +4903,10 @@ async function handleNativeMessages(config, payload, options, write) {
|
|
|
4560
4903
|
}
|
|
4561
4904
|
const searchCall = searches[0];
|
|
4562
4905
|
if (!searchCall.id || !searchCall.name || typeof searchCall.input?.query !== "string" || !searchCall.input.query.trim()) throw new Error("Invalid native search call.");
|
|
4563
|
-
const search = await createClaudeWebSearchExecution(config,
|
|
4906
|
+
const search = await createClaudeWebSearchExecution(config, {
|
|
4907
|
+
...payload,
|
|
4908
|
+
messages: normalizeNativeHistory(payload.messages)
|
|
4909
|
+
}, searchCall.input.query, {
|
|
4564
4910
|
...options,
|
|
4565
4911
|
signal
|
|
4566
4912
|
});
|
|
@@ -4672,7 +5018,7 @@ const createTokenCountModel = (modelId, tokenizer = "o200k_base") => ({
|
|
|
4672
5018
|
id: modelId
|
|
4673
5019
|
});
|
|
4674
5020
|
const isNonStreamingResponse = (response) => typeof response === "object" && response !== null && Object.hasOwn(response, "choices");
|
|
4675
|
-
const getClaudeRequestedThinkEffort = (payload) =>
|
|
5021
|
+
const getClaudeRequestedThinkEffort = (payload) => getClaudeTurnEffort(payload).requested ?? "unset";
|
|
4676
5022
|
const getClaudeRequestedThinking = (payload) => {
|
|
4677
5023
|
if (!payload.thinking) return "none";
|
|
4678
5024
|
return [`type:${payload.thinking.type}`, `budget:${payload.thinking.budget_tokens ?? "none"}`].join(",");
|
|
@@ -4877,10 +5223,7 @@ const handleClaudeMessageRequest = async (config, claudePayload, requestSignal,
|
|
|
4877
5223
|
const webSearchToolCall = shouldLetModelDecideWebSearch ? getClaudeWebSearchToolCallFromChatResponse(effectiveResponse, toolNameMapper) : void 0;
|
|
4878
5224
|
let claudeResponse;
|
|
4879
5225
|
if (webSearchToolCall) {
|
|
4880
|
-
const search = await createClaudeWebSearchExecution(config, {
|
|
4881
|
-
...claudePayload,
|
|
4882
|
-
reasoning_effort: openAIPayload.reasoning_effort
|
|
4883
|
-
}, webSearchToolCall.query, {
|
|
5226
|
+
const search = await createClaudeWebSearchExecution(config, claudePayload, webSearchToolCall.query, {
|
|
4884
5227
|
requestId,
|
|
4885
5228
|
signal: requestSignal,
|
|
4886
5229
|
timeoutMs: config.upstreamTimeoutMs
|
|
@@ -4995,11 +5338,11 @@ claudeRoutes.post("/messages", async (c) => {
|
|
|
4995
5338
|
const claudePayload = await c.req.json();
|
|
4996
5339
|
const requestSignal = createCopilotRequestSignal(c.req.raw.signal, config.upstreamTimeoutMs);
|
|
4997
5340
|
try {
|
|
4998
|
-
|
|
5341
|
+
getRequestReasoningEffort(claudePayload);
|
|
4999
5342
|
const upstreamModel = translateModelName(claudePayload.model);
|
|
5000
5343
|
const validate = () => {
|
|
5001
5344
|
if (shouldUseNativeMessages(config, upstreamModel)) validateNativeMessages(claudePayload);
|
|
5002
|
-
else validateClaudeMessages(claudePayload.messages
|
|
5345
|
+
else validateClaudeMessages(claudePayload.messages);
|
|
5003
5346
|
};
|
|
5004
5347
|
validateClaudeMessages(claudePayload.messages, true);
|
|
5005
5348
|
if (config.claudeUpstreamApi !== "auto") validate();
|
|
@@ -5009,8 +5352,25 @@ claudeRoutes.post("/messages", async (c) => {
|
|
|
5009
5352
|
} catch (error) {
|
|
5010
5353
|
if (!(error instanceof HTTPError)) throw error;
|
|
5011
5354
|
c.set("requestErrorMessage", error.message);
|
|
5355
|
+
const trace = c.get("requestTrace");
|
|
5356
|
+
if (error.response.status === 400 && trace?.manifest.exchanges.length === 0) trace.recordFailure("local-validation");
|
|
5012
5357
|
log.error(`request_id=${requestId} ${error.message}`);
|
|
5013
|
-
|
|
5358
|
+
const headers = new Headers();
|
|
5359
|
+
for (const name of [
|
|
5360
|
+
"content-type",
|
|
5361
|
+
"retry-after",
|
|
5362
|
+
"x-request-id",
|
|
5363
|
+
"x-github-request-id",
|
|
5364
|
+
"x-copilot-service-request-id"
|
|
5365
|
+
]) {
|
|
5366
|
+
const value = error.response.headers.get(name);
|
|
5367
|
+
if (value !== null) headers.set(name, value);
|
|
5368
|
+
}
|
|
5369
|
+
return new Response(await error.response.arrayBuffer(), {
|
|
5370
|
+
status: error.response.status,
|
|
5371
|
+
statusText: error.response.statusText,
|
|
5372
|
+
headers
|
|
5373
|
+
});
|
|
5014
5374
|
}
|
|
5015
5375
|
if (claudePayload.stream) {
|
|
5016
5376
|
const trace = c.get("requestTrace");
|
|
@@ -5025,7 +5385,10 @@ claudeRoutes.post("/messages", async (c) => {
|
|
|
5025
5385
|
await handleClaudeMessageRequest(config, claudePayload, requestSignal, requestId, writeEvent, c.req.raw.headers);
|
|
5026
5386
|
} catch (error) {
|
|
5027
5387
|
log.error(`request_id=${requestId} Error during Claude stream request:`, error);
|
|
5028
|
-
|
|
5388
|
+
if (error instanceof UpstreamToolInputError) trace?.recordFailure("invalid-tool-input");
|
|
5389
|
+
const errorEvent = translateErrorToClaudeErrorEvent(error);
|
|
5390
|
+
if (errorEvent.type === "error") errorEvent.error.message += ` (request_id=${requestId})`;
|
|
5391
|
+
await writeEvent(errorEvent);
|
|
5029
5392
|
} finally {
|
|
5030
5393
|
trace?.handlerSettled();
|
|
5031
5394
|
log.info(`request_id=${requestId} stream completed ${Math.round(performance.now() - streamStarted)}ms`);
|
|
@@ -5043,6 +5406,7 @@ claudeRoutes.post("/messages", async (c) => {
|
|
|
5043
5406
|
} }, 501);
|
|
5044
5407
|
}
|
|
5045
5408
|
if (error instanceof HTTPError) {
|
|
5409
|
+
if (error instanceof UpstreamToolInputError) c.get("requestTrace")?.recordFailure("invalid-tool-input");
|
|
5046
5410
|
const text$1 = await error.response.text().catch(() => "");
|
|
5047
5411
|
c.set("requestErrorMessage", error.detail ?? text$1.slice(0, 240));
|
|
5048
5412
|
return new Response(text$1, {
|
|
@@ -5133,7 +5497,7 @@ const createServer = (config) => {
|
|
|
5133
5497
|
const { host, port } = config;
|
|
5134
5498
|
const configuredHost = parseAuthority(host.includes(":") && !host.startsWith("[") ? `[${host}]` : host, "http:")?.hostname;
|
|
5135
5499
|
app.use("*", async (c, next) => {
|
|
5136
|
-
const requestId = randomUUID();
|
|
5500
|
+
const requestId = getReplayRequestId() ?? randomUUID();
|
|
5137
5501
|
c.set("config", config);
|
|
5138
5502
|
c.set("requestId", requestId);
|
|
5139
5503
|
c.header("x-copilot-relay-request-id", requestId);
|
|
@@ -5142,6 +5506,7 @@ const createServer = (config) => {
|
|
|
5142
5506
|
try {
|
|
5143
5507
|
await next();
|
|
5144
5508
|
} finally {
|
|
5509
|
+
c.header("x-copilot-relay-request-id", requestId);
|
|
5145
5510
|
const ms = Math.round(performance.now() - started);
|
|
5146
5511
|
log.info(formatStatusLog(requestId, c.req.method, c.req.path, c.res.status, ms, c.get("requestErrorMessage")));
|
|
5147
5512
|
}
|
|
@@ -5201,7 +5566,8 @@ const createServer = (config) => {
|
|
|
5201
5566
|
requestId: c.get("requestId"),
|
|
5202
5567
|
error
|
|
5203
5568
|
});
|
|
5204
|
-
|
|
5569
|
+
c.get("requestTrace")?.recordFailure("internal-error");
|
|
5570
|
+
return c.json({ error: { message: `Internal server error (request_id=${c.get("requestId")})` } }, 500);
|
|
5205
5571
|
});
|
|
5206
5572
|
app.get("/", (c) => c.json({
|
|
5207
5573
|
name: "copilot-relay",
|
|
@@ -5258,9 +5624,34 @@ const httpCategory = (status$1) => {
|
|
|
5258
5624
|
if (status$1 === 504) return "upstream-timeout-or-HTTP-504";
|
|
5259
5625
|
return `HTTP-${status$1}`;
|
|
5260
5626
|
};
|
|
5627
|
+
const settleDiagnostic = async (trace) => {
|
|
5628
|
+
let timer;
|
|
5629
|
+
const deadline = new Promise((resolve$1) => {
|
|
5630
|
+
timer = setTimeout(resolve$1, 1e3);
|
|
5631
|
+
});
|
|
5632
|
+
try {
|
|
5633
|
+
await Promise.race([trace.finished, deadline]);
|
|
5634
|
+
} finally {
|
|
5635
|
+
clearTimeout(timer);
|
|
5636
|
+
}
|
|
5637
|
+
};
|
|
5638
|
+
const failureFromEvidence = (row, diagnostic) => {
|
|
5639
|
+
if ([
|
|
5640
|
+
"probe-timeout",
|
|
5641
|
+
"cancelled",
|
|
5642
|
+
"total-deadline"
|
|
5643
|
+
].includes(row.detail)) return row.detail;
|
|
5644
|
+
if (diagnostic.refreshes.includes("failure")) return "refresh-failed";
|
|
5645
|
+
if (diagnostic.failure === "invalid-tool-input" || diagnostic.failure === "local-validation") return diagnostic.failure;
|
|
5646
|
+
const lastExchange = diagnostic.exchanges.findLast((exchange) => !exchange.discarded);
|
|
5647
|
+
if (lastExchange?.error && ["HTTP-500", "network-or-invalid-response"].includes(row.detail)) return lastExchange.error === "SyntaxError" ? "malformed-response" : "transport-error";
|
|
5648
|
+
if (diagnostic.failure === "internal-error" || row.detail === "network-or-invalid-response") return "unknown-error";
|
|
5649
|
+
return row.detail;
|
|
5650
|
+
};
|
|
5261
5651
|
async function probeModels(config, entries, options) {
|
|
5262
|
-
console.log(
|
|
5263
|
-
console.log(
|
|
5652
|
+
console.log(`\nModel check · ${entries.length} ${entries.length === 1 ? "model" : "models"} · real Copilot usage`);
|
|
5653
|
+
console.log("Isolated relay pipeline; not running-daemon health.");
|
|
5654
|
+
console.log(`Up to ${options.maxTokens} output tokens/probe; existing retries may add calls.\n`);
|
|
5264
5655
|
const controller = new AbortController();
|
|
5265
5656
|
const interrupt = () => controller.abort();
|
|
5266
5657
|
process.on("SIGINT", interrupt);
|
|
@@ -5273,21 +5664,14 @@ async function probeModels(config, entries, options) {
|
|
|
5273
5664
|
};
|
|
5274
5665
|
const results = [];
|
|
5275
5666
|
const app = createServer(config);
|
|
5276
|
-
const
|
|
5277
|
-
const
|
|
5278
|
-
|
|
5667
|
+
const columns = Math.max(40, process.stdout.columns || 80);
|
|
5668
|
+
const width = probeColumnWidth(entries.map(([id]) => safeId(id, config.copilotToken) ? id : "[unsupported ID]"), columns);
|
|
5669
|
+
const color = colorEnabled();
|
|
5670
|
+
if (entries.length) console.log(renderProbeHeader(width));
|
|
5279
5671
|
try {
|
|
5280
5672
|
runtimeState.modelCatalog = config.modelCatalog;
|
|
5281
5673
|
runtimeState.upstreamBaseUrl = config.copilotBaseUrl;
|
|
5282
5674
|
for (const [id, model] of entries) {
|
|
5283
|
-
const row = {
|
|
5284
|
-
id,
|
|
5285
|
-
status: "NOT_TESTED",
|
|
5286
|
-
reported: "-",
|
|
5287
|
-
sent: false,
|
|
5288
|
-
latency: 0,
|
|
5289
|
-
detail: ""
|
|
5290
|
-
};
|
|
5291
5675
|
const efforts = [
|
|
5292
5676
|
"none",
|
|
5293
5677
|
"low",
|
|
@@ -5300,6 +5684,18 @@ async function probeModels(config, entries, options) {
|
|
|
5300
5684
|
const maxTokens = Math.min(options.maxTokens, model.limits?.max_output_tokens ?? options.maxTokens, model.limits?.max_non_streaming_output_tokens ?? options.maxTokens);
|
|
5301
5685
|
const timeoutMs = Math.min(options.timeoutMs, config.upstreamTimeoutMs > 0 ? config.upstreamTimeoutMs : Infinity);
|
|
5302
5686
|
const endpoint = shouldUseNativeMessages(config, id) ? "/v1/messages" : shouldUseResponsesApiForModel(id) ? "/responses" : "/chat/completions";
|
|
5687
|
+
const row = {
|
|
5688
|
+
id: safeId(id, config.copilotToken) ? id : "[unsupported ID]",
|
|
5689
|
+
status: "NOT_TESTED",
|
|
5690
|
+
reported: "-",
|
|
5691
|
+
sent: false,
|
|
5692
|
+
endpoint,
|
|
5693
|
+
latency: 0,
|
|
5694
|
+
detail: "",
|
|
5695
|
+
maxTokens,
|
|
5696
|
+
effort,
|
|
5697
|
+
unverified: model.reasoningEfforts === void 0 || model.supportedEndpoints === void 0
|
|
5698
|
+
};
|
|
5303
5699
|
if (controller.signal.aborted || total.aborted) row.detail = controller.signal.aborted ? "cancelled" : "total-deadline";
|
|
5304
5700
|
else if (!safeId(id, config.copilotToken) || normalizeCopilotModelId(id) !== id) {
|
|
5305
5701
|
row.status = "SKIPPED";
|
|
@@ -5325,23 +5721,33 @@ async function probeModels(config, entries, options) {
|
|
|
5325
5721
|
};
|
|
5326
5722
|
const started = performance.now();
|
|
5327
5723
|
row.sent = true;
|
|
5724
|
+
let trace;
|
|
5725
|
+
let completedFetch = false;
|
|
5328
5726
|
try {
|
|
5329
|
-
const response = await withoutLogging(() =>
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5727
|
+
const { response, body } = await withoutLogging(() => withTraceObserver((value) => {
|
|
5728
|
+
trace = value;
|
|
5729
|
+
}, async () => {
|
|
5730
|
+
const response$1 = await app.fetch(new Request(`http://localhost${config.port ? `:${config.port}` : ""}/v1/messages`, {
|
|
5731
|
+
method: "POST",
|
|
5732
|
+
headers: { "content-type": "application/json" },
|
|
5733
|
+
signal,
|
|
5734
|
+
body: JSON.stringify({
|
|
5735
|
+
model: id,
|
|
5736
|
+
stream: false,
|
|
5737
|
+
max_tokens: maxTokens,
|
|
5738
|
+
output_config: { effort },
|
|
5739
|
+
messages: [{
|
|
5740
|
+
role: "user",
|
|
5741
|
+
content: "Reply with OK only."
|
|
5742
|
+
}]
|
|
5743
|
+
})
|
|
5744
|
+
}));
|
|
5745
|
+
return {
|
|
5746
|
+
response: response$1,
|
|
5747
|
+
body: await response$1.json().catch(() => void 0)
|
|
5748
|
+
};
|
|
5749
|
+
}));
|
|
5750
|
+
completedFetch = true;
|
|
5345
5751
|
row.status = "FAIL";
|
|
5346
5752
|
if (signal.aborted) {
|
|
5347
5753
|
row.status = controller.signal.aborted || total.aborted ? "NOT_TESTED" : "FAIL";
|
|
@@ -5364,7 +5770,7 @@ async function probeModels(config, entries, options) {
|
|
|
5364
5770
|
row.status = "INCOMPLETE";
|
|
5365
5771
|
const tokens = record$1(body.usage) ? body.usage.output_tokens : void 0;
|
|
5366
5772
|
row.detail = typeof tokens === "number" && Number.isSafeInteger(tokens) && tokens > 0 ? "reachable-output-budget-exhausted" : "output-budget-exhausted-usage-unreported";
|
|
5367
|
-
} else if (body.stop_reason !== "end_turn" || body.content.some((part) => record$1(part) && part.type !== "text" && part.type !== "thinking")) row.detail = "refusal-or-unexpected-completion";
|
|
5773
|
+
} else if (body.stop_reason !== "end_turn" || body.content.some((part) => record$1(part) && part.type !== "text" && part.type !== "thinking")) row.detail = body.stop_reason === "refusal" ? "refusal" : "refusal-or-unexpected-completion";
|
|
5368
5774
|
else if (body.content.some((part) => record$1(part) && part.type === "text" && typeof part.text === "string" && part.text.trim())) {
|
|
5369
5775
|
row.status = "PASS";
|
|
5370
5776
|
row.detail = "completed-text";
|
|
@@ -5375,11 +5781,19 @@ async function probeModels(config, entries, options) {
|
|
|
5375
5781
|
row.detail = controller.signal.aborted ? "cancelled" : total.aborted ? "total-deadline" : signal.aborted ? "probe-timeout" : "network-or-invalid-response";
|
|
5376
5782
|
}
|
|
5377
5783
|
row.latency = Math.round(performance.now() - started);
|
|
5784
|
+
if (trace) {
|
|
5785
|
+
if (completedFetch) await settleDiagnostic(trace);
|
|
5786
|
+
row.diagnostic = trace.diagnosticSnapshot();
|
|
5787
|
+
if (row.reported !== "-" && !row.diagnostic.reportedModel) row.reported = "unreported";
|
|
5788
|
+
if (row.status === "FAIL") row.detail = failureFromEvidence(row, row.diagnostic);
|
|
5789
|
+
}
|
|
5378
5790
|
}
|
|
5379
|
-
|
|
5791
|
+
if (!safeId(id, config.copilotToken)) row.id = "[unsupported ID]";
|
|
5380
5792
|
results.push(row);
|
|
5381
|
-
const
|
|
5382
|
-
|
|
5793
|
+
for (const line of renderProbeRow(row, width, color, columns)) console.log(scrubSensitiveUrls(line));
|
|
5794
|
+
if (options.details) for (const line of renderProbeDetails(row, row.diagnostic)) console.log(line);
|
|
5795
|
+
else if (row.status !== "PASS" && row.diagnostic) console.log(` request_id=${row.diagnostic.requestId}`);
|
|
5796
|
+
if (row.status !== "PASS" && row.sent) withoutConsoleLogging(() => log.info(`request_id=${row.diagnostic?.requestId ?? "unknown"} model_probe model=${row.id} status=${row.status} reason=${row.detail}`));
|
|
5383
5797
|
}
|
|
5384
5798
|
} finally {
|
|
5385
5799
|
for (const key of [
|
|
@@ -5393,9 +5807,11 @@ async function probeModels(config, entries, options) {
|
|
|
5393
5807
|
process.off("SIGINT", interrupt);
|
|
5394
5808
|
process.off("SIGTERM", interrupt);
|
|
5395
5809
|
}
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5810
|
+
console.log(`\n${renderProbeSummary(results)}`);
|
|
5811
|
+
if (results.some((row) => row.unverified)) console.log("* Effort or endpoint metadata is unverified.");
|
|
5812
|
+
const hints = new Set(results.filter((row) => row.status !== "PASS").map((row) => probeHint(row.detail)).filter(Boolean));
|
|
5813
|
+
for (const hint of hints) console.log(hint);
|
|
5814
|
+
if (!options.details && results.some((row) => row.status !== "PASS" && row.sent)) console.log("Use --details for request evidence; another deep run consumes usage.");
|
|
5399
5815
|
return controller.signal.aborted ? 130 : results.length > 0 && results.every((row) => row.status === "PASS") ? 0 : 2;
|
|
5400
5816
|
}
|
|
5401
5817
|
|
|
@@ -5411,6 +5827,10 @@ const models = defineCommand({
|
|
|
5411
5827
|
type: "boolean",
|
|
5412
5828
|
description: "Send real inference probes through an isolated relay pipeline; consumes Copilot usage."
|
|
5413
5829
|
},
|
|
5830
|
+
details: {
|
|
5831
|
+
type: "boolean",
|
|
5832
|
+
description: "Show safe request, route and replay evidence for each probe (requires --deep)."
|
|
5833
|
+
},
|
|
5414
5834
|
model: {
|
|
5415
5835
|
type: "string",
|
|
5416
5836
|
description: "Test only this exact upstream ID (requires --deep)."
|
|
@@ -5441,29 +5861,36 @@ const models = defineCommand({
|
|
|
5441
5861
|
if (!/^\d+$/.test(value) || !Number.isSafeInteger(number) || number <= 0 || number > 2147483) throw new Error("invalid option");
|
|
5442
5862
|
return number;
|
|
5443
5863
|
};
|
|
5444
|
-
if (!args.deep && [
|
|
5864
|
+
if (!args.deep && (args.details || [
|
|
5445
5865
|
args.model,
|
|
5446
5866
|
args.effort,
|
|
5447
5867
|
args["max-tokens"],
|
|
5448
5868
|
args.timeout,
|
|
5449
5869
|
args["total-timeout"]
|
|
5450
|
-
].some((value) => value !== void 0)) throw new Error("deep required");
|
|
5870
|
+
].some((value) => value !== void 0))) throw new Error("deep required");
|
|
5451
5871
|
if (args.effort !== void 0 && !isReasoningEffort(args.effort)) throw new Error("invalid effort");
|
|
5452
5872
|
const probeOptions = {
|
|
5453
5873
|
maxTokens: positive(args["max-tokens"], 4096),
|
|
5454
5874
|
timeoutMs: positive(args.timeout, 30) * 1e3,
|
|
5455
5875
|
totalTimeoutMs: positive(args["total-timeout"], 300) * 1e3,
|
|
5456
|
-
effort: args.effort
|
|
5876
|
+
effort: args.effort,
|
|
5877
|
+
details: Boolean(args.details)
|
|
5457
5878
|
};
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5879
|
+
const prepare = async () => {
|
|
5880
|
+
failure = "Could not load relay configuration";
|
|
5881
|
+
const appConfig = await readAppConfig();
|
|
5882
|
+
setLogLevel(appConfig.logLevel);
|
|
5883
|
+
registerSensitiveOrigin(appConfig.copilotBaseUrl);
|
|
5884
|
+
const config$1 = readProxyConfig(appConfig);
|
|
5885
|
+
failure = "Could not authenticate with GitHub Copilot";
|
|
5886
|
+
await setupProxyAuth(config$1, args.deep ? { onDeviceCode: (url, code) => console.error(`Sign in: open ${terminalText(url)} and enter ${terminalText(code)}.`) } : void 0);
|
|
5887
|
+
failure = "Could not fetch upstream model catalog";
|
|
5888
|
+
return {
|
|
5889
|
+
config: config$1,
|
|
5890
|
+
catalog: await loadCopilotModelCatalog(config$1)
|
|
5891
|
+
};
|
|
5892
|
+
};
|
|
5893
|
+
const { config, catalog } = args.deep ? await withoutConsoleLogging(prepare) : await prepare();
|
|
5467
5894
|
if (args.deep) {
|
|
5468
5895
|
failure = "Selected model is not advertised by upstream";
|
|
5469
5896
|
if (args.model !== void 0 && !catalog.models.has(args.model)) throw new Error("unknown model");
|
|
@@ -5483,6 +5910,8 @@ const models = defineCommand({
|
|
|
5483
5910
|
}
|
|
5484
5911
|
log.error(`${failure}: ${detail}`);
|
|
5485
5912
|
process.exitCode = 1;
|
|
5913
|
+
} finally {
|
|
5914
|
+
await flushLogs();
|
|
5486
5915
|
}
|
|
5487
5916
|
}
|
|
5488
5917
|
});
|
|
@@ -5956,6 +6385,7 @@ const replayCapture = async (target) => {
|
|
|
5956
6385
|
return operation;
|
|
5957
6386
|
};
|
|
5958
6387
|
await withoutLogging(() => withRuntimeState(runtime, () => withRecordedTransport({
|
|
6388
|
+
requestId: manifest.requestId,
|
|
5959
6389
|
fetch: async (request) => {
|
|
5960
6390
|
const operation = take("fetch");
|
|
5961
6391
|
if (operation.kind !== "fetch") throw new Error("Unrecorded replay attempt");
|
|
@@ -6083,23 +6513,45 @@ const commandArgs = (command) => {
|
|
|
6083
6513
|
if (tokens.join(" ") !== command.trim().replace(/\s+(?=(?:[^"']*["'][^"']*["'])*[^"']*$)/g, " ")) return [];
|
|
6084
6514
|
return tokens.map((token) => token.replace(/^(["'])(.*)\1$/, "$2").replaceAll("\\", "/"));
|
|
6085
6515
|
};
|
|
6086
|
-
const
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
const isDaemonCommand = (value) => value === "start" || value === "restart";
|
|
6090
|
-
if (/^copilot-relay(?:\.cmd|\.exe)?$/i.test(executable)) return isDaemonCommand(args[1]);
|
|
6091
|
-
if (!/^node(?:\.exe)?$/i.test(executable)) return false;
|
|
6516
|
+
const isDaemonCommand = (value) => value === "start" || value === "restart";
|
|
6517
|
+
const nodeDaemonEntry = (args) => {
|
|
6518
|
+
if (!/^node(?:\.exe)?$/i.test(posix.basename(args[0] ?? ""))) return void 0;
|
|
6092
6519
|
let index = 1;
|
|
6093
6520
|
if (args[index] === "--import" && args[index + 1] === "tsx") index += 2;
|
|
6094
6521
|
else if (args[index] === "--import=tsx") index++;
|
|
6095
6522
|
const entry = args[index];
|
|
6096
|
-
|
|
6097
|
-
|
|
6523
|
+
return entry && isDaemonCommand(args[index + 1]) ? entry : void 0;
|
|
6524
|
+
};
|
|
6525
|
+
const resolveEntry = (entry, cwd) => {
|
|
6098
6526
|
const normalizedCwd = cwd?.replaceAll("\\", "/");
|
|
6099
6527
|
const absolute = posix.isAbsolute(entry) || /^[A-Za-z]:\//.test(entry);
|
|
6100
|
-
if (!absolute && !normalizedCwd) return
|
|
6101
|
-
|
|
6102
|
-
|
|
6528
|
+
if (!absolute && !normalizedCwd) return void 0;
|
|
6529
|
+
return posix.normalize(absolute ? entry : `${normalizedCwd}/${entry}`);
|
|
6530
|
+
};
|
|
6531
|
+
const isRelayStartProcess = (command, cwd) => {
|
|
6532
|
+
const args = commandArgs(command);
|
|
6533
|
+
if (/^copilot-relay(?:\.cmd|\.exe)?$/i.test(posix.basename(args[0] ?? ""))) return isDaemonCommand(args[1]);
|
|
6534
|
+
const entry = nodeDaemonEntry(args);
|
|
6535
|
+
if (!entry) return false;
|
|
6536
|
+
if (posix.basename(entry) === "copilot-relay") return true;
|
|
6537
|
+
const resolved = resolveEntry(entry, cwd);
|
|
6538
|
+
return resolved !== void 0 && /(?:^|\/)copilot-relay(?:-[A-Za-z0-9._-]+)?\/(?:dist\/main\.js|src\/main\.ts)$/.test(resolved);
|
|
6539
|
+
};
|
|
6540
|
+
const packagedEntryCandidate = (command, cwd) => {
|
|
6541
|
+
const entry = nodeDaemonEntry(commandArgs(command));
|
|
6542
|
+
const resolved = entry === void 0 ? void 0 : resolveEntry(entry, cwd);
|
|
6543
|
+
return resolved && /\/(?:dist\/main\.js|src\/main\.ts)$/.test(resolved) ? resolved : void 0;
|
|
6544
|
+
};
|
|
6545
|
+
const packageEntryProof = async (entry) => {
|
|
6546
|
+
try {
|
|
6547
|
+
if (!(await fs.stat(entry)).isFile()) return "nonrelay";
|
|
6548
|
+
const canonical = (await fs.realpath(entry)).replaceAll("\\", "/");
|
|
6549
|
+
if (!/\/(?:dist\/main\.js|src\/main\.ts)$/.test(canonical)) return "nonrelay";
|
|
6550
|
+
const manifest = JSON.parse(await fs.readFile(posix.join(posix.dirname(posix.dirname(canonical)), "package.json"), "utf8"));
|
|
6551
|
+
return typeof manifest === "object" && manifest !== null && "name" in manifest && manifest.name === "copilot-relay" ? "relay" : "nonrelay";
|
|
6552
|
+
} catch {
|
|
6553
|
+
return "nonrelay";
|
|
6554
|
+
}
|
|
6103
6555
|
};
|
|
6104
6556
|
var RelayInspectionError = class extends Error {
|
|
6105
6557
|
constructor(pid) {
|
|
@@ -6133,6 +6585,8 @@ const flatInvocation = (command) => {
|
|
|
6133
6585
|
};
|
|
6134
6586
|
const verifyCommand = async (command, cwd) => {
|
|
6135
6587
|
if (isRelayStartProcess(command, cwd)) return "relay";
|
|
6588
|
+
const packaged = packagedEntryCandidate(command, cwd);
|
|
6589
|
+
if (packaged && await packageEntryProof(packaged) === "relay") return "relay";
|
|
6136
6590
|
const flat = flatInvocation(command);
|
|
6137
6591
|
if (!flat) return "nonrelay";
|
|
6138
6592
|
if (!cwd) return "unknown";
|
|
@@ -6183,16 +6637,6 @@ const processState = (pid) => {
|
|
|
6183
6637
|
}
|
|
6184
6638
|
};
|
|
6185
6639
|
const isProcessAlive = (pid) => processState(pid) !== "gone";
|
|
6186
|
-
/**
|
|
6187
|
-
* Full pid-file record, for callers that need the listening address or start
|
|
6188
|
-
* time rather than just the pid.
|
|
6189
|
-
*
|
|
6190
|
-
* Deliberately tolerant: a corrupt or half-written pid file yields undefined
|
|
6191
|
-
* rather than throwing, because `status` must be able to report "not running"
|
|
6192
|
-
* instead of crashing on a file the user may have been mid-edit on.
|
|
6193
|
-
*
|
|
6194
|
-
* Returns undefined for the legacy bare-pid format, which carries no address.
|
|
6195
|
-
*/
|
|
6196
6640
|
const readRelayPidFileEntry = async () => {
|
|
6197
6641
|
try {
|
|
6198
6642
|
const trimmed = (await fs.readFile(paths.pidPath, "utf8")).trim();
|
|
@@ -6395,7 +6839,7 @@ const findRelayProcesses = async (config, snapshot) => {
|
|
|
6395
6839
|
if (typeof config.port === "number" && Number.isInteger(config.port) && config.port > 0 && config.port <= 65535) for (const pid of await getPortListenerPids(config.port)) candidates.add(pid);
|
|
6396
6840
|
for (const { command, pid } of await getProcessList()) {
|
|
6397
6841
|
if (pid === process.pid) continue;
|
|
6398
|
-
if (isRelayStartProcess(command) || flatInvocation(command) || isRelayStartProcess(command, "/copilot-relay")) candidates.add(pid);
|
|
6842
|
+
if (isRelayStartProcess(command) || flatInvocation(command) || isRelayStartProcess(command, "/copilot-relay") || packagedEntryCandidate(command)) candidates.add(pid);
|
|
6399
6843
|
}
|
|
6400
6844
|
const relays = [];
|
|
6401
6845
|
for (const pid of candidates) {
|
|
@@ -6844,20 +7288,20 @@ const renderConfig = (config, running) => {
|
|
|
6844
7288
|
* Pure state-to-lines mapping, kept free of IO so it can be tested directly
|
|
6845
7289
|
* without spawning a relay or opening a socket.
|
|
6846
7290
|
*/
|
|
6847
|
-
const renderStatus = (status$1) => {
|
|
7291
|
+
const renderStatus = (status$1, color = false) => {
|
|
6848
7292
|
const lines = [`copilot-relay ${status$1.version}`];
|
|
6849
7293
|
if (!status$1.running) {
|
|
6850
|
-
lines.push(
|
|
7294
|
+
lines.push(` process ${colorText("not running", "warning", color)}`, ` log ${status$1.logPath}`, ` config ${status$1.configPath}`, ...renderConfig(status$1.config, false), "", " Start it with: copilot-relay start");
|
|
6851
7295
|
return lines;
|
|
6852
7296
|
}
|
|
6853
|
-
lines.push(` process running (pid ${status$1.pid}, up ${formatUptime(status$1.startedAt)})`);
|
|
6854
|
-
lines.push(status$1.daemonVersion === void 0 ?
|
|
7297
|
+
lines.push(` process ${colorText("running", "good", color)} (pid ${status$1.pid}, up ${formatUptime(status$1.startedAt)})`);
|
|
7298
|
+
lines.push(status$1.daemonVersion === void 0 ? ` version ${colorText("unknown", "muted", color)} (daemon predates version reporting — restart to report it)` : hasVersionMismatch(status$1) ? ` version ${status$1.daemonVersion} — ${colorText("MISMATCH", "warning", color)}, ${status$1.version} is installed` : ` version ${status$1.daemonVersion}`);
|
|
6855
7299
|
lines.push(` listening ${status$1.baseUrl}`);
|
|
6856
7300
|
const health = status$1.health;
|
|
6857
|
-
lines.push(health?.ok ? ` health ok (${health.ms}ms)` : ` health FAILED${health?.detail ? ` — ${health.detail}` : ""}`);
|
|
6858
|
-
lines.push(status$1.models?.length ? ` models ${status$1.models.join(", ")}` :
|
|
6859
|
-
if (status$1.deep) lines.push(status$1.deep.ok ? ` upstream ok (${status$1.deep.ms}ms) — ${status$1.deep.detail ?? "end-to-end Copilot round trip"}` : ` upstream FAILED${status$1.deep.detail ? ` — ${status$1.deep.detail}` : ""}`);
|
|
6860
|
-
else lines.push(
|
|
7301
|
+
lines.push(health?.ok ? ` health ${colorText("ok", "good", color)} (${health.ms}ms)` : ` health ${colorText("FAILED", "bad", color)}${health?.detail ? ` — ${health.detail}` : ""}`);
|
|
7302
|
+
lines.push(status$1.models?.length ? ` models ${status$1.models.join(", ")}` : ` models ${colorText("unavailable", "warning", color)}`);
|
|
7303
|
+
if (status$1.deep) lines.push(status$1.deep.ok ? ` upstream ${colorText("ok", "good", color)} (${status$1.deep.ms}ms) — ${status$1.deep.detail ?? "end-to-end Copilot round trip"}` : ` upstream ${colorText("FAILED", "bad", color)}${status$1.deep.detail ? ` — ${status$1.deep.detail}` : ""}`);
|
|
7304
|
+
else lines.push(` upstream ${colorText("not checked", "muted", color)} (use --deep)`);
|
|
6861
7305
|
lines.push(` log ${status$1.logPath}`, ` config ${status$1.configPath}`, ...renderConfig(status$1.config, true));
|
|
6862
7306
|
if (status$1.health?.ok && status$1.deep && !status$1.deep.ok) lines.push("", " The relay is listening but cannot reach Copilot.", " Try: copilot-relay auth");
|
|
6863
7307
|
if (hasVersionMismatch(status$1)) lines.push("", ` The running relay is ${status$1.daemonVersion}; ${status$1.version} is installed.`, " Restart it to serve the installed version: copilot-relay restart");
|
|
@@ -7039,7 +7483,7 @@ const status = defineCommand({
|
|
|
7039
7483
|
return;
|
|
7040
7484
|
}
|
|
7041
7485
|
if (args.json) console.log(scrubSensitiveUrls(JSON.stringify(result, null, 2)));
|
|
7042
|
-
else console.log(scrubSensitiveUrls(renderStatus(result).join("\n")));
|
|
7486
|
+
else console.log(scrubSensitiveUrls(renderStatus(result, colorEnabled()).join("\n")));
|
|
7043
7487
|
process.exitCode = resolveExitCode(result);
|
|
7044
7488
|
}
|
|
7045
7489
|
});
|