copilot-relay 0.0.6 → 0.0.8
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 +5 -0
- package/config.default.yaml +3 -0
- package/dist/main.js +167 -32
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,6 +60,7 @@ claudeSetup: true
|
|
|
60
60
|
logLevel: info
|
|
61
61
|
logRetentionDays: 3
|
|
62
62
|
thinkEffort: xhigh
|
|
63
|
+
upstreamTimeoutSeconds: 180
|
|
63
64
|
webSearchBackend:
|
|
64
65
|
```
|
|
65
66
|
|
|
@@ -75,6 +76,10 @@ Any other `logLevel` value is invalid and stops startup.
|
|
|
75
76
|
|
|
76
77
|
Valid `thinkEffort`: `none`, `low`, `medium`, `high`, `xhigh`.
|
|
77
78
|
|
|
79
|
+
`upstreamTimeoutSeconds` controls the maximum time a single Claude request can
|
|
80
|
+
spend waiting on upstream Copilot calls, including chat, Responses, preflight,
|
|
81
|
+
and bridge-managed WebSearch calls. The default is `180`.
|
|
82
|
+
|
|
78
83
|
`webSearchBackend` controls bridge-managed Claude WebSearch. Leave it empty to
|
|
79
84
|
use `gptModel`, or set a Copilot Responses model ID such as `gpt-5.5`.
|
|
80
85
|
|
package/config.default.yaml
CHANGED
|
@@ -35,6 +35,9 @@ logRetentionDays: 3
|
|
|
35
35
|
# Valid values: none, low, medium, high, xhigh.
|
|
36
36
|
thinkEffort: xhigh
|
|
37
37
|
|
|
38
|
+
# Max seconds to wait for one Claude request's upstream Copilot calls.
|
|
39
|
+
upstreamTimeoutSeconds: 180
|
|
40
|
+
|
|
38
41
|
# Copilot model used for bridge-managed Claude WebSearch.
|
|
39
42
|
# Leave empty to use gptModel.
|
|
40
43
|
webSearchBackend:
|
package/dist/main.js
CHANGED
|
@@ -372,6 +372,7 @@ const defaultConfig = {
|
|
|
372
372
|
opusModel: "claude-opus-4.8",
|
|
373
373
|
port: 4142,
|
|
374
374
|
thinkEffort: defaultReasoningEffort,
|
|
375
|
+
upstreamTimeoutSeconds: 180,
|
|
375
376
|
webSearchBackend: void 0
|
|
376
377
|
};
|
|
377
378
|
const isLogLevelName = (value) => typeof value === "string" && logLevels.includes(value.toLowerCase());
|
|
@@ -403,6 +404,12 @@ const normalizeThinkEffort = (value) => {
|
|
|
403
404
|
const normalized = value.toLowerCase() === "minimal" ? "low" : value.toLowerCase();
|
|
404
405
|
return isReasoningEffort(normalized) ? normalized : void 0;
|
|
405
406
|
};
|
|
407
|
+
const normalizeUpstreamTimeoutSeconds = (value) => {
|
|
408
|
+
if (value === void 0) return;
|
|
409
|
+
const timeout = normalizePositiveInteger(value);
|
|
410
|
+
if (timeout === void 0) throw new Error("Invalid upstreamTimeoutSeconds: expected a positive integer");
|
|
411
|
+
return timeout;
|
|
412
|
+
};
|
|
406
413
|
const readRawConfig = async () => {
|
|
407
414
|
try {
|
|
408
415
|
return parseConfigYaml(await fs.readFile(paths.configPath, "utf8"));
|
|
@@ -476,6 +483,10 @@ const parseConfigYaml = (content) => {
|
|
|
476
483
|
case "think_effort":
|
|
477
484
|
config.thinkEffort = unquoteYamlScalar(value);
|
|
478
485
|
break;
|
|
486
|
+
case "upstreamTimeoutSeconds":
|
|
487
|
+
case "upstream_timeout_seconds":
|
|
488
|
+
config.upstreamTimeoutSeconds = unquoteYamlScalar(value);
|
|
489
|
+
break;
|
|
479
490
|
case "webSearchBackend":
|
|
480
491
|
case "web_search_backend":
|
|
481
492
|
config.webSearchBackend = unquoteYamlScalar(value);
|
|
@@ -522,6 +533,9 @@ const serializeConfig = (config) => [
|
|
|
522
533
|
"# Default upstream thinking/reasoning effort: none, low, medium, high, xhigh.",
|
|
523
534
|
`thinkEffort: ${config.thinkEffort}`,
|
|
524
535
|
"",
|
|
536
|
+
"# Max seconds to wait for a single Claude request's upstream Copilot calls.",
|
|
537
|
+
`upstreamTimeoutSeconds: ${config.upstreamTimeoutSeconds}`,
|
|
538
|
+
"",
|
|
525
539
|
"# Copilot model used for bridge-managed Claude WebSearch. Empty uses gptModel.",
|
|
526
540
|
`webSearchBackend: ${config.webSearchBackend ?? ""}`,
|
|
527
541
|
"",
|
|
@@ -542,6 +556,7 @@ async function readAppConfig() {
|
|
|
542
556
|
const logRetentionDays$1 = normalizePositiveInteger(raw.logRetentionDays);
|
|
543
557
|
const port = normalizePort(raw.port);
|
|
544
558
|
const thinkEffort = normalizeThinkEffort(raw.thinkEffort);
|
|
559
|
+
const upstreamTimeoutSeconds = normalizeUpstreamTimeoutSeconds(raw.upstreamTimeoutSeconds);
|
|
545
560
|
const config = {
|
|
546
561
|
claudeSetup: claudeSetup ?? defaultConfig.claudeSetup,
|
|
547
562
|
copilotBaseUrl: normalizeString(raw.copilotBaseUrl) ?? defaultConfig.copilotBaseUrl,
|
|
@@ -552,6 +567,7 @@ async function readAppConfig() {
|
|
|
552
567
|
opusModel: normalizeString(raw.opusModel) ?? defaultConfig.opusModel,
|
|
553
568
|
port: port ?? defaultConfig.port,
|
|
554
569
|
thinkEffort: thinkEffort ?? defaultConfig.thinkEffort,
|
|
570
|
+
upstreamTimeoutSeconds: upstreamTimeoutSeconds ?? defaultConfig.upstreamTimeoutSeconds,
|
|
555
571
|
webSearchBackend: normalizeString(raw.webSearchBackend)
|
|
556
572
|
};
|
|
557
573
|
await writeConfig(config);
|
|
@@ -590,6 +606,7 @@ const readProxyConfig = (config) => ({
|
|
|
590
606
|
copilotToken: void 0,
|
|
591
607
|
host: config.host,
|
|
592
608
|
port: config.port,
|
|
609
|
+
upstreamTimeoutMs: config.upstreamTimeoutSeconds * 1e3,
|
|
593
610
|
vsCodeVersion: vscodeVersion,
|
|
594
611
|
webSearchBackend: config.webSearchBackend
|
|
595
612
|
});
|
|
@@ -674,12 +691,50 @@ const editorPluginVersion = `copilot-chat/${copilotVersion}`;
|
|
|
674
691
|
const userAgent = `GitHubCopilotChat/${copilotVersion}`;
|
|
675
692
|
const apiVersion = "2025-04-01";
|
|
676
693
|
const maxFetchAttempts = 2;
|
|
694
|
+
const copilotRequestTimeoutMs = 18e4;
|
|
677
695
|
const getCopilotProviderContext = (config) => ({
|
|
678
696
|
baseUrl: config.copilotBaseUrl,
|
|
679
697
|
token: config.copilotToken,
|
|
680
698
|
vsCodeVersion: config.vsCodeVersion
|
|
681
699
|
});
|
|
682
700
|
const shouldRetryResponse = (response) => response.status >= 500 && response.status <= 599;
|
|
701
|
+
const createCopilotRequestSignal = (signal, timeoutMs = copilotRequestTimeoutMs) => {
|
|
702
|
+
const signals = [signal, timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : void 0].filter((value) => value !== void 0);
|
|
703
|
+
if (signals.length === 0) return;
|
|
704
|
+
return signals.length === 1 ? signals[0] : AbortSignal.any(signals);
|
|
705
|
+
};
|
|
706
|
+
const getAbortName = (value) => typeof value === "object" && value !== null && "name" in value && typeof value.name === "string" ? value.name : void 0;
|
|
707
|
+
const isAbortLikeError = (error) => {
|
|
708
|
+
const name = getAbortName(error);
|
|
709
|
+
return name === "AbortError" || name === "TimeoutError";
|
|
710
|
+
};
|
|
711
|
+
const toCopilotAbortHTTPError = (error, signal, timeoutMs = copilotRequestTimeoutMs) => {
|
|
712
|
+
if (!signal?.aborted && !isAbortLikeError(error)) return;
|
|
713
|
+
const timedOut = getAbortName(signal?.reason) === "TimeoutError" || getAbortName(error) === "TimeoutError";
|
|
714
|
+
const message = timedOut ? `Copilot upstream request timed out after ${Math.round(timeoutMs / 1e3)}s.` : "Client request cancelled before Copilot upstream completed.";
|
|
715
|
+
const code = timedOut ? "upstream_timeout" : "request_cancelled";
|
|
716
|
+
return new HTTPError(message, new Response(JSON.stringify({ error: {
|
|
717
|
+
message,
|
|
718
|
+
code
|
|
719
|
+
} }), {
|
|
720
|
+
status: timedOut ? 504 : 499,
|
|
721
|
+
headers: { "content-type": "application/json" }
|
|
722
|
+
}), message);
|
|
723
|
+
};
|
|
724
|
+
const readCopilotJson = async (response, signal, timeoutMs = copilotRequestTimeoutMs) => {
|
|
725
|
+
try {
|
|
726
|
+
return await response.json();
|
|
727
|
+
} catch (error) {
|
|
728
|
+
throw toCopilotAbortHTTPError(error, signal, timeoutMs) ?? error;
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
const readCopilotText = async (response, signal, timeoutMs = copilotRequestTimeoutMs) => {
|
|
732
|
+
try {
|
|
733
|
+
return await response.text();
|
|
734
|
+
} catch (error) {
|
|
735
|
+
throw toCopilotAbortHTTPError(error, signal, timeoutMs) ?? error;
|
|
736
|
+
}
|
|
737
|
+
};
|
|
683
738
|
const buildHeaders = (provider, init, options) => {
|
|
684
739
|
const headers = new Headers(init.headers);
|
|
685
740
|
headers.set("authorization", `Bearer ${provider.token}`);
|
|
@@ -699,18 +754,23 @@ const buildHeaders = (provider, init, options) => {
|
|
|
699
754
|
};
|
|
700
755
|
const fetchCopilot = async (provider, path$1, init, options = {}) => {
|
|
701
756
|
if (!provider.token) throw new ProxyNotImplementedError("Copilot token is not configured for copilot-relay.");
|
|
757
|
+
const timeoutMs = options.timeoutMs ?? copilotRequestTimeoutMs;
|
|
758
|
+
const signal = createCopilotRequestSignal(options.signal, timeoutMs);
|
|
702
759
|
let lastError;
|
|
703
760
|
for (let attempt = 1; attempt <= maxFetchAttempts; attempt++) try {
|
|
704
761
|
const started = performance.now();
|
|
705
762
|
const response = await fetch(`${provider.baseUrl}${path$1}`, {
|
|
706
763
|
...init,
|
|
707
|
-
headers: buildHeaders(provider, init, options)
|
|
764
|
+
headers: buildHeaders(provider, init, options),
|
|
765
|
+
signal
|
|
708
766
|
});
|
|
709
767
|
const ms = Math.round(performance.now() - started);
|
|
710
768
|
log.debug(`Copilot ${init.method ?? "GET"} ${path$1} -> ${response.status} ${ms}ms (attempt ${attempt})`);
|
|
711
769
|
if (!shouldRetryResponse(response) || attempt === maxFetchAttempts) return response;
|
|
712
770
|
log.error(`Copilot ${path$1} returned ${response.status}; retrying (${attempt}/${maxFetchAttempts})`);
|
|
713
771
|
} catch (error) {
|
|
772
|
+
const abortError = toCopilotAbortHTTPError(error, signal, timeoutMs);
|
|
773
|
+
if (abortError) throw abortError;
|
|
714
774
|
lastError = error;
|
|
715
775
|
if (attempt === maxFetchAttempts) throw error;
|
|
716
776
|
log.error(`Copilot ${path$1} request failed; retrying (${attempt}/${maxFetchAttempts})`, error);
|
|
@@ -733,12 +793,21 @@ function buildResponsesRequestPayload(payload, reasoningEffort) {
|
|
|
733
793
|
temperature: payload.temperature,
|
|
734
794
|
top_p: payload.top_p,
|
|
735
795
|
user: sanitizeUserIdentifier(payload.user),
|
|
796
|
+
prompt_cache_key: deriveResponsesCacheKey(payload),
|
|
736
797
|
tools: translateTools(payload.tools),
|
|
737
798
|
tool_choice: translateToolChoice(payload.tool_choice),
|
|
738
799
|
reasoning: reasoningEffort ? { effort: reasoningEffort } : void 0,
|
|
739
800
|
text: payload.response_format?.type === "json_object" ? { format: { type: "json_object" } } : void 0
|
|
740
801
|
};
|
|
741
802
|
}
|
|
803
|
+
function deriveResponsesCacheKey(payload) {
|
|
804
|
+
const user = sanitizeUserIdentifier(payload.user);
|
|
805
|
+
if (user) return `cr-${createHash("sha256").update(user).digest("hex").slice(0, 32)}`;
|
|
806
|
+
const systemPrompt = payload.messages.find((message) => message.role === "system");
|
|
807
|
+
const systemText = typeof systemPrompt?.content === "string" ? systemPrompt.content : Array.isArray(systemPrompt?.content) ? systemPrompt.content.map((part) => part.type === "text" ? part.text : "").join("") : "";
|
|
808
|
+
if (!systemText) return;
|
|
809
|
+
return `cr-sys-${createHash("sha256").update(systemText).digest("hex").slice(0, 32)}`;
|
|
810
|
+
}
|
|
742
811
|
function translateResponsesToChatCompletion(response) {
|
|
743
812
|
const assistantMessages = response.output.filter((item) => item.type === "message");
|
|
744
813
|
const functionCalls = response.output.filter((item) => item.type === "function_call");
|
|
@@ -1077,6 +1146,7 @@ function getFinishReason(response, hasFunctionCalls) {
|
|
|
1077
1146
|
//#endregion
|
|
1078
1147
|
//#region src/copilot/chat.ts
|
|
1079
1148
|
const usesMaxCompletionTokens = (modelId) => modelId.startsWith("gpt-5");
|
|
1149
|
+
const continuePrefillPrompt$1 = "Continue the assistant response from the previous assistant message.";
|
|
1080
1150
|
const maxUserLength = 64;
|
|
1081
1151
|
const sanitizeReasoningEffortForModel = (_modelId, reasoningEffort) => {
|
|
1082
1152
|
return reasoningEffort ?? void 0;
|
|
@@ -1103,6 +1173,38 @@ const buildRequestPayload = (payload) => {
|
|
|
1103
1173
|
user: sanitizeUserIdentifier(payload.user)
|
|
1104
1174
|
};
|
|
1105
1175
|
};
|
|
1176
|
+
const normalizeFinalAssistantPrefill$1 = (payload) => {
|
|
1177
|
+
const lastMessage = payload.messages.at(-1);
|
|
1178
|
+
if (lastMessage?.role !== "assistant") return payload;
|
|
1179
|
+
const messages = [...payload.messages];
|
|
1180
|
+
if (typeof lastMessage.content !== "string") {
|
|
1181
|
+
messages.pop();
|
|
1182
|
+
return {
|
|
1183
|
+
...payload,
|
|
1184
|
+
messages
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
const content = lastMessage.content.trimEnd();
|
|
1188
|
+
if (!content) {
|
|
1189
|
+
messages.pop();
|
|
1190
|
+
return {
|
|
1191
|
+
...payload,
|
|
1192
|
+
messages
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
messages[messages.length - 1] = {
|
|
1196
|
+
...lastMessage,
|
|
1197
|
+
content
|
|
1198
|
+
};
|
|
1199
|
+
messages.push({
|
|
1200
|
+
role: "user",
|
|
1201
|
+
content: continuePrefillPrompt$1
|
|
1202
|
+
});
|
|
1203
|
+
return {
|
|
1204
|
+
...payload,
|
|
1205
|
+
messages
|
|
1206
|
+
};
|
|
1207
|
+
};
|
|
1106
1208
|
const isAgentInitiator = (messages) => messages.some((msg) => msg.role === "assistant" || msg.role === "tool") ? "agent" : "user";
|
|
1107
1209
|
const messagesIncludeImage = (messages) => messages.some((msg) => typeof msg.content !== "string" && msg.content?.some((part) => part.type === "image_url"));
|
|
1108
1210
|
const createChatCompletions = async (config, payload, options = {}) => {
|
|
@@ -1110,53 +1212,62 @@ const createChatCompletions = async (config, payload, options = {}) => {
|
|
|
1110
1212
|
const requestedModel = options.requestedModel ?? payload.model;
|
|
1111
1213
|
const requestedThinkEffort = options.requestedThinkEffort ?? payload.reasoning_effort ?? "none";
|
|
1112
1214
|
const requestedThinking = options.requestedThinking ?? "none";
|
|
1215
|
+
const signal = createCopilotRequestSignal(options.signal, options.timeoutMs);
|
|
1113
1216
|
const upstreamModelId = routeModelId(payload.model);
|
|
1114
1217
|
const upstreamPayload = upstreamModelId === payload.model ? payload : {
|
|
1115
1218
|
...payload,
|
|
1116
1219
|
model: upstreamModelId
|
|
1117
1220
|
};
|
|
1221
|
+
const useResponsesApi = shouldUseResponsesApiForModel(upstreamPayload.model);
|
|
1222
|
+
const compatiblePayload = useResponsesApi ? upstreamPayload : normalizeFinalAssistantPrefill$1(upstreamPayload);
|
|
1118
1223
|
const provider = getCopilotProviderContext(config);
|
|
1119
|
-
const enableVision = messagesIncludeImage(
|
|
1120
|
-
const initiator = isAgentInitiator(
|
|
1121
|
-
const requestPayload = buildRequestPayload(
|
|
1224
|
+
const enableVision = messagesIncludeImage(compatiblePayload.messages);
|
|
1225
|
+
const initiator = isAgentInitiator(compatiblePayload.messages);
|
|
1226
|
+
const requestPayload = buildRequestPayload(compatiblePayload);
|
|
1122
1227
|
log.debug([
|
|
1123
1228
|
"Model request",
|
|
1124
1229
|
`client=${client}`,
|
|
1125
1230
|
`requested_model=${requestedModel}`,
|
|
1126
|
-
`upstream_model=${
|
|
1231
|
+
`upstream_model=${compatiblePayload.model}`,
|
|
1127
1232
|
`requested_think_effort=${requestedThinkEffort}`,
|
|
1128
1233
|
`requested_thinking=${requestedThinking}`,
|
|
1129
1234
|
`effective_think_effort=${requestPayload.reasoning_effort ?? "none"}`
|
|
1130
1235
|
].join(" "));
|
|
1131
1236
|
log.debug("Full request payload", { payload: requestPayload });
|
|
1132
|
-
if (
|
|
1237
|
+
if (useResponsesApi) return createResponses(provider, compatiblePayload, {
|
|
1133
1238
|
vision: enableVision,
|
|
1134
|
-
initiator
|
|
1239
|
+
initiator,
|
|
1240
|
+
signal,
|
|
1241
|
+
timeoutMs: options.timeoutMs
|
|
1135
1242
|
});
|
|
1136
1243
|
const response = await fetchCopilot(provider, "/chat/completions", {
|
|
1137
1244
|
method: "POST",
|
|
1138
1245
|
headers: {
|
|
1139
|
-
accept:
|
|
1246
|
+
accept: compatiblePayload.stream ? "text/event-stream" : "application/json",
|
|
1140
1247
|
"content-type": "application/json"
|
|
1141
1248
|
},
|
|
1142
1249
|
body: JSON.stringify(requestPayload)
|
|
1143
1250
|
}, {
|
|
1144
1251
|
vision: enableVision,
|
|
1145
|
-
initiator
|
|
1252
|
+
initiator,
|
|
1253
|
+
signal,
|
|
1254
|
+
timeoutMs: options.timeoutMs
|
|
1146
1255
|
});
|
|
1147
1256
|
if (!response.ok) {
|
|
1148
|
-
if (await shouldRetryWithResponses(response)) return createResponses(provider,
|
|
1257
|
+
if (await shouldRetryWithResponses(response, signal, options.timeoutMs)) return createResponses(provider, compatiblePayload, {
|
|
1149
1258
|
vision: enableVision,
|
|
1150
|
-
initiator
|
|
1259
|
+
initiator,
|
|
1260
|
+
signal,
|
|
1261
|
+
timeoutMs: options.timeoutMs
|
|
1151
1262
|
});
|
|
1152
1263
|
throw new HTTPError("Failed to create chat completions", response, await logUpstreamError("Failed to create chat completions", response, {
|
|
1153
1264
|
model: payload.model,
|
|
1154
1265
|
request: requestPayload,
|
|
1155
1266
|
route: "/chat/completions"
|
|
1156
|
-
}));
|
|
1267
|
+
}, signal, options.timeoutMs));
|
|
1157
1268
|
}
|
|
1158
|
-
if (
|
|
1159
|
-
return
|
|
1269
|
+
if (compatiblePayload.stream) return events(response);
|
|
1270
|
+
return readCopilotJson(response, signal, options.timeoutMs);
|
|
1160
1271
|
};
|
|
1161
1272
|
async function createResponses(provider, payload, options) {
|
|
1162
1273
|
const requestPayload = buildResponsesRequestPayload(payload, getRequestedReasoningEffort(payload));
|
|
@@ -1169,18 +1280,20 @@ async function createResponses(provider, payload, options) {
|
|
|
1169
1280
|
body: JSON.stringify(requestPayload)
|
|
1170
1281
|
}, {
|
|
1171
1282
|
vision: options.vision,
|
|
1172
|
-
initiator: options.initiator
|
|
1283
|
+
initiator: options.initiator,
|
|
1284
|
+
signal: options.signal,
|
|
1285
|
+
timeoutMs: options.timeoutMs
|
|
1173
1286
|
});
|
|
1174
1287
|
if (!response.ok) throw new HTTPError("Failed to create responses", response, await logUpstreamError("Failed to create responses", response, {
|
|
1175
1288
|
model: payload.model,
|
|
1176
1289
|
request: requestPayload,
|
|
1177
1290
|
route: "/responses"
|
|
1178
|
-
}));
|
|
1291
|
+
}, options.signal, options.timeoutMs));
|
|
1179
1292
|
if (payload.stream) return translateResponsesStreamToChatCompletionStream(events(response));
|
|
1180
|
-
return translateResponsesToChatCompletion(await response.
|
|
1293
|
+
return translateResponsesToChatCompletion(await readCopilotJson(response, options.signal, options.timeoutMs));
|
|
1181
1294
|
}
|
|
1182
|
-
async function logUpstreamError(message, response, context) {
|
|
1183
|
-
const errorBody = await response.clone()
|
|
1295
|
+
async function logUpstreamError(message, response, context, signal, timeoutMs) {
|
|
1296
|
+
const errorBody = await readCopilotText(response.clone(), signal, timeoutMs).catch(() => "");
|
|
1184
1297
|
const detail = getUpstreamErrorDetail(response, errorBody);
|
|
1185
1298
|
log.error(`${message}: route=${context.route} model=${context.model} status=${response.status}`, {
|
|
1186
1299
|
message,
|
|
@@ -1206,9 +1319,9 @@ function getUpstreamErrorDetail(response, body) {
|
|
|
1206
1319
|
return body.slice(0, 240);
|
|
1207
1320
|
}
|
|
1208
1321
|
}
|
|
1209
|
-
async function shouldRetryWithResponses(response) {
|
|
1322
|
+
async function shouldRetryWithResponses(response, signal, timeoutMs) {
|
|
1210
1323
|
try {
|
|
1211
|
-
return (await response.clone()
|
|
1324
|
+
return (await readCopilotJson(response.clone(), signal, timeoutMs)).error?.code === "unsupported_api_for_model";
|
|
1212
1325
|
} catch {
|
|
1213
1326
|
return false;
|
|
1214
1327
|
}
|
|
@@ -1217,12 +1330,17 @@ async function shouldRetryWithResponses(response) {
|
|
|
1217
1330
|
//#endregion
|
|
1218
1331
|
//#region src/lib/preflight.ts
|
|
1219
1332
|
const getUpstreamModelIds = async (config) => {
|
|
1220
|
-
const
|
|
1333
|
+
const provider = getCopilotProviderContext(config);
|
|
1334
|
+
const signal = createCopilotRequestSignal(void 0, config.upstreamTimeoutMs);
|
|
1335
|
+
const response = await fetchCopilot(provider, "/models", {
|
|
1221
1336
|
method: "GET",
|
|
1222
1337
|
headers: { accept: "application/json" }
|
|
1338
|
+
}, {
|
|
1339
|
+
signal,
|
|
1340
|
+
timeoutMs: config.upstreamTimeoutMs
|
|
1223
1341
|
});
|
|
1224
1342
|
if (!response.ok) throw new HTTPError("Failed to validate upstream models", response);
|
|
1225
|
-
const payload = await response.
|
|
1343
|
+
const payload = await readCopilotJson(response, signal, config.upstreamTimeoutMs);
|
|
1226
1344
|
return new Set((payload.data ?? []).map((model) => model.id).filter((id) => typeof id === "string" && id.length > 0));
|
|
1227
1345
|
};
|
|
1228
1346
|
const ensureRequiredModels = async (config) => {
|
|
@@ -1245,7 +1363,8 @@ const validateModelRequest = async (config, model, thinkEffort) => {
|
|
|
1245
1363
|
try {
|
|
1246
1364
|
const response = await createChatCompletions(config, createProbePayload(model), {
|
|
1247
1365
|
client: "generic",
|
|
1248
|
-
requestedModel: model
|
|
1366
|
+
requestedModel: model,
|
|
1367
|
+
timeoutMs: config.upstreamTimeoutMs
|
|
1249
1368
|
});
|
|
1250
1369
|
if (typeof response !== "object" || response === null || !("choices" in response)) throw new Error(`Preflight request for ${model} unexpectedly streamed`);
|
|
1251
1370
|
log.info(`Preflight OK: model=${model} think_effort=${thinkEffort}`);
|
|
@@ -1862,8 +1981,9 @@ const createFailedSearchExecution = (payload, requestedQuery, model, message) =>
|
|
|
1862
1981
|
results: [],
|
|
1863
1982
|
text: message
|
|
1864
1983
|
});
|
|
1865
|
-
const createClaudeWebSearchExecution = async (config, payload, requestedQuery) => {
|
|
1984
|
+
const createClaudeWebSearchExecution = async (config, payload, requestedQuery, options = {}) => {
|
|
1866
1985
|
const backendModel = getWebSearchBackendModel(config);
|
|
1986
|
+
const signal = createCopilotRequestSignal(options.signal, options.timeoutMs);
|
|
1867
1987
|
const response = await fetchCopilot(getCopilotProviderContext(config), "/responses", {
|
|
1868
1988
|
method: "POST",
|
|
1869
1989
|
headers: {
|
|
@@ -1871,12 +1991,16 @@ const createClaudeWebSearchExecution = async (config, payload, requestedQuery) =
|
|
|
1871
1991
|
"content-type": "application/json"
|
|
1872
1992
|
},
|
|
1873
1993
|
body: JSON.stringify(buildWebSearchRequestPayload(payload, requestedQuery, backendModel))
|
|
1874
|
-
}, {
|
|
1994
|
+
}, {
|
|
1995
|
+
initiator: "agent",
|
|
1996
|
+
signal,
|
|
1997
|
+
timeoutMs: options.timeoutMs
|
|
1998
|
+
});
|
|
1875
1999
|
if (!response.ok) {
|
|
1876
|
-
const detail = await response.
|
|
2000
|
+
const detail = await readCopilotText(response, signal, options.timeoutMs).catch(() => "");
|
|
1877
2001
|
return createFailedSearchExecution(payload, requestedQuery, backendModel, [`Copilot web search is not available for model ${backendModel}.`, detail ? `Upstream response: ${detail}` : ""].filter(Boolean).join("\n"));
|
|
1878
2002
|
}
|
|
1879
|
-
const upstream = await response.
|
|
2003
|
+
const upstream = await readCopilotJson(response, signal, options.timeoutMs);
|
|
1880
2004
|
const text = getResponseText(upstream);
|
|
1881
2005
|
const query = getSearchQuery(upstream, requestedQuery);
|
|
1882
2006
|
const results = parseSearchResults(text);
|
|
@@ -2224,6 +2348,7 @@ claudeRoutes.get("/models", (c) => c.json({
|
|
|
2224
2348
|
claudeRoutes.post("/messages", async (c) => {
|
|
2225
2349
|
const config = c.get("config");
|
|
2226
2350
|
const claudePayload = await c.req.json();
|
|
2351
|
+
const requestSignal = createCopilotRequestSignal(c.req.raw.signal, config.upstreamTimeoutMs);
|
|
2227
2352
|
log.debug("Full Claude request payload", { payload: claudePayload });
|
|
2228
2353
|
try {
|
|
2229
2354
|
const upstreamModel = translateModelName(claudePayload.model);
|
|
@@ -2236,13 +2361,18 @@ claudeRoutes.post("/messages", async (c) => {
|
|
|
2236
2361
|
client: "claude",
|
|
2237
2362
|
requestedModel: claudePayload.model,
|
|
2238
2363
|
requestedThinkEffort: getClaudeRequestedThinkEffort(claudePayload),
|
|
2239
|
-
requestedThinking: getClaudeRequestedThinking(claudePayload)
|
|
2364
|
+
requestedThinking: getClaudeRequestedThinking(claudePayload),
|
|
2365
|
+
signal: requestSignal,
|
|
2366
|
+
timeoutMs: config.upstreamTimeoutMs
|
|
2240
2367
|
});
|
|
2241
2368
|
if (isNonStreamingResponse(response)) {
|
|
2242
2369
|
const webSearchToolCall = shouldLetModelDecideWebSearch ? getClaudeWebSearchToolCallFromChatResponse(response, toolNameMapper) : void 0;
|
|
2243
2370
|
let claudeResponse;
|
|
2244
2371
|
if (webSearchToolCall) {
|
|
2245
|
-
const search = await createClaudeWebSearchExecution(config, claudePayload, webSearchToolCall.query
|
|
2372
|
+
const search = await createClaudeWebSearchExecution(config, claudePayload, webSearchToolCall.query, {
|
|
2373
|
+
signal: requestSignal,
|
|
2374
|
+
timeoutMs: config.upstreamTimeoutMs
|
|
2375
|
+
});
|
|
2246
2376
|
const searchResponse = createClaudeWebSearchResponse(search);
|
|
2247
2377
|
if (search.results.length === 0) claudeResponse = searchResponse;
|
|
2248
2378
|
else {
|
|
@@ -2250,7 +2380,9 @@ claudeRoutes.post("/messages", async (c) => {
|
|
|
2250
2380
|
client: "claude",
|
|
2251
2381
|
requestedModel: claudePayload.model,
|
|
2252
2382
|
requestedThinkEffort: getClaudeRequestedThinkEffort(claudePayload),
|
|
2253
|
-
requestedThinking: getClaudeRequestedThinking(claudePayload)
|
|
2383
|
+
requestedThinking: getClaudeRequestedThinking(claudePayload),
|
|
2384
|
+
signal: requestSignal,
|
|
2385
|
+
timeoutMs: config.upstreamTimeoutMs
|
|
2254
2386
|
});
|
|
2255
2387
|
if (!isNonStreamingResponse(finalResponse)) throw new HTTPError("Claude web search final answer request unexpectedly streamed", new Response("Claude web search final answer request unexpectedly streamed", {
|
|
2256
2388
|
status: 502,
|
|
@@ -2433,6 +2565,8 @@ const start = defineCommand({
|
|
|
2433
2565
|
config.copilotBaseUrl = nextConfig.copilotBaseUrl;
|
|
2434
2566
|
config.host = nextConfig.host;
|
|
2435
2567
|
config.port = nextConfig.port;
|
|
2568
|
+
config.upstreamTimeoutMs = nextConfig.upstreamTimeoutSeconds * 1e3;
|
|
2569
|
+
config.webSearchBackend = nextConfig.webSearchBackend;
|
|
2436
2570
|
runtimeState.modelRouting = {
|
|
2437
2571
|
gptModel: nextConfig.gptModel,
|
|
2438
2572
|
opusModel: nextConfig.opusModel
|
|
@@ -2441,6 +2575,7 @@ const start = defineCommand({
|
|
|
2441
2575
|
applyRuntimeConfig(appConfig);
|
|
2442
2576
|
log.info(`Log level: ${appConfig.logLevel}`);
|
|
2443
2577
|
log.info(`Think effort: ${appConfig.thinkEffort}`);
|
|
2578
|
+
log.info(`Upstream timeout: ${appConfig.upstreamTimeoutSeconds}s`);
|
|
2444
2579
|
log.info(`Exposed models: ${getExposedModelIds().join(", ")}`);
|
|
2445
2580
|
const authSession = await setupProxyAuth(config);
|
|
2446
2581
|
try {
|
|
@@ -2470,7 +2605,7 @@ const start = defineCommand({
|
|
|
2470
2605
|
}
|
|
2471
2606
|
watchAppConfig((nextConfig) => {
|
|
2472
2607
|
applyRuntimeConfig(nextConfig);
|
|
2473
|
-
log.info(`Config reloaded: logLevel=${nextConfig.logLevel} thinkEffort=${nextConfig.thinkEffort}`);
|
|
2608
|
+
log.info(`Config reloaded: logLevel=${nextConfig.logLevel} thinkEffort=${nextConfig.thinkEffort} upstreamTimeoutSeconds=${nextConfig.upstreamTimeoutSeconds}`);
|
|
2474
2609
|
});
|
|
2475
2610
|
await new Promise((resolve$1, reject) => {
|
|
2476
2611
|
server.on("close", resolve$1);
|