copilot-relay 0.4.3 → 0.4.4
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 +400 -133
- 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
|
});
|
|
@@ -1839,8 +1912,7 @@ var RequestTrace = class RequestTrace {
|
|
|
1839
1912
|
request: emptyBody(`upstream-${order}-request.bin`, "complete")
|
|
1840
1913
|
};
|
|
1841
1914
|
this.manifest.exchanges.push(exchange);
|
|
1842
|
-
|
|
1843
|
-
this.append(exchange.request, bytes);
|
|
1915
|
+
this.append(exchange.request, input.body ?? "");
|
|
1844
1916
|
try {
|
|
1845
1917
|
const response = await (transportContext.getStore()?.fetch(input) ?? fallback());
|
|
1846
1918
|
exchange.status = response.status;
|
|
@@ -1955,16 +2027,17 @@ var RequestTrace = class RequestTrace {
|
|
|
1955
2027
|
}
|
|
1956
2028
|
}, { highWaterMark: 0 });
|
|
1957
2029
|
}
|
|
1958
|
-
append(body,
|
|
1959
|
-
|
|
2030
|
+
append(body, data) {
|
|
2031
|
+
const byteLength = typeof data === "string" ? Buffer.byteLength(data) : data.byteLength;
|
|
2032
|
+
body.bytes += byteLength;
|
|
1960
2033
|
if (!this.captureDirectory || this.manifest.captureError) return;
|
|
1961
|
-
if (body.chunks.length >= 1e5 || this.queuedBytes +
|
|
2034
|
+
if (body.chunks.length >= 1e5 || this.queuedBytes + byteLength > maxQueuedBytes) {
|
|
1962
2035
|
this.manifest.captureError = "capture_queue_limit";
|
|
1963
2036
|
this.manifest.captureState = "incomplete";
|
|
1964
2037
|
return;
|
|
1965
2038
|
}
|
|
1966
|
-
body.chunks.push(
|
|
1967
|
-
const buffer = Buffer.from(
|
|
2039
|
+
body.chunks.push(byteLength);
|
|
2040
|
+
const buffer = Buffer.from(data);
|
|
1968
2041
|
this.queuedBytes += buffer.length;
|
|
1969
2042
|
this.queue = this.queue.then(async () => {
|
|
1970
2043
|
await this.checkDirectories();
|
|
@@ -2162,7 +2235,8 @@ const maxFetchAttempts = 2;
|
|
|
2162
2235
|
const copilotRequestTimeoutMs = 18e4;
|
|
2163
2236
|
const copilotDispatcher = new Agent({
|
|
2164
2237
|
allowH2: false,
|
|
2165
|
-
connect: { allowH2: false }
|
|
2238
|
+
connect: { allowH2: false },
|
|
2239
|
+
keepAliveTimeout: 5e4
|
|
2166
2240
|
});
|
|
2167
2241
|
const getCopilotProviderContext = (config) => ({
|
|
2168
2242
|
baseUrl: config.copilotBaseUrl,
|
|
@@ -2414,6 +2488,11 @@ const parseStringArray = (value) => {
|
|
|
2414
2488
|
if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) return;
|
|
2415
2489
|
return value;
|
|
2416
2490
|
};
|
|
2491
|
+
const parseReasoningEfforts = (supports) => {
|
|
2492
|
+
if (supports === void 0) return;
|
|
2493
|
+
if (supports.reasoning_effort === false || !Object.hasOwn(supports, "reasoning_effort")) return [];
|
|
2494
|
+
return parseStringArray(supports.reasoning_effort);
|
|
2495
|
+
};
|
|
2417
2496
|
async function loadCopilotModelCatalog(config) {
|
|
2418
2497
|
const provider = getCopilotProviderContext(config);
|
|
2419
2498
|
const pending = pendingCatalogs.get(config);
|
|
@@ -2437,13 +2516,14 @@ async function loadCopilotModelCatalog(config) {
|
|
|
2437
2516
|
const limits = parseModelTokenLimits(capabilities?.limits);
|
|
2438
2517
|
const supports = isRecord$3(capabilities?.supports) ? capabilities.supports : void 0;
|
|
2439
2518
|
const supportedEndpoints = parseStringArray(model.supported_endpoints);
|
|
2440
|
-
const reasoningEfforts =
|
|
2519
|
+
const reasoningEfforts = parseReasoningEfforts(supports);
|
|
2441
2520
|
models$1.set(model.id, {
|
|
2442
2521
|
...limits && { limits },
|
|
2443
2522
|
...typeof capabilities?.tokenizer === "string" && { tokenizer: capabilities.tokenizer },
|
|
2444
2523
|
...supportedEndpoints !== void 0 && { supportedEndpoints },
|
|
2445
2524
|
...typeof capabilities?.type === "string" && { type: capabilities.type },
|
|
2446
|
-
...reasoningEfforts !== void 0 && { reasoningEfforts }
|
|
2525
|
+
...reasoningEfforts !== void 0 && { reasoningEfforts },
|
|
2526
|
+
...typeof model.name === "string" && model.name !== "" && { name: model.name }
|
|
2447
2527
|
});
|
|
2448
2528
|
}
|
|
2449
2529
|
const catalog = {
|
|
@@ -2483,6 +2563,75 @@ async function boundModelOutputTokens(config, model, requested) {
|
|
|
2483
2563
|
return bounded;
|
|
2484
2564
|
}
|
|
2485
2565
|
|
|
2566
|
+
//#endregion
|
|
2567
|
+
//#region src/copilot/endpoint.ts
|
|
2568
|
+
const copilotEndpoints = [
|
|
2569
|
+
"/chat/completions",
|
|
2570
|
+
"/responses",
|
|
2571
|
+
"/v1/messages"
|
|
2572
|
+
];
|
|
2573
|
+
const legacyResponsesPattern = /^(?:gpt-5\.5|gpt-5\.6|gpt-6-astra)(?:-|$)/i;
|
|
2574
|
+
function selectCopilotEndpoint(config, model) {
|
|
2575
|
+
const capabilities = getCachedCopilotModel(config, model);
|
|
2576
|
+
const advertisedEndpoints = capabilities?.supportedEndpoints;
|
|
2577
|
+
if (capabilities?.type !== void 0 && capabilities.type !== "chat") return { reason: "unsupported-model-type" };
|
|
2578
|
+
const isClaude = model.startsWith("claude-");
|
|
2579
|
+
const protocolMode = config.claudeUpstreamApi ?? "chat-completions";
|
|
2580
|
+
if (isClaude && protocolMode === "messages") return {
|
|
2581
|
+
endpoint: "/v1/messages",
|
|
2582
|
+
source: "policy",
|
|
2583
|
+
responsesFallback: false
|
|
2584
|
+
};
|
|
2585
|
+
if (isClaude && protocolMode === "auto" && advertisedEndpoints?.includes("/v1/messages")) return {
|
|
2586
|
+
endpoint: "/v1/messages",
|
|
2587
|
+
source: "catalog",
|
|
2588
|
+
responsesFallback: false
|
|
2589
|
+
};
|
|
2590
|
+
const isChatPinned = isClaude && protocolMode === "chat-completions";
|
|
2591
|
+
const preferredEndpoint = legacyResponsesPattern.test(model) ? "/responses" : "/chat/completions";
|
|
2592
|
+
if (advertisedEndpoints === void 0) return {
|
|
2593
|
+
endpoint: isChatPinned ? "/chat/completions" : preferredEndpoint,
|
|
2594
|
+
source: isChatPinned ? "policy" : "legacy",
|
|
2595
|
+
responsesFallback: !isChatPinned && preferredEndpoint === "/chat/completions"
|
|
2596
|
+
};
|
|
2597
|
+
if (advertisedEndpoints.length === 0) return { reason: "no-advertised-endpoint" };
|
|
2598
|
+
if (isChatPinned) {
|
|
2599
|
+
if (advertisedEndpoints.includes("/chat/completions")) return {
|
|
2600
|
+
endpoint: "/chat/completions",
|
|
2601
|
+
source: "policy",
|
|
2602
|
+
responsesFallback: false
|
|
2603
|
+
};
|
|
2604
|
+
return { reason: advertisedEndpoints.some((endpoint) => endpoint === "/responses" || endpoint === "/v1/messages") ? "protocol-policy-conflict" : "unsupported-relay-endpoint" };
|
|
2605
|
+
}
|
|
2606
|
+
const selectedEndpoint = [preferredEndpoint, preferredEndpoint === "/responses" ? "/chat/completions" : "/responses"].find((endpoint) => advertisedEndpoints.includes(endpoint));
|
|
2607
|
+
if (!selectedEndpoint) return { reason: "unsupported-relay-endpoint" };
|
|
2608
|
+
return {
|
|
2609
|
+
endpoint: selectedEndpoint,
|
|
2610
|
+
source: "catalog",
|
|
2611
|
+
responsesFallback: selectedEndpoint === "/chat/completions" && advertisedEndpoints.includes("/responses")
|
|
2612
|
+
};
|
|
2613
|
+
}
|
|
2614
|
+
const unavailableEndpointMessages = {
|
|
2615
|
+
"unsupported-model-type": "The selected upstream model does not advertise a chat interface.",
|
|
2616
|
+
"no-advertised-endpoint": "The selected upstream model advertises no inference endpoints.",
|
|
2617
|
+
"unsupported-relay-endpoint": "The selected upstream model advertises no compatible relay endpoint.",
|
|
2618
|
+
"protocol-policy-conflict": "The selected upstream model has no Chat endpoint allowed by claudeUpstreamApi. Choose an advertised protocol explicitly."
|
|
2619
|
+
};
|
|
2620
|
+
function requireCopilotEndpoint(config, model) {
|
|
2621
|
+
const selection = selectCopilotEndpoint(config, model);
|
|
2622
|
+
if (selection.endpoint) return selection;
|
|
2623
|
+
const message = unavailableEndpointMessages[selection.reason];
|
|
2624
|
+
throw new HTTPError(message, Response.json({
|
|
2625
|
+
type: "error",
|
|
2626
|
+
error: {
|
|
2627
|
+
type: "invalid_request_error",
|
|
2628
|
+
code: "relay_unsupported_endpoint",
|
|
2629
|
+
reason: selection.reason,
|
|
2630
|
+
message
|
|
2631
|
+
}
|
|
2632
|
+
}, { status: 400 }), message);
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2486
2635
|
//#endregion
|
|
2487
2636
|
//#region src/lib/terminal.ts
|
|
2488
2637
|
const colorEnabled = (env = process.env, isTTY = Boolean(process.stdout.isTTY)) => {
|
|
@@ -2527,6 +2676,7 @@ const reasonLabels = {
|
|
|
2527
2676
|
"no-advertised-endpoint": "No advertised route",
|
|
2528
2677
|
"protocol-policy-conflict": "Protocol policy conflict",
|
|
2529
2678
|
"unsupported-api": "Unsupported API",
|
|
2679
|
+
"model-not-supported": "Model not supported",
|
|
2530
2680
|
"unsupported-effort": "Unsupported effort",
|
|
2531
2681
|
"unsafe-or-noncanonical-id": "Unsupported model ID",
|
|
2532
2682
|
"upstream-response-failed": "Upstream failed",
|
|
@@ -2626,6 +2776,7 @@ const probeHint = (code) => {
|
|
|
2626
2776
|
if (code === "refusal") return "Refusal: inspect the failing request's evidence; a short probe does not explain its cause.";
|
|
2627
2777
|
if (code.includes("budget-exhausted")) return "Output limit: reachability is not completion; review the probe's output budget.";
|
|
2628
2778
|
if (code === "model-mismatch") return "Model mismatch: compare selected, sent and reported IDs with --details.";
|
|
2779
|
+
if (code === "model-not-supported") return "Model not supported: upstream lists this ID but rejects inference for it; choose another model.";
|
|
2629
2780
|
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
2781
|
};
|
|
2631
2782
|
|
|
@@ -3905,6 +4056,12 @@ const getEncodeChatFunction = async (encoding) => {
|
|
|
3905
4056
|
encodingCache.set(encoding, encoder);
|
|
3906
4057
|
return encoder;
|
|
3907
4058
|
};
|
|
4059
|
+
const preloadTokenizers = async (reported) => {
|
|
4060
|
+
const encodings = new Set(["o200k_base"]);
|
|
4061
|
+
for (const tokenizer of reported) if (tokenizer !== void 0 && isSupportedTokenizer(tokenizer)) encodings.add(tokenizer);
|
|
4062
|
+
for (const encoding of encodings) await getEncodeChatFunction(encoding);
|
|
4063
|
+
};
|
|
4064
|
+
const loadedTokenizers = () => [...encodingCache.keys()];
|
|
3908
4065
|
const getTokenizerFromModel = (model) => model.capabilities.tokenizer || "o200k_base";
|
|
3909
4066
|
const getModelConstants = (model) => model.id === "gpt-3.5-turbo" || model.id === "gpt-4" ? {
|
|
3910
4067
|
funcInit: 10,
|
|
@@ -4001,75 +4158,6 @@ const getTokenCount = async (payload, model) => {
|
|
|
4001
4158
|
};
|
|
4002
4159
|
};
|
|
4003
4160
|
|
|
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
4161
|
//#endregion
|
|
4074
4162
|
//#region src/copilot/tool-schema.ts
|
|
4075
4163
|
const schemaMapKeywords = new Set([
|
|
@@ -4120,17 +4208,45 @@ function hasUnsupportedPatternFeatures(pattern) {
|
|
|
4120
4208
|
}
|
|
4121
4209
|
return false;
|
|
4122
4210
|
}
|
|
4211
|
+
const removed = Symbol("removed");
|
|
4212
|
+
function mapRecord(record$3, map) {
|
|
4213
|
+
const keys = Object.keys(record$3);
|
|
4214
|
+
let entries;
|
|
4215
|
+
for (let index = 0; index < keys.length; index++) {
|
|
4216
|
+
const key = keys[index];
|
|
4217
|
+
const value = record$3[key];
|
|
4218
|
+
const mapped = map(key, value);
|
|
4219
|
+
if (!entries && Object.is(mapped, value)) continue;
|
|
4220
|
+
entries ??= keys.slice(0, index).map((kept) => [kept, record$3[kept]]);
|
|
4221
|
+
if (mapped !== removed) entries.push([key, mapped]);
|
|
4222
|
+
}
|
|
4223
|
+
return entries ? Object.fromEntries(entries) : record$3;
|
|
4224
|
+
}
|
|
4225
|
+
function mapArray(values) {
|
|
4226
|
+
let copy;
|
|
4227
|
+
for (let index = 0; index < values.length; index++) {
|
|
4228
|
+
if (!(index in values)) continue;
|
|
4229
|
+
const value = values[index];
|
|
4230
|
+
const mapped = normalizeSchemaValue(value);
|
|
4231
|
+
if (!copy && Object.is(mapped, value)) continue;
|
|
4232
|
+
copy ??= values.slice(0, index);
|
|
4233
|
+
copy[index] = mapped;
|
|
4234
|
+
}
|
|
4235
|
+
if (!copy) return values;
|
|
4236
|
+
copy.length = values.length;
|
|
4237
|
+
return copy;
|
|
4238
|
+
}
|
|
4123
4239
|
function normalizeSchemaValue(value) {
|
|
4124
|
-
if (Array.isArray(value)) return value
|
|
4240
|
+
if (Array.isArray(value)) return mapArray(value);
|
|
4125
4241
|
return isRecord$1(value) ? normalizeResponsesToolSchema(value) : value;
|
|
4126
4242
|
}
|
|
4127
4243
|
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
|
-
})
|
|
4244
|
+
return mapRecord(schema, (key, value) => {
|
|
4245
|
+
if (key === "pattern" && typeof value === "string" && hasUnsupportedPatternFeatures(value)) return removed;
|
|
4246
|
+
if (schemaMapKeywords.has(key) && isRecord$1(value)) return mapRecord(value, (_name, nested) => normalizeSchemaValue(nested));
|
|
4247
|
+
if (nestedSchemaKeywords.has(key)) return normalizeSchemaValue(value);
|
|
4248
|
+
return value;
|
|
4249
|
+
});
|
|
4134
4250
|
}
|
|
4135
4251
|
|
|
4136
4252
|
//#endregion
|
|
@@ -5824,6 +5940,13 @@ const settleDiagnostic = async (trace) => {
|
|
|
5824
5940
|
clearTimeout(timer);
|
|
5825
5941
|
}
|
|
5826
5942
|
};
|
|
5943
|
+
const datedSnapshot = /^(.+)-\d{4}-\d{2}-\d{2}$/;
|
|
5944
|
+
const reportsSelectedModel = (endpoint, id, reported) => {
|
|
5945
|
+
if (normalizeCopilotModelId(reported) === id) return true;
|
|
5946
|
+
if (endpoint === "/v1/messages" && id === "claude-opus-5.5" && reported === "claude-opus-5-5") return true;
|
|
5947
|
+
if (endpoint === "/responses" && id === "gpt-5.6-sol-fast" && normalizeCopilotModelId(reported) === "gpt-5.6-sol") return true;
|
|
5948
|
+
return !datedSnapshot.test(id) && datedSnapshot.exec(reported)?.[1] === id;
|
|
5949
|
+
};
|
|
5827
5950
|
const failureFromEvidence = (row, diagnostic) => {
|
|
5828
5951
|
if ([
|
|
5829
5952
|
"probe-timeout",
|
|
@@ -5869,7 +5992,8 @@ async function probeModels(config, entries, options) {
|
|
|
5869
5992
|
"xhigh",
|
|
5870
5993
|
"max"
|
|
5871
5994
|
].filter((effort$1) => model.reasoningEfforts?.includes(effort$1));
|
|
5872
|
-
const
|
|
5995
|
+
const lowestEffort = recognizedEfforts.find((effort$1) => effort$1 !== "none") ?? recognizedEfforts.find(isReasoningEffort);
|
|
5996
|
+
const defaultProbeEffort = advertisesNoReasoningEffort(model) ? void 0 : lowestEffort ?? "low";
|
|
5873
5997
|
const effort = options.effort ?? defaultProbeEffort;
|
|
5874
5998
|
const maxTokens = Math.min(options.maxTokens, model.limits?.max_output_tokens ?? options.maxTokens, model.limits?.max_non_streaming_output_tokens ?? options.maxTokens);
|
|
5875
5999
|
const timeoutMs = Math.min(options.timeoutMs, config.upstreamTimeoutMs > 0 ? config.upstreamTimeoutMs : Infinity);
|
|
@@ -5959,11 +6083,11 @@ async function probeModels(config, entries, options) {
|
|
|
5959
6083
|
row.detail = "upstream-response-incomplete";
|
|
5960
6084
|
}
|
|
5961
6085
|
if (code === "unsupported_api_for_model") row.detail = "unsupported-api";
|
|
6086
|
+
if (code === "model_not_supported") row.detail = "model-not-supported";
|
|
5962
6087
|
} else if (!record$1(body) || !Array.isArray(body.content)) row.detail = "malformed-response";
|
|
5963
6088
|
else {
|
|
5964
6089
|
row.reported = safeId(body.model, config.copilotToken) ? body.model : "unreported";
|
|
5965
|
-
|
|
5966
|
-
if (normalizeCopilotModelId(row.reported) !== id && !nativeOpusSpelling) row.detail = "model-mismatch";
|
|
6090
|
+
if (!reportsSelectedModel(endpoint, id, row.reported)) row.detail = "model-mismatch";
|
|
5967
6091
|
else if (body.stop_reason === "max_tokens") {
|
|
5968
6092
|
row.status = "INCOMPLETE";
|
|
5969
6093
|
const outputTokens = record$1(body.usage) ? body.usage.output_tokens : void 0;
|
|
@@ -6022,14 +6146,139 @@ async function probeModels(config, entries, options) {
|
|
|
6022
6146
|
return results.length > 0 && results.every((row) => row.status === "PASS") ? 0 : 2;
|
|
6023
6147
|
}
|
|
6024
6148
|
|
|
6149
|
+
//#endregion
|
|
6150
|
+
//#region src/lib/model-listing.ts
|
|
6151
|
+
const normalizeModelSearch = (text$1) => text$1.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
6152
|
+
const contains = (model, fragment) => normalizeModelSearch(model.id).includes(fragment) || normalizeModelSearch(model.name ?? "").includes(fragment);
|
|
6153
|
+
const minimumFragment = 4;
|
|
6154
|
+
const firstMatches = (models$1, fragments) => {
|
|
6155
|
+
for (const fragment of fragments) {
|
|
6156
|
+
const matches = models$1.filter((model) => contains(model, fragment));
|
|
6157
|
+
if (matches.length > 0) return matches;
|
|
6158
|
+
}
|
|
6159
|
+
return [];
|
|
6160
|
+
};
|
|
6161
|
+
const closestModels = (models$1, search) => {
|
|
6162
|
+
const starts = [];
|
|
6163
|
+
const ends = [];
|
|
6164
|
+
for (let length = search.length - 1; length >= minimumFragment; length--) {
|
|
6165
|
+
starts.push(search.slice(0, length));
|
|
6166
|
+
ends.push(search.slice(search.length - length));
|
|
6167
|
+
}
|
|
6168
|
+
const near = new Set([...firstMatches(models$1, starts), ...firstMatches(models$1, ends)]);
|
|
6169
|
+
return models$1.filter((model) => near.has(model));
|
|
6170
|
+
};
|
|
6171
|
+
const searchModels = (models$1, search) => {
|
|
6172
|
+
const wanted = normalizeModelSearch(search);
|
|
6173
|
+
const matches = models$1.filter((model) => contains(model, wanted));
|
|
6174
|
+
if (matches.length === 0) return {
|
|
6175
|
+
found: false,
|
|
6176
|
+
models: closestModels(models$1, wanted)
|
|
6177
|
+
};
|
|
6178
|
+
const usable = matches.filter((model) => model.unusable === void 0);
|
|
6179
|
+
return {
|
|
6180
|
+
found: true,
|
|
6181
|
+
models: matches,
|
|
6182
|
+
chosen: usable.find((model) => normalizeModelSearch(model.id) === wanted) ?? (usable.length === 1 ? usable[0] : void 0)
|
|
6183
|
+
};
|
|
6184
|
+
};
|
|
6185
|
+
const maximumIdWidth = 40;
|
|
6186
|
+
const lowerFirst = (text$1) => text$1.charAt(0).toLowerCase() + text$1.slice(1);
|
|
6187
|
+
const detailsOf = (model) => {
|
|
6188
|
+
const details = [];
|
|
6189
|
+
if (model.name !== void 0) details.push(model.name);
|
|
6190
|
+
if (model.unusable !== void 0) details.push(`cannot be gptModel or opusModel: ${lowerFirst(model.unusable)}`);
|
|
6191
|
+
return details.join(" · ");
|
|
6192
|
+
};
|
|
6193
|
+
const renderModelRows = (models$1) => {
|
|
6194
|
+
const described = models$1.filter((model) => detailsOf(model) !== "");
|
|
6195
|
+
const width = Math.min(maximumIdWidth, Math.max(0, ...described.map((model) => model.id.length)));
|
|
6196
|
+
return models$1.map((model) => {
|
|
6197
|
+
const details = detailsOf(model);
|
|
6198
|
+
if (details === "") return model.id;
|
|
6199
|
+
return `${model.id.padEnd(width)} ${details}`;
|
|
6200
|
+
});
|
|
6201
|
+
};
|
|
6202
|
+
const routes = {
|
|
6203
|
+
gptModel: "requests without \"opus\" in the model name",
|
|
6204
|
+
opusModel: "requests with \"opus\" in the model name"
|
|
6205
|
+
};
|
|
6206
|
+
const applyNote = "A running relay applies the change to new requests; restarting reruns the startup check.";
|
|
6207
|
+
const keyFor = (id) => id.toLowerCase().includes("opus") ? "opusModel" : "gptModel";
|
|
6208
|
+
const problemOf = (model) => {
|
|
6209
|
+
if (!model.advertised) return "is not advertised by upstream";
|
|
6210
|
+
if (model.unusable !== void 0) return `cannot be used (${lowerFirst(model.unusable)})`;
|
|
6211
|
+
};
|
|
6212
|
+
const settingOf = (model) => `${model.key}: ${model.value}`;
|
|
6213
|
+
const renderConfigGuide = (configPath$1, configured, chosen) => {
|
|
6214
|
+
const width = Math.max(...configured.map((model) => settingOf(model).length));
|
|
6215
|
+
const lines = [`Config file: ${configPath$1}`];
|
|
6216
|
+
for (const model of configured) lines.push(` ${settingOf(model).padEnd(width)} # ${routes[model.key]}`);
|
|
6217
|
+
for (const model of configured) {
|
|
6218
|
+
const problem = problemOf(model);
|
|
6219
|
+
if (problem !== void 0) lines.push(` Warning: ${model.key} ${model.value} ${problem}; the startup check rejects it.`);
|
|
6220
|
+
}
|
|
6221
|
+
if (chosen === void 0) {
|
|
6222
|
+
lines.push("Set gptModel or opusModel to a chat model ID exactly as listed.", applyNote);
|
|
6223
|
+
return lines;
|
|
6224
|
+
}
|
|
6225
|
+
const key = keyFor(chosen.id);
|
|
6226
|
+
if (configured.some((model) => model.key === key && model.value === chosen.id)) {
|
|
6227
|
+
lines.push(`${key} already uses ${chosen.id}.`);
|
|
6228
|
+
return lines;
|
|
6229
|
+
}
|
|
6230
|
+
lines.push(`To use ${chosen.id}, set this line in the config file:`, ` ${key}: ${chosen.id}`, applyNote);
|
|
6231
|
+
return lines;
|
|
6232
|
+
};
|
|
6233
|
+
|
|
6025
6234
|
//#endregion
|
|
6026
6235
|
//#region src/models.ts
|
|
6236
|
+
var ModelsUsageError = class extends Error {};
|
|
6237
|
+
const displayName = (name) => {
|
|
6238
|
+
const text$1 = sanitizeTerminalString(name ?? "").trim();
|
|
6239
|
+
if (text$1 === "") return;
|
|
6240
|
+
return text$1.length > 80 ? `${text$1.slice(0, 77)}...` : text$1;
|
|
6241
|
+
};
|
|
6242
|
+
const listedModels = (config, catalog) => [...catalog.models.keys()].sort().map((id) => {
|
|
6243
|
+
const selection = selectCopilotEndpoint(config, id);
|
|
6244
|
+
const name = displayName(catalog.models.get(id)?.name);
|
|
6245
|
+
return {
|
|
6246
|
+
id: sanitizeTerminalString(id),
|
|
6247
|
+
...name !== void 0 && { name },
|
|
6248
|
+
...!selection.endpoint && { unusable: probeReason(selection.reason) }
|
|
6249
|
+
};
|
|
6250
|
+
});
|
|
6251
|
+
const configuredModels = (appConfig, config, catalog) => ["gptModel", "opusModel"].map((key) => {
|
|
6252
|
+
const id = normalizeCopilotModelId(appConfig[key]);
|
|
6253
|
+
const selection = selectCopilotEndpoint(config, id);
|
|
6254
|
+
return {
|
|
6255
|
+
key,
|
|
6256
|
+
value: sanitizeTerminalString(appConfig[key]),
|
|
6257
|
+
advertised: catalog.models.has(id),
|
|
6258
|
+
...!selection.endpoint && { unusable: probeReason(selection.reason) }
|
|
6259
|
+
};
|
|
6260
|
+
});
|
|
6261
|
+
const listingHeading = (search, result) => {
|
|
6262
|
+
if (search === "") {
|
|
6263
|
+
if (result.models.length === 0) return "No models advertised by upstream.";
|
|
6264
|
+
return `Upstream-advertised models (${result.models.length}):`;
|
|
6265
|
+
}
|
|
6266
|
+
const label = sanitizeTerminalString(search);
|
|
6267
|
+
if (result.found) return `Upstream models matching "${label}" (${result.models.length}):`;
|
|
6268
|
+
if (result.models.length === 0) return `No upstream model matches "${label}". List them all with copilot-relay models.`;
|
|
6269
|
+
return `No upstream model matches "${label}". Closest IDs (${result.models.length}):`;
|
|
6270
|
+
};
|
|
6027
6271
|
const models = defineCommand({
|
|
6028
6272
|
meta: {
|
|
6029
6273
|
name: "models",
|
|
6030
6274
|
description: "List upstream models; use --deep to test inference availability."
|
|
6031
6275
|
},
|
|
6032
6276
|
args: {
|
|
6277
|
+
search: {
|
|
6278
|
+
type: "positional",
|
|
6279
|
+
required: false,
|
|
6280
|
+
description: "Find models by ID or display name and print the config line to use."
|
|
6281
|
+
},
|
|
6033
6282
|
deep: {
|
|
6034
6283
|
type: "boolean",
|
|
6035
6284
|
description: "Send real inference probes through an isolated relay pipeline; consumes Copilot usage."
|
|
@@ -6076,6 +6325,9 @@ const models = defineCommand({
|
|
|
6076
6325
|
args["total-timeout"]
|
|
6077
6326
|
].some((value) => value !== void 0))) throw new Error("deep required");
|
|
6078
6327
|
if (args.effort !== void 0 && !isReasoningEffort(args.effort)) throw new Error("invalid effort");
|
|
6328
|
+
const search = args._.join(" ").trim();
|
|
6329
|
+
if (search !== "" && args.deep) throw new ModelsUsageError("A search lists models; check one with copilot-relay models --deep --model <id>.");
|
|
6330
|
+
if (search !== "" && normalizeModelSearch(search) === "") throw new ModelsUsageError("A search needs at least one letter or digit.");
|
|
6079
6331
|
const probeOptions = {
|
|
6080
6332
|
maxTokens: positive(args["max-tokens"], 4096),
|
|
6081
6333
|
timeoutMs: positive(args.timeout, 30) * 1e3,
|
|
@@ -6085,22 +6337,23 @@ const models = defineCommand({
|
|
|
6085
6337
|
};
|
|
6086
6338
|
const prepare = async () => {
|
|
6087
6339
|
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);
|
|
6340
|
+
const appConfig$1 = await readAppConfig();
|
|
6341
|
+
setLogLevel(appConfig$1.logLevel);
|
|
6342
|
+
registerSensitiveOrigin(appConfig$1.copilotBaseUrl);
|
|
6343
|
+
const config$1 = readProxyConfig(appConfig$1);
|
|
6092
6344
|
failure = "Could not authenticate with GitHub Copilot";
|
|
6093
6345
|
await setupProxyAuth(config$1, args.deep ? { onDeviceCode: (url, code) => console.error(`Sign in: open ${terminalText(url)} and enter ${terminalText(code)}.`) } : void 0);
|
|
6094
6346
|
failure = "Could not fetch upstream model catalog";
|
|
6095
6347
|
return {
|
|
6348
|
+
appConfig: appConfig$1,
|
|
6096
6349
|
config: config$1,
|
|
6097
6350
|
catalog: await loadCopilotModelCatalog(config$1)
|
|
6098
6351
|
};
|
|
6099
6352
|
};
|
|
6100
|
-
const { config, catalog } = args.deep ? await withoutConsoleLogging(prepare) : await prepare();
|
|
6353
|
+
const { appConfig, config, catalog } = args.deep ? await withoutConsoleLogging(prepare) : await prepare();
|
|
6101
6354
|
if (args.deep) {
|
|
6102
6355
|
failure = "Selected model is not advertised by upstream";
|
|
6103
|
-
if (args.model !== void 0 && !catalog.models.has(args.model)) throw new
|
|
6356
|
+
if (args.model !== void 0 && !catalog.models.has(args.model)) throw new ModelsUsageError("Find the exact ID with copilot-relay models <search>.");
|
|
6104
6357
|
const entries = [...catalog.models].sort(([a], [b]) => {
|
|
6105
6358
|
if (a < b) return -1;
|
|
6106
6359
|
return a > b ? 1 : 0;
|
|
@@ -6109,12 +6362,20 @@ const models = defineCommand({
|
|
|
6109
6362
|
process.exitCode = await probeModels(config, entries, probeOptions);
|
|
6110
6363
|
return;
|
|
6111
6364
|
}
|
|
6112
|
-
const
|
|
6113
|
-
|
|
6114
|
-
|
|
6365
|
+
const listed = listedModels(config, catalog);
|
|
6366
|
+
const result = search === "" ? {
|
|
6367
|
+
found: true,
|
|
6368
|
+
models: listed
|
|
6369
|
+
} : searchModels(listed, search);
|
|
6370
|
+
const lines = [listingHeading(search, result), ...renderModelRows(result.models)];
|
|
6371
|
+
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.`);
|
|
6372
|
+
lines.push("", ...renderConfigGuide(paths.configPath, configuredModels(appConfig, config, catalog), result.chosen));
|
|
6373
|
+
console.log(scrubSensitiveUrls(lines.join("\n")));
|
|
6374
|
+
if (!result.found) process.exitCode = 1;
|
|
6115
6375
|
} catch (error) {
|
|
6116
6376
|
let detail = "Check configuration, authentication, and upstream connectivity.";
|
|
6117
|
-
if (error instanceof
|
|
6377
|
+
if (error instanceof ModelsUsageError) detail = error.message;
|
|
6378
|
+
else if (error instanceof HTTPError) {
|
|
6118
6379
|
if (error.detail !== void 0) detail = error.response.status === 504 ? "request timed out" : "request cancelled";
|
|
6119
6380
|
else detail = `HTTP ${error.response.status}`;
|
|
6120
6381
|
await error.response.body?.cancel().catch(() => {});
|
|
@@ -6670,9 +6931,9 @@ const replay = defineCommand({
|
|
|
6670
6931
|
const result = await replayCapture(args.target);
|
|
6671
6932
|
console.log(result.verdict);
|
|
6672
6933
|
if (result.summary) {
|
|
6673
|
-
const { method, path: path$1, status: status$1, routes, outcome } = result.summary;
|
|
6934
|
+
const { method, path: path$1, status: status$1, routes: routes$1, outcome } = result.summary;
|
|
6674
6935
|
console.log(`${method} ${path$1} status=${status$1 ?? "unknown"} outcome=${outcome}`);
|
|
6675
|
-
console.log(`upstream=${routes.join(" -> ") || "none"}`);
|
|
6936
|
+
console.log(`upstream=${routes$1.join(" -> ") || "none"}`);
|
|
6676
6937
|
for (const exchange of result.summary.exchanges) {
|
|
6677
6938
|
const usage = Object.entries(exchange.usage).map(([key, value]) => `${key}=${value}`).join(" ");
|
|
6678
6939
|
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 +7567,12 @@ async function startRelay(appConfig) {
|
|
|
7306
7567
|
process.exit(1);
|
|
7307
7568
|
}
|
|
7308
7569
|
log.info(`Exposed models: ${getExposedModelIds().join(", ")}`);
|
|
7570
|
+
try {
|
|
7571
|
+
await preloadTokenizers(getUpstreamModelIds().map((id) => getCachedCopilotModel(config, id)?.tokenizer));
|
|
7572
|
+
log.info(`Tokenizers loaded: ${loadedTokenizers().join(", ")}`);
|
|
7573
|
+
} catch (error) {
|
|
7574
|
+
log.error("Tokenizer preload failed; count_tokens loads tokenizers on first use:", error);
|
|
7575
|
+
}
|
|
7309
7576
|
const server = await startServer(config);
|
|
7310
7577
|
await writeRelayPidFile(config);
|
|
7311
7578
|
log.info(`copilot-relay version: ${appVersion}`);
|