copilot-relay 0.3.6 → 0.3.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
@@ -117,12 +117,19 @@ for live model discovery, compatible thinking efforts, and copy-paste updates.
117
117
  | Level | Logs |
118
118
  | --- | --- |
119
119
  | `error` | Startup, preflight, and request failures |
120
- | `info` | Errors plus startup status, preflight status, request IDs, upstream lifecycle, and local HTTP status codes |
121
- | `debug` | Info plus model routing summaries, Copilot upstream timings, and request payloads |
120
+ | `info` | Errors plus startup/preflight status, request IDs, model/effort summaries, upstream lifecycle, and local HTTP status codes |
121
+ | `debug` | Info plus detailed Copilot timings and request payloads |
122
122
 
123
123
  Any other `logLevel` value is invalid and stops startup.
124
124
 
125
- Valid `thinkEffort`: `none`, `low`, `medium`, `high`, `xhigh`, `max`.
125
+ Valid `thinkEffort` defaults: `low`, `medium`, `high`, `xhigh`, `max`.
126
+ It is a fallback, not a forced override: Claude Code's `output_config.effort`
127
+ wins, followed by the legacy `reasoning_effort` field, then `thinkEffort` when
128
+ neither request field supplies a value. The selected effort is preserved through
129
+ all model and WebSearch passes; existing config values are not rewritten.
130
+ `none` and other invalid explicit defaults fail startup before authentication or
131
+ config write-back, with a clear list of valid choices. They are not silently
132
+ replaced with `max`. Explicit request-level `none` remains model-dependent.
126
133
 
127
134
  `upstreamTimeoutSeconds` controls the maximum time a single Claude request can
128
135
  spend waiting on upstream Copilot calls, including chat, Responses, preflight,
@@ -208,7 +215,10 @@ health probe, so a relay that cannot answer `/healthz` never reports success.
208
215
 
209
216
  ## Logging
210
217
 
211
- At `debug`, every model request logs the requested model, upstream model, requested think effort, requested thinking, and effective think effort.
218
+ At `info`, every model request logs the requested model, upstream model, requested
219
+ think effort, requested thinking, and effective think effort. Missing request
220
+ effort is shown as `unset`, distinct from explicit `none`; full normal request
221
+ payloads remain at `debug`.
212
222
 
213
223
  Upstream failures are logged at `error` with full request and response context in the same log file.
214
224
 
@@ -26,10 +26,9 @@ claudeSetup: true
26
26
 
27
27
  # Log verbosity:
28
28
  # error - startup/preflight/request failures only
29
- # info - error logs plus startup status, preflight status, and local HTTP
30
- # status codes
31
- # debug - info logs plus model routing summaries, Copilot upstream timings,
32
- # and request payloads
29
+ # info - error logs plus startup/preflight status, model/effort summaries,
30
+ # and local HTTP status codes
31
+ # debug - info logs plus detailed Copilot timings and request payloads
33
32
  # Any other value is invalid and stops startup.
34
33
  logLevel: info
35
34
 
@@ -39,8 +38,9 @@ logLevel: info
39
38
  # midnight, which is what makes this setting take effect at all.
40
39
  logRetentionDays: 3
41
40
 
42
- # Default upstream thinking/reasoning effort sent to Copilot.
43
- # Valid values: none, low, medium, high, xhigh, max.
41
+ # Fallback upstream effort when Claude Code does not specify one.
42
+ # Request output_config.effort wins, followed by legacy reasoning_effort.
43
+ # Valid values: low, medium, high, xhigh, max.
44
44
  thinkEffort: max
45
45
 
46
46
  # Max seconds to wait for one Claude request's upstream Copilot calls.
package/dist/main.js CHANGED
@@ -541,12 +541,43 @@ const runtimeState = {};
541
541
 
542
542
  //#endregion
543
543
  //#region src/lib/models.ts
544
+ const configurableReasoningEfforts = [
545
+ "low",
546
+ "medium",
547
+ "high",
548
+ "xhigh",
549
+ "max"
550
+ ];
544
551
  const defaultReasoningEffort = "max";
545
552
  const defaultModelRouting = {
546
553
  gptModel: "gpt-6-astra",
547
554
  opusModel: "claude-opus-5"
548
555
  };
