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 +10 -4
- package/config.default.yaml +4 -5
- package/dist/main.js +195 -40
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
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
|
|
121
|
-
| `debug` | Info plus
|
|
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
|
|
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 `
|
|
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
|
|
package/config.default.yaml
CHANGED
|
@@ -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,
|
|
30
|
-
# status codes
|
|
31
|
-
# debug - info logs plus
|
|
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:
|
|
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
|
-
|
|
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`, {
|
|
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
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
|
|
526
|
+
await applyCopilotToken();
|
|
505
527
|
log.info("Refreshed Copilot token");
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
741
|
-
|
|
742
|
-
|
|
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,
|
|
862
|
-
"# status codes",
|
|
863
|
-
"# debug - info logs plus
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1486
|
+
response = await fetch$1(`${provider.baseUrl}${path$1}`, {
|
|
1393
1487
|
...init,
|
|
1394
|
-
headers: buildHeaders(
|
|
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 (
|
|
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
|
-
|
|
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) ?? "
|
|
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.
|
|
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 ?? "
|
|
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
|
|
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) ?? "
|
|
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;
|