copilot-relay 0.3.5 → 0.3.7
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 +18 -2
- package/config.default.yaml +5 -1
- package/dist/main.js +341 -109
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -547,13 +547,39 @@ const defaultModelRouting = {
|
|
|
547
547
|
opusModel: "claude-opus-5"
|
|
548
548
|
};
|
|
549
549
|
const isReasoningEffort = (value) => value === "none" || value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "max";
|
|
550
|
+
const invalidRequestEffort = (message) => new HTTPError(message, new Response(JSON.stringify({
|
|
551
|
+
type: "error",
|
|
552
|
+
error: {
|
|
553
|
+
type: "invalid_request_error",
|
|
554
|
+
message
|
|
555
|
+
}
|
|
556
|
+
}), {
|
|
557
|
+
status: 400,
|
|
558
|
+
headers: { "content-type": "application/json" }
|
|
559
|
+
}), message);
|
|
560
|
+
function getRequestReasoningEffort(request) {
|
|
561
|
+
let nativeEffort;
|
|
562
|
+
const outputConfig = request.output_config;
|
|
563
|
+
if (outputConfig !== void 0 && outputConfig !== null) {
|
|
564
|
+
if (typeof outputConfig !== "object" || Array.isArray(outputConfig)) throw invalidRequestEffort("Invalid output_config: expected an object.");
|
|
565
|
+
if ("effort" in outputConfig) nativeEffort = outputConfig.effort;
|
|
566
|
+
}
|
|
567
|
+
const requested = nativeEffort ?? request.reasoning_effort;
|
|
568
|
+
if (requested === void 0 || requested === null) return void 0;
|
|
569
|
+
if (!isReasoningEffort(requested)) throw invalidRequestEffort(`Invalid ${nativeEffort !== void 0 && nativeEffort !== null ? "output_config.effort" : "reasoning_effort"}: expected none, low, medium, high, xhigh, or max.`);
|
|
570
|
+
return requested;
|
|
571
|
+
}
|
|
572
|
+
const resolveReasoningEffort = (requested) => requested ?? runtimeState.thinkEffort ?? defaultReasoningEffort;
|
|
550
573
|
const oneMillionContextModelPattern = /^(gpt-5\.6-sol|gpt-6-astra)(?:\[1m\])*$/i;
|
|
551
574
|
const normalizeOneMillionContextModel = (model) => {
|
|
552
575
|
return oneMillionContextModelPattern.exec(model)?.[1]?.toLowerCase();
|
|
553
576
|
};
|
|
554
|
-
const normalizeClaudeModelId = (model) => {
|
|
577
|
+
const normalizeClaudeModelId = (model, contextWindowTokens) => {
|
|
555
578
|
const normalized = normalizeOneMillionContextModel(model);
|
|
556
|
-
|
|
579
|
+
if (!normalized) return model;
|
|
580
|
+
const catalog = runtimeState.modelCatalog;
|
|
581
|
+
contextWindowTokens ??= catalog?.baseUrl === runtimeState.upstreamBaseUrl ? catalog?.models.get(normalized)?.limits?.max_context_window_tokens : void 0;
|
|
582
|
+
return contextWindowTokens !== void 0 && contextWindowTokens !== 1e6 ? normalized : `${normalized}[1m]`;
|
|
557
583
|
};
|
|
558
584
|
const normalizeCopilotModelId = (model) => normalizeOneMillionContextModel(model) ?? model;
|
|
559
585
|
const getConfiguredModelRouting = () => runtimeState.modelRouting ?? defaultModelRouting;
|
|
@@ -717,8 +743,8 @@ const normalizeThinkEffort = (value) => {
|
|
|
717
743
|
};
|
|
718
744
|
const normalizeUpstreamTimeoutSeconds = (value) => {
|
|
719
745
|
if (value === void 0) return;
|
|
720
|
-
const timeout =
|
|
721
|
-
if (timeout
|
|
746
|
+
const timeout = typeof value === "string" && /^\d+$/.test(value) ? Number(value) : value;
|
|
747
|
+
if (typeof timeout !== "number" || !Number.isSafeInteger(timeout) || timeout < 0) throw new Error("Invalid upstreamTimeoutSeconds: expected a non-negative integer (0 disables the deadline)");
|
|
722
748
|
return timeout;
|
|
723
749
|
};
|
|
724
750
|
const readRawConfig = async () => {
|
|
@@ -841,10 +867,10 @@ const serializeConfig = (config) => [
|
|
|
841
867
|
"# Number of days to keep files in ~/.copilot-relay/logs.",
|
|
842
868
|
`logRetentionDays: ${config.logRetentionDays}`,
|
|
843
869
|
"",
|
|
844
|
-
"#
|
|
870
|
+
"# Fallback effort when the request omits it: none, low, medium, high, xhigh, max.",
|
|
845
871
|
`thinkEffort: ${config.thinkEffort}`,
|
|
846
872
|
"",
|
|
847
|
-
"# Max seconds
|
|
873
|
+
"# Max seconds for one request's upstream calls; 0 disables the relay deadline.",
|
|
848
874
|
`upstreamTimeoutSeconds: ${config.upstreamTimeoutSeconds}`,
|
|
849
875
|
"",
|
|
850
876
|
"# Copilot model used for bridge-managed Claude WebSearch. Empty uses gptModel.",
|
|
@@ -1190,11 +1216,11 @@ const knownModelEnvKeys = [
|
|
|
1190
1216
|
"ANTHROPIC_SMALL_FAST_MODEL",
|
|
1191
1217
|
"CLAUDE_CODE_SUBAGENT_MODEL"
|
|
1192
1218
|
];
|
|
1193
|
-
const isRecord$
|
|
1219
|
+
const isRecord$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1194
1220
|
const hasMalformedModelOverride = (parsed) => {
|
|
1195
1221
|
if (parsed.model !== void 0 && typeof parsed.model !== "string") return true;
|
|
1196
1222
|
const env = parsed.env;
|
|
1197
|
-
if (!isRecord$
|
|
1223
|
+
if (!isRecord$3(env)) return false;
|
|
1198
1224
|
return knownModelEnvKeys.some((key) => env[key] !== void 0 && typeof env[key] !== "string");
|
|
1199
1225
|
};
|
|
1200
1226
|
const hasPrimaryModelOverride = (parsed, env) => typeof parsed.model === "string" || typeof env.ANTHROPIC_MODEL === "string";
|
|
@@ -1221,7 +1247,7 @@ async function applyClaudeConfig(input) {
|
|
|
1221
1247
|
};
|
|
1222
1248
|
if (raw.trim().length > 0) try {
|
|
1223
1249
|
const value = JSON.parse(raw);
|
|
1224
|
-
if (!isRecord$
|
|
1250
|
+
if (!isRecord$3(value) || value.env !== void 0 && !isRecord$3(value.env) || hasMalformedModelOverride(value)) return {
|
|
1225
1251
|
configPath: configPath$1,
|
|
1226
1252
|
changed: false,
|
|
1227
1253
|
created: false
|
|
@@ -1234,15 +1260,18 @@ async function applyClaudeConfig(input) {
|
|
|
1234
1260
|
created: false
|
|
1235
1261
|
};
|
|
1236
1262
|
}
|
|
1237
|
-
const env = isRecord$
|
|
1263
|
+
const env = isRecord$3(parsed.env) ? { ...parsed.env } : {};
|
|
1238
1264
|
const previousBaseUrl = typeof env.ANTHROPIC_BASE_URL === "string" ? env.ANTHROPIC_BASE_URL : void 0;
|
|
1239
1265
|
const hadPrimaryModelOverride = hasPrimaryModelOverride(parsed, env);
|
|
1240
|
-
const
|
|
1266
|
+
const contextWindow = input.gptLimits?.max_context_window_tokens;
|
|
1267
|
+
const claudeGptModel = normalizeClaudeModelId(gptModel, contextWindow);
|
|
1241
1268
|
const shouldSeedTopLevelModel = !hadPrimaryModelOverride;
|
|
1242
|
-
const nextModel = typeof parsed.model === "string" ? normalizeClaudeModelId(parsed.model) : void 0;
|
|
1243
|
-
for (const key of knownModelEnvKeys) if (typeof env[key] === "string") env[key] = normalizeClaudeModelId(env[key]);
|
|
1269
|
+
const nextModel = typeof parsed.model === "string" ? normalizeClaudeModelId(parsed.model, contextWindow) : void 0;
|
|
1270
|
+
for (const key of knownModelEnvKeys) if (typeof env[key] === "string") env[key] = normalizeClaudeModelId(env[key], contextWindow);
|
|
1244
1271
|
env.ANTHROPIC_BASE_URL = baseUrl;
|
|
1245
1272
|
if (typeof env.ANTHROPIC_AUTH_TOKEN !== "string" || !env.ANTHROPIC_AUTH_TOKEN) env.ANTHROPIC_AUTH_TOKEN = "dummy";
|
|
1273
|
+
if (contextWindow !== void 0 && env.CLAUDE_CODE_MAX_CONTEXT_TOKENS === void 0) env.CLAUDE_CODE_MAX_CONTEXT_TOKENS = String(contextWindow);
|
|
1274
|
+
if (input.maxOutputTokens !== void 0 && env.CLAUDE_CODE_MAX_OUTPUT_TOKENS === void 0) env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = String(input.maxOutputTokens);
|
|
1246
1275
|
const next = {
|
|
1247
1276
|
...parsed,
|
|
1248
1277
|
...nextModel !== void 0 && { model: nextModel },
|
|
@@ -1389,6 +1418,229 @@ const fetchCopilot = async (provider, path$1, init, options = {}) => {
|
|
|
1389
1418
|
throw lastError;
|
|
1390
1419
|
};
|
|
1391
1420
|
|
|
1421
|
+
//#endregion
|
|
1422
|
+
//#region src/copilot/models.ts
|
|
1423
|
+
const pendingCatalogs = /* @__PURE__ */ new WeakMap();
|
|
1424
|
+
const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1425
|
+
const isPositiveInteger = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
1426
|
+
function parseModelTokenLimits(value) {
|
|
1427
|
+
if (!isRecord$2(value)) return void 0;
|
|
1428
|
+
const context = value.max_context_window_tokens;
|
|
1429
|
+
const prompt = value.max_prompt_tokens;
|
|
1430
|
+
const output = value.max_output_tokens;
|
|
1431
|
+
const nonStreaming = value.max_non_streaming_output_tokens;
|
|
1432
|
+
if (!isPositiveInteger(context) || !isPositiveInteger(prompt) || !isPositiveInteger(output) || prompt > context || output > context || nonStreaming !== void 0 && (!isPositiveInteger(nonStreaming) || nonStreaming > output)) return;
|
|
1433
|
+
return {
|
|
1434
|
+
max_context_window_tokens: context,
|
|
1435
|
+
max_prompt_tokens: prompt,
|
|
1436
|
+
max_output_tokens: output,
|
|
1437
|
+
...nonStreaming !== void 0 && { max_non_streaming_output_tokens: nonStreaming }
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
function getCachedCopilotModel(config, model) {
|
|
1441
|
+
return config.modelCatalog?.baseUrl === config.copilotBaseUrl ? config.modelCatalog.models.get(normalizeCopilotModelId(model)) : void 0;
|
|
1442
|
+
}
|
|
1443
|
+
async function loadCopilotModelCatalog(config) {
|
|
1444
|
+
const provider = getCopilotProviderContext(config);
|
|
1445
|
+
const pending = pendingCatalogs.get(config);
|
|
1446
|
+
if (pending?.baseUrl === provider.baseUrl) return pending.promise;
|
|
1447
|
+
const promise = (async () => {
|
|
1448
|
+
const signal = createCopilotRequestSignal(void 0, config.upstreamTimeoutMs);
|
|
1449
|
+
const response = await fetchCopilot(provider, "/models", {
|
|
1450
|
+
method: "GET",
|
|
1451
|
+
headers: { accept: "application/json" }
|
|
1452
|
+
}, {
|
|
1453
|
+
signal,
|
|
1454
|
+
timeoutMs: config.upstreamTimeoutMs
|
|
1455
|
+
});
|
|
1456
|
+
if (!response.ok) throw new HTTPError("Failed to validate upstream models", response);
|
|
1457
|
+
const payload = await readCopilotJson(response, signal, config.upstreamTimeoutMs);
|
|
1458
|
+
if (!isRecord$2(payload) || !Array.isArray(payload.data)) throw new Error("Copilot /models returned an invalid model catalog.");
|
|
1459
|
+
const models = /* @__PURE__ */ new Map();
|
|
1460
|
+
for (const model of payload.data) {
|
|
1461
|
+
if (!isRecord$2(model) || typeof model.id !== "string" || !model.id) continue;
|
|
1462
|
+
const capabilities = isRecord$2(model.capabilities) ? model.capabilities : void 0;
|
|
1463
|
+
const limits = parseModelTokenLimits(capabilities?.limits);
|
|
1464
|
+
models.set(model.id, {
|
|
1465
|
+
...limits && { limits },
|
|
1466
|
+
...typeof capabilities?.tokenizer === "string" && { tokenizer: capabilities.tokenizer }
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
const catalog = {
|
|
1470
|
+
baseUrl: provider.baseUrl,
|
|
1471
|
+
models
|
|
1472
|
+
};
|
|
1473
|
+
if (config.copilotBaseUrl === provider.baseUrl) {
|
|
1474
|
+
config.modelCatalog = catalog;
|
|
1475
|
+
if (runtimeState.upstreamBaseUrl === provider.baseUrl) runtimeState.modelCatalog = catalog;
|
|
1476
|
+
}
|
|
1477
|
+
return catalog;
|
|
1478
|
+
})();
|
|
1479
|
+
pendingCatalogs.set(config, {
|
|
1480
|
+
baseUrl: provider.baseUrl,
|
|
1481
|
+
promise
|
|
1482
|
+
});
|
|
1483
|
+
try {
|
|
1484
|
+
return await promise;
|
|
1485
|
+
} finally {
|
|
1486
|
+
if (pendingCatalogs.get(config)?.promise === promise) pendingCatalogs.delete(config);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
async function boundModelOutputTokens(config, model, requested) {
|
|
1490
|
+
if (!config.modelCatalog) return requested;
|
|
1491
|
+
if (config.modelCatalog.baseUrl !== config.copilotBaseUrl || !config.modelCatalog.models.has(model)) {
|
|
1492
|
+
await loadCopilotModelCatalog(config);
|
|
1493
|
+
if (config.modelCatalog.baseUrl !== config.copilotBaseUrl) throw new Error("Copilot base URL changed during model discovery; retry the request.");
|
|
1494
|
+
}
|
|
1495
|
+
const limits = getCachedCopilotModel(config, model)?.limits;
|
|
1496
|
+
if (!limits || !isPositiveInteger(requested)) return requested;
|
|
1497
|
+
const bounded = Math.min(requested, limits.max_output_tokens);
|
|
1498
|
+
if (bounded !== requested) log.info(`Model output budget: model=${model} requested=${requested} effective=${bounded}`);
|
|
1499
|
+
return bounded;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
//#endregion
|
|
1503
|
+
//#region src/copilot/stream.ts
|
|
1504
|
+
const accumulateChunks = (chunks) => {
|
|
1505
|
+
const first = chunks[0];
|
|
1506
|
+
const toolCalls = [];
|
|
1507
|
+
let content = "";
|
|
1508
|
+
let reasoning = "";
|
|
1509
|
+
let finishReason = null;
|
|
1510
|
+
let usage;
|
|
1511
|
+
for (const chunk of chunks) {
|
|
1512
|
+
if (chunk.usage) usage = chunk.usage;
|
|
1513
|
+
const choice = chunk.choices[0];
|
|
1514
|
+
if (!choice) continue;
|
|
1515
|
+
if (typeof choice.delta?.content === "string") content += choice.delta.content;
|
|
1516
|
+
const chunkReasoning = choice.delta?.reasoning_text ?? choice.delta?.reasoning_content;
|
|
1517
|
+
if (typeof chunkReasoning === "string") reasoning += chunkReasoning;
|
|
1518
|
+
if (choice.finish_reason) finishReason = choice.finish_reason;
|
|
1519
|
+
for (const call of choice.delta?.tool_calls ?? []) {
|
|
1520
|
+
const existing = toolCalls[call.index];
|
|
1521
|
+
if (existing) {
|
|
1522
|
+
existing.function.arguments += call.function?.arguments ?? "";
|
|
1523
|
+
continue;
|
|
1524
|
+
}
|
|
1525
|
+
toolCalls[call.index] = {
|
|
1526
|
+
id: call.id ?? `call_${call.index}`,
|
|
1527
|
+
type: "function",
|
|
1528
|
+
function: {
|
|
1529
|
+
name: call.function?.name ?? "",
|
|
1530
|
+
arguments: call.function?.arguments ?? ""
|
|
1531
|
+
}
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
const collectedToolCalls = toolCalls.filter(Boolean);
|
|
1536
|
+
return {
|
|
1537
|
+
id: first?.id ?? "chat_stream_accumulated",
|
|
1538
|
+
object: "chat.completion",
|
|
1539
|
+
created: first?.created ?? 0,
|
|
1540
|
+
model: first?.model ?? "",
|
|
1541
|
+
choices: [{
|
|
1542
|
+
index: 0,
|
|
1543
|
+
message: {
|
|
1544
|
+
role: "assistant",
|
|
1545
|
+
content: content || null,
|
|
1546
|
+
...reasoning ? { reasoning_text: reasoning } : {},
|
|
1547
|
+
...collectedToolCalls.length > 0 ? { tool_calls: collectedToolCalls } : {}
|
|
1548
|
+
},
|
|
1549
|
+
finish_reason: finishReason ?? "stop"
|
|
1550
|
+
}],
|
|
1551
|
+
...usage ? { usage } : {}
|
|
1552
|
+
};
|
|
1553
|
+
};
|
|
1554
|
+
const incompleteStreamError = () => {
|
|
1555
|
+
const message = "Copilot stream ended without a completed response.";
|
|
1556
|
+
return new HTTPError(message, new Response(JSON.stringify({ error: {
|
|
1557
|
+
type: "api_error",
|
|
1558
|
+
message
|
|
1559
|
+
} }), {
|
|
1560
|
+
status: 502,
|
|
1561
|
+
headers: { "content-type": "application/json" }
|
|
1562
|
+
}), message);
|
|
1563
|
+
};
|
|
1564
|
+
async function collectChatCompletionStream(stream, signal, timeoutMs) {
|
|
1565
|
+
const chunks = [];
|
|
1566
|
+
try {
|
|
1567
|
+
for await (const event of stream) {
|
|
1568
|
+
if (event.data === "[DONE]") break;
|
|
1569
|
+
if (!event.data) continue;
|
|
1570
|
+
const chunk = JSON.parse(event.data);
|
|
1571
|
+
if (!chunk || !Array.isArray(chunk.choices)) throw incompleteStreamError();
|
|
1572
|
+
chunks.push(chunk);
|
|
1573
|
+
}
|
|
1574
|
+
if (!chunks.some((chunk) => chunk.choices[0]?.finish_reason)) throw incompleteStreamError();
|
|
1575
|
+
return accumulateChunks(chunks);
|
|
1576
|
+
} catch (error) {
|
|
1577
|
+
throw toCopilotAbortHTTPError(error, signal, timeoutMs) ?? error;
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
//#endregion
|
|
1582
|
+
//#region src/copilot/tool-schema.ts
|
|
1583
|
+
const schemaMapKeywords = new Set([
|
|
1584
|
+
"$defs",
|
|
1585
|
+
"definitions",
|
|
1586
|
+
"dependencies",
|
|
1587
|
+
"dependentSchemas",
|
|
1588
|
+
"patternProperties",
|
|
1589
|
+
"properties"
|
|
1590
|
+
]);
|
|
1591
|
+
const nestedSchemaKeywords = new Set([
|
|
1592
|
+
"additionalItems",
|
|
1593
|
+
"additionalProperties",
|
|
1594
|
+
"allOf",
|
|
1595
|
+
"anyOf",
|
|
1596
|
+
"contains",
|
|
1597
|
+
"contentSchema",
|
|
1598
|
+
"else",
|
|
1599
|
+
"if",
|
|
1600
|
+
"items",
|
|
1601
|
+
"not",
|
|
1602
|
+
"oneOf",
|
|
1603
|
+
"prefixItems",
|
|
1604
|
+
"propertyNames",
|
|
1605
|
+
"then",
|
|
1606
|
+
"unevaluatedItems",
|
|
1607
|
+
"unevaluatedProperties"
|
|
1608
|
+
]);
|
|
1609
|
+
const lookaroundPrefixes = [
|
|
1610
|
+
"(?=",
|
|
1611
|
+
"(?!",
|
|
1612
|
+
"(?<=",
|
|
1613
|
+
"(?<!"
|
|
1614
|
+
];
|
|
1615
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1616
|
+
function hasUnsupportedPatternFeatures(pattern) {
|
|
1617
|
+
let inCharacterClass = false;
|
|
1618
|
+
for (let index = 0; index < pattern.length; index++) {
|
|
1619
|
+
const character = pattern[index];
|
|
1620
|
+
if (character === "\\") {
|
|
1621
|
+
if ((pattern[index + 1] === "p" || pattern[index + 1] === "P") && pattern[index + 2] === "{") return true;
|
|
1622
|
+
index += 1;
|
|
1623
|
+
continue;
|
|
1624
|
+
}
|
|
1625
|
+
if (character === "[") inCharacterClass = true;
|
|
1626
|
+
else if (character === "]") inCharacterClass = false;
|
|
1627
|
+
else if (!inCharacterClass && character === "(" && lookaroundPrefixes.some((prefix) => pattern.startsWith(prefix, index))) return true;
|
|
1628
|
+
}
|
|
1629
|
+
return false;
|
|
1630
|
+
}
|
|
1631
|
+
function normalizeSchemaValue(value) {
|
|
1632
|
+
if (Array.isArray(value)) return value.map(normalizeSchemaValue);
|
|
1633
|
+
return isRecord$1(value) ? normalizeResponsesToolSchema(value) : value;
|
|
1634
|
+
}
|
|
1635
|
+
function normalizeResponsesToolSchema(schema) {
|
|
1636
|
+
return Object.fromEntries(Object.entries(schema).flatMap(([key, value]) => {
|
|
1637
|
+
if (key === "pattern" && typeof value === "string" && hasUnsupportedPatternFeatures(value)) return [];
|
|
1638
|
+
if (schemaMapKeywords.has(key) && isRecord$1(value)) return [[key, Object.fromEntries(Object.entries(value).map(([name, nested]) => [name, normalizeSchemaValue(nested)]))]];
|
|
1639
|
+
if (nestedSchemaKeywords.has(key)) return [[key, normalizeSchemaValue(value)]];
|
|
1640
|
+
return [[key, value]];
|
|
1641
|
+
}));
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1392
1644
|
//#endregion
|
|
1393
1645
|
//#region src/copilot/responses.ts
|
|
1394
1646
|
const responsesOnlyModelPattern = /^(?:gpt-5\.5|gpt-5\.6|gpt-6-astra)(?:-|$)/i;
|
|
@@ -1710,7 +1962,7 @@ function translateTools(tools) {
|
|
|
1710
1962
|
type: "function",
|
|
1711
1963
|
name: tool.function.name,
|
|
1712
1964
|
description: tool.function.description,
|
|
1713
|
-
parameters: tool.function.parameters
|
|
1965
|
+
parameters: normalizeResponsesToolSchema(tool.function.parameters)
|
|
1714
1966
|
}));
|
|
1715
1967
|
}
|
|
1716
1968
|
function translateToolChoice(toolChoice) {
|
|
@@ -1764,7 +2016,7 @@ const sanitizeReasoningEffortForModel = (_modelId, reasoningEffort) => {
|
|
|
1764
2016
|
return reasoningEffort ?? void 0;
|
|
1765
2017
|
};
|
|
1766
2018
|
const getRequestedReasoningEffort = (payload) => {
|
|
1767
|
-
return sanitizeReasoningEffortForModel(payload.model,
|
|
2019
|
+
return sanitizeReasoningEffortForModel(payload.model, resolveReasoningEffort(getRequestReasoningEffort(payload)));
|
|
1768
2020
|
};
|
|
1769
2021
|
const sanitizeUserIdentifier = (user) => {
|
|
1770
2022
|
if (!user) return;
|
|
@@ -1829,13 +2081,21 @@ const messagesIncludeImage = (messages) => messages.some((msg) => typeof msg.con
|
|
|
1829
2081
|
const createChatCompletions = async (config, payload, options = {}) => {
|
|
1830
2082
|
const client = options.client ?? "generic";
|
|
1831
2083
|
const requestedModel = options.requestedModel ?? payload.model;
|
|
1832
|
-
const requestedThinkEffort = options.requestedThinkEffort ?? payload
|
|
2084
|
+
const requestedThinkEffort = options.requestedThinkEffort ?? getRequestReasoningEffort(payload) ?? "none";
|
|
2085
|
+
const reasoningEffort = getRequestedReasoningEffort(payload);
|
|
1833
2086
|
const requestedThinking = options.requestedThinking ?? "none";
|
|
1834
2087
|
const signal = createCopilotRequestSignal(options.signal, options.timeoutMs);
|
|
1835
2088
|
const upstreamModelId = routeModelId(payload.model);
|
|
1836
|
-
const
|
|
2089
|
+
const maxTokens = await boundModelOutputTokens(config, upstreamModelId, payload.max_tokens);
|
|
2090
|
+
const nonStreamingLimit = getCachedCopilotModel(config, upstreamModelId)?.limits?.max_non_streaming_output_tokens;
|
|
2091
|
+
const bufferResponse = !payload.stream && typeof maxTokens === "number" && nonStreamingLimit !== void 0 && maxTokens > nonStreamingLimit;
|
|
2092
|
+
const completeResponse = (response$1) => bufferResponse && !("choices" in response$1) ? collectChatCompletionStream(response$1, signal, options.timeoutMs) : response$1;
|
|
2093
|
+
const upstreamPayload = {
|
|
1837
2094
|
...payload,
|
|
1838
|
-
model: upstreamModelId
|
|
2095
|
+
model: upstreamModelId,
|
|
2096
|
+
reasoning_effort: reasoningEffort,
|
|
2097
|
+
max_tokens: maxTokens,
|
|
2098
|
+
stream: bufferResponse ? true : payload.stream
|
|
1839
2099
|
};
|
|
1840
2100
|
const useResponsesApi = shouldUseResponsesApiForModel(upstreamPayload.model);
|
|
1841
2101
|
const compatiblePayload = useResponsesApi ? upstreamPayload : normalizeFinalAssistantPrefill$1(upstreamPayload);
|
|
@@ -1854,13 +2114,13 @@ const createChatCompletions = async (config, payload, options = {}) => {
|
|
|
1854
2114
|
`effective_think_effort=${requestPayload.reasoning_effort ?? "none"}`
|
|
1855
2115
|
].join(" "));
|
|
1856
2116
|
log.debug("Full request payload", { payload: requestPayload });
|
|
1857
|
-
if (useResponsesApi) return createResponses(provider, compatiblePayload, {
|
|
2117
|
+
if (useResponsesApi) return completeResponse(await createResponses(provider, compatiblePayload, {
|
|
1858
2118
|
vision: enableVision,
|
|
1859
2119
|
initiator,
|
|
1860
2120
|
requestId: options.requestId,
|
|
1861
2121
|
signal,
|
|
1862
2122
|
timeoutMs: options.timeoutMs
|
|
1863
|
-
});
|
|
2123
|
+
}));
|
|
1864
2124
|
const response = await fetchCopilot(provider, "/chat/completions", {
|
|
1865
2125
|
method: "POST",
|
|
1866
2126
|
headers: {
|
|
@@ -1876,13 +2136,13 @@ const createChatCompletions = async (config, payload, options = {}) => {
|
|
|
1876
2136
|
timeoutMs: options.timeoutMs
|
|
1877
2137
|
});
|
|
1878
2138
|
if (!response.ok) {
|
|
1879
|
-
if (await shouldRetryWithResponses(response, signal, options.timeoutMs)) return createResponses(provider, compatiblePayload, {
|
|
2139
|
+
if (await shouldRetryWithResponses(response, signal, options.timeoutMs)) return completeResponse(await createResponses(provider, compatiblePayload, {
|
|
1880
2140
|
vision: enableVision,
|
|
1881
2141
|
initiator,
|
|
1882
2142
|
requestId: options.requestId,
|
|
1883
2143
|
signal,
|
|
1884
2144
|
timeoutMs: options.timeoutMs
|
|
1885
|
-
});
|
|
2145
|
+
}));
|
|
1886
2146
|
throw new HTTPError("Failed to create chat completions", response, await logUpstreamError("Failed to create chat completions", response, {
|
|
1887
2147
|
model: payload.model,
|
|
1888
2148
|
request: requestPayload,
|
|
@@ -1890,7 +2150,7 @@ const createChatCompletions = async (config, payload, options = {}) => {
|
|
|
1890
2150
|
requestId: options.requestId
|
|
1891
2151
|
}, signal, options.timeoutMs));
|
|
1892
2152
|
}
|
|
1893
|
-
if (compatiblePayload.stream) return events(response);
|
|
2153
|
+
if (compatiblePayload.stream) return completeResponse(events(response));
|
|
1894
2154
|
return readCopilotJson(response, signal, options.timeoutMs);
|
|
1895
2155
|
};
|
|
1896
2156
|
async function createResponses(provider, payload, options) {
|
|
@@ -1956,26 +2216,17 @@ async function shouldRetryWithResponses(response, signal, timeoutMs) {
|
|
|
1956
2216
|
|
|
1957
2217
|
//#endregion
|
|
1958
2218
|
//#region src/lib/preflight.ts
|
|
1959
|
-
const fetchUpstreamModelIds = async (config) => {
|
|
1960
|
-
const provider = getCopilotProviderContext(config);
|
|
1961
|
-
const signal = createCopilotRequestSignal(void 0, config.upstreamTimeoutMs);
|
|
1962
|
-
const response = await fetchCopilot(provider, "/models", {
|
|
1963
|
-
method: "GET",
|
|
1964
|
-
headers: { accept: "application/json" }
|
|
1965
|
-
}, {
|
|
1966
|
-
signal,
|
|
1967
|
-
timeoutMs: config.upstreamTimeoutMs
|
|
1968
|
-
});
|
|
1969
|
-
if (!response.ok) throw new HTTPError("Failed to validate upstream models", response);
|
|
1970
|
-
const payload = await readCopilotJson(response, signal, config.upstreamTimeoutMs);
|
|
1971
|
-
return new Set((payload.data ?? []).map((model) => model.id).filter((id) => typeof id === "string" && id.length > 0));
|
|
1972
|
-
};
|
|
1973
2219
|
const ensureRequiredModels = async (config) => {
|
|
1974
|
-
const
|
|
2220
|
+
const catalog = await loadCopilotModelCatalog(config);
|
|
1975
2221
|
const requiredModels = getUpstreamModelIds();
|
|
1976
|
-
const missingModels = requiredModels.filter((model) => !
|
|
2222
|
+
const missingModels = requiredModels.filter((model) => !catalog.models.has(model));
|
|
1977
2223
|
if (missingModels.length > 0) throw new Error(`Required Copilot model(s) unavailable upstream: ${missingModels.join(", ")}`);
|
|
1978
2224
|
log.info(`Upstream models available: ${requiredModels.join(", ")}`);
|
|
2225
|
+
for (const model of requiredModels) {
|
|
2226
|
+
const limits = catalog.models.get(model)?.limits;
|
|
2227
|
+
if (limits) log.info(`Model token limits: model=${model} context=${limits.max_context_window_tokens} input=${limits.max_prompt_tokens} output=${limits.max_output_tokens} non_streaming_output=${limits.max_non_streaming_output_tokens ?? limits.max_output_tokens}`);
|
|
2228
|
+
else log.error(`Model token limits unavailable: model=${model}; preserving client budgets without assuming a capacity.`);
|
|
2229
|
+
}
|
|
1979
2230
|
};
|
|
1980
2231
|
const createProbePayload = (model) => ({
|
|
1981
2232
|
model,
|
|
@@ -2099,7 +2350,7 @@ function translateToOpenAI(payload, _settings, toolNameMapper) {
|
|
|
2099
2350
|
stream: payload.stream,
|
|
2100
2351
|
temperature: payload.temperature,
|
|
2101
2352
|
top_p: payload.top_p,
|
|
2102
|
-
reasoning_effort:
|
|
2353
|
+
reasoning_effort: resolveReasoningEffort(getRequestReasoningEffort(payload)),
|
|
2103
2354
|
user: payload.metadata?.user_id,
|
|
2104
2355
|
tools,
|
|
2105
2356
|
tool_choice: tools && tools.length > 0 ? translateClaudeToolChoiceToOpenAI(payload.tool_choice, mapper) : void 0
|
|
@@ -2544,6 +2795,7 @@ const buildWebSearchRequestPayload = (payload, requestedQuery, model) => ({
|
|
|
2544
2795
|
model,
|
|
2545
2796
|
input: buildSearchInput(payload, requestedQuery),
|
|
2546
2797
|
tools: [{ type: "web_search_preview" }],
|
|
2798
|
+
reasoning: { effort: resolveReasoningEffort(getRequestReasoningEffort(payload)) },
|
|
2547
2799
|
max_output_tokens: Math.max(256, Math.min(payload.max_tokens ?? 1024, 1200)),
|
|
2548
2800
|
temperature: payload.temperature,
|
|
2549
2801
|
top_p: payload.top_p
|
|
@@ -2729,56 +2981,6 @@ const chunkSettlesDecision = (chunk) => {
|
|
|
2729
2981
|
return choice.delta?.tool_calls?.some((call) => call.function?.name) === true || !!choice.finish_reason;
|
|
2730
2982
|
};
|
|
2731
2983
|
const chunkHasWebSearchCall = (chunk, toolNameMapper, isWebSearchToolName) => chunk.choices[0]?.delta?.tool_calls?.some((call) => isWebSearchName(call.function?.name, toolNameMapper, isWebSearchToolName)) ?? false;
|
|
2732
|
-
const accumulateChunks = (chunks) => {
|
|
2733
|
-
const first = chunks[0];
|
|
2734
|
-
const toolCalls = [];
|
|
2735
|
-
let content = "";
|
|
2736
|
-
let reasoning = "";
|
|
2737
|
-
let finishReason = null;
|
|
2738
|
-
let usage;
|
|
2739
|
-
for (const chunk of chunks) {
|
|
2740
|
-
if (chunk.usage) usage = chunk.usage;
|
|
2741
|
-
const choice = chunk.choices[0];
|
|
2742
|
-
if (!choice) continue;
|
|
2743
|
-
if (typeof choice.delta?.content === "string") content += choice.delta.content;
|
|
2744
|
-
const chunkReasoning = choice.delta?.reasoning_text ?? choice.delta?.reasoning_content;
|
|
2745
|
-
if (typeof chunkReasoning === "string") reasoning += chunkReasoning;
|
|
2746
|
-
if (choice.finish_reason) finishReason = choice.finish_reason;
|
|
2747
|
-
for (const call of choice.delta?.tool_calls ?? []) {
|
|
2748
|
-
const existing = toolCalls[call.index];
|
|
2749
|
-
if (existing) {
|
|
2750
|
-
existing.function.arguments += call.function?.arguments ?? "";
|
|
2751
|
-
continue;
|
|
2752
|
-
}
|
|
2753
|
-
toolCalls[call.index] = {
|
|
2754
|
-
id: call.id ?? `call_${call.index}`,
|
|
2755
|
-
type: "function",
|
|
2756
|
-
function: {
|
|
2757
|
-
name: call.function?.name ?? "",
|
|
2758
|
-
arguments: call.function?.arguments ?? ""
|
|
2759
|
-
}
|
|
2760
|
-
};
|
|
2761
|
-
}
|
|
2762
|
-
}
|
|
2763
|
-
const collectedToolCalls = toolCalls.filter(Boolean);
|
|
2764
|
-
return {
|
|
2765
|
-
id: first?.id ?? "chat_stream_accumulated",
|
|
2766
|
-
object: "chat.completion",
|
|
2767
|
-
created: first?.created ?? 0,
|
|
2768
|
-
model: first?.model ?? "",
|
|
2769
|
-
choices: [{
|
|
2770
|
-
index: 0,
|
|
2771
|
-
message: {
|
|
2772
|
-
role: "assistant",
|
|
2773
|
-
content: content || null,
|
|
2774
|
-
...reasoning ? { reasoning_text: reasoning } : {},
|
|
2775
|
-
...collectedToolCalls.length > 0 ? { tool_calls: collectedToolCalls } : {}
|
|
2776
|
-
},
|
|
2777
|
-
finish_reason: finishReason ?? "stop"
|
|
2778
|
-
}],
|
|
2779
|
-
...usage ? { usage } : {}
|
|
2780
|
-
};
|
|
2781
|
-
};
|
|
2782
2984
|
const resolveWebSearchStreamDecision = async (stream, toolNameMapper, isWebSearchToolName, onChunk) => {
|
|
2783
2985
|
const buffered = [];
|
|
2784
2986
|
const iterator = stream[Symbol.asyncIterator]();
|
|
@@ -2860,6 +3062,7 @@ const encodingMap = {
|
|
|
2860
3062
|
p50k_edit: () => import("gpt-tokenizer/encoding/p50k_edit"),
|
|
2861
3063
|
r50k_base: () => import("gpt-tokenizer/encoding/r50k_base")
|
|
2862
3064
|
};
|
|
3065
|
+
const isSupportedTokenizer = (encoding) => Object.hasOwn(encodingMap, encoding);
|
|
2863
3066
|
const encodingCache = /* @__PURE__ */ new Map();
|
|
2864
3067
|
const calculateToolCallsTokens = (toolCalls, encoder, constants) => {
|
|
2865
3068
|
let tokens = 0;
|
|
@@ -2900,13 +3103,12 @@ const getEncodeChatFunction = async (encoding) => {
|
|
|
2900
3103
|
const cached = encodingCache.get(encoding);
|
|
2901
3104
|
if (cached) return cached;
|
|
2902
3105
|
}
|
|
2903
|
-
|
|
2904
|
-
if (!(supported in encodingMap)) {
|
|
3106
|
+
if (!isSupportedTokenizer(encoding)) {
|
|
2905
3107
|
const fallback = await encodingMap.o200k_base();
|
|
2906
3108
|
encodingCache.set(encoding, fallback);
|
|
2907
3109
|
return fallback;
|
|
2908
3110
|
}
|
|
2909
|
-
const mod = await encodingMap[
|
|
3111
|
+
const mod = await encodingMap[encoding]();
|
|
2910
3112
|
encodingCache.set(encoding, mod);
|
|
2911
3113
|
return mod;
|
|
2912
3114
|
};
|
|
@@ -3009,12 +3211,12 @@ const getTokenCount = async (payload, model) => {
|
|
|
3009
3211
|
//#endregion
|
|
3010
3212
|
//#region src/routes/claude.ts
|
|
3011
3213
|
const claudeRoutes = new Hono();
|
|
3012
|
-
const createTokenCountModel = (modelId) => ({
|
|
3013
|
-
capabilities: { tokenizer
|
|
3214
|
+
const createTokenCountModel = (modelId, tokenizer = "o200k_base") => ({
|
|
3215
|
+
capabilities: { tokenizer },
|
|
3014
3216
|
id: modelId
|
|
3015
3217
|
});
|
|
3016
3218
|
const isNonStreamingResponse = (response) => typeof response === "object" && response !== null && Object.hasOwn(response, "choices");
|
|
3017
|
-
const getClaudeRequestedThinkEffort = (payload) => payload
|
|
3219
|
+
const getClaudeRequestedThinkEffort = (payload) => getRequestReasoningEffort(payload) ?? "none";
|
|
3018
3220
|
const getClaudeRequestedThinking = (payload) => {
|
|
3019
3221
|
if (!payload.thinking) return "none";
|
|
3020
3222
|
return [`type:${payload.thinking.type}`, `budget:${payload.thinking.budget_tokens ?? "none"}`].join(",");
|
|
@@ -3192,7 +3394,10 @@ const handleClaudeMessageRequest = async (config, claudePayload, requestSignal,
|
|
|
3192
3394
|
const webSearchToolCall = shouldLetModelDecideWebSearch ? getClaudeWebSearchToolCallFromChatResponse(effectiveResponse, toolNameMapper) : void 0;
|
|
3193
3395
|
let claudeResponse;
|
|
3194
3396
|
if (webSearchToolCall) {
|
|
3195
|
-
const search = await createClaudeWebSearchExecution(config,
|
|
3397
|
+
const search = await createClaudeWebSearchExecution(config, {
|
|
3398
|
+
...claudePayload,
|
|
3399
|
+
reasoning_effort: openAIPayload.reasoning_effort
|
|
3400
|
+
}, webSearchToolCall.query, {
|
|
3196
3401
|
requestId,
|
|
3197
3402
|
signal: requestSignal,
|
|
3198
3403
|
timeoutMs: config.upstreamTimeoutMs
|
|
@@ -3246,17 +3451,33 @@ const handleClaudeMessageRequest = async (config, claudePayload, requestSignal,
|
|
|
3246
3451
|
};
|
|
3247
3452
|
claudeRoutes.get("/models", (c) => c.json({
|
|
3248
3453
|
object: "list",
|
|
3249
|
-
data: getExposedModelIds().map((id) =>
|
|
3250
|
-
id
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3454
|
+
data: getExposedModelIds().map((id) => {
|
|
3455
|
+
const limits = getCachedCopilotModel(c.get("config"), id)?.limits;
|
|
3456
|
+
return {
|
|
3457
|
+
id,
|
|
3458
|
+
object: "model",
|
|
3459
|
+
created: 0,
|
|
3460
|
+
owned_by: "github-copilot",
|
|
3461
|
+
...limits && {
|
|
3462
|
+
context_window: limits.max_context_window_tokens,
|
|
3463
|
+
max_input_tokens: limits.max_prompt_tokens,
|
|
3464
|
+
max_tokens: limits.max_output_tokens
|
|
3465
|
+
}
|
|
3466
|
+
};
|
|
3467
|
+
})
|
|
3255
3468
|
}));
|
|
3256
3469
|
claudeRoutes.post("/messages", async (c) => {
|
|
3257
3470
|
const config = c.get("config");
|
|
3258
3471
|
const requestId = c.get("requestId");
|
|
3259
3472
|
const claudePayload = await c.req.json();
|
|
3473
|
+
try {
|
|
3474
|
+
getRequestReasoningEffort(claudePayload);
|
|
3475
|
+
} catch (error) {
|
|
3476
|
+
if (!(error instanceof HTTPError)) throw error;
|
|
3477
|
+
c.set("requestErrorMessage", error.message);
|
|
3478
|
+
log.error(`request_id=${requestId} ${error.message}`);
|
|
3479
|
+
return error.response;
|
|
3480
|
+
}
|
|
3260
3481
|
const requestSignal = createCopilotRequestSignal(c.req.raw.signal, config.upstreamTimeoutMs);
|
|
3261
3482
|
log.debug("Full Claude request payload", { payload: claudePayload });
|
|
3262
3483
|
if (claudePayload.stream) return streamSSE(c, async (stream) => {
|
|
@@ -3301,19 +3522,25 @@ claudeRoutes.post("/messages/count_tokens", async (c) => {
|
|
|
3301
3522
|
const claudePayload = await c.req.json();
|
|
3302
3523
|
const openAIPayload = translateToOpenAI(claudePayload);
|
|
3303
3524
|
const exposedModels = getExposedModelIds();
|
|
3304
|
-
const
|
|
3525
|
+
const upstreamModel = getCachedCopilotModel(c.get("config"), openAIPayload.model);
|
|
3526
|
+
const hasDiscoveredTokenizer = upstreamModel?.tokenizer !== void 0 && isSupportedTokenizer(upstreamModel.tokenizer);
|
|
3527
|
+
const selectedModel = createTokenCountModel(openAIPayload.model === exposedModels[1] ? exposedModels[1] : exposedModels[0], hasDiscoveredTokenizer ? upstreamModel?.tokenizer : void 0);
|
|
3305
3528
|
const tokenCount = await getTokenCount(openAIPayload, selectedModel);
|
|
3306
3529
|
const effectiveModelId = selectedModel.id;
|
|
3307
3530
|
if (claudePayload.tools && claudePayload.tools.length > 0) {
|
|
3308
3531
|
let mcpToolExist = false;
|
|
3309
3532
|
if (claudeBeta?.startsWith("claude-code")) mcpToolExist = claudePayload.tools.some((tool) => tool.name.startsWith("mcp__"));
|
|
3310
|
-
if (!mcpToolExist && effectiveModelId.startsWith("claude")) tokenCount.input += 346;
|
|
3533
|
+
if (!hasDiscoveredTokenizer && !mcpToolExist && effectiveModelId.startsWith("claude")) tokenCount.input += 346;
|
|
3311
3534
|
}
|
|
3312
|
-
const multiplier = effectiveModelId.startsWith("claude") ? 1.15 : 1;
|
|
3535
|
+
const multiplier = !hasDiscoveredTokenizer && effectiveModelId.startsWith("claude") ? 1.15 : 1;
|
|
3313
3536
|
const finalTokenCount = Math.round((tokenCount.input + tokenCount.output) * multiplier);
|
|
3314
3537
|
return c.json({ input_tokens: Math.max(1, finalTokenCount) });
|
|
3315
3538
|
} catch (error) {
|
|
3316
3539
|
log.error("Error counting tokens:", error);
|
|
3540
|
+
if (error instanceof HTTPError) {
|
|
3541
|
+
c.set("requestErrorMessage", error.message);
|
|
3542
|
+
return error.response;
|
|
3543
|
+
}
|
|
3317
3544
|
return c.json({ input_tokens: 1 });
|
|
3318
3545
|
}
|
|
3319
3546
|
});
|
|
@@ -3429,6 +3656,7 @@ async function startRelay(appConfig) {
|
|
|
3429
3656
|
runtimeState.debug = nextConfig.logLevel === "debug";
|
|
3430
3657
|
runtimeState.thinkEffort = nextConfig.thinkEffort;
|
|
3431
3658
|
config.copilotBaseUrl = nextConfig.copilotBaseUrl;
|
|
3659
|
+
runtimeState.upstreamBaseUrl = nextConfig.copilotBaseUrl;
|
|
3432
3660
|
config.host = nextConfig.host;
|
|
3433
3661
|
config.port = nextConfig.port;
|
|
3434
3662
|
config.upstreamTimeoutMs = nextConfig.upstreamTimeoutSeconds * 1e3;
|
|
@@ -3440,9 +3668,8 @@ async function startRelay(appConfig) {
|
|
|
3440
3668
|
};
|
|
3441
3669
|
applyRuntimeConfig(appConfig);
|
|
3442
3670
|
log.info(`Log level: ${appConfig.logLevel}`);
|
|
3443
|
-
log.info(`
|
|
3671
|
+
log.info(`Default think effort: ${appConfig.thinkEffort}`);
|
|
3444
3672
|
log.info(`Upstream timeout: ${appConfig.upstreamTimeoutSeconds}s`);
|
|
3445
|
-
log.info(`Exposed models: ${getExposedModelIds().join(", ")}`);
|
|
3446
3673
|
const authSession = await setupProxyAuth(config);
|
|
3447
3674
|
try {
|
|
3448
3675
|
await validateUpstream(config, appConfig.thinkEffort);
|
|
@@ -3450,6 +3677,7 @@ async function startRelay(appConfig) {
|
|
|
3450
3677
|
log.error("Startup preflight failed:", error);
|
|
3451
3678
|
process.exit(1);
|
|
3452
3679
|
}
|
|
3680
|
+
log.info(`Exposed models: ${getExposedModelIds().join(", ")}`);
|
|
3453
3681
|
const server = await startServer(config);
|
|
3454
3682
|
await writeRelayPidFile(config);
|
|
3455
3683
|
log.info(`copilot-relay version: ${appVersion}`);
|
|
@@ -3459,10 +3687,14 @@ async function startRelay(appConfig) {
|
|
|
3459
3687
|
log.info(`copilot base url: ${formatUrlForDisplay(config.copilotBaseUrl)}`);
|
|
3460
3688
|
const baseUrl = `http://${config.host}:${config.port}`;
|
|
3461
3689
|
if (appConfig.claudeSetup) try {
|
|
3690
|
+
const gptLimits = getCachedCopilotModel(config, appConfig.gptModel)?.limits;
|
|
3691
|
+
const opusLimits = getCachedCopilotModel(config, appConfig.opusModel)?.limits;
|
|
3462
3692
|
const claudeResult = await applyClaudeConfig({
|
|
3463
3693
|
baseUrl,
|
|
3464
3694
|
configPath: claudeConfigPath$1,
|
|
3465
|
-
gptModel: appConfig.gptModel
|
|
3695
|
+
gptModel: appConfig.gptModel,
|
|
3696
|
+
gptLimits,
|
|
3697
|
+
maxOutputTokens: gptLimits && opusLimits ? Math.max(gptLimits.max_output_tokens, opusLimits.max_output_tokens) : void 0
|
|
3466
3698
|
});
|
|
3467
3699
|
if (claudeResult.changed) {
|
|
3468
3700
|
log.info(`claude settings ${claudeResult.created ? "created" : "updated"}: ${claudeResult.configPath}`);
|