copilot-relay 0.3.7 → 0.3.9

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 CHANGED
@@ -117,16 +117,19 @@ for live model discovery, compatible thinking efforts, and copy-paste updates.
117
117
  | Level | Logs |
118
118
  | --- | --- |
119
119
  | `error` | Startup, preflight, and request failures |
120
- | `info` | Errors plus startup status, preflight status, request IDs, upstream lifecycle, and local HTTP status codes |
121
- | `debug` | Info plus model routing summaries, Copilot upstream timings, and request payloads |
120
+ | `info` | Errors plus startup/preflight status, request IDs, model/effort summaries, upstream lifecycle, and local HTTP status codes |
121
+ | `debug` | Info plus detailed Copilot timings and request payloads |
122
122
 
123
123
  Any other `logLevel` value is invalid and stops startup.
124
124
 
125
- Valid `thinkEffort`: `none`, `low`, `medium`, `high`, `xhigh`, `max`.
125
+ Valid `thinkEffort` defaults: `low`, `medium`, `high`, `xhigh`, `max`.
126
126
  It is a fallback, not a forced override: Claude Code's `output_config.effort`
127
127
  wins, followed by the legacy `reasoning_effort` field, then `thinkEffort` when
128
128
  neither request field supplies a value. The selected effort is preserved through
129
129
  all model and WebSearch passes; existing config values are not rewritten.
130
+ `none` and other invalid explicit defaults fail startup before authentication or
131
+ config write-back, with a clear list of valid choices. They are not silently
132
+ replaced with `max`. Explicit request-level `none` remains model-dependent.
130
133
 
131
134
  `upstreamTimeoutSeconds` controls the maximum time a single Claude request can
132
135
  spend waiting on upstream Copilot calls, including chat, Responses, preflight,
@@ -212,7 +215,10 @@ health probe, so a relay that cannot answer `/healthz` never reports success.
212
215
 
213
216
  ## Logging
214
217
 
215
- At `debug`, every model request logs the requested model, upstream model, requested think effort, requested thinking, and effective think effort.
218
+ At `info`, every model request logs the requested model, upstream model, requested
219
+ think effort, requested thinking, and effective think effort. Missing request
220
+ effort is shown as `unset`, distinct from explicit `none`; full normal request
221
+ payloads remain at `debug`.
216
222
 
217
223
  Upstream failures are logged at `error` with full request and response context in the same log file.
218
224
 
@@ -26,10 +26,9 @@ claudeSetup: true
26
26
 
27
27
  # Log verbosity:
28
28
  # error - startup/preflight/request failures only
29
- # info - error logs plus startup status, preflight status, and local HTTP
30
- # status codes
31
- # debug - info logs plus model routing summaries, Copilot upstream timings,
32
- # and request payloads
29
+ # info - error logs plus startup/preflight status, model/effort summaries,
30
+ # and local HTTP status codes
31
+ # debug - info logs plus detailed Copilot timings and request payloads
33
32
  # Any other value is invalid and stops startup.
34
33
  logLevel: info
35
34
 
@@ -41,7 +40,7 @@ logRetentionDays: 3
41
40
 
42
41
  # Fallback upstream effort when Claude Code does not specify one.
43
42
  # Request output_config.effort wins, followed by legacy reasoning_effort.
44
- # Valid values: none, low, medium, high, xhigh, max.
43
+ # Valid values: low, medium, high, xhigh, max.
45
44
  thinkEffort: max
46
45
 
47
46
  # Max seconds to wait for one Claude request's upstream Copilot calls.
package/dist/main.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { defineCommand, runMain } from "citty";
3
3
  import fs from "node:fs/promises";
4
+ import { createHash, randomUUID } from "node:crypto";
4
5
  import { inspect, promisify } from "node:util";
5
6
  import consola from "consola";
6
7
  import os from "node:os";
@@ -9,7 +10,6 @@ import { fileURLToPath } from "node:url";
9
10
  import { execFile } from "node:child_process";
10
11
  import { readFileSync } from "node:fs";
11
12
  import { createAdaptorServer } from "@hono/node-server";
12
- import { createHash, randomUUID } from "node:crypto";
13
13
  import { Agent, fetch as fetch$1 } from "undici";
14
14
  import { events } from "fetch-event-stream";
15
15
  import { Hono } from "hono";
