copilot-relay 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/main.js +454 -152
- package/dist/main.js.map +1 -1
- package/package.json +2 -2
package/dist/main.js
CHANGED
|
@@ -3,6 +3,7 @@ import { defineCommand, runMain } from "citty";
|
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
5
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
|
+
import timers from "node:timers";
|
|
6
7
|
import { inspect, isDeepStrictEqual, promisify } from "node:util";
|
|
7
8
|
import consola from "consola";
|
|
8
9
|
import os from "node:os";
|
|
@@ -268,7 +269,6 @@ const consoleSuppressed = new AsyncLocalStorage();
|
|
|
268
269
|
const withoutLogging = (run) => loggingSuppressed.run(true, run);
|
|
269
270
|
const withoutConsoleLogging = (run) => consoleSuppressed.run(true, run);
|
|
270
271
|
let currentLogLevel = consolaLevelByName.info;
|
|
271
|
-
const pendingLogWrites = /* @__PURE__ */ new Set();
|
|
272
272
|
const registeredLogSecrets = /* @__PURE__ */ new Set();
|
|
273
273
|
const logSecretForms = /* @__PURE__ */ new Set();
|
|
274
274
|
let logSecretPattern;
|
|
@@ -320,9 +320,6 @@ const boundLogText = (value, maxBytes) => {
|
|
|
320
320
|
return bytes.subarray(0, end).toString("utf8") + truncatedMarker;
|
|
321
321
|
};
|
|
322
322
|
const isDebugLogging = () => currentLogLevel >= consolaLevelByName.debug;
|
|
323
|
-
const flushLogs = async () => {
|
|
324
|
-
while (pendingLogWrites.size > 0) await Promise.all([...pendingLogWrites]);
|
|
325
|
-
};
|
|
326
323
|
const setLogLevel = (level) => {
|
|
327
324
|
currentLogLevel = consolaLevelByName[level];
|
|
328
325
|
consola.level = consolaLevelByName[level];
|
|
@@ -353,7 +350,7 @@ const ensurePrivateDirectory = async (directory) => {
|
|
|
353
350
|
});
|
|
354
351
|
const observed = await fs.lstat(directory);
|
|
355
352
|
if (!observed.isDirectory() || observed.isSymbolicLink()) throw new Error("Refusing an unsafe log directory");
|
|
356
|
-
if (process.platform === "win32") return;
|
|
353
|
+
if (process.platform === "win32") return observed;
|
|
357
354
|
const handle = await fs.open(directory, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
358
355
|
try {
|
|
359
356
|
const opened = await handle.stat();
|
|
@@ -362,12 +359,26 @@ const ensurePrivateDirectory = async (directory) => {
|
|
|
362
359
|
} finally {
|
|
363
360
|
await handle.close();
|
|
364
361
|
}
|
|
362
|
+
return observed;
|
|
365
363
|
};
|
|
364
|
+
const logDirectories = [paths.appDir, paths.logsDir];
|
|
366
365
|
const ensureLogDirectory = async () => {
|
|
367
|
-
|
|
368
|
-
await ensurePrivateDirectory(
|
|
369
|
-
|
|
370
|
-
|
|
366
|
+
const checked = [];
|
|
367
|
+
for (const directory of logDirectories) checked.push(await ensurePrivateDirectory(directory));
|
|
368
|
+
return checked;
|
|
369
|
+
};
|
|
370
|
+
let logQueue = [];
|
|
371
|
+
let logWriteChain = Promise.resolve();
|
|
372
|
+
let activeLog;
|
|
373
|
+
let logIdleClose;
|
|
374
|
+
const maxLogWriteBytes = 256 * 1024;
|
|
375
|
+
const hasMode = (stats, mode) => process.platform === "win32" || (stats.mode & 511) === mode;
|
|
376
|
+
const isSameDirectory = (current, checked) => current !== void 0 && current.isDirectory() && sameFile$2(current, checked) && hasMode(current, 448);
|
|
377
|
+
const isStillActive = async (active) => {
|
|
378
|
+
const [current, ...directories] = await Promise.all([active.filePath, ...logDirectories].map((target) => fs.lstat(target).catch(() => void 0)));
|
|
379
|
+
return current !== void 0 && current.isFile() && current.nlink === 1 && sameFile$2(current, active.identity) && hasMode(current, 384) && active.directories.every((checked, index) => isSameDirectory(directories[index], checked));
|
|
380
|
+
};
|
|
381
|
+
const openPrivateLog = async (filePath, directories) => {
|
|
371
382
|
const observed = await fs.lstat(filePath).catch((error) => {
|
|
372
383
|
if (!isMissing$1(error)) throw error;
|
|
373
384
|
});
|
|
@@ -379,28 +390,88 @@ const appendPrivateLog = async (filePath, content) => {
|
|
|
379
390
|
const current = await fs.lstat(filePath);
|
|
380
391
|
if (!opened.isFile() || opened.nlink !== 1 || current.isSymbolicLink() || !sameFile$2(opened, current) || observed && !sameFile$2(observed, opened)) throw new Error("Log file changed while opening");
|
|
381
392
|
if (process.platform !== "win32") await handle.chmod(384);
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
393
|
+
return {
|
|
394
|
+
filePath,
|
|
395
|
+
handle,
|
|
396
|
+
identity: opened,
|
|
397
|
+
directories
|
|
398
|
+
};
|
|
399
|
+
} catch (error) {
|
|
400
|
+
await handle.close().catch(() => void 0);
|
|
401
|
+
throw error;
|
|
385
402
|
}
|
|
386
403
|
};
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
await
|
|
404
|
+
const closeActiveLog = async () => {
|
|
405
|
+
const closing = activeLog;
|
|
406
|
+
activeLog = void 0;
|
|
407
|
+
await closing?.handle.close().catch(() => void 0);
|
|
408
|
+
};
|
|
409
|
+
const logIdleCloseMs = 1e3;
|
|
410
|
+
const scheduleLogIdleClose = () => {
|
|
411
|
+
timers.clearTimeout(logIdleClose);
|
|
412
|
+
logIdleClose = timers.setTimeout(() => {
|
|
413
|
+
logWriteChain = logWriteChain.then(closeActiveLog);
|
|
414
|
+
}, logIdleCloseMs);
|
|
415
|
+
logIdleClose.unref();
|
|
416
|
+
};
|
|
417
|
+
const openActiveLog = async (filePath) => {
|
|
418
|
+
if (activeLog?.filePath === filePath && await isStillActive(activeLog)) return activeLog.handle;
|
|
419
|
+
await closeActiveLog();
|
|
420
|
+
activeLog = await openPrivateLog(filePath, await ensureLogDirectory());
|
|
421
|
+
return activeLog.handle;
|
|
422
|
+
};
|
|
423
|
+
const appendLogLines = async (filePath, lines) => {
|
|
424
|
+
const handle = await openActiveLog(filePath);
|
|
425
|
+
let chunk = "";
|
|
426
|
+
let chunkBytes = 0;
|
|
427
|
+
for (const line of lines) {
|
|
428
|
+
const lineBytes = Buffer.byteLength(line);
|
|
429
|
+
if (chunk !== "" && chunkBytes + lineBytes > maxLogWriteBytes) {
|
|
430
|
+
await handle.appendFile(chunk);
|
|
431
|
+
chunk = "";
|
|
432
|
+
chunkBytes = 0;
|
|
433
|
+
}
|
|
434
|
+
chunk += line;
|
|
435
|
+
chunkBytes += lineBytes;
|
|
436
|
+
}
|
|
437
|
+
await handle.appendFile(chunk);
|
|
438
|
+
};
|
|
439
|
+
const drainLogQueue = async () => {
|
|
440
|
+
const entries = logQueue;
|
|
441
|
+
logQueue = [];
|
|
442
|
+
await cleanupLogsIfDue().catch(() => void 0);
|
|
443
|
+
const groups = [];
|
|
444
|
+
for (const entry of entries) {
|
|
445
|
+
const last = groups.at(-1);
|
|
446
|
+
if (last?.filePath === entry.filePath) last.lines.push(entry.line);
|
|
447
|
+
else groups.push({
|
|
448
|
+
filePath: entry.filePath,
|
|
449
|
+
lines: [entry.line]
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
for (const group of groups) try {
|
|
453
|
+
await appendLogLines(group.filePath, group.lines);
|
|
454
|
+
} catch {
|
|
455
|
+
await closeActiveLog();
|
|
456
|
+
}
|
|
457
|
+
scheduleLogIdleClose();
|
|
458
|
+
};
|
|
459
|
+
const queueLogEntry = (entry) => {
|
|
460
|
+
logQueue.push(entry);
|
|
461
|
+
if (logQueue.length === 1) {
|
|
462
|
+
timers.clearTimeout(logIdleClose);
|
|
463
|
+
logWriteChain = logWriteChain.then(drainLogQueue).catch(() => void 0);
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
const flushLogs = async () => {
|
|
467
|
+
let pending = true;
|
|
468
|
+
while (pending) {
|
|
469
|
+
const closed = logWriteChain.then(closeActiveLog);
|
|
470
|
+
logWriteChain = closed;
|
|
471
|
+
await closed;
|
|
472
|
+
pending = logQueue.length > 0;
|
|
473
|
+
}
|
|
474
|
+
timers.clearTimeout(logIdleClose);
|
|
404
475
|
};
|
|
405
476
|
const cleanupLogsIfDue = async () => {
|
|
406
477
|
const now = Date.now();
|
|
@@ -430,9 +501,11 @@ const wrapFileLog = (level, fn) => ((...args) => {
|
|
|
430
501
|
if (!writesToFile && !writesToConsole) return quiet ? void 0 : fn(...args);
|
|
431
502
|
const rendered = [boundLogText(args.map((value) => boundLogText(escapeLogLineSeparators(scrubLogSecrets(scrubSensitiveUrls(formatLogValue(value)))), maxLogArgumentBytes)).join(" "), maxLogPayloadBytes)];
|
|
432
503
|
if (writesToFile) {
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
504
|
+
const now = /* @__PURE__ */ new Date();
|
|
505
|
+
queueLogEntry({
|
|
506
|
+
filePath: getLogPath(now),
|
|
507
|
+
line: `${now.toISOString()} ${level} ${rendered.join(" ")}\n`
|
|
508
|
+
});
|
|
436
509
|
}
|
|
437
510
|
return quiet ? void 0 : fn(...rendered);
|
|
438
511
|
});
|
|
@@ -758,6 +831,7 @@ const normalizeClaudeModelId = (model, contextWindowTokens) => {
|
|
|
758
831
|
return contextWindowTokens !== void 0 && contextWindowTokens !== 1e6 ? normalized : `${normalized}[1m]`;
|
|
759
832
|
};
|
|
760
833
|
const normalizeCopilotModelId = (model) => normalizeOneMillionContextModel(model) ?? model;
|
|
834
|
+
const isClaudeModelId = (model) => model.startsWith("claude-");
|
|
761
835
|
const getConfiguredModelRouting = () => getRuntimeState().modelRouting ?? defaultModelRouting;
|
|
762
836
|
const getModelRouting = () => {
|
|
763
837
|
const routing = getConfiguredModelRouting();
|
|
@@ -1839,8 +1913,7 @@ var RequestTrace = class RequestTrace {
|
|
|
1839
1913
|
request: emptyBody(`upstream-${order}-request.bin`, "complete")
|
|
1840
1914
|
};
|
|
1841
1915
|
this.manifest.exchanges.push(exchange);
|
|
1842
|
-
|
|
1843
|
-
this.append(exchange.request, bytes);
|
|
1916
|
+
this.append(exchange.request, input.body ?? "");
|
|
1844
1917
|
try {
|
|
1845
1918
|
const response = await (transportContext.getStore()?.fetch(input) ?? fallback());
|
|
1846
1919
|
exchange.status = response.status;
|
|
@@ -1955,16 +2028,17 @@ var RequestTrace = class RequestTrace {
|
|
|
1955
2028
|
}
|
|
1956
2029
|
}, { highWaterMark: 0 });
|
|
1957
2030
|
}
|
|
1958
|
-
append(body,
|
|
1959
|
-
|
|
2031
|
+
append(body, data) {
|
|
2032
|
+
const byteLength = typeof data === "string" ? Buffer.byteLength(data) : data.byteLength;
|
|
2033
|
+
body.bytes += byteLength;
|
|
1960
2034
|
if (!this.captureDirectory || this.manifest.captureError) return;
|
|
1961
|
-
if (body.chunks.length >= 1e5 || this.queuedBytes +
|
|
2035
|
+
if (body.chunks.length >= 1e5 || this.queuedBytes + byteLength > maxQueuedBytes) {
|
|
1962
2036
|
this.manifest.captureError = "capture_queue_limit";
|
|
1963
2037
|
this.manifest.captureState = "incomplete";
|
|
1964
2038
|
return;
|
|
1965
2039
|
}
|
|
1966
|
-
body.chunks.push(
|
|
1967
|
-
const buffer = Buffer.from(
|
|
2040
|
+
body.chunks.push(byteLength);
|
|
2041
|
+
const buffer = Buffer.from(data);
|
|
1968
2042
|
this.queuedBytes += buffer.length;
|
|
1969
2043
|
this.queue = this.queue.then(async () => {
|
|
1970
2044
|
await this.checkDirectories();
|
|
@@ -2162,7 +2236,8 @@ const maxFetchAttempts = 2;
|
|
|
2162
2236
|
const copilotRequestTimeoutMs = 18e4;
|
|
2163
2237
|
const copilotDispatcher = new Agent({
|
|
2164
2238
|
allowH2: false,
|
|
2165
|
-
connect: { allowH2: false }
|
|
2239
|
+
connect: { allowH2: false },
|
|
2240
|
+
keepAliveTimeout: 5e4
|
|
2166
2241
|
});
|
|
2167
2242
|
const getCopilotProviderContext = (config) => ({
|
|
2168
2243
|
baseUrl: config.copilotBaseUrl,
|
|
@@ -2414,6 +2489,11 @@ const parseStringArray = (value) => {
|
|
|
2414
2489
|
if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) return;
|
|
2415
2490
|
return value;
|
|
2416
2491
|
};
|
|
2492
|
+
const parseReasoningEfforts = (supports) => {
|
|
2493
|
+
if (supports === void 0) return;
|
|
2494
|
+
if (supports.reasoning_effort === false || !Object.hasOwn(supports, "reasoning_effort")) return [];
|
|
2495
|
+
return parseStringArray(supports.reasoning_effort);
|
|
2496
|
+
};
|
|
2417
2497
|
async function loadCopilotModelCatalog(config) {
|
|
2418
2498
|
const provider = getCopilotProviderContext(config);
|
|
2419
2499
|
const pending = pendingCatalogs.get(config);
|
|
@@ -2437,13 +2517,14 @@ async function loadCopilotModelCatalog(config) {
|
|
|
2437
2517
|
const limits = parseModelTokenLimits(capabilities?.limits);
|
|
2438
2518
|
const supports = isRecord$3(capabilities?.supports) ? capabilities.supports : void 0;
|
|
2439
2519
|
const supportedEndpoints = parseStringArray(model.supported_endpoints);
|
|
2440
|
-
const reasoningEfforts =
|
|
2520
|
+
const reasoningEfforts = parseReasoningEfforts(supports);
|
|
2441
2521
|
models$1.set(model.id, {
|
|
2442
2522
|
...limits && { limits },
|
|
2443
2523
|
...typeof capabilities?.tokenizer === "string" && { tokenizer: capabilities.tokenizer },
|
|
2444
2524
|
...supportedEndpoints !== void 0 && { supportedEndpoints },
|
|
2445
2525
|
...typeof capabilities?.type === "string" && { type: capabilities.type },
|
|
2446
|
-
...reasoningEfforts !== void 0 && { reasoningEfforts }
|
|
2526
|
+
...reasoningEfforts !== void 0 && { reasoningEfforts },
|
|
2527
|
+
...typeof model.name === "string" && model.name !== "" && { name: model.name }
|
|
2447
2528
|
});
|
|
2448
2529
|
}
|
|
2449
2530
|
const catalog = {
|
|
@@ -2483,6 +2564,75 @@ async function boundModelOutputTokens(config, model, requested) {
|
|
|
2483
2564
|
return bounded;
|
|
2484
2565
|
}
|
|
2485
2566
|
|
|
2567
|
+
//#endregion
|
|
2568
|
+
//#region src/copilot/endpoint.ts
|
|
2569
|
+
const copilotEndpoints = [
|
|
2570
|
+
"/chat/completions",
|
|
2571
|
+
"/responses",
|
|
2572
|
+
"/v1/messages"
|
|
2573
|
+
];
|
|
2574
|
+
const legacyResponsesPattern = /^(?:gpt-5\.4(?!-mini)|gpt-5\.5|gpt-5\.6|gpt-6-astra)(?:-|$)/i;
|
|
2575
|
+
function selectCopilotEndpoint(config, model) {
|
|
2576
|
+
const capabilities = getCachedCopilotModel(config, model);
|
|
2577
|
+
const advertisedEndpoints = capabilities?.supportedEndpoints;
|
|
2578
|
+
if (capabilities?.type !== void 0 && capabilities.type !== "chat") return { reason: "unsupported-model-type" };
|
|
2579
|
+
const isClaude = isClaudeModelId(model);
|
|
2580
|
+
const protocolMode = config.claudeUpstreamApi ?? "chat-completions";
|
|
2581
|
+
if (isClaude && protocolMode === "messages") return {
|
|
2582
|
+
endpoint: "/v1/messages",
|
|
2583
|
+
source: "policy",
|
|
2584
|
+
responsesFallback: false
|
|
2585
|
+
};
|
|
2586
|
+
if (isClaude && protocolMode === "auto" && advertisedEndpoints?.includes("/v1/messages")) return {
|
|
2587
|
+
endpoint: "/v1/messages",
|
|
2588
|
+
source: "catalog",
|
|
2589
|
+
responsesFallback: false
|
|
2590
|
+
};
|
|
2591
|
+
const isChatPinned = isClaude && protocolMode === "chat-completions";
|
|
2592
|
+
const preferredEndpoint = legacyResponsesPattern.test(model) ? "/responses" : "/chat/completions";
|
|
2593
|
+
if (advertisedEndpoints === void 0) return {
|
|
2594
|
+
endpoint: isChatPinned ? "/chat/completions" : preferredEndpoint,
|
|
2595
|
+
source: isChatPinned ? "policy" : "legacy",
|
|
2596
|
+
responsesFallback: !isChatPinned && preferredEndpoint === "/chat/completions"
|
|
2597
|
+
};
|
|
2598
|
+
if (advertisedEndpoints.length === 0) return { reason: "no-advertised-endpoint" };
|
|
2599
|
+
if (isChatPinned) {
|
|
2600
|
+
if (advertisedEndpoints.includes("/chat/completions")) return {
|
|
2601
|
+
endpoint: "/chat/completions",
|
|
2602
|
+
source: "policy",
|
|
2603
|
+
responsesFallback: false
|
|
2604
|
+
};
|
|
2605
|
+
return { reason: advertisedEndpoints.some((endpoint) => endpoint === "/responses" || endpoint === "/v1/messages") ? "protocol-policy-conflict" : "unsupported-relay-endpoint" };
|
|
2606
|
+
}
|
|
2607
|
+
const selectedEndpoint = [preferredEndpoint, preferredEndpoint === "/responses" ? "/chat/completions" : "/responses"].find((endpoint) => advertisedEndpoints.includes(endpoint));
|
|
2608
|
+
if (!selectedEndpoint) return { reason: "unsupported-relay-endpoint" };
|
|
2609
|
+
return {
|
|
2610
|
+
endpoint: selectedEndpoint,
|
|
2611
|
+
source: "catalog",
|
|
2612
|
+
responsesFallback: selectedEndpoint === "/chat/completions" && advertisedEndpoints.includes("/responses")
|
|
2613
|
+
};
|
|
2614
|
+
}
|
|
2615
|
+
const unavailableEndpointMessages = {
|
|
2616
|
+
"unsupported-model-type": "The selected upstream model does not advertise a chat interface.",
|
|
2617
|
+
"no-advertised-endpoint": "The selected upstream model advertises no inference endpoints.",
|
|
2618
|
+
"unsupported-relay-endpoint": "The selected upstream model advertises no compatible relay endpoint.",
|
|
2619
|
+
"protocol-policy-conflict": "The selected upstream model has no Chat endpoint allowed by claudeUpstreamApi. Choose an advertised protocol explicitly."
|
|
2620
|
+
};
|
|
2621
|
+
function requireCopilotEndpoint(config, model) {
|
|
2622
|
+
const selection = selectCopilotEndpoint(config, model);
|
|
2623
|
+
if (selection.endpoint) return selection;
|
|
2624
|
+
const message = unavailableEndpointMessages[selection.reason];
|
|
2625
|
+
throw new HTTPError(message, Response.json({
|
|
2626
|
+
type: "error",
|
|
2627
|
+
error: {
|
|
2628
|
+
type: "invalid_request_error",
|
|
2629
|
+
code: "relay_unsupported_endpoint",
|
|
2630
|
+
reason: selection.reason,
|
|
2631
|
+
message
|
|
2632
|
+
}
|
|
2633
|
+
}, { status: 400 }), message);
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2486
2636
|
//#endregion
|
|
2487
2637
|
//#region src/lib/terminal.ts
|
|
2488
2638
|
const colorEnabled = (env = process.env, isTTY = Boolean(process.stdout.isTTY)) => {
|
|
@@ -2527,6 +2677,7 @@ const reasonLabels = {
|
|
|
2527
2677
|
"no-advertised-endpoint": "No advertised route",
|
|
2528
2678
|
"protocol-policy-conflict": "Protocol policy conflict",
|
|
2529
2679
|
"unsupported-api": "Unsupported API",
|
|
2680
|
+
"model-not-supported": "Model not supported",
|
|
2530
2681
|
"unsupported-effort": "Unsupported effort",
|
|
2531
2682
|
"unsafe-or-noncanonical-id": "Unsupported model ID",
|
|
2532
2683
|
"upstream-response-failed": "Upstream failed",
|
|
@@ -2626,6 +2777,7 @@ const probeHint = (code) => {
|
|
|
2626
2777
|
if (code === "refusal") return "Refusal: inspect the failing request's evidence; a short probe does not explain its cause.";
|
|
2627
2778
|
if (code.includes("budget-exhausted")) return "Output limit: reachability is not completion; review the probe's output budget.";
|
|
2628
2779
|
if (code === "model-mismatch") return "Model mismatch: compare selected, sent and reported IDs with --details.";
|
|
2780
|
+
if (code === "model-not-supported") return "Model not supported: upstream lists this ID but rejects inference for it; choose another model.";
|
|
2629
2781
|
if (apiFailureCodes.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.";
|
|
2630
2782
|
};
|
|
2631
2783
|
|
|
@@ -2760,7 +2912,7 @@ const createClaudeToolNameMapper = (tools, options = {}) => {
|
|
|
2760
2912
|
function translateModelName(model) {
|
|
2761
2913
|
return routeModelId(model);
|
|
2762
2914
|
}
|
|
2763
|
-
function translateToOpenAI(payload, _settings, toolNameMapper) {
|
|
2915
|
+
function translateToOpenAI(payload, _settings, toolNameMapper, options = {}) {
|
|
2764
2916
|
const model = translateModelName(payload.model);
|
|
2765
2917
|
const mapper = toolNameMapper ?? createClaudeToolNameMapper(payload.tools, { ...getToolNameMapperOptionsForModel(model) });
|
|
2766
2918
|
const tools = translateClaudeToolsToOpenAI(payload.tools, mapper);
|
|
@@ -2768,7 +2920,7 @@ function translateToOpenAI(payload, _settings, toolNameMapper) {
|
|
|
2768
2920
|
validateClaudeMessages(payload.messages);
|
|
2769
2921
|
return {
|
|
2770
2922
|
model,
|
|
2771
|
-
messages: normalizeFinalAssistantPrefill$1(translateClaudeMessagesToOpenAI(payload.messages, payload.system, mapper)),
|
|
2923
|
+
messages: normalizeFinalAssistantPrefill$1(translateClaudeMessagesToOpenAI(payload.messages, payload.system, mapper, options.endpoint === "/chat/completions" && isClaudeModelId(model))),
|
|
2772
2924
|
max_tokens: payload.max_tokens,
|
|
2773
2925
|
stop: payload.stop_sequences,
|
|
2774
2926
|
stream: payload.stream,
|
|
@@ -2794,20 +2946,50 @@ function validateClaudeMessages(messages, native = false) {
|
|
|
2794
2946
|
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.");
|
|
2795
2947
|
}
|
|
2796
2948
|
}
|
|
2797
|
-
function translateClaudeMessagesToOpenAI(claudeMessages, system, toolNameMapper) {
|
|
2798
|
-
const
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2949
|
+
function translateClaudeMessagesToOpenAI(claudeMessages, system, toolNameMapper, claudeChatRoute) {
|
|
2950
|
+
const translated = handleSystemPrompt(system);
|
|
2951
|
+
if (claudeChatRoute && hasCacheBreakpoint(system)) markCacheBreakpoint(translated.at(-1));
|
|
2952
|
+
for (const message of claudeMessages) {
|
|
2953
|
+
const start$1 = translated.length;
|
|
2954
|
+
translated.push(...translateClaudeMessage(message, toolNameMapper, claudeChatRoute));
|
|
2955
|
+
if (claudeChatRoute) markMessageBreakpoints(message, translated, start$1);
|
|
2956
|
+
}
|
|
2957
|
+
return translated;
|
|
2958
|
+
}
|
|
2959
|
+
function markMessageBreakpoints(message, translated, start$1) {
|
|
2960
|
+
if (message.role !== "user" || !Array.isArray(message.content)) {
|
|
2961
|
+
if (hasCacheBreakpoint(message.content)) markCacheBreakpoint(translated.at(-1));
|
|
2962
|
+
return;
|
|
2963
|
+
}
|
|
2964
|
+
let toolIndex = start$1;
|
|
2965
|
+
for (const block of message.content) if (block.type === "tool_result") {
|
|
2966
|
+
if (isCacheBreakpoint(block)) markCacheBreakpoint(translated[toolIndex]);
|
|
2967
|
+
toolIndex += 1;
|
|
2968
|
+
} else if (isCacheBreakpoint(block)) markCacheBreakpoint(translated.at(-1));
|
|
2969
|
+
}
|
|
2970
|
+
function translateClaudeMessage(message, toolNameMapper, claudeChatRoute) {
|
|
2971
|
+
switch (message.role) {
|
|
2972
|
+
case "user": return handleUserMessage(message);
|
|
2973
|
+
case "assistant": return handleAssistantMessage(message, toolNameMapper);
|
|
2974
|
+
case "system":
|
|
2975
|
+
if (message.output_config !== void 0 && (typeof message.content === "string" ? message.content.length === 0 : message.content.every((block) => block.text.length === 0))) return [];
|
|
2976
|
+
return claudeChatRoute ? toSystemReminder(message.content) : handleSystemPrompt(message.content);
|
|
2977
|
+
default: throw invalidMessage("Unsupported message role.");
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
function toSystemReminder(content) {
|
|
2981
|
+
const text$1 = typeof content === "string" ? content : content.map((block) => block.text).join("\n\n");
|
|
2982
|
+
if (text$1.trim().length === 0) return [];
|
|
2983
|
+
return [{
|
|
2984
|
+
role: "user",
|
|
2985
|
+
content: `<system-reminder>\n${text$1}\n</system-reminder>`
|
|
2986
|
+
}];
|
|
2810
2987
|
}
|
|
2988
|
+
const isCacheBreakpoint = (block) => "cache_control" in block && Boolean(block.cache_control);
|
|
2989
|
+
const hasCacheBreakpoint = (content) => Array.isArray(content) && content.some(isCacheBreakpoint);
|
|
2990
|
+
const markCacheBreakpoint = (message) => {
|
|
2991
|
+
if (message) message.copilot_cache_control = { type: "ephemeral" };
|
|
2992
|
+
};
|
|
2811
2993
|
const invalidMessage = (message) => new HTTPError(message, new Response(JSON.stringify({
|
|
2812
2994
|
type: "error",
|
|
2813
2995
|
error: {
|
|
@@ -3905,6 +4087,12 @@ const getEncodeChatFunction = async (encoding) => {
|
|
|
3905
4087
|
encodingCache.set(encoding, encoder);
|
|
3906
4088
|
return encoder;
|
|
3907
4089
|
};
|
|
4090
|
+
const preloadTokenizers = async (reported) => {
|
|
4091
|
+
const encodings = new Set(["o200k_base"]);
|
|
4092
|
+
for (const tokenizer of reported) if (tokenizer !== void 0 && isSupportedTokenizer(tokenizer)) encodings.add(tokenizer);
|
|
4093
|
+
for (const encoding of encodings) await getEncodeChatFunction(encoding);
|
|
4094
|
+
};
|
|
4095
|
+
const loadedTokenizers = () => [...encodingCache.keys()];
|
|
3908
4096
|
const getTokenizerFromModel = (model) => model.capabilities.tokenizer || "o200k_base";
|
|
3909
4097
|
const getModelConstants = (model) => model.id === "gpt-3.5-turbo" || model.id === "gpt-4" ? {
|
|
3910
4098
|
funcInit: 10,
|
|
@@ -4001,75 +4189,6 @@ const getTokenCount = async (payload, model) => {
|
|
|
4001
4189
|
};
|
|
4002
4190
|
};
|
|
4003
4191
|
|
|
4004
|
-
//#endregion
|
|
4005
|
-
//#region src/copilot/endpoint.ts
|
|
4006
|
-
const copilotEndpoints = [
|
|
4007
|
-
"/chat/completions",
|
|
4008
|
-
"/responses",
|
|
4009
|
-
"/v1/messages"
|
|
4010
|
-
];
|
|
4011
|
-
const legacyResponsesPattern = /^(?:gpt-5\.5|gpt-5\.6|gpt-6-astra)(?:-|$)/i;
|
|
4012
|
-
function selectCopilotEndpoint(config, model) {
|
|
4013
|
-
const capabilities = getCachedCopilotModel(config, model);
|
|
4014
|
-
const advertisedEndpoints = capabilities?.supportedEndpoints;
|
|
4015
|
-
if (capabilities?.type !== void 0 && capabilities.type !== "chat") return { reason: "unsupported-model-type" };
|
|
4016
|
-
const isClaude = model.startsWith("claude-");
|
|
4017
|
-
const protocolMode = config.claudeUpstreamApi ?? "chat-completions";
|
|
4018
|
-
if (isClaude && protocolMode === "messages") return {
|
|
4019
|
-
endpoint: "/v1/messages",
|
|
4020
|
-
source: "policy",
|
|
4021
|
-
responsesFallback: false
|
|
4022
|
-
};
|
|
4023
|
-
if (isClaude && protocolMode === "auto" && advertisedEndpoints?.includes("/v1/messages")) return {
|
|
4024
|
-
endpoint: "/v1/messages",
|
|
4025
|
-
source: "catalog",
|
|
4026
|
-
responsesFallback: false
|
|
4027
|
-
};
|
|
4028
|
-
const isChatPinned = isClaude && protocolMode === "chat-completions";
|
|
4029
|
-
const preferredEndpoint = legacyResponsesPattern.test(model) ? "/responses" : "/chat/completions";
|
|
4030
|
-
if (advertisedEndpoints === void 0) return {
|
|
4031
|
-
endpoint: isChatPinned ? "/chat/completions" : preferredEndpoint,
|
|
4032
|
-
source: isChatPinned ? "policy" : "legacy",
|
|
4033
|
-
responsesFallback: !isChatPinned && preferredEndpoint === "/chat/completions"
|
|
4034
|
-
};
|
|
4035
|
-
if (advertisedEndpoints.length === 0) return { reason: "no-advertised-endpoint" };
|
|
4036
|
-
if (isChatPinned) {
|
|
4037
|
-
if (advertisedEndpoints.includes("/chat/completions")) return {
|
|
4038
|
-
endpoint: "/chat/completions",
|
|
4039
|
-
source: "policy",
|
|
4040
|
-
responsesFallback: false
|
|
4041
|
-
};
|
|
4042
|
-
return { reason: advertisedEndpoints.some((endpoint) => endpoint === "/responses" || endpoint === "/v1/messages") ? "protocol-policy-conflict" : "unsupported-relay-endpoint" };
|
|
4043
|
-
}
|
|
4044
|
-
const selectedEndpoint = [preferredEndpoint, preferredEndpoint === "/responses" ? "/chat/completions" : "/responses"].find((endpoint) => advertisedEndpoints.includes(endpoint));
|
|
4045
|
-
if (!selectedEndpoint) return { reason: "unsupported-relay-endpoint" };
|
|
4046
|
-
return {
|
|
4047
|
-
endpoint: selectedEndpoint,
|
|
4048
|
-
source: "catalog",
|
|
4049
|
-
responsesFallback: selectedEndpoint === "/chat/completions" && advertisedEndpoints.includes("/responses")
|
|
4050
|
-
};
|
|
4051
|
-
}
|
|
4052
|
-
const unavailableEndpointMessages = {
|
|
4053
|
-
"unsupported-model-type": "The selected upstream model does not advertise a chat interface.",
|
|
4054
|
-
"no-advertised-endpoint": "The selected upstream model advertises no inference endpoints.",
|
|
4055
|
-
"unsupported-relay-endpoint": "The selected upstream model advertises no compatible relay endpoint.",
|
|
4056
|
-
"protocol-policy-conflict": "The selected upstream model has no Chat endpoint allowed by claudeUpstreamApi. Choose an advertised protocol explicitly."
|
|
4057
|
-
};
|
|
4058
|
-
function requireCopilotEndpoint(config, model) {
|
|
4059
|
-
const selection = selectCopilotEndpoint(config, model);
|
|
4060
|
-
if (selection.endpoint) return selection;
|
|
4061
|
-
const message = unavailableEndpointMessages[selection.reason];
|
|
4062
|
-
throw new HTTPError(message, Response.json({
|
|
4063
|
-
type: "error",
|
|
4064
|
-
error: {
|
|
4065
|
-
type: "invalid_request_error",
|
|
4066
|
-
code: "relay_unsupported_endpoint",
|
|
4067
|
-
reason: selection.reason,
|
|
4068
|
-
message
|
|
4069
|
-
}
|
|
4070
|
-
}, { status: 400 }), message);
|
|
4071
|
-
}
|
|
4072
|
-
|
|
4073
4192
|
//#endregion
|
|
4074
4193
|
//#region src/copilot/tool-schema.ts
|
|
4075
4194
|
const schemaMapKeywords = new Set([
|
|
@@ -4120,17 +4239,45 @@ function hasUnsupportedPatternFeatures(pattern) {
|
|
|
4120
4239
|
}
|
|
4121
4240
|
return false;
|
|
4122
4241
|
}
|
|
4242
|
+
const removed = Symbol("removed");
|
|
4243
|
+
function mapRecord(record$3, map) {
|
|
4244
|
+
const keys = Object.keys(record$3);
|
|
4245
|
+
let entries;
|
|
4246
|
+
for (let index = 0; index < keys.length; index++) {
|
|
4247
|
+
const key = keys[index];
|
|
4248
|
+
const value = record$3[key];
|
|
4249
|
+
const mapped = map(key, value);
|
|
4250
|
+
if (!entries && Object.is(mapped, value)) continue;
|
|
4251
|
+
entries ??= keys.slice(0, index).map((kept) => [kept, record$3[kept]]);
|
|
4252
|
+
if (mapped !== removed) entries.push([key, mapped]);
|
|
4253
|
+
}
|
|
4254
|
+
return entries ? Object.fromEntries(entries) : record$3;
|
|
4255
|
+
}
|
|
4256
|
+
function mapArray(values) {
|
|
4257
|
+
let copy;
|
|
4258
|
+
for (let index = 0; index < values.length; index++) {
|
|
4259
|
+
if (!(index in values)) continue;
|
|
4260
|
+
const value = values[index];
|
|
4261
|
+
const mapped = normalizeSchemaValue(value);
|
|
4262
|
+
if (!copy && Object.is(mapped, value)) continue;
|
|
4263
|
+
copy ??= values.slice(0, index);
|
|
4264
|
+
copy[index] = mapped;
|
|
4265
|
+
}
|
|
4266
|
+
if (!copy) return values;
|
|
4267
|
+
copy.length = values.length;
|
|
4268
|
+
return copy;
|
|
4269
|
+
}
|
|
4123
4270
|
function normalizeSchemaValue(value) {
|
|
4124
|
-
if (Array.isArray(value)) return value
|
|
4271
|
+
if (Array.isArray(value)) return mapArray(value);
|
|
4125
4272
|
return isRecord$1(value) ? normalizeResponsesToolSchema(value) : value;
|
|
4126
4273
|
}
|
|
4127
4274
|
function normalizeResponsesToolSchema(schema) {
|
|
4128
|
-
return
|
|
4129
|
-
if (key === "pattern" && typeof value === "string" && hasUnsupportedPatternFeatures(value)) return
|
|
4130
|
-
if (schemaMapKeywords.has(key) && isRecord$1(value)) return
|
|
4131
|
-
if (nestedSchemaKeywords.has(key)) return
|
|
4132
|
-
return
|
|
4133
|
-
})
|
|
4275
|
+
return mapRecord(schema, (key, value) => {
|
|
4276
|
+
if (key === "pattern" && typeof value === "string" && hasUnsupportedPatternFeatures(value)) return removed;
|
|
4277
|
+
if (schemaMapKeywords.has(key) && isRecord$1(value)) return mapRecord(value, (_name, nested) => normalizeSchemaValue(nested));
|
|
4278
|
+
if (nestedSchemaKeywords.has(key)) return normalizeSchemaValue(value);
|
|
4279
|
+
return value;
|
|
4280
|
+
});
|
|
4134
4281
|
}
|
|
4135
4282
|
|
|
4136
4283
|
//#endregion
|
|
@@ -4702,14 +4849,14 @@ async function logUpstreamError(message, response, context, signal, timeoutMs) {
|
|
|
4702
4849
|
route: context.route,
|
|
4703
4850
|
model: context.model,
|
|
4704
4851
|
requestId: context.requestId,
|
|
4705
|
-
request: context.request,
|
|
4706
4852
|
response: {
|
|
4707
4853
|
status: response.status,
|
|
4708
4854
|
statusText: response.statusText || void 0,
|
|
4709
4855
|
url: response.url || void 0,
|
|
4710
4856
|
headers: Object.fromEntries(response.headers.entries()),
|
|
4711
4857
|
body: errorBody || void 0
|
|
4712
|
-
}
|
|
4858
|
+
},
|
|
4859
|
+
request: context.request
|
|
4713
4860
|
});
|
|
4714
4861
|
return detail;
|
|
4715
4862
|
}
|
|
@@ -5336,6 +5483,10 @@ const createQueuedClaudeStreamWriter = (write) => {
|
|
|
5336
5483
|
const writeClaudeStreamEvents = async (events$1, writeEvent) => {
|
|
5337
5484
|
for (const event of events$1) await writeEvent(event);
|
|
5338
5485
|
};
|
|
5486
|
+
const translationEndpoint = (config, model) => {
|
|
5487
|
+
const selection = selectCopilotEndpoint(config, normalizeCopilotModelId(model));
|
|
5488
|
+
return selection.endpoint && !selection.responsesFallback ? selection.endpoint : void 0;
|
|
5489
|
+
};
|
|
5339
5490
|
const handleClaudeMessageRequest = async (config, claudePayload, requestSignal, requestId, writeEvent, requestHeaders) => {
|
|
5340
5491
|
const upstreamModel = translateModelName(claudePayload.model);
|
|
5341
5492
|
if (shouldUseNativeMessages(config, upstreamModel)) return handleNativeMessages(config, {
|
|
@@ -5349,7 +5500,7 @@ const handleClaudeMessageRequest = async (config, claudePayload, requestSignal,
|
|
|
5349
5500
|
const shouldLetModelDecideWebSearch = hasClaudeWebSearch(claudePayload);
|
|
5350
5501
|
const decisionPayload = shouldLetModelDecideWebSearch ? prepareClaudeWebSearchDecisionPayload(claudePayload) : claudePayload;
|
|
5351
5502
|
const toolNameMapper = createClaudeToolNameMapper(decisionPayload.tools, { ...getToolNameMapperOptionsForModel(upstreamModel) });
|
|
5352
|
-
const openAIPayload = translateToOpenAI(decisionPayload, void 0, toolNameMapper);
|
|
5503
|
+
const openAIPayload = translateToOpenAI(decisionPayload, void 0, toolNameMapper, { endpoint: translationEndpoint(config, upstreamModel) });
|
|
5353
5504
|
const canStreamWebSearchDecision = shouldLetModelDecideWebSearch && !!writeEvent;
|
|
5354
5505
|
if (shouldLetModelDecideWebSearch && !canStreamWebSearchDecision) openAIPayload.stream = false;
|
|
5355
5506
|
const response = await createChatCompletions(config, openAIPayload, {
|
|
@@ -5601,7 +5752,7 @@ claudeRoutes.post("/messages/count_tokens", async (c) => {
|
|
|
5601
5752
|
const { output_config: _outputConfig, clear_at: _clearAt,...textMessage } = message;
|
|
5602
5753
|
return textMessage;
|
|
5603
5754
|
})
|
|
5604
|
-
} : claudePayload);
|
|
5755
|
+
} : claudePayload, void 0, void 0, { endpoint: translationEndpoint(c.get("config"), translateModelName(claudePayload.model)) });
|
|
5605
5756
|
const exposedModels = getExposedModelIds();
|
|
5606
5757
|
const upstreamModel = getCachedCopilotModel(c.get("config"), openAIPayload.model);
|
|
5607
5758
|
const hasDiscoveredTokenizer = upstreamModel?.tokenizer !== void 0 && isSupportedTokenizer(upstreamModel.tokenizer);
|
|
@@ -5824,6 +5975,13 @@ const settleDiagnostic = async (trace) => {
|
|
|
5824
5975
|
clearTimeout(timer);
|
|
5825
5976
|
}
|
|
5826
5977
|
};
|
|
5978
|
+
const datedSnapshot = /^(.+)-\d{4}-\d{2}-\d{2}$/;
|
|
5979
|
+
const reportsSelectedModel = (endpoint, id, reported) => {
|
|
5980
|
+
if (normalizeCopilotModelId(reported) === id) return true;
|
|
5981
|
+
if (endpoint === "/v1/messages" && id === "claude-opus-5.5" && reported === "claude-opus-5-5") return true;
|
|
5982
|
+
if (endpoint === "/responses" && id === "gpt-5.6-sol-fast" && normalizeCopilotModelId(reported) === "gpt-5.6-sol") return true;
|
|
5983
|
+
return !datedSnapshot.test(id) && datedSnapshot.exec(reported)?.[1] === id;
|
|
5984
|
+
};
|
|
5827
5985
|
const failureFromEvidence = (row, diagnostic) => {
|
|
5828
5986
|
if ([
|
|
5829
5987
|
"probe-timeout",
|
|
@@ -5869,7 +6027,8 @@ async function probeModels(config, entries, options) {
|
|
|
5869
6027
|
"xhigh",
|
|
5870
6028
|
"max"
|
|
5871
6029
|
].filter((effort$1) => model.reasoningEfforts?.includes(effort$1));
|
|
5872
|
-
const
|
|
6030
|
+
const lowestEffort = recognizedEfforts.find((effort$1) => effort$1 !== "none") ?? recognizedEfforts.find(isReasoningEffort);
|
|
6031
|
+
const defaultProbeEffort = advertisesNoReasoningEffort(model) ? void 0 : lowestEffort ?? "low";
|
|
5873
6032
|
const effort = options.effort ?? defaultProbeEffort;
|
|
5874
6033
|
const maxTokens = Math.min(options.maxTokens, model.limits?.max_output_tokens ?? options.maxTokens, model.limits?.max_non_streaming_output_tokens ?? options.maxTokens);
|
|
5875
6034
|
const timeoutMs = Math.min(options.timeoutMs, config.upstreamTimeoutMs > 0 ? config.upstreamTimeoutMs : Infinity);
|
|
@@ -5959,11 +6118,11 @@ async function probeModels(config, entries, options) {
|
|
|
5959
6118
|
row.detail = "upstream-response-incomplete";
|
|
5960
6119
|
}
|
|
5961
6120
|
if (code === "unsupported_api_for_model") row.detail = "unsupported-api";
|
|
6121
|
+
if (code === "model_not_supported") row.detail = "model-not-supported";
|
|
5962
6122
|
} else if (!record$1(body) || !Array.isArray(body.content)) row.detail = "malformed-response";
|
|
5963
6123
|
else {
|
|
5964
6124
|
row.reported = safeId(body.model, config.copilotToken) ? body.model : "unreported";
|
|
5965
|
-
|
|
5966
|
-
if (normalizeCopilotModelId(row.reported) !== id && !nativeOpusSpelling) row.detail = "model-mismatch";
|
|
6125
|
+
if (!reportsSelectedModel(endpoint, id, row.reported)) row.detail = "model-mismatch";
|
|
5967
6126
|
else if (body.stop_reason === "max_tokens") {
|
|
5968
6127
|
row.status = "INCOMPLETE";
|
|
5969
6128
|
const outputTokens = record$1(body.usage) ? body.usage.output_tokens : void 0;
|
|
@@ -6022,14 +6181,139 @@ async function probeModels(config, entries, options) {
|
|
|
6022
6181
|
return results.length > 0 && results.every((row) => row.status === "PASS") ? 0 : 2;
|
|
6023
6182
|
}
|
|
6024
6183
|
|
|
6184
|
+
//#endregion
|
|
6185
|
+
//#region src/lib/model-listing.ts
|
|
6186
|
+
const normalizeModelSearch = (text$1) => text$1.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
6187
|
+
const contains = (model, fragment) => normalizeModelSearch(model.id).includes(fragment) || normalizeModelSearch(model.name ?? "").includes(fragment);
|
|
6188
|
+
const minimumFragment = 4;
|
|
6189
|
+
const firstMatches = (models$1, fragments) => {
|
|
6190
|
+
for (const fragment of fragments) {
|
|
6191
|
+
const matches = models$1.filter((model) => contains(model, fragment));
|
|
6192
|
+
if (matches.length > 0) return matches;
|
|
6193
|
+
}
|
|
6194
|
+
return [];
|
|
6195
|
+
};
|
|
6196
|
+
const closestModels = (models$1, search) => {
|
|
6197
|
+
const starts = [];
|
|
6198
|
+
const ends = [];
|
|
6199
|
+
for (let length = search.length - 1; length >= minimumFragment; length--) {
|
|
6200
|
+
starts.push(search.slice(0, length));
|
|
6201
|
+
ends.push(search.slice(search.length - length));
|
|
6202
|
+
}
|
|
6203
|
+
const near = new Set([...firstMatches(models$1, starts), ...firstMatches(models$1, ends)]);
|
|
6204
|
+
return models$1.filter((model) => near.has(model));
|
|
6205
|
+
};
|
|
6206
|
+
const searchModels = (models$1, search) => {
|
|
6207
|
+
const wanted = normalizeModelSearch(search);
|
|
6208
|
+
const matches = models$1.filter((model) => contains(model, wanted));
|
|
6209
|
+
if (matches.length === 0) return {
|
|
6210
|
+
found: false,
|
|
6211
|
+
models: closestModels(models$1, wanted)
|
|
6212
|
+
};
|
|
6213
|
+
const usable = matches.filter((model) => model.unusable === void 0);
|
|
6214
|
+
return {
|
|
6215
|
+
found: true,
|
|
6216
|
+
models: matches,
|
|
6217
|
+
chosen: usable.find((model) => normalizeModelSearch(model.id) === wanted) ?? (usable.length === 1 ? usable[0] : void 0)
|
|
6218
|
+
};
|
|
6219
|
+
};
|
|
6220
|
+
const maximumIdWidth = 40;
|
|
6221
|
+
const lowerFirst = (text$1) => text$1.charAt(0).toLowerCase() + text$1.slice(1);
|
|
6222
|
+
const detailsOf = (model) => {
|
|
6223
|
+
const details = [];
|
|
6224
|
+
if (model.name !== void 0) details.push(model.name);
|
|
6225
|
+
if (model.unusable !== void 0) details.push(`cannot be gptModel or opusModel: ${lowerFirst(model.unusable)}`);
|
|
6226
|
+
return details.join(" · ");
|
|
6227
|
+
};
|
|
6228
|
+
const renderModelRows = (models$1) => {
|
|
6229
|
+
const described = models$1.filter((model) => detailsOf(model) !== "");
|
|
6230
|
+
const width = Math.min(maximumIdWidth, Math.max(0, ...described.map((model) => model.id.length)));
|
|
6231
|
+
return models$1.map((model) => {
|
|
6232
|
+
const details = detailsOf(model);
|
|
6233
|
+
if (details === "") return model.id;
|
|
6234
|
+
return `${model.id.padEnd(width)} ${details}`;
|
|
6235
|
+
});
|
|
6236
|
+
};
|
|
6237
|
+
const routes = {
|
|
6238
|
+
gptModel: "requests without \"opus\" in the model name",
|
|
6239
|
+
opusModel: "requests with \"opus\" in the model name"
|
|
6240
|
+
};
|
|
6241
|
+
const applyNote = "A running relay applies the change to new requests; restarting reruns the startup check.";
|
|
6242
|
+
const keyFor = (id) => id.toLowerCase().includes("opus") ? "opusModel" : "gptModel";
|
|
6243
|
+
const problemOf = (model) => {
|
|
6244
|
+
if (!model.advertised) return "is not advertised by upstream";
|
|
6245
|
+
if (model.unusable !== void 0) return `cannot be used (${lowerFirst(model.unusable)})`;
|
|
6246
|
+
};
|
|
6247
|
+
const settingOf = (model) => `${model.key}: ${model.value}`;
|
|
6248
|
+
const renderConfigGuide = (configPath$1, configured, chosen) => {
|
|
6249
|
+
const width = Math.max(...configured.map((model) => settingOf(model).length));
|
|
6250
|
+
const lines = [`Config file: ${configPath$1}`];
|
|
6251
|
+
for (const model of configured) lines.push(` ${settingOf(model).padEnd(width)} # ${routes[model.key]}`);
|
|
6252
|
+
for (const model of configured) {
|
|
6253
|
+
const problem = problemOf(model);
|
|
6254
|
+
if (problem !== void 0) lines.push(` Warning: ${model.key} ${model.value} ${problem}; the startup check rejects it.`);
|
|
6255
|
+
}
|
|
6256
|
+
if (chosen === void 0) {
|
|
6257
|
+
lines.push("Set gptModel or opusModel to a chat model ID exactly as listed.", applyNote);
|
|
6258
|
+
return lines;
|
|
6259
|
+
}
|
|
6260
|
+
const key = keyFor(chosen.id);
|
|
6261
|
+
if (configured.some((model) => model.key === key && model.value === chosen.id)) {
|
|
6262
|
+
lines.push(`${key} already uses ${chosen.id}.`);
|
|
6263
|
+
return lines;
|
|
6264
|
+
}
|
|
6265
|
+
lines.push(`To use ${chosen.id}, set this line in the config file:`, ` ${key}: ${chosen.id}`, applyNote);
|
|
6266
|
+
return lines;
|
|
6267
|
+
};
|
|
6268
|
+
|
|
6025
6269
|
//#endregion
|
|
6026
6270
|
//#region src/models.ts
|
|
6271
|
+
var ModelsUsageError = class extends Error {};
|
|
6272
|
+
const displayName = (name) => {
|
|
6273
|
+
const text$1 = sanitizeTerminalString(name ?? "").trim();
|
|
6274
|
+
if (text$1 === "") return;
|
|
6275
|
+
return text$1.length > 80 ? `${text$1.slice(0, 77)}...` : text$1;
|
|
6276
|
+
};
|
|
6277
|
+
const listedModels = (config, catalog) => [...catalog.models.keys()].sort().map((id) => {
|
|
6278
|
+
const selection = selectCopilotEndpoint(config, id);
|
|
6279
|
+
const name = displayName(catalog.models.get(id)?.name);
|
|
6280
|
+
return {
|
|
6281
|
+
id: sanitizeTerminalString(id),
|
|
6282
|
+
...name !== void 0 && { name },
|
|
6283
|
+
...!selection.endpoint && { unusable: probeReason(selection.reason) }
|
|
6284
|
+
};
|
|
6285
|
+
});
|
|
6286
|
+
const configuredModels = (appConfig, config, catalog) => ["gptModel", "opusModel"].map((key) => {
|
|
6287
|
+
const id = normalizeCopilotModelId(appConfig[key]);
|
|
6288
|
+
const selection = selectCopilotEndpoint(config, id);
|
|
6289
|
+
return {
|
|
6290
|
+
key,
|
|
6291
|
+
value: sanitizeTerminalString(appConfig[key]),
|
|
6292
|
+
advertised: catalog.models.has(id),
|
|
6293
|
+
...!selection.endpoint && { unusable: probeReason(selection.reason) }
|
|
6294
|
+
};
|
|
6295
|
+
});
|
|
6296
|
+
const listingHeading = (search, result) => {
|
|
6297
|
+
if (search === "") {
|
|
6298
|
+
if (result.models.length === 0) return "No models advertised by upstream.";
|
|
6299
|
+
return `Upstream-advertised models (${result.models.length}):`;
|
|
6300
|
+
}
|
|
6301
|
+
const label = sanitizeTerminalString(search);
|
|
6302
|
+
if (result.found) return `Upstream models matching "${label}" (${result.models.length}):`;
|
|
6303
|
+
if (result.models.length === 0) return `No upstream model matches "${label}". List them all with copilot-relay models.`;
|
|
6304
|
+
return `No upstream model matches "${label}". Closest IDs (${result.models.length}):`;
|
|
6305
|
+
};
|
|
6027
6306
|
const models = defineCommand({
|
|
6028
6307
|
meta: {
|
|
6029
6308
|
name: "models",
|
|
6030
6309
|
description: "List upstream models; use --deep to test inference availability."
|
|
6031
6310
|
},
|
|
6032
6311
|
args: {
|
|
6312
|
+
search: {
|
|
6313
|
+
type: "positional",
|
|
6314
|
+
required: false,
|
|
6315
|
+
description: "Find models by ID or display name and print the config line to use."
|
|
6316
|
+
},
|
|
6033
6317
|
deep: {
|
|
6034
6318
|
type: "boolean",
|
|
6035
6319
|
description: "Send real inference probes through an isolated relay pipeline; consumes Copilot usage."
|
|
@@ -6076,6 +6360,9 @@ const models = defineCommand({
|
|
|
6076
6360
|
args["total-timeout"]
|
|
6077
6361
|
].some((value) => value !== void 0))) throw new Error("deep required");
|
|
6078
6362
|
if (args.effort !== void 0 && !isReasoningEffort(args.effort)) throw new Error("invalid effort");
|
|
6363
|
+
const search = args._.join(" ").trim();
|
|
6364
|
+
if (search !== "" && args.deep) throw new ModelsUsageError("A search lists models; check one with copilot-relay models --deep --model <id>.");
|
|
6365
|
+
if (search !== "" && normalizeModelSearch(search) === "") throw new ModelsUsageError("A search needs at least one letter or digit.");
|
|
6079
6366
|
const probeOptions = {
|
|
6080
6367
|
maxTokens: positive(args["max-tokens"], 4096),
|
|
6081
6368
|
timeoutMs: positive(args.timeout, 30) * 1e3,
|
|
@@ -6085,22 +6372,23 @@ const models = defineCommand({
|
|
|
6085
6372
|
};
|
|
6086
6373
|
const prepare = async () => {
|
|
6087
6374
|
failure = "Could not load relay configuration";
|
|
6088
|
-
const appConfig = await readAppConfig();
|
|
6089
|
-
setLogLevel(appConfig.logLevel);
|
|
6090
|
-
registerSensitiveOrigin(appConfig.copilotBaseUrl);
|
|
6091
|
-
const config$1 = readProxyConfig(appConfig);
|
|
6375
|
+
const appConfig$1 = await readAppConfig();
|
|
6376
|
+
setLogLevel(appConfig$1.logLevel);
|
|
6377
|
+
registerSensitiveOrigin(appConfig$1.copilotBaseUrl);
|
|
6378
|
+
const config$1 = readProxyConfig(appConfig$1);
|
|
6092
6379
|
failure = "Could not authenticate with GitHub Copilot";
|
|
6093
6380
|
await setupProxyAuth(config$1, args.deep ? { onDeviceCode: (url, code) => console.error(`Sign in: open ${terminalText(url)} and enter ${terminalText(code)}.`) } : void 0);
|
|
6094
6381
|
failure = "Could not fetch upstream model catalog";
|
|
6095
6382
|
return {
|
|
6383
|
+
appConfig: appConfig$1,
|
|
6096
6384
|
config: config$1,
|
|
6097
6385
|
catalog: await loadCopilotModelCatalog(config$1)
|
|
6098
6386
|
};
|
|
6099
6387
|
};
|
|
6100
|
-
const { config, catalog } = args.deep ? await withoutConsoleLogging(prepare) : await prepare();
|
|
6388
|
+
const { appConfig, config, catalog } = args.deep ? await withoutConsoleLogging(prepare) : await prepare();
|
|
6101
6389
|
if (args.deep) {
|
|
6102
6390
|
failure = "Selected model is not advertised by upstream";
|
|
6103
|
-
if (args.model !== void 0 && !catalog.models.has(args.model)) throw new
|
|
6391
|
+
if (args.model !== void 0 && !catalog.models.has(args.model)) throw new ModelsUsageError("Find the exact ID with copilot-relay models <search>.");
|
|
6104
6392
|
const entries = [...catalog.models].sort(([a], [b]) => {
|
|
6105
6393
|
if (a < b) return -1;
|
|
6106
6394
|
return a > b ? 1 : 0;
|
|
@@ -6109,12 +6397,20 @@ const models = defineCommand({
|
|
|
6109
6397
|
process.exitCode = await probeModels(config, entries, probeOptions);
|
|
6110
6398
|
return;
|
|
6111
6399
|
}
|
|
6112
|
-
const
|
|
6113
|
-
|
|
6114
|
-
|
|
6400
|
+
const listed = listedModels(config, catalog);
|
|
6401
|
+
const result = search === "" ? {
|
|
6402
|
+
found: true,
|
|
6403
|
+
models: listed
|
|
6404
|
+
} : searchModels(listed, search);
|
|
6405
|
+
const lines = [listingHeading(search, result), ...renderModelRows(result.models)];
|
|
6406
|
+
if (result.models.length > 0) lines.push("Inference, tool, and effort compatibility are not verified by this listing.", `Test one with copilot-relay models --deep --model ${result.chosen?.id ?? "<id>"}; it consumes Copilot usage.`);
|
|
6407
|
+
lines.push("", ...renderConfigGuide(paths.configPath, configuredModels(appConfig, config, catalog), result.chosen));
|
|
6408
|
+
console.log(scrubSensitiveUrls(lines.join("\n")));
|
|
6409
|
+
if (!result.found) process.exitCode = 1;
|
|
6115
6410
|
} catch (error) {
|
|
6116
6411
|
let detail = "Check configuration, authentication, and upstream connectivity.";
|
|
6117
|
-
if (error instanceof
|
|
6412
|
+
if (error instanceof ModelsUsageError) detail = error.message;
|
|
6413
|
+
else if (error instanceof HTTPError) {
|
|
6118
6414
|
if (error.detail !== void 0) detail = error.response.status === 504 ? "request timed out" : "request cancelled";
|
|
6119
6415
|
else detail = `HTTP ${error.response.status}`;
|
|
6120
6416
|
await error.response.body?.cancel().catch(() => {});
|
|
@@ -6670,9 +6966,9 @@ const replay = defineCommand({
|
|
|
6670
6966
|
const result = await replayCapture(args.target);
|
|
6671
6967
|
console.log(result.verdict);
|
|
6672
6968
|
if (result.summary) {
|
|
6673
|
-
const { method, path: path$1, status: status$1, routes, outcome } = result.summary;
|
|
6969
|
+
const { method, path: path$1, status: status$1, routes: routes$1, outcome } = result.summary;
|
|
6674
6970
|
console.log(`${method} ${path$1} status=${status$1 ?? "unknown"} outcome=${outcome}`);
|
|
6675
|
-
console.log(`upstream=${routes.join(" -> ") || "none"}`);
|
|
6971
|
+
console.log(`upstream=${routes$1.join(" -> ") || "none"}`);
|
|
6676
6972
|
for (const exchange of result.summary.exchanges) {
|
|
6677
6973
|
const usage = Object.entries(exchange.usage).map(([key, value]) => `${key}=${value}`).join(" ");
|
|
6678
6974
|
console.log(`exchange=${exchange.order} path=${exchange.path} status=${exchange.status ?? "unknown"} outcome=${exchange.outcome}${usage ? ` ${usage}` : ""}${exchange.refusalCategory ? ` refusal_category=${exchange.refusalCategory}` : ""}`);
|
|
@@ -7306,6 +7602,12 @@ async function startRelay(appConfig) {
|
|
|
7306
7602
|
process.exit(1);
|
|
7307
7603
|
}
|
|
7308
7604
|
log.info(`Exposed models: ${getExposedModelIds().join(", ")}`);
|
|
7605
|
+
try {
|
|
7606
|
+
await preloadTokenizers(getUpstreamModelIds().map((id) => getCachedCopilotModel(config, id)?.tokenizer));
|
|
7607
|
+
log.info(`Tokenizers loaded: ${loadedTokenizers().join(", ")}`);
|
|
7608
|
+
} catch (error) {
|
|
7609
|
+
log.error("Tokenizer preload failed; count_tokens loads tokenizers on first use:", error);
|
|
7610
|
+
}
|
|
7309
7611
|
const server = await startServer(config);
|
|
7310
7612
|
await writeRelayPidFile(config);
|
|
7311
7613
|
log.info(`copilot-relay version: ${appVersion}`);
|