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/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 writesToConsole = methodLevel <= consola.level;
427
- if (!writesToFile && !writesToConsole) return fn(...args);
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
- log.info(`Open ${deviceCode.verification_uri} and enter code ${deviceCode.user_code}`);
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 readFileSnapshot = async (filePath) => {
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
- if (!after.isFile() || !sameIdentity(identityOf(before), identityOf(after)) || await resolveTarget(requestedPath) !== resolvedPath) throw new FileConflictError();
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 = resolveReasoningEffort(getRequestReasoningEffort(payload));
2383
- validateClaudeMessages(payload.messages, false, effort);
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, effort) {
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
- const redundantEffort = control !== null && typeof control === "object" && !Array.isArray(control) && Object.keys(control).length === 1 && isConfiguredReasoningEffort(control.effort) && control.effort === effort;
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": return handleSystemPrompt(message.content);
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
- type: "tool_use",
2626
- id: toolCall.id,
2627
- name: toolNameMapper.toClaude(toolCall.function.name),
2628
- input: safeJsonParse(toolCall.function.arguments)
2629
- }));
2630
- }
2631
- function safeJsonParse(input) {
2632
- const parsed = JSON.parse(input);
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
- const input = JSON.parse(argumentsText);
2739
- if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("Upstream tool input must be a JSON object.");
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: argumentsText
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.map((message) => `${message.role}: ${textFromMessageContent(message.content)}`).join("\n\n");
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: resolveReasoningEffort(getRequestReasoningEffort(payload)) },
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, getRequestReasoningEffort(payload) ?? "unset", options.requestId);
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.output_config.effort}`));
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, payload, searchCall.input.query, {
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) => getRequestReasoningEffort(payload) ?? "unset";
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
- const effort = resolveReasoningEffort(getRequestReasoningEffort(claudePayload));
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, false, effort);
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
- return error.response;
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
- await writeEvent(translateErrorToClaudeErrorEvent());
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
- return c.json({ error: { message: "Internal server error" } }, 500);
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("\nDeep availability checks — isolated relay pipeline; not running-daemon health");
5263
- console.log(`Real inference usage: up to ${entries.length} sequential probes, ${options.maxTokens} output tokens each (bounded client retries may add calls).`);
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 width = entries.reduce((width$1, [id]) => Math.max(width$1, safeId(id, config.copilotToken) ? id.length : 16), 5);
5277
- const reportWidth = width * 2 + 6;
5278
- console.log(`${"MODEL".padEnd(width)} STATUS ${"SENT/REPORTED".padEnd(reportWidth)} LATENCY DETAILS`);
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(() => app.fetch(new Request(`http://localhost${config.port ? `:${config.port}` : ""}/v1/messages`, {
5330
- method: "POST",
5331
- headers: { "content-type": "application/json" },
5332
- signal,
5333
- body: JSON.stringify({
5334
- model: id,
5335
- stream: false,
5336
- max_tokens: maxTokens,
5337
- output_config: { effort },
5338
- messages: [{
5339
- role: "user",
5340
- content: "Reply with OK only."
5341
- }]
5342
- })
5343
- })));
5344
- const body = await response.json().catch(() => void 0);
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
- row.detail += ` tokens=${maxTokens} effort=${effort}${model.reasoningEfforts === void 0 ? "(unverified)" : ""}${model.supportedEndpoints === void 0 ? " endpoints=unverified" : ""}`;
5791
+ if (!safeId(id, config.copilotToken)) row.id = "[unsupported ID]";
5380
5792
  results.push(row);
5381
- const displayId = safeId(id, config.copilotToken) ? id : "[unsupported ID]";
5382
- console.log(scrubSensitiveUrls(`${displayId.padEnd(width)} ${row.status.padEnd(10)} ${(row.sent ? `${id}/${row.reported}` : "-/-").padEnd(reportWidth)} ${`${row.latency}ms`.padEnd(8)} ${row.detail}`));
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
- const count = (status$1) => results.filter((row) => row.status === status$1).length;
5397
- console.log(`\nSummary: ${count("PASS")} passed, ${count("FAIL")} failed, ${count("INCOMPLETE")} incomplete, ${count("SKIPPED")} skipped, ${count("NOT_TESTED")} not tested`);
5398
- console.log("PASS means completed text from the reported model, not tool/effort coverage or running-daemon health.");
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
- failure = "Could not load relay configuration";
5459
- const appConfig = await readAppConfig();
5460
- setLogLevel(appConfig.logLevel);
5461
- registerSensitiveOrigin(appConfig.copilotBaseUrl);
5462
- const config = readProxyConfig(appConfig);
5463
- failure = "Could not authenticate with GitHub Copilot";
5464
- await setupProxyAuth(config);
5465
- failure = "Could not fetch upstream model catalog";
5466
- const catalog = await loadCopilotModelCatalog(config);
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 isRelayStartProcess = (command, cwd) => {
6087
- const args = commandArgs(command);
6088
- const executable = posix.basename(args[0] ?? "");
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
- if (!entry || !isDaemonCommand(args[index + 1])) return false;
6097
- if (posix.basename(entry) === "copilot-relay") return true;
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 false;
6101
- const resolved = posix.normalize(absolute ? entry : `${normalizedCwd}/${entry}`);
6102
- return /(?:^|\/)copilot-relay(?:-[A-Za-z0-9._-]+)?\/(?:dist\/main\.js|src\/main\.ts)$/.test(resolved);
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(" process not running", ` log ${status$1.logPath}`, ` config ${status$1.configPath}`, ...renderConfig(status$1.config, false), "", " Start it with: copilot-relay start");
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 ? " version unknown (daemon predates version reporting — restart to report it)" : hasVersionMismatch(status$1) ? ` version ${status$1.daemonVersion} — MISMATCH, ${status$1.version} is installed` : ` version ${status$1.daemonVersion}`);
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(", ")}` : " models unavailable");
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(" upstream not checked (use --deep)");
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
  });