copilot-relay 0.4.1 → 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 +529 -106
- 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,6 +2445,145 @@ 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
|
|
2302
2589
|
const isEffortOnlyControl = (control) => control !== null && typeof control === "object" && !Array.isArray(control) && Object.keys(control).length === 1 && "effort" in control && isConfiguredReasoningEffort(control.effort);
|
|
@@ -2324,6 +2611,43 @@ function mapOpenAIStopReasonToClaude(finishReason) {
|
|
|
2324
2611
|
content_filter: "refusal"
|
|
2325
2612
|
}[finishReason];
|
|
2326
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
|
+
}
|
|
2327
2651
|
|
|
2328
2652
|
//#endregion
|
|
2329
2653
|
//#region src/claude/tool-names.ts
|
|
@@ -2638,17 +2962,15 @@ function getClaudeTextBlocks(messageContent) {
|
|
|
2638
2962
|
}
|
|
2639
2963
|
function getClaudeToolUseBlocks(toolCalls, toolNameMapper) {
|
|
2640
2964
|
if (!toolCalls) return [];
|
|
2641
|
-
return toolCalls.map((toolCall) =>
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Upstream tool input must be a JSON object.");
|
|
2651
|
-
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
|
+
});
|
|
2652
2974
|
}
|
|
2653
2975
|
|
|
2654
2976
|
//#endregion
|
|
@@ -2752,8 +3074,8 @@ function translateChunkToClaudeEvents(chunk, state, toolNameMapper = createClaud
|
|
|
2752
3074
|
closeOpenContentBlock(events$1, state);
|
|
2753
3075
|
if (choice.finish_reason === "tool_calls") for (const toolCall of Object.values(state.toolCalls)) {
|
|
2754
3076
|
const argumentsText = toolCall.arguments ?? "";
|
|
2755
|
-
|
|
2756
|
-
|
|
3077
|
+
parseUpstreamToolInput(toolCall.name, argumentsText);
|
|
3078
|
+
const partialJson = argumentsText.trim() ? argumentsText : "{}";
|
|
2757
3079
|
const index = state.contentBlockIndex++;
|
|
2758
3080
|
toolCall.claudeBlockIndex = index;
|
|
2759
3081
|
events$1.push({
|
|
@@ -2770,7 +3092,7 @@ function translateChunkToClaudeEvents(chunk, state, toolNameMapper = createClaud
|
|
|
2770
3092
|
index,
|
|
2771
3093
|
delta: {
|
|
2772
3094
|
type: "input_json_delta",
|
|
2773
|
-
partial_json:
|
|
3095
|
+
partial_json: partialJson
|
|
2774
3096
|
}
|
|
2775
3097
|
}, {
|
|
2776
3098
|
type: "content_block_stop",
|
|
@@ -2792,12 +3114,12 @@ function translateChunkToClaudeEvents(chunk, state, toolNameMapper = createClaud
|
|
|
2792
3114
|
}
|
|
2793
3115
|
return events$1;
|
|
2794
3116
|
}
|
|
2795
|
-
function translateErrorToClaudeErrorEvent() {
|
|
3117
|
+
function translateErrorToClaudeErrorEvent(error) {
|
|
2796
3118
|
return {
|
|
2797
3119
|
type: "error",
|
|
2798
3120
|
error: {
|
|
2799
3121
|
type: "api_error",
|
|
2800
|
-
message: "An unexpected error occurred during streaming."
|
|
3122
|
+
message: error instanceof UpstreamToolInputError ? error.message : "An unexpected error occurred during streaming."
|
|
2801
3123
|
}
|
|
2802
3124
|
};
|
|
2803
3125
|
}
|
|
@@ -5030,8 +5352,25 @@ claudeRoutes.post("/messages", async (c) => {
|
|
|
5030
5352
|
} catch (error) {
|
|
5031
5353
|
if (!(error instanceof HTTPError)) throw error;
|
|
5032
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");
|
|
5033
5357
|
log.error(`request_id=${requestId} ${error.message}`);
|
|
5034
|
-
|
|
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
|
+
});
|
|
5035
5374
|
}
|
|
5036
5375
|
if (claudePayload.stream) {
|
|
5037
5376
|
const trace = c.get("requestTrace");
|
|
@@ -5046,7 +5385,10 @@ claudeRoutes.post("/messages", async (c) => {
|
|
|
5046
5385
|
await handleClaudeMessageRequest(config, claudePayload, requestSignal, requestId, writeEvent, c.req.raw.headers);
|
|
5047
5386
|
} catch (error) {
|
|
5048
5387
|
log.error(`request_id=${requestId} Error during Claude stream request:`, error);
|
|
5049
|
-
|
|
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);
|
|
5050
5392
|
} finally {
|
|
5051
5393
|
trace?.handlerSettled();
|
|
5052
5394
|
log.info(`request_id=${requestId} stream completed ${Math.round(performance.now() - streamStarted)}ms`);
|
|
@@ -5064,6 +5406,7 @@ claudeRoutes.post("/messages", async (c) => {
|
|
|
5064
5406
|
} }, 501);
|
|
5065
5407
|
}
|
|
5066
5408
|
if (error instanceof HTTPError) {
|
|
5409
|
+
if (error instanceof UpstreamToolInputError) c.get("requestTrace")?.recordFailure("invalid-tool-input");
|
|
5067
5410
|
const text$1 = await error.response.text().catch(() => "");
|
|
5068
5411
|
c.set("requestErrorMessage", error.detail ?? text$1.slice(0, 240));
|
|
5069
5412
|
return new Response(text$1, {
|
|
@@ -5154,7 +5497,7 @@ const createServer = (config) => {
|
|
|
5154
5497
|
const { host, port } = config;
|
|
5155
5498
|
const configuredHost = parseAuthority(host.includes(":") && !host.startsWith("[") ? `[${host}]` : host, "http:")?.hostname;
|
|
5156
5499
|
app.use("*", async (c, next) => {
|
|
5157
|
-
const requestId = randomUUID();
|
|
5500
|
+
const requestId = getReplayRequestId() ?? randomUUID();
|
|
5158
5501
|
c.set("config", config);
|
|
5159
5502
|
c.set("requestId", requestId);
|
|
5160
5503
|
c.header("x-copilot-relay-request-id", requestId);
|
|
@@ -5163,6 +5506,7 @@ const createServer = (config) => {
|
|
|
5163
5506
|
try {
|
|
5164
5507
|
await next();
|
|
5165
5508
|
} finally {
|
|
5509
|
+
c.header("x-copilot-relay-request-id", requestId);
|
|
5166
5510
|
const ms = Math.round(performance.now() - started);
|
|
5167
5511
|
log.info(formatStatusLog(requestId, c.req.method, c.req.path, c.res.status, ms, c.get("requestErrorMessage")));
|
|
5168
5512
|
}
|
|
@@ -5222,7 +5566,8 @@ const createServer = (config) => {
|
|
|
5222
5566
|
requestId: c.get("requestId"),
|
|
5223
5567
|
error
|
|
5224
5568
|
});
|
|
5225
|
-
|
|
5569
|
+
c.get("requestTrace")?.recordFailure("internal-error");
|
|
5570
|
+
return c.json({ error: { message: `Internal server error (request_id=${c.get("requestId")})` } }, 500);
|
|
5226
5571
|
});
|
|
5227
5572
|
app.get("/", (c) => c.json({
|
|
5228
5573
|
name: "copilot-relay",
|
|
@@ -5279,9 +5624,34 @@ const httpCategory = (status$1) => {
|
|
|
5279
5624
|
if (status$1 === 504) return "upstream-timeout-or-HTTP-504";
|
|
5280
5625
|
return `HTTP-${status$1}`;
|
|
5281
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
|
+
};
|
|
5282
5651
|
async function probeModels(config, entries, options) {
|
|
5283
|
-
console.log(
|
|
5284
|
-
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`);
|
|
5285
5655
|
const controller = new AbortController();
|
|
5286
5656
|
const interrupt = () => controller.abort();
|
|
5287
5657
|
process.on("SIGINT", interrupt);
|
|
@@ -5294,21 +5664,14 @@ async function probeModels(config, entries, options) {
|
|
|
5294
5664
|
};
|
|
5295
5665
|
const results = [];
|
|
5296
5666
|
const app = createServer(config);
|
|
5297
|
-
const
|
|
5298
|
-
const
|
|
5299
|
-
|
|
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));
|
|
5300
5671
|
try {
|
|
5301
5672
|
runtimeState.modelCatalog = config.modelCatalog;
|
|
5302
5673
|
runtimeState.upstreamBaseUrl = config.copilotBaseUrl;
|
|
5303
5674
|
for (const [id, model] of entries) {
|
|
5304
|
-
const row = {
|
|
5305
|
-
id,
|
|
5306
|
-
status: "NOT_TESTED",
|
|
5307
|
-
reported: "-",
|
|
5308
|
-
sent: false,
|
|
5309
|
-
latency: 0,
|
|
5310
|
-
detail: ""
|
|
5311
|
-
};
|
|
5312
5675
|
const efforts = [
|
|
5313
5676
|
"none",
|
|
5314
5677
|
"low",
|
|
@@ -5321,6 +5684,18 @@ async function probeModels(config, entries, options) {
|
|
|
5321
5684
|
const maxTokens = Math.min(options.maxTokens, model.limits?.max_output_tokens ?? options.maxTokens, model.limits?.max_non_streaming_output_tokens ?? options.maxTokens);
|
|
5322
5685
|
const timeoutMs = Math.min(options.timeoutMs, config.upstreamTimeoutMs > 0 ? config.upstreamTimeoutMs : Infinity);
|
|
5323
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
|
+
};
|
|
5324
5699
|
if (controller.signal.aborted || total.aborted) row.detail = controller.signal.aborted ? "cancelled" : "total-deadline";
|
|
5325
5700
|
else if (!safeId(id, config.copilotToken) || normalizeCopilotModelId(id) !== id) {
|
|
5326
5701
|
row.status = "SKIPPED";
|
|
@@ -5346,23 +5721,33 @@ async function probeModels(config, entries, options) {
|
|
|
5346
5721
|
};
|
|
5347
5722
|
const started = performance.now();
|
|
5348
5723
|
row.sent = true;
|
|
5724
|
+
let trace;
|
|
5725
|
+
let completedFetch = false;
|
|
5349
5726
|
try {
|
|
5350
|
-
const response = await withoutLogging(() =>
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
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;
|
|
5366
5751
|
row.status = "FAIL";
|
|
5367
5752
|
if (signal.aborted) {
|
|
5368
5753
|
row.status = controller.signal.aborted || total.aborted ? "NOT_TESTED" : "FAIL";
|
|
@@ -5385,7 +5770,7 @@ async function probeModels(config, entries, options) {
|
|
|
5385
5770
|
row.status = "INCOMPLETE";
|
|
5386
5771
|
const tokens = record$1(body.usage) ? body.usage.output_tokens : void 0;
|
|
5387
5772
|
row.detail = typeof tokens === "number" && Number.isSafeInteger(tokens) && tokens > 0 ? "reachable-output-budget-exhausted" : "output-budget-exhausted-usage-unreported";
|
|
5388
|
-
} 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";
|
|
5389
5774
|
else if (body.content.some((part) => record$1(part) && part.type === "text" && typeof part.text === "string" && part.text.trim())) {
|
|
5390
5775
|
row.status = "PASS";
|
|
5391
5776
|
row.detail = "completed-text";
|
|
@@ -5396,11 +5781,19 @@ async function probeModels(config, entries, options) {
|
|
|
5396
5781
|
row.detail = controller.signal.aborted ? "cancelled" : total.aborted ? "total-deadline" : signal.aborted ? "probe-timeout" : "network-or-invalid-response";
|
|
5397
5782
|
}
|
|
5398
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
|
+
}
|
|
5399
5790
|
}
|
|
5400
|
-
|
|
5791
|
+
if (!safeId(id, config.copilotToken)) row.id = "[unsupported ID]";
|
|
5401
5792
|
results.push(row);
|
|
5402
|
-
const
|
|
5403
|
-
|
|
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}`));
|
|
5404
5797
|
}
|
|
5405
5798
|
} finally {
|
|
5406
5799
|
for (const key of [
|
|
@@ -5414,9 +5807,11 @@ async function probeModels(config, entries, options) {
|
|
|
5414
5807
|
process.off("SIGINT", interrupt);
|
|
5415
5808
|
process.off("SIGTERM", interrupt);
|
|
5416
5809
|
}
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
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.");
|
|
5420
5815
|
return controller.signal.aborted ? 130 : results.length > 0 && results.every((row) => row.status === "PASS") ? 0 : 2;
|
|
5421
5816
|
}
|
|
5422
5817
|
|
|
@@ -5432,6 +5827,10 @@ const models = defineCommand({
|
|
|
5432
5827
|
type: "boolean",
|
|
5433
5828
|
description: "Send real inference probes through an isolated relay pipeline; consumes Copilot usage."
|
|
5434
5829
|
},
|
|
5830
|
+
details: {
|
|
5831
|
+
type: "boolean",
|
|
5832
|
+
description: "Show safe request, route and replay evidence for each probe (requires --deep)."
|
|
5833
|
+
},
|
|
5435
5834
|
model: {
|
|
5436
5835
|
type: "string",
|
|
5437
5836
|
description: "Test only this exact upstream ID (requires --deep)."
|
|
@@ -5462,29 +5861,36 @@ const models = defineCommand({
|
|
|
5462
5861
|
if (!/^\d+$/.test(value) || !Number.isSafeInteger(number) || number <= 0 || number > 2147483) throw new Error("invalid option");
|
|
5463
5862
|
return number;
|
|
5464
5863
|
};
|
|
5465
|
-
if (!args.deep && [
|
|
5864
|
+
if (!args.deep && (args.details || [
|
|
5466
5865
|
args.model,
|
|
5467
5866
|
args.effort,
|
|
5468
5867
|
args["max-tokens"],
|
|
5469
5868
|
args.timeout,
|
|
5470
5869
|
args["total-timeout"]
|
|
5471
|
-
].some((value) => value !== void 0)) throw new Error("deep required");
|
|
5870
|
+
].some((value) => value !== void 0))) throw new Error("deep required");
|
|
5472
5871
|
if (args.effort !== void 0 && !isReasoningEffort(args.effort)) throw new Error("invalid effort");
|
|
5473
5872
|
const probeOptions = {
|
|
5474
5873
|
maxTokens: positive(args["max-tokens"], 4096),
|
|
5475
5874
|
timeoutMs: positive(args.timeout, 30) * 1e3,
|
|
5476
5875
|
totalTimeoutMs: positive(args["total-timeout"], 300) * 1e3,
|
|
5477
|
-
effort: args.effort
|
|
5876
|
+
effort: args.effort,
|
|
5877
|
+
details: Boolean(args.details)
|
|
5478
5878
|
};
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
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();
|
|
5488
5894
|
if (args.deep) {
|
|
5489
5895
|
failure = "Selected model is not advertised by upstream";
|
|
5490
5896
|
if (args.model !== void 0 && !catalog.models.has(args.model)) throw new Error("unknown model");
|
|
@@ -5504,6 +5910,8 @@ const models = defineCommand({
|
|
|
5504
5910
|
}
|
|
5505
5911
|
log.error(`${failure}: ${detail}`);
|
|
5506
5912
|
process.exitCode = 1;
|
|
5913
|
+
} finally {
|
|
5914
|
+
await flushLogs();
|
|
5507
5915
|
}
|
|
5508
5916
|
}
|
|
5509
5917
|
});
|
|
@@ -5977,6 +6385,7 @@ const replayCapture = async (target) => {
|
|
|
5977
6385
|
return operation;
|
|
5978
6386
|
};
|
|
5979
6387
|
await withoutLogging(() => withRuntimeState(runtime, () => withRecordedTransport({
|
|
6388
|
+
requestId: manifest.requestId,
|
|
5980
6389
|
fetch: async (request) => {
|
|
5981
6390
|
const operation = take("fetch");
|
|
5982
6391
|
if (operation.kind !== "fetch") throw new Error("Unrecorded replay attempt");
|
|
@@ -6104,23 +6513,45 @@ const commandArgs = (command) => {
|
|
|
6104
6513
|
if (tokens.join(" ") !== command.trim().replace(/\s+(?=(?:[^"']*["'][^"']*["'])*[^"']*$)/g, " ")) return [];
|
|
6105
6514
|
return tokens.map((token) => token.replace(/^(["'])(.*)\1$/, "$2").replaceAll("\\", "/"));
|
|
6106
6515
|
};
|
|
6107
|
-
const
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
const isDaemonCommand = (value) => value === "start" || value === "restart";
|
|
6111
|
-
if (/^copilot-relay(?:\.cmd|\.exe)?$/i.test(executable)) return isDaemonCommand(args[1]);
|
|
6112
|
-
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;
|
|
6113
6519
|
let index = 1;
|
|
6114
6520
|
if (args[index] === "--import" && args[index + 1] === "tsx") index += 2;
|
|
6115
6521
|
else if (args[index] === "--import=tsx") index++;
|
|
6116
6522
|
const entry = args[index];
|
|
6117
|
-
|
|
6118
|
-
|
|
6523
|
+
return entry && isDaemonCommand(args[index + 1]) ? entry : void 0;
|
|
6524
|
+
};
|
|
6525
|
+
const resolveEntry = (entry, cwd) => {
|
|
6119
6526
|
const normalizedCwd = cwd?.replaceAll("\\", "/");
|
|
6120
6527
|
const absolute = posix.isAbsolute(entry) || /^[A-Za-z]:\//.test(entry);
|
|
6121
|
-
if (!absolute && !normalizedCwd) return
|
|
6122
|
-
|
|
6123
|
-
|
|
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
|
+
}
|
|
6124
6555
|
};
|
|
6125
6556
|
var RelayInspectionError = class extends Error {
|
|
6126
6557
|
constructor(pid) {
|
|
@@ -6154,6 +6585,8 @@ const flatInvocation = (command) => {
|
|
|
6154
6585
|
};
|
|
6155
6586
|
const verifyCommand = async (command, cwd) => {
|
|
6156
6587
|
if (isRelayStartProcess(command, cwd)) return "relay";
|
|
6588
|
+
const packaged = packagedEntryCandidate(command, cwd);
|
|
6589
|
+
if (packaged && await packageEntryProof(packaged) === "relay") return "relay";
|
|
6157
6590
|
const flat = flatInvocation(command);
|
|
6158
6591
|
if (!flat) return "nonrelay";
|
|
6159
6592
|
if (!cwd) return "unknown";
|
|
@@ -6204,16 +6637,6 @@ const processState = (pid) => {
|
|
|
6204
6637
|
}
|
|
6205
6638
|
};
|
|
6206
6639
|
const isProcessAlive = (pid) => processState(pid) !== "gone";
|
|
6207
|
-
/**
|
|
6208
|
-
* Full pid-file record, for callers that need the listening address or start
|
|
6209
|
-
* time rather than just the pid.
|
|
6210
|
-
*
|
|
6211
|
-
* Deliberately tolerant: a corrupt or half-written pid file yields undefined
|
|
6212
|
-
* rather than throwing, because `status` must be able to report "not running"
|
|
6213
|
-
* instead of crashing on a file the user may have been mid-edit on.
|
|
6214
|
-
*
|
|
6215
|
-
* Returns undefined for the legacy bare-pid format, which carries no address.
|
|
6216
|
-
*/
|
|
6217
6640
|
const readRelayPidFileEntry = async () => {
|
|
6218
6641
|
try {
|
|
6219
6642
|
const trimmed = (await fs.readFile(paths.pidPath, "utf8")).trim();
|
|
@@ -6416,7 +6839,7 @@ const findRelayProcesses = async (config, snapshot) => {
|
|
|
6416
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);
|
|
6417
6840
|
for (const { command, pid } of await getProcessList()) {
|
|
6418
6841
|
if (pid === process.pid) continue;
|
|
6419
|
-
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);
|
|
6420
6843
|
}
|
|
6421
6844
|
const relays = [];
|
|
6422
6845
|
for (const pid of candidates) {
|
|
@@ -6865,20 +7288,20 @@ const renderConfig = (config, running) => {
|
|
|
6865
7288
|
* Pure state-to-lines mapping, kept free of IO so it can be tested directly
|
|
6866
7289
|
* without spawning a relay or opening a socket.
|
|
6867
7290
|
*/
|
|
6868
|
-
const renderStatus = (status$1) => {
|
|
7291
|
+
const renderStatus = (status$1, color = false) => {
|
|
6869
7292
|
const lines = [`copilot-relay ${status$1.version}`];
|
|
6870
7293
|
if (!status$1.running) {
|
|
6871
|
-
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");
|
|
6872
7295
|
return lines;
|
|
6873
7296
|
}
|
|
6874
|
-
lines.push(` process running (pid ${status$1.pid}, up ${formatUptime(status$1.startedAt)})`);
|
|
6875
|
-
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}`);
|
|
6876
7299
|
lines.push(` listening ${status$1.baseUrl}`);
|
|
6877
7300
|
const health = status$1.health;
|
|
6878
|
-
lines.push(health?.ok ? ` health ok (${health.ms}ms)` : ` health FAILED${health?.detail ? ` — ${health.detail}` : ""}`);
|
|
6879
|
-
lines.push(status$1.models?.length ? ` models ${status$1.models.join(", ")}` :
|
|
6880
|
-
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}` : ""}`);
|
|
6881
|
-
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)`);
|
|
6882
7305
|
lines.push(` log ${status$1.logPath}`, ` config ${status$1.configPath}`, ...renderConfig(status$1.config, true));
|
|
6883
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");
|
|
6884
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");
|
|
@@ -7060,7 +7483,7 @@ const status = defineCommand({
|
|
|
7060
7483
|
return;
|
|
7061
7484
|
}
|
|
7062
7485
|
if (args.json) console.log(scrubSensitiveUrls(JSON.stringify(result, null, 2)));
|
|
7063
|
-
else console.log(scrubSensitiveUrls(renderStatus(result).join("\n")));
|
|
7486
|
+
else console.log(scrubSensitiveUrls(renderStatus(result, colorEnabled()).join("\n")));
|
|
7064
7487
|
process.exitCode = resolveExitCode(result);
|
|
7065
7488
|
}
|
|
7066
7489
|
});
|