@@ -407,7 +407,16 @@ const writeStoredCopilotToken = async (input) => {
407
407
  refreshIn: input.refresh_in,
408
408
  token: input.token
409
409
  };
410
- await fs.writeFile(paths.copilotTokenPath, `${JSON.stringify(payload, null, 2)}\n`, { mode: 384 });
410
+ const temporaryPath = `${paths.copilotTokenPath}.${randomUUID()}.tmp`;
411
+ try {
412
+ await fs.writeFile(temporaryPath, `${JSON.stringify(payload, null, 2)}\n`, {
413
+ mode: 384,
414
+ flag: "wx"
415
+ });
416
+ await fs.rename(temporaryPath, paths.copilotTokenPath);
417
+ } finally {
418
+ await fs.rm(temporaryPath, { force: true });
419
+ }
411
420
  };
412
421
  const getStoredTokenRemainingSeconds = (token) => {
413
422
  const ageSeconds = Math.floor((Date.now() - token.refreshedAt) / 1e3);
@@ -460,8 +469,11 @@ const getGitHubUser = async (githubToken, vsCodeVersion) => {
460
469
  if (!response.ok) throw new HTTPError("Failed to get GitHub user", response);
461
470
  return await response.json();
462
471
  };
463
- const getCopilotToken = async (githubToken, vsCodeVersion) => {
464
- const response = await fetch(`${githubApiBaseUrl}/copilot_internal/v2/token`, { headers: githubHeaders(githubToken, vsCodeVersion) });
472
+ const getCopilotToken = async (githubToken, vsCodeVersion, signal) => {
473
+ const response = await fetch(`${githubApiBaseUrl}/copilot_internal/v2/token`, {
474
+ headers: githubHeaders(githubToken, vsCodeVersion),
475
+ signal
476
+ });
465
477
  if (!response.ok) throw new HTTPError("Failed to get Copilot token", response);
466
478
  return await response.json();
467
479
  };
@@ -489,11 +501,21 @@ const loadGitHubLogin = async (githubToken, vsCodeVersion) => {
489
501
  };
490
502
  const setupProxyAuth = async (config, options = {}) => {
491
503
  let githubToken = await ensureGitHubToken(options);
492
- const applyCopilotToken = async () => {
493
- const tokenResponse = await getCopilotToken(githubToken, config.vsCodeVersion);
494
- config.copilotToken = tokenResponse.token;
495
- await writeStoredCopilotToken(tokenResponse);
496
- return tokenResponse.refresh_in;
504
+ let refreshInFlight;
505
+ const applyCopilotToken = () => {
506
+ if (refreshInFlight) return refreshInFlight;
507
+ refreshInFlight = (async () => {
508
+ const tokenResponse = await getCopilotToken(githubToken, config.vsCodeVersion, AbortSignal.timeout(config.upstreamTimeoutMs > 0 ? config.upstreamTimeoutMs : 18e4));
509
+ if (typeof tokenResponse.token !== "string" || !tokenResponse.token.trim() || !Number.isFinite(tokenResponse.refresh_in) || tokenResponse.refresh_in <= 0) throw new Error("Copilot token exchange returned invalid credentials metadata.");
510
+ await writeStoredCopilotToken(tokenResponse);
511
+ config.copilotToken = tokenResponse.token;
512
+ config.copilotTokenGeneration = (config.copilotTokenGeneration ?? 0) + 1;
513
+ scheduleCopilotTokenRefresh(tokenResponse.refresh_in);
514
+ return tokenResponse.refresh_in;
515
+ })().finally(() => {
516
+ refreshInFlight = void 0;
517
+ });
518
+ return refreshInFlight;
497
519
  };
498
520
  const scheduleCopilotTokenRefresh = (refreshIn$1) => {
499
521
  if (copilotTokenRefreshTimer) clearTimeout(copilotTokenRefreshTimer);
@@ -501,11 +523,10 @@ const setupProxyAuth = async (config, options = {}) => {
501
523
  log.info(`Next Copilot token refresh in ${Math.round(refreshMs / 1e3)}s`);
502
524
  copilotTokenRefreshTimer = setTimeout(async () => {
503
525
  try {
504
- const nextRefreshIn = await applyCopilotToken();
526
+ await applyCopilotToken();
505
527
  log.info("Refreshed Copilot token");
506
- scheduleCopilotTokenRefresh(nextRefreshIn);
507
- } catch (error) {
508
- log.error("Failed to refresh Copilot token:", error);
528
+ } catch {
529
+ log.error("Failed to refresh Copilot token; retrying in 60s");
509
530
  scheduleCopilotTokenRefresh(60);
510
531
  }
511
532
  }, refreshMs);
@@ -517,6 +538,7 @@ const setupProxyAuth = async (config, options = {}) => {
517
538
  config.copilotToken = storedToken.token;
518
539
  refreshIn = getStoredTokenRemainingSeconds(storedToken);
519
540
  log.info(`Using cached Copilot token at ${paths.copilotTokenPath}`);
541
+ scheduleCopilotTokenRefresh(refreshIn);
520
542
  } else try {
521
543
  refreshIn = await applyCopilotToken();
522
544
  } catch (error) {
@@ -528,7 +550,14 @@ const setupProxyAuth = async (config, options = {}) => {
528
550
  });
529
551
  refreshIn = await applyCopilotToken();
530
552
  }
531
- scheduleCopilotTokenRefresh(refreshIn);
553
+ config.refreshCopilotToken = async (rejectedToken, generation) => {
554
+ if (config.copilotToken !== rejectedToken || (config.copilotTokenGeneration ?? 0) !== generation) return;
555
+ try {
556
+ await applyCopilotToken();
557
+ } catch {
558
+ throw new Error("Copilot token refresh failed; upstream request was not replayed.");
559
+ }
560
+ };
532
561
  return {
533
562
  githubLogin: await loadGitHubLogin(githubToken, config.vsCodeVersion),
534
563
  source: "github-token"
@@ -541,12 +570,20 @@ const runtimeState = {};
541
570
 
542
571
  //#endregion
543
572
  //#region src/lib/models.ts
573
+ const configurableReasoningEfforts = [
574
+ "low",
575
+ "medium",
576
+ "high",
577
+ "xhigh",
578
+ "max"
579
+ ];
544
580
  const defaultReasoningEffort = "max";
545
581
  const defaultModelRouting = {
546
582
  gptModel: "gpt-6-astra",
547
583
  opusModel: "claude-opus-5"
548
584
  };
549
- const isReasoningEffort = (value) => value === "none" || value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "max";
585
+ const isConfiguredReasoningEffort = (value) => configurableReasoningEfforts.some((effort) => effort === value);
586
+ const isReasoningEffort = (value) => value === "none" || isConfiguredReasoningEffort(value);
550
587
  const invalidRequestEffort = (message) => new HTTPError(message, new Response(JSON.stringify({
551
588
  type: "error",
552
589
  error: {
@@ -736,10 +773,19 @@ const normalizeCopilotBaseUrl = (value) => {
736
773
  if (parsed.username || parsed.password) throw new Error(copilotBaseUrlCredentialsMessage);
737
774
  return trimmed;
738
775
  };
776
+ var InvalidThinkEffortError = class extends Error {
777
+ constructor() {
778
+ super(`Invalid thinkEffort. Valid values: ${configurableReasoningEfforts.join(", ")}. "none" is not allowed as a configured default.`);
779
+ this.name = "InvalidThinkEffortError";
780
+ }
781
+ };
739
782
  const normalizeThinkEffort = (value) => {
740
- if (typeof value !== "string") return;
741
- const normalized = value.toLowerCase() === "minimal" ? "low" : value.toLowerCase();
742
- return isReasoningEffort(normalized) ? normalized : void 0;
783
+ if (value === void 0) return void 0;
784
+ if (typeof value === "string") {
785
+ const normalized = value.toLowerCase() === "minimal" ? "low" : value.toLowerCase();
786
+ if (isConfiguredReasoningEffort(normalized)) return normalized;
787
+ }
788
+ throw new InvalidThinkEffortError();
743
789
  };
744
790
  const normalizeUpstreamTimeoutSeconds = (value) => {
745
791
  if (value === void 0) return;
@@ -858,16 +904,15 @@ const serializeConfig = (config) => [
858
904
  "",
859
905
  "# Log verbosity:",
860
906
  "# error - startup/preflight/request failures only",
861
- "# info - error logs plus startup status, preflight status, and local HTTP",
862
- "# status codes",
863
- "# debug - info logs plus model routing summaries, Copilot upstream timings,",
864
- "# and request payloads",
907
+ "# info - error logs plus startup/preflight status, model/effort summaries,",
908
+ "# and local HTTP status codes",
909
+ "# debug - info logs plus detailed Copilot timings and request payloads",
865
910
  `logLevel: ${config.logLevel}`,
866
911
  "",
867
912
  "# Number of days to keep files in ~/.copilot-relay/logs.",
868
913
  `logRetentionDays: ${config.logRetentionDays}`,
869
914
  "",
870
- "# Fallback effort when the request omits it: none, low, medium, high, xhigh, max.",
915
+ `# Fallback effort when the request omits it: ${configurableReasoningEfforts.join(", ")}.`,
871
916
  `thinkEffort: ${config.thinkEffort}`,
872
917
  "",
873
918
  "# Max seconds for one request's upstream calls; 0 disables the relay deadline.",
@@ -926,7 +971,10 @@ const watchAppConfig = (onReload) => {
926
971
  if (nextMtime === lastMtime) return;
927
972
  lastMtime = nextMtime;
928
973
  onReload(await readAppConfig());
929
- } catch {}
974
+ } catch (error) {
975
+ if (error instanceof InvalidThinkEffortError) log.error(`${error.message} Keeping the previous runtime settings.`);
976
+ else log.error(`Could not reload config. Check ${paths.configPath} for invalid values or file errors.`);
977
+ }
930
978
  }, 1e3);
931
979
  if (typeof timer.unref === "function") timer.unref();
932
980
  getConfigMtime().then((mtime) => {
@@ -1313,7 +1361,13 @@ const copilotDispatcher = new Agent({
1313
1361
  });
1314
1362
  const getCopilotProviderContext = (config) => ({
1315
1363
  baseUrl: config.copilotBaseUrl,
1316
- token: config.copilotToken,
1364
+ get token() {
1365
+ return config.copilotToken;
1366
+ },
1367
+ get tokenGeneration() {
1368
+ return config.copilotTokenGeneration ?? 0;
1369
+ },
1370
+ refreshToken: config.refreshCopilotToken,
1317
1371
  vsCodeVersion: config.vsCodeVersion
1318
1372
  });
1319
1373
  const shouldRetryResponse = (response) => response.status >= 500 && response.status <= 599;
@@ -1379,27 +1433,68 @@ const logUpstreamLifecycle = (requestId, message) => {
1379
1433
  }
1380
1434
  log.debug(message);
1381
1435
  };
1436
+ const waitWithSignal = (pending, signal) => {
1437
+ if (!signal) return pending;
1438
+ if (signal.aborted) {
1439
+ pending.catch(() => {});
1440
+ return Promise.reject(signal.reason);
1441
+ }
1442
+ return new Promise((resolve$1, reject) => {
1443
+ const abort = () => reject(signal.reason);
1444
+ signal.addEventListener("abort", abort, { once: true });
1445
+ pending.then(resolve$1, reject).finally(() => signal.removeEventListener("abort", abort));
1446
+ });
1447
+ };
1448
+ const isAuthRejection = async (response, signal) => {
1449
+ if (response.status === 401) return true;
1450
+ if (response.status !== 403) return false;
1451
+ const reader = response.clone().body?.getReader();
1452
+ if (!reader) return false;
1453
+ const decoder = new TextDecoder();
1454
+ let text = "";
1455
+ let size = 0;
1456
+ try {
1457
+ for (;;) {
1458
+ const chunk = await waitWithSignal(reader.read(), signal);
1459
+ if (chunk.done) return (text + decoder.decode()).trim().toLowerCase() === "forbidden";
1460
+ size += chunk.value.byteLength;
1461
+ if (size > 128) return false;
1462
+ text += decoder.decode(chunk.value, { stream: true });
1463
+ }
1464
+ } catch (error) {
1465
+ if (signal?.aborted) throw error;
1466
+ return false;
1467
+ } finally {
1468
+ reader.cancel().catch(() => {});
1469
+ }
1470
+ };
1382
1471
  const fetchCopilot = async (provider, path$1, init, options = {}) => {
1383
1472
  if (!provider.token) throw new ProxyNotImplementedError("Copilot token is not configured for copilot-relay.");
1384
1473
  const timeoutMs = options.timeoutMs ?? copilotRequestTimeoutMs;
1385
1474
  const signal = createCopilotRequestSignal(options.signal, timeoutMs);
1386
1475
  let lastError;
1387
- for (let attempt = 1; attempt <= maxFetchAttempts; attempt++) {
1476
+ let transientRetries = 0;
1477
+ let authRecoveryUsed = false;
1478
+ for (let attempt = 1; attempt <= maxFetchAttempts + 1; attempt++) {
1388
1479
  const upstreamRequestId = randomUUID();
1480
+ const attemptedToken = provider.token;
1481
+ const attemptedGeneration = provider.tokenGeneration ?? 0;
1482
+ let response;
1389
1483
  try {
1390
1484
  const started = performance.now();
1391
1485
  logUpstreamLifecycle(options.requestId, `send upstream method=${init.method ?? "GET"} path=${path$1} attempt=${attempt} upstream_request_id=${upstreamRequestId}`);
1392
- const response = await fetch$1(`${provider.baseUrl}${path$1}`, {
1486
+ response = await fetch$1(`${provider.baseUrl}${path$1}`, {
1393
1487
  ...init,
1394
- headers: buildHeaders(provider, init, options, upstreamRequestId),
1488
+ headers: buildHeaders({
1489
+ ...provider,
1490
+ token: attemptedToken
1491
+ }, init, options, upstreamRequestId),
1395
1492
  dispatcher: copilotDispatcher,
1396
1493
  signal
1397
1494
  });
1398
1495
  const ms = Math.round(performance.now() - started);
1399
1496
  logUpstreamLifecycle(options.requestId, `return from upstream method=${init.method ?? "GET"} path=${path$1} status=${response.status} ms=${ms} attempt=${attempt} upstream_request_id=${upstreamRequestId}`);
1400
1497
  log.debug(`${formatRequestId(options.requestId)}Copilot ${init.method ?? "GET"} ${path$1} -> ${response.status} ${ms}ms (attempt ${attempt}) upstream_request_id=${upstreamRequestId}`);
1401
- if (!shouldRetryResponse(response) || attempt === maxFetchAttempts) return response;
1402
- log.error(`${formatRequestId(options.requestId)}Copilot ${path$1} returned ${response.status}; retrying (${attempt}/${maxFetchAttempts}) upstream_request_id=${upstreamRequestId}`);
1403
1498
  } catch (error) {
1404
1499
  const abortError = toCopilotAbortHTTPError(error, signal, timeoutMs);
1405
1500
  if (abortError) {
@@ -1408,12 +1503,37 @@ const fetchCopilot = async (provider, path$1, init, options = {}) => {
1408
1503
  }
1409
1504
  logUpstreamLifecycle(options.requestId, `upstream failed method=${init.method ?? "GET"} path=${path$1} attempt=${attempt} upstream_request_id=${upstreamRequestId}`);
1410
1505
  lastError = error;
1411
- if (attempt === maxFetchAttempts) {
1506
+ if (transientRetries >= maxFetchAttempts - 1) {
1412
1507
  log.error(`${formatRequestId(options.requestId)}Copilot ${path$1} request failed after ${attempt} attempts upstream_request_id=${upstreamRequestId}`, error);
1413
1508
  throw error;
1414
1509
  }
1415
- log.error(`${formatRequestId(options.requestId)}Copilot ${path$1} request failed; retrying (${attempt}/${maxFetchAttempts}) upstream_request_id=${upstreamRequestId}`, error);
1510
+ transientRetries++;
1511
+ log.error(`${formatRequestId(options.requestId)}Copilot ${path$1} request failed; retrying (${transientRetries}/${maxFetchAttempts}) upstream_request_id=${upstreamRequestId}`, error);
1512
+ continue;
1513
+ }
1514
+ try {
1515
+ signal?.throwIfAborted();
1516
+ if (!authRecoveryUsed && provider.refreshToken && await isAuthRejection(response, signal)) {
1517
+ authRecoveryUsed = true;
1518
+ log.info(`${formatRequestId(options.requestId)}Copilot ${path$1} authentication rejected status=${response.status}; refreshing token`);
1519
+ await response.body?.cancel();
1520
+ signal?.throwIfAborted();
1521
+ await waitWithSignal(provider.refreshToken(attemptedToken, attemptedGeneration), signal);
1522
+ signal?.throwIfAborted();
1523
+ log.info(`${formatRequestId(options.requestId)}Copilot token refresh completed; retrying ${path$1}`);
1524
+ continue;
1525
+ }
1526
+ } catch (error) {
1527
+ response.body?.cancel().catch(() => {});
1528
+ const abortError = toCopilotAbortHTTPError(error, signal, timeoutMs);
1529
+ if (abortError) throw abortError;
1530
+ log.error(`${formatRequestId(options.requestId)}Copilot token recovery failed; request not replayed`);
1531
+ throw new Error("Copilot token recovery failed; request not replayed.");
1416
1532
  }
1533
+ if (!shouldRetryResponse(response) || transientRetries >= maxFetchAttempts - 1) return response;
1534
+ transientRetries++;
1535
+ log.error(`${formatRequestId(options.requestId)}Copilot ${path$1} returned ${response.status}; retrying (${transientRetries}/${maxFetchAttempts}) upstream_request_id=${upstreamRequestId}`);
1536
+ await response.body?.cancel();
1417
1537
  }
1418
1538
  throw lastError;
1419
1539
  };
@@ -1962,7 +2082,8 @@ function translateTools(tools) {
1962
2082
  type: "function",
1963
2083
  name: tool.function.name,
1964
2084
  description: tool.function.description,
1965
- parameters: normalizeResponsesToolSchema(tool.function.parameters)
2085
+ parameters: normalizeResponsesToolSchema(tool.function.parameters),
2086
+ strict: false
1966
2087
  }));
1967
2088
  }
1968
2089
  function translateToolChoice(toolChoice) {
@@ -2081,7 +2202,7 @@ const messagesIncludeImage = (messages) => messages.some((msg) => typeof msg.con
2081
2202
  const createChatCompletions = async (config, payload, options = {}) => {
2082
2203
  const client = options.client ?? "generic";
2083
2204
  const requestedModel = options.requestedModel ?? payload.model;
2084
- const requestedThinkEffort = options.requestedThinkEffort ?? getRequestReasoningEffort(payload) ?? "none";
2205
+ const requestedThinkEffort = options.requestedThinkEffort ?? getRequestReasoningEffort(payload) ?? "unset";
2085
2206
  const reasoningEffort = getRequestedReasoningEffort(payload);
2086
2207
  const requestedThinking = options.requestedThinking ?? "none";
2087
2208
  const signal = createCopilotRequestSignal(options.signal, options.timeoutMs);
@@ -2103,7 +2224,7 @@ const createChatCompletions = async (config, payload, options = {}) => {
2103
2224
  const enableVision = messagesIncludeImage(compatiblePayload.messages);
2104
2225
  const initiator = isAgentInitiator(compatiblePayload.messages);
2105
2226
  const requestPayload = buildRequestPayload(compatiblePayload);
2106
- log.debug([
2227
+ log.info(sanitizeTerminalString([
2107
2228
  "Model request",
2108
2229
  `client=${client}`,
2109
2230
  `requested_model=${requestedModel}`,
@@ -2111,8 +2232,8 @@ const createChatCompletions = async (config, payload, options = {}) => {
2111
2232
  `upstream_model=${compatiblePayload.model}`,
2112
2233
  `requested_think_effort=${requestedThinkEffort}`,
2113
2234
  `requested_thinking=${requestedThinking}`,
2114
- `effective_think_effort=${requestPayload.reasoning_effort ?? "none"}`
2115
- ].join(" "));
2235
+ `effective_think_effort=${requestPayload.reasoning_effort ?? "unset"}`
2236
+ ].join(" ")));
2116
2237
  log.debug("Full request payload", { payload: requestPayload });
2117
2238
  if (useResponsesApi) return completeResponse(await createResponses(provider, compatiblePayload, {
2118
2239
  vision: enableVision,
@@ -2842,6 +2963,36 @@ const createFailedSearchExecution = (payload, requestedQuery, model, message) =>
2842
2963
  results: [],
2843
2964
  text: message
2844
2965
  });
2966
+ const getSearchFailureCategory = (status$1) => {
2967
+ if (status$1 === 503) return "service unavailable";
2968
+ if (status$1 >= 500) return "server failure";
2969
+ if (status$1 === 429) return "rate limited";
2970
+ if (status$1 === 401) return "authentication rejected";
2971
+ if (status$1 === 403) return "access denied";
2972
+ return "request failed";
2973
+ };
2974
+ const getSearchFailureDetail = (body, token) => {
2975
+ let detail = body.trim();
2976
+ try {
2977
+ const parsed = JSON.parse(detail);
2978
+ const error = isRecord(parsed) && isRecord(parsed.error) ? parsed.error : void 0;
2979
+ detail = typeof error?.message === "string" ? error.message : typeof error?.code === "string" ? error.code : "";
2980
+ } catch {
2981
+ if (/^[{[<]/.test(detail)) return "";
2982
+ }
2983
+ detail = scrubSensitiveUrls(sanitizeTerminalString(detail));
2984
+ detail = detail.replace(/https?:\/\/[^\s'"`<>]+/gi, (raw) => {
2985
+ try {
2986
+ const url = new URL(raw);
2987
+ return url.username || url.password || url.search || url.hash ? `${url.origin}/[redacted]` : raw;
2988
+ } catch {
2989
+ return "[redacted]";
2990
+ }
2991
+ });
2992
+ if (token) detail = detail.replaceAll(token, "[redacted]");
2993
+ if (/\b(?:authorization|bearer|api[_ -]?key|password|secret|access[_ -]?token|refresh[_ -]?token)\b/i.test(detail) || /\btoken\s*[=:]/i.test(detail) || /\b(?:gh[pousr]_|github_pat_|sk-|eyJ)[A-Za-z0-9_-]+/.test(detail) || /\b(?:request|payload|prompt|messages|input|headers|conversation)\b["']?\s*[:=]/i.test(detail)) return "";
2994
+ return detail.trim().slice(0, 240);
2995
+ };
2845
2996
  const createClaudeWebSearchExecution = async (config, payload, requestedQuery, options = {}) => {
2846
2997
  const backendModel = getWebSearchBackendModel(config);
2847
2998
  const signal = createCopilotRequestSignal(options.signal, options.timeoutMs);
@@ -2859,8 +3010,8 @@ const createClaudeWebSearchExecution = async (config, payload, requestedQuery, o
2859
3010
  timeoutMs: options.timeoutMs
2860
3011
  });
2861
3012
  if (!response.ok) {
2862
- const detail = await readCopilotText(response, signal, options.timeoutMs).catch(() => "");
2863
- return createFailedSearchExecution(payload, requestedQuery, backendModel, [`Copilot web search is not available for model ${backendModel}.`, detail ? `Upstream response: ${detail}` : ""].filter(Boolean).join("\n"));
3013
+ const detail = getSearchFailureDetail(await readCopilotText(response, signal, options.timeoutMs).catch(() => ""), config.copilotToken);
3014
+ return createFailedSearchExecution(payload, requestedQuery, backendModel, [`Copilot web search upstream ${getSearchFailureCategory(response.status)} (HTTP ${response.status}; model ${backendModel}).`, detail ? `Upstream response: ${detail}` : ""].filter(Boolean).join("\n"));
2864
3015
  }
2865
3016
  const upstream = await readCopilotJson(response, signal, options.timeoutMs);
2866
3017
  const text = getResponseText(upstream);
@@ -3216,7 +3367,7 @@ const createTokenCountModel = (modelId, tokenizer = "o200k_base") => ({
3216
3367
  id: modelId
3217
3368
  });
3218
3369
  const isNonStreamingResponse = (response) => typeof response === "object" && response !== null && Object.hasOwn(response, "choices");
3219
- const getClaudeRequestedThinkEffort = (payload) => getRequestReasoningEffort(payload) ?? "none";
3370
+ const getClaudeRequestedThinkEffort = (payload) => getRequestReasoningEffort(payload) ?? "unset";
3220
3371
  const getClaudeRequestedThinking = (payload) => {
3221
3372
  if (!payload.thinking) return "none";
3222
3373
  return [`type:${payload.thinking.type}`, `budget:${payload.thinking.budget_tokens ?? "none"}`].join(",");
@@ -3645,6 +3796,10 @@ const startServer = (config) => new Promise((resolve$1, reject) => {
3645
3796
  const canCloseConnections = (server) => typeof server.closeIdleConnections === "function" && typeof server.closeAllConnections === "function";
3646
3797
  async function startRelay(appConfig) {
3647
3798
  appConfig ??= await readAppConfig();
3799
+ appConfig = {
3800
+ ...appConfig,
3801
+ thinkEffort: normalizeThinkEffort(appConfig.thinkEffort) ?? defaultReasoningEffort
3802
+ };
3648
3803
  setLogLevel(appConfig.logLevel);
3649
3804
  await cleanupLogs(appConfig.logRetentionDays);
3650
3805
  const claudeConfigPath$1 = claudeConfigPath;