copilot-relay 0.0.7 → 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 +158 -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);
|
|
@@ -1086,6 +1146,7 @@ function getFinishReason(response, hasFunctionCalls) {
|
|
|
1086
1146
|
//#endregion
|
|
1087
1147
|
//#region src/copilot/chat.ts
|
|
1088
1148
|
const usesMaxCompletionTokens = (modelId) => modelId.startsWith("gpt-5");
|
|
1149
|
+
const continuePrefillPrompt$1 = "Continue the assistant response from the previous assistant message.";
|
|
1089
1150
|
const maxUserLength = 64;
|
|
1090
1151
|
const sanitizeReasoningEffortForModel = (_modelId, reasoningEffort) => {
|
|
1091
1152
|
return reasoningEffort ?? void 0;
|
|
@@ -1112,6 +1173,38 @@ const buildRequestPayload = (payload) => {
|
|
|
1112
1173
|
user: sanitizeUserIdentifier(payload.user)
|
|
1113
1174
|
};
|
|
1114
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
|
+
};
|
|
1115
1208
|
const isAgentInitiator = (messages) => messages.some((msg) => msg.role === "assistant" || msg.role === "tool") ? "agent" : "user";
|
|
1116
1209
|
const messagesIncludeImage = (messages) => messages.some((msg) => typeof msg.content !== "string" && msg.content?.some((part) => part.type === "image_url"));
|
|
1117
1210
|
const createChatCompletions = async (config, payload, options = {}) => {
|
|
@@ -1119,53 +1212,62 @@ const createChatCompletions = async (config, payload, options = {}) => {
|
|
|
1119
1212
|
const requestedModel = options.requestedModel ?? payload.model;
|
|
1120
1213
|
const requestedThinkEffort = options.requestedThinkEffort ?? payload.reasoning_effort ?? "none";
|
|
1121
1214
|
const requestedThinking = options.requestedThinking ?? "none";
|
|
1215
|
+
const signal = createCopilotRequestSignal(options.signal, options.timeoutMs);
|
|
1122
1216
|
const upstreamModelId = routeModelId(payload.model);
|
|
1123
1217
|
const upstreamPayload = upstreamModelId === payload.model ? payload : {
|
|
1124
1218
|
...payload,
|
|
1125
1219
|
model: upstreamModelId
|
|
1126
1220
|
};
|
|
1221
|
+
const useResponsesApi = shouldUseResponsesApiForModel(upstreamPayload.model);
|
|
1222
|
+
const compatiblePayload = useResponsesApi ? upstreamPayload : normalizeFinalAssistantPrefill$1(upstreamPayload);
|
|
1127
1223
|
const provider = getCopilotProviderContext(config);
|
|
1128
|
-
const enableVision = messagesIncludeImage(
|
|
1129
|
-
const initiator = isAgentInitiator(
|
|
1130
|
-
const requestPayload = buildRequestPayload(
|
|
1224
|
+
const enableVision = messagesIncludeImage(compatiblePayload.messages);
|
|
1225
|
+
const initiator = isAgentInitiator(compatiblePayload.messages);
|
|
1226
|
+
const requestPayload = buildRequestPayload(compatiblePayload);
|
|
1131
1227
|
log.debug([
|
|
1132
1228
|
"Model request",
|
|
1133
1229
|
`client=${client}`,
|
|
1134
1230
|
`requested_model=${requestedModel}`,
|
|
1135
|
-
`upstream_model=${
|
|
1231
|
+
`upstream_model=${compatiblePayload.model}`,
|
|
1136
1232
|
`requested_think_effort=${requestedThinkEffort}`,
|
|
1137
1233
|
`requested_thinking=${requestedThinking}`,
|
|
1138
1234
|
`effective_think_effort=${requestPayload.reasoning_effort ?? "none"}`
|
|
1139
1235
|
].join(" "));
|
|
1140
1236
|
log.debug("Full request payload", { payload: requestPayload });
|
|
1141
|
-
if (
|
|
1237
|
+
if (useResponsesApi) return createResponses(provider, compatiblePayload, {
|
|
1142
1238
|
vision: enableVision,
|
|
1143
|
-
initiator
|
|
1239
|
+
initiator,
|
|
1240
|
+
signal,
|
|
1241
|
+
timeoutMs: options.timeoutMs
|
|
1144
1242
|
});
|
|
1145
1243
|
const response = await fetchCopilot(provider, "/chat/completions", {
|
|
1146
1244
|
method: "POST",
|
|
1147
1245
|
headers: {
|
|
1148
|
-
accept:
|
|
1246
|
+
accept: compatiblePayload.stream ? "text/event-stream" : "application/json",
|
|
1149
1247
|
"content-type": "application/json"
|
|
1150
1248
|
},
|
|
1151
1249
|
body: JSON.stringify(requestPayload)
|
|
1152
1250
|
}, {
|
|
1153
1251
|
vision: enableVision,
|
|
1154
|
-
initiator
|
|
1252
|
+
initiator,
|
|
1253
|
+
signal,
|
|
1254
|
+
timeoutMs: options.timeoutMs
|
|
1155
1255
|
});
|
|
1156
1256
|
if (!response.ok) {
|
|
1157
|
-
if (await shouldRetryWithResponses(response)) return createResponses(provider,
|
|
1257
|
+
if (await shouldRetryWithResponses(response, signal, options.timeoutMs)) return createResponses(provider, compatiblePayload, {
|
|
1158
1258
|
vision: enableVision,
|
|
1159
|
-
initiator
|
|
1259
|
+
initiator,
|
|
1260
|
+
signal,
|
|
1261
|
+
timeoutMs: options.timeoutMs
|
|
1160
1262
|
});
|
|
1161
1263
|
throw new HTTPError("Failed to create chat completions", response, await logUpstreamError("Failed to create chat completions", response, {
|
|
1162
1264
|
model: payload.model,
|
|
1163
1265
|
request: requestPayload,
|
|
1164
1266
|
route: "/chat/completions"
|
|
1165
|
-
}));
|
|
1267
|
+
}, signal, options.timeoutMs));
|
|
1166
1268
|
}
|
|
1167
|
-
if (
|
|
1168
|
-
return
|
|
1269
|
+
if (compatiblePayload.stream) return events(response);
|
|
1270
|
+
return readCopilotJson(response, signal, options.timeoutMs);
|
|
1169
1271
|
};
|
|
1170
1272
|
async function createResponses(provider, payload, options) {
|
|
1171
1273
|
const requestPayload = buildResponsesRequestPayload(payload, getRequestedReasoningEffort(payload));
|
|
@@ -1178,18 +1280,20 @@ async function createResponses(provider, payload, options) {
|
|
|
1178
1280
|
body: JSON.stringify(requestPayload)
|
|
1179
1281
|
}, {
|
|
1180
1282
|
vision: options.vision,
|
|
1181
|
-
initiator: options.initiator
|
|
1283
|
+
initiator: options.initiator,
|
|
1284
|
+
signal: options.signal,
|
|
1285
|
+
timeoutMs: options.timeoutMs
|
|
1182
1286
|
});
|
|
1183
1287
|
if (!response.ok) throw new HTTPError("Failed to create responses", response, await logUpstreamError("Failed to create responses", response, {
|
|
1184
1288
|
model: payload.model,
|
|
1185
1289
|
request: requestPayload,
|
|
1186
1290
|
route: "/responses"
|
|
1187
|
-
}));
|
|
1291
|
+
}, options.signal, options.timeoutMs));
|
|
1188
1292
|
if (payload.stream) return translateResponsesStreamToChatCompletionStream(events(response));
|
|
1189
|
-
return translateResponsesToChatCompletion(await response.
|
|
1293
|
+
return translateResponsesToChatCompletion(await readCopilotJson(response, options.signal, options.timeoutMs));
|
|
1190
1294
|
}
|
|
1191
|
-
async function logUpstreamError(message, response, context) {
|
|
1192
|
-
const errorBody = await response.clone()
|
|
1295
|
+
async function logUpstreamError(message, response, context, signal, timeoutMs) {
|
|
1296
|
+
const errorBody = await readCopilotText(response.clone(), signal, timeoutMs).catch(() => "");
|
|
1193
1297
|
const detail = getUpstreamErrorDetail(response, errorBody);
|
|
1194
1298
|
log.error(`${message}: route=${context.route} model=${context.model} status=${response.status}`, {
|
|
1195
1299
|
message,
|
|
@@ -1215,9 +1319,9 @@ function getUpstreamErrorDetail(response, body) {
|
|
|
1215
1319
|
return body.slice(0, 240);
|
|
1216
1320
|
}
|
|
1217
1321
|
}
|
|
1218
|
-
async function shouldRetryWithResponses(response) {
|
|
1322
|
+
async function shouldRetryWithResponses(response, signal, timeoutMs) {
|
|
1219
1323
|
try {
|
|
1220
|
-
return (await response.clone()
|
|
1324
|
+
return (await readCopilotJson(response.clone(), signal, timeoutMs)).error?.code === "unsupported_api_for_model";
|
|
1221
1325
|
} catch {
|
|
1222
1326
|
return false;
|
|
1223
1327
|
}
|
|
@@ -1226,12 +1330,17 @@ async function shouldRetryWithResponses(response) {
|
|
|
1226
1330
|
//#endregion
|
|
1227
1331
|
//#region src/lib/preflight.ts
|
|
1228
1332
|
const getUpstreamModelIds = async (config) => {
|
|
1229
|
-
const
|
|
1333
|
+
const provider = getCopilotProviderContext(config);
|
|
1334
|
+
const signal = createCopilotRequestSignal(void 0, config.upstreamTimeoutMs);
|
|
1335
|
+
const response = await fetchCopilot(provider, "/models", {
|
|
1230
1336
|
method: "GET",
|
|
1231
1337
|
headers: { accept: "application/json" }
|
|
1338
|
+
}, {
|
|
1339
|
+
signal,
|
|
1340
|
+
timeoutMs: config.upstreamTimeoutMs
|
|
1232
1341
|
});
|
|
1233
1342
|
if (!response.ok) throw new HTTPError("Failed to validate upstream models", response);
|
|
1234
|
-
const payload = await response.
|
|
1343
|
+
const payload = await readCopilotJson(response, signal, config.upstreamTimeoutMs);
|
|
1235
1344
|
return new Set((payload.data ?? []).map((model) => model.id).filter((id) => typeof id === "string" && id.length > 0));
|
|
1236
1345
|
};
|
|
1237
1346
|
const ensureRequiredModels = async (config) => {
|
|
@@ -1254,7 +1363,8 @@ const validateModelRequest = async (config, model, thinkEffort) => {
|
|
|
1254
1363
|
try {
|
|
1255
1364
|
const response = await createChatCompletions(config, createProbePayload(model), {
|
|
1256
1365
|
client: "generic",
|
|
1257
|
-
requestedModel: model
|
|
1366
|
+
requestedModel: model,
|
|
1367
|
+
timeoutMs: config.upstreamTimeoutMs
|
|
1258
1368
|
});
|
|
1259
1369
|
if (typeof response !== "object" || response === null || !("choices" in response)) throw new Error(`Preflight request for ${model} unexpectedly streamed`);
|
|
1260
1370
|
log.info(`Preflight OK: model=${model} think_effort=${thinkEffort}`);
|
|
@@ -1871,8 +1981,9 @@ const createFailedSearchExecution = (payload, requestedQuery, model, message) =>
|
|
|
1871
1981
|
results: [],
|
|
1872
1982
|
text: message
|
|
1873
1983
|
});
|
|
1874
|
-
const createClaudeWebSearchExecution = async (config, payload, requestedQuery) => {
|
|
1984
|
+
const createClaudeWebSearchExecution = async (config, payload, requestedQuery, options = {}) => {
|
|
1875
1985
|
const backendModel = getWebSearchBackendModel(config);
|
|
1986
|
+
const signal = createCopilotRequestSignal(options.signal, options.timeoutMs);
|
|
1876
1987
|
const response = await fetchCopilot(getCopilotProviderContext(config), "/responses", {
|
|
1877
1988
|
method: "POST",
|
|
1878
1989
|
headers: {
|
|
@@ -1880,12 +1991,16 @@ const createClaudeWebSearchExecution = async (config, payload, requestedQuery) =
|
|
|
1880
1991
|
"content-type": "application/json"
|
|
1881
1992
|
},
|
|
1882
1993
|
body: JSON.stringify(buildWebSearchRequestPayload(payload, requestedQuery, backendModel))
|
|
1883
|
-
}, {
|
|
1994
|
+
}, {
|
|
1995
|
+
initiator: "agent",
|
|
1996
|
+
signal,
|
|
1997
|
+
timeoutMs: options.timeoutMs
|
|
1998
|
+
});
|
|
1884
1999
|
if (!response.ok) {
|
|
1885
|
-
const detail = await response.
|
|
2000
|
+
const detail = await readCopilotText(response, signal, options.timeoutMs).catch(() => "");
|
|
1886
2001
|
return createFailedSearchExecution(payload, requestedQuery, backendModel, [`Copilot web search is not available for model ${backendModel}.`, detail ? `Upstream response: ${detail}` : ""].filter(Boolean).join("\n"));
|
|
1887
2002
|
}
|
|
1888
|
-
const upstream = await response.
|
|
2003
|
+
const upstream = await readCopilotJson(response, signal, options.timeoutMs);
|
|
1889
2004
|
const text = getResponseText(upstream);
|
|
1890
2005
|
const query = getSearchQuery(upstream, requestedQuery);
|
|
1891
2006
|
const results = parseSearchResults(text);
|
|
@@ -2233,6 +2348,7 @@ claudeRoutes.get("/models", (c) => c.json({
|
|
|
2233
2348
|
claudeRoutes.post("/messages", async (c) => {
|
|
2234
2349
|
const config = c.get("config");
|
|
2235
2350
|
const claudePayload = await c.req.json();
|
|
2351
|
+
const requestSignal = createCopilotRequestSignal(c.req.raw.signal, config.upstreamTimeoutMs);
|
|
2236
2352
|
log.debug("Full Claude request payload", { payload: claudePayload });
|
|
2237
2353
|
try {
|
|
2238
2354
|
const upstreamModel = translateModelName(claudePayload.model);
|
|
@@ -2245,13 +2361,18 @@ claudeRoutes.post("/messages", async (c) => {
|
|
|
2245
2361
|
client: "claude",
|
|
2246
2362
|
requestedModel: claudePayload.model,
|
|
2247
2363
|
requestedThinkEffort: getClaudeRequestedThinkEffort(claudePayload),
|
|
2248
|
-
requestedThinking: getClaudeRequestedThinking(claudePayload)
|
|
2364
|
+
requestedThinking: getClaudeRequestedThinking(claudePayload),
|
|
2365
|
+
signal: requestSignal,
|
|
2366
|
+
timeoutMs: config.upstreamTimeoutMs
|
|
2249
2367
|
});
|
|
2250
2368
|
if (isNonStreamingResponse(response)) {
|
|
2251
2369
|
const webSearchToolCall = shouldLetModelDecideWebSearch ? getClaudeWebSearchToolCallFromChatResponse(response, toolNameMapper) : void 0;
|
|
2252
2370
|
let claudeResponse;
|
|
2253
2371
|
if (webSearchToolCall) {
|
|
2254
|
-
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
|
+
});
|
|
2255
2376
|
const searchResponse = createClaudeWebSearchResponse(search);
|
|
2256
2377
|
if (search.results.length === 0) claudeResponse = searchResponse;
|
|
2257
2378
|
else {
|
|
@@ -2259,7 +2380,9 @@ claudeRoutes.post("/messages", async (c) => {
|
|
|
2259
2380
|
client: "claude",
|
|
2260
2381
|
requestedModel: claudePayload.model,
|
|
2261
2382
|
requestedThinkEffort: getClaudeRequestedThinkEffort(claudePayload),
|
|
2262
|
-
requestedThinking: getClaudeRequestedThinking(claudePayload)
|
|
2383
|
+
requestedThinking: getClaudeRequestedThinking(claudePayload),
|
|
2384
|
+
signal: requestSignal,
|
|
2385
|
+
timeoutMs: config.upstreamTimeoutMs
|
|
2263
2386
|
});
|
|
2264
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", {
|
|
2265
2388
|
status: 502,
|
|
@@ -2442,6 +2565,8 @@ const start = defineCommand({
|
|
|
2442
2565
|
config.copilotBaseUrl = nextConfig.copilotBaseUrl;
|
|
2443
2566
|
config.host = nextConfig.host;
|
|
2444
2567
|
config.port = nextConfig.port;
|
|
2568
|
+
config.upstreamTimeoutMs = nextConfig.upstreamTimeoutSeconds * 1e3;
|
|
2569
|
+
config.webSearchBackend = nextConfig.webSearchBackend;
|
|
2445
2570
|
runtimeState.modelRouting = {
|
|
2446
2571
|
gptModel: nextConfig.gptModel,
|
|
2447
2572
|
opusModel: nextConfig.opusModel
|
|
@@ -2450,6 +2575,7 @@ const start = defineCommand({
|
|
|
2450
2575
|
applyRuntimeConfig(appConfig);
|
|
2451
2576
|
log.info(`Log level: ${appConfig.logLevel}`);
|
|
2452
2577
|
log.info(`Think effort: ${appConfig.thinkEffort}`);
|
|
2578
|
+
log.info(`Upstream timeout: ${appConfig.upstreamTimeoutSeconds}s`);
|
|
2453
2579
|
log.info(`Exposed models: ${getExposedModelIds().join(", ")}`);
|
|
2454
2580
|
const authSession = await setupProxyAuth(config);
|
|
2455
2581
|
try {
|
|
@@ -2479,7 +2605,7 @@ const start = defineCommand({
|
|
|
2479
2605
|
}
|
|
2480
2606
|
watchAppConfig((nextConfig) => {
|
|
2481
2607
|
applyRuntimeConfig(nextConfig);
|
|
2482
|
-
log.info(`Config reloaded: logLevel=${nextConfig.logLevel} thinkEffort=${nextConfig.thinkEffort}`);
|
|
2608
|
+
log.info(`Config reloaded: logLevel=${nextConfig.logLevel} thinkEffort=${nextConfig.thinkEffort} upstreamTimeoutSeconds=${nextConfig.upstreamTimeoutSeconds}`);
|
|
2483
2609
|
});
|
|
2484
2610
|
await new Promise((resolve$1, reject) => {
|
|
2485
2611
|
server.on("close", resolve$1);
|