549
- const isReasoningEffort = (value) => value === "none" || value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "max";
556
+ const isConfiguredReasoningEffort = (value) => configurableReasoningEfforts.some((effort) => effort === value);
557
+ const isReasoningEffort = (value) => value === "none" || isConfiguredReasoningEffort(value);
558
+ const invalidRequestEffort = (message) => new HTTPError(message, new Response(JSON.stringify({
559
+ type: "error",
560
+ error: {
561
+ type: "invalid_request_error",
562
+ message
563
+ }
564
+ }), {
565
+ status: 400,
566
+ headers: { "content-type": "application/json" }
567
+ }), message);
568
+ function getRequestReasoningEffort(request) {
569
+ let nativeEffort;
570
+ const outputConfig = request.output_config;
571
+ if (outputConfig !== void 0 && outputConfig !== null) {
572
+ if (typeof outputConfig !== "object" || Array.isArray(outputConfig)) throw invalidRequestEffort("Invalid output_config: expected an object.");
573
+ if ("effort" in outputConfig) nativeEffort = outputConfig.effort;
574
+ }
575
+ const requested = nativeEffort ?? request.reasoning_effort;
576
+ if (requested === void 0 || requested === null) return void 0;
577
+ if (!isReasoningEffort(requested)) throw invalidRequestEffort(`Invalid ${nativeEffort !== void 0 && nativeEffort !== null ? "output_config.effort" : "reasoning_effort"}: expected none, low, medium, high, xhigh, or max.`);
578
+ return requested;
579
+ }
580
+ const resolveReasoningEffort = (requested) => requested ?? runtimeState.thinkEffort ?? defaultReasoningEffort;
550
581
  const oneMillionContextModelPattern = /^(gpt-5\.6-sol|gpt-6-astra)(?:\[1m\])*$/i;
551
582
  const normalizeOneMillionContextModel = (model) => {
552
583
  return oneMillionContextModelPattern.exec(model)?.[1]?.toLowerCase();
@@ -713,10 +744,19 @@ const normalizeCopilotBaseUrl = (value) => {
713
744
  if (parsed.username || parsed.password) throw new Error(copilotBaseUrlCredentialsMessage);
714
745
  return trimmed;
715
746
  };
747
+ var InvalidThinkEffortError = class extends Error {
748
+ constructor() {
749
+ super(`Invalid thinkEffort. Valid values: ${configurableReasoningEfforts.join(", ")}. "none" is not allowed as a configured default.`);
750
+ this.name = "InvalidThinkEffortError";
751
+ }
752
+ };
716
753
  const normalizeThinkEffort = (value) => {
717
- if (typeof value !== "string") return;
718
- const normalized = value.toLowerCase() === "minimal" ? "low" : value.toLowerCase();
719
- return isReasoningEffort(normalized) ? normalized : void 0;
754
+ if (value === void 0) return void 0;
755
+ if (typeof value === "string") {
756
+ const normalized = value.toLowerCase() === "minimal" ? "low" : value.toLowerCase();
757
+ if (isConfiguredReasoningEffort(normalized)) return normalized;
758
+ }
759
+ throw new InvalidThinkEffortError();
720
760
  };
721
761
  const normalizeUpstreamTimeoutSeconds = (value) => {
722
762
  if (value === void 0) return;
@@ -835,16 +875,15 @@ const serializeConfig = (config) => [
835
875
  "",
836
876
  "# Log verbosity:",
837
877
  "# error - startup/preflight/request failures only",
838
- "# info - error logs plus startup status, preflight status, and local HTTP",
839
- "# status codes",
840
- "# debug - info logs plus model routing summaries, Copilot upstream timings,",
841
- "# and request payloads",
878
+ "# info - error logs plus startup/preflight status, model/effort summaries,",
879
+ "# and local HTTP status codes",
880
+ "# debug - info logs plus detailed Copilot timings and request payloads",
842
881
  `logLevel: ${config.logLevel}`,
843
882
  "",
844
883
  "# Number of days to keep files in ~/.copilot-relay/logs.",
845
884
  `logRetentionDays: ${config.logRetentionDays}`,
846
885
  "",
847
- "# Default upstream thinking/reasoning effort: none, low, medium, high, xhigh, max.",
886
+ `# Fallback effort when the request omits it: ${configurableReasoningEfforts.join(", ")}.`,
848
887
  `thinkEffort: ${config.thinkEffort}`,
849
888
  "",
850
889
  "# Max seconds for one request's upstream calls; 0 disables the relay deadline.",
@@ -903,7 +942,10 @@ const watchAppConfig = (onReload) => {
903
942
  if (nextMtime === lastMtime) return;
904
943
  lastMtime = nextMtime;
905
944
  onReload(await readAppConfig());
906
- } catch {}
945
+ } catch (error) {
946
+ if (error instanceof InvalidThinkEffortError) log.error(`${error.message} Keeping the previous runtime settings.`);
947
+ else log.error(`Could not reload config. Check ${paths.configPath} for invalid values or file errors.`);
948
+ }
907
949
  }, 1e3);
908
950
  if (typeof timer.unref === "function") timer.unref();
909
951
  getConfigMtime().then((mtime) => {
@@ -1993,7 +2035,7 @@ const sanitizeReasoningEffortForModel = (_modelId, reasoningEffort) => {
1993
2035
  return reasoningEffort ?? void 0;
1994
2036
  };
1995
2037
  const getRequestedReasoningEffort = (payload) => {
1996
- return sanitizeReasoningEffortForModel(payload.model, runtimeState.thinkEffort ?? defaultReasoningEffort);
2038
+ return sanitizeReasoningEffortForModel(payload.model, resolveReasoningEffort(getRequestReasoningEffort(payload)));
1997
2039
  };
1998
2040
  const sanitizeUserIdentifier = (user) => {
1999
2041
  if (!user) return;
@@ -2058,7 +2100,8 @@ const messagesIncludeImage = (messages) => messages.some((msg) => typeof msg.con
2058
2100
  const createChatCompletions = async (config, payload, options = {}) => {
2059
2101
  const client = options.client ?? "generic";
2060
2102
  const requestedModel = options.requestedModel ?? payload.model;
2061
- const requestedThinkEffort = options.requestedThinkEffort ?? payload.reasoning_effort ?? "none";
2103
+ const requestedThinkEffort = options.requestedThinkEffort ?? getRequestReasoningEffort(payload) ?? "unset";
2104
+ const reasoningEffort = getRequestedReasoningEffort(payload);
2062
2105
  const requestedThinking = options.requestedThinking ?? "none";
2063
2106
  const signal = createCopilotRequestSignal(options.signal, options.timeoutMs);
2064
2107
  const upstreamModelId = routeModelId(payload.model);
@@ -2069,6 +2112,7 @@ const createChatCompletions = async (config, payload, options = {}) => {
2069
2112
  const upstreamPayload = {
2070
2113
  ...payload,
2071
2114
  model: upstreamModelId,
2115
+ reasoning_effort: reasoningEffort,
2072
2116
  max_tokens: maxTokens,
2073
2117
  stream: bufferResponse ? true : payload.stream
2074
2118
  };
@@ -2078,7 +2122,7 @@ const createChatCompletions = async (config, payload, options = {}) => {
2078
2122
  const enableVision = messagesIncludeImage(compatiblePayload.messages);
2079
2123
  const initiator = isAgentInitiator(compatiblePayload.messages);
2080
2124
  const requestPayload = buildRequestPayload(compatiblePayload);
2081
- log.debug([
2125
+ log.info(sanitizeTerminalString([
2082
2126
  "Model request",
2083
2127
  `client=${client}`,
2084
2128
  `requested_model=${requestedModel}`,
@@ -2086,8 +2130,8 @@ const createChatCompletions = async (config, payload, options = {}) => {
2086
2130
  `upstream_model=${compatiblePayload.model}`,
2087
2131
  `requested_think_effort=${requestedThinkEffort}`,
2088
2132
  `requested_thinking=${requestedThinking}`,
2089
- `effective_think_effort=${requestPayload.reasoning_effort ?? "none"}`
2090
- ].join(" "));
2133
+ `effective_think_effort=${requestPayload.reasoning_effort ?? "unset"}`
2134
+ ].join(" ")));
2091
2135
  log.debug("Full request payload", { payload: requestPayload });
2092
2136
  if (useResponsesApi) return completeResponse(await createResponses(provider, compatiblePayload, {
2093
2137
  vision: enableVision,
@@ -2325,7 +2369,7 @@ function translateToOpenAI(payload, _settings, toolNameMapper) {
2325
2369
  stream: payload.stream,
2326
2370
  temperature: payload.temperature,
2327
2371
  top_p: payload.top_p,
2328
- reasoning_effort: runtimeState.thinkEffort ?? defaultReasoningEffort,
2372
+ reasoning_effort: resolveReasoningEffort(getRequestReasoningEffort(payload)),
2329
2373
  user: payload.metadata?.user_id,
2330
2374
  tools,
2331
2375
  tool_choice: tools && tools.length > 0 ? translateClaudeToolChoiceToOpenAI(payload.tool_choice, mapper) : void 0
@@ -2770,6 +2814,7 @@ const buildWebSearchRequestPayload = (payload, requestedQuery, model) => ({
2770
2814
  model,
2771
2815
  input: buildSearchInput(payload, requestedQuery),
2772
2816
  tools: [{ type: "web_search_preview" }],
2817
+ reasoning: { effort: resolveReasoningEffort(getRequestReasoningEffort(payload)) },
2773
2818
  max_output_tokens: Math.max(256, Math.min(payload.max_tokens ?? 1024, 1200)),
2774
2819
  temperature: payload.temperature,
2775
2820
  top_p: payload.top_p
@@ -3190,7 +3235,7 @@ const createTokenCountModel = (modelId, tokenizer = "o200k_base") => ({
3190
3235
  id: modelId
3191
3236
  });
3192
3237
  const isNonStreamingResponse = (response) => typeof response === "object" && response !== null && Object.hasOwn(response, "choices");
3193
- const getClaudeRequestedThinkEffort = (payload) => payload.reasoning_effort ?? "none";
3238
+ const getClaudeRequestedThinkEffort = (payload) => getRequestReasoningEffort(payload) ?? "unset";
3194
3239
  const getClaudeRequestedThinking = (payload) => {
3195
3240
  if (!payload.thinking) return "none";
3196
3241
  return [`type:${payload.thinking.type}`, `budget:${payload.thinking.budget_tokens ?? "none"}`].join(",");
@@ -3368,7 +3413,10 @@ const handleClaudeMessageRequest = async (config, claudePayload, requestSignal,
3368
3413
  const webSearchToolCall = shouldLetModelDecideWebSearch ? getClaudeWebSearchToolCallFromChatResponse(effectiveResponse, toolNameMapper) : void 0;
3369
3414
  let claudeResponse;
3370
3415
  if (webSearchToolCall) {
3371
- const search = await createClaudeWebSearchExecution(config, claudePayload, webSearchToolCall.query, {
3416
+ const search = await createClaudeWebSearchExecution(config, {
3417
+ ...claudePayload,
3418
+ reasoning_effort: openAIPayload.reasoning_effort
3419
+ }, webSearchToolCall.query, {
3372
3420
  requestId,
3373
3421
  signal: requestSignal,
3374
3422
  timeoutMs: config.upstreamTimeoutMs
@@ -3441,6 +3489,14 @@ claudeRoutes.post("/messages", async (c) => {
3441
3489
  const config = c.get("config");
3442
3490
  const requestId = c.get("requestId");
3443
3491
  const claudePayload = await c.req.json();
3492
+ try {
3493
+ getRequestReasoningEffort(claudePayload);
3494
+ } catch (error) {
3495
+ if (!(error instanceof HTTPError)) throw error;
3496
+ c.set("requestErrorMessage", error.message);
3497
+ log.error(`request_id=${requestId} ${error.message}`);
3498
+ return error.response;
3499
+ }
3444
3500
  const requestSignal = createCopilotRequestSignal(c.req.raw.signal, config.upstreamTimeoutMs);
3445
3501
  log.debug("Full Claude request payload", { payload: claudePayload });
3446
3502
  if (claudePayload.stream) return streamSSE(c, async (stream) => {
@@ -3500,6 +3556,10 @@ claudeRoutes.post("/messages/count_tokens", async (c) => {
3500
3556
  return c.json({ input_tokens: Math.max(1, finalTokenCount) });
3501
3557
  } catch (error) {
3502
3558
  log.error("Error counting tokens:", error);
3559
+ if (error instanceof HTTPError) {
3560
+ c.set("requestErrorMessage", error.message);
3561
+ return error.response;
3562
+ }
3503
3563
  return c.json({ input_tokens: 1 });
3504
3564
  }
3505
3565
  });
@@ -3604,6 +3664,10 @@ const startServer = (config) => new Promise((resolve$1, reject) => {
3604
3664
  const canCloseConnections = (server) => typeof server.closeIdleConnections === "function" && typeof server.closeAllConnections === "function";
3605
3665
  async function startRelay(appConfig) {
3606
3666
  appConfig ??= await readAppConfig();
3667
+ appConfig = {
3668
+ ...appConfig,
3669
+ thinkEffort: normalizeThinkEffort(appConfig.thinkEffort) ?? defaultReasoningEffort
3670
+ };
3607
3671
  setLogLevel(appConfig.logLevel);
3608
3672
  await cleanupLogs(appConfig.logRetentionDays);
3609
3673
  const claudeConfigPath$1 = claudeConfigPath;
@@ -3627,7 +3691,7 @@ async function startRelay(appConfig) {
3627
3691
  };
3628
3692
  applyRuntimeConfig(appConfig);
3629
3693
  log.info(`Log level: ${appConfig.logLevel}`);
3630
- log.info(`Think effort: ${appConfig.thinkEffort}`);
3694
+ log.info(`Default think effort: ${appConfig.thinkEffort}`);
3631
3695
  log.info(`Upstream timeout: ${appConfig.upstreamTimeoutSeconds}s`);
3632
3696
  const authSession = await setupProxyAuth(config);
3633
3697
  try {