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 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
 
@@ -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(upstreamPayload.messages);
1129
- const initiator = isAgentInitiator(upstreamPayload.messages);
1130
- const requestPayload = buildRequestPayload(upstreamPayload);
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=${upstreamPayload.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 (shouldUseResponsesApiForModel(upstreamPayload.model)) return createResponses(provider, upstreamPayload, {
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: upstreamPayload.stream ? "text/event-stream" : "application/json",
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, upstreamPayload, {
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 (upstreamPayload.stream) return events(response);
1168
- return await response.json();
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.json());
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().text().catch(() => "");
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().json()).error?.code === "unsupported_api_for_model";
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 response = await fetchCopilot(getCopilotProviderContext(config), "/models", {
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.json();
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
- }, { initiator: "agent" });
1994
+ }, {
1995
+ initiator: "agent",
1996
+ signal,
1997
+ timeoutMs: options.timeoutMs
1998
+ });
1884
1999
  if (!response.ok) {
1885
- const detail = await response.text().catch(() => "");
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.json();
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);