copilot-relay 0.3.6 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -123,6 +123,10 @@ for live model discovery, compatible thinking efforts, and copy-paste updates.
123
123
  Any other `logLevel` value is invalid and stops startup.
124
124
 
125
125
  Valid `thinkEffort`: `none`, `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.
126
130
 
127
131
  `upstreamTimeoutSeconds` controls the maximum time a single Claude request can
128
132
  spend waiting on upstream Copilot calls, including chat, Responses, preflight,
@@ -39,7 +39,8 @@ logLevel: info
39
39
  # midnight, which is what makes this setting take effect at all.
40
40
  logRetentionDays: 3
41
41
 
42
- # Default upstream thinking/reasoning effort sent to Copilot.
42
+ # Fallback upstream effort when Claude Code does not specify one.
43
+ # Request output_config.effort wins, followed by legacy reasoning_effort.
43
44
  # Valid values: none, low, medium, high, xhigh, max.
44
45
  thinkEffort: max
45
46
 
package/dist/main.js CHANGED
@@ -547,6 +547,29 @@ const defaultModelRouting = {
547
547
  opusModel: "claude-opus-5"
548
548
  };
549
549
  const isReasoningEffort = (value) => value === "none" || value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "max";
550
+ const invalidRequestEffort = (message) => new HTTPError(message, new Response(JSON.stringify({
551
+ type: "error",
552
+ error: {
553
+ type: "invalid_request_error",
554
+ message
555
+ }
556
+ }), {
557
+ status: 400,
558
+ headers: { "content-type": "application/json" }
559
+ }), message);
560
+ function getRequestReasoningEffort(request) {
561
+ let nativeEffort;
562
+ const outputConfig = request.output_config;
563
+ if (outputConfig !== void 0 && outputConfig !== null) {
564
+ if (typeof outputConfig !== "object" || Array.isArray(outputConfig)) throw invalidRequestEffort("Invalid output_config: expected an object.");
565
+ if ("effort" in outputConfig) nativeEffort = outputConfig.effort;
566
+ }
567
+ const requested = nativeEffort ?? request.reasoning_effort;
568
+ if (requested === void 0 || requested === null) return void 0;
569
+ if (!isReasoningEffort(requested)) throw invalidRequestEffort(`Invalid ${nativeEffort !== void 0 && nativeEffort !== null ? "output_config.effort" : "reasoning_effort"}: expected none, low, medium, high, xhigh, or max.`);
570
+ return requested;
571
+ }
572
+ const resolveReasoningEffort = (requested) => requested ?? runtimeState.thinkEffort ?? defaultReasoningEffort;
550
573
  const oneMillionContextModelPattern = /^(gpt-5\.6-sol|gpt-6-astra)(?:\[1m\])*$/i;
551
574
  const normalizeOneMillionContextModel = (model) => {
552
575
  return oneMillionContextModelPattern.exec(model)?.[1]?.toLowerCase();
@@ -844,7 +867,7 @@ const serializeConfig = (config) => [
844
867
  "# Number of days to keep files in ~/.copilot-relay/logs.",
845
868
  `logRetentionDays: ${config.logRetentionDays}`,
846
869
  "",
847
- "# Default upstream thinking/reasoning effort: none, low, medium, high, xhigh, max.",
870
+ "# Fallback effort when the request omits it: none, low, medium, high, xhigh, max.",
848
871
  `thinkEffort: ${config.thinkEffort}`,
849
872
  "",
850
873
  "# Max seconds for one request's upstream calls; 0 disables the relay deadline.",
@@ -1993,7 +2016,7 @@ const sanitizeReasoningEffortForModel = (_modelId, reasoningEffort) => {
1993
2016
  return reasoningEffort ?? void 0;
1994
2017
  };
1995
2018
  const getRequestedReasoningEffort = (payload) => {
1996
- return sanitizeReasoningEffortForModel(payload.model, runtimeState.thinkEffort ?? defaultReasoningEffort);
2019
+ return sanitizeReasoningEffortForModel(payload.model, resolveReasoningEffort(getRequestReasoningEffort(payload)));
1997
2020
  };
1998
2021
  const sanitizeUserIdentifier = (user) => {
1999
2022
  if (!user) return;
@@ -2058,7 +2081,8 @@ const messagesIncludeImage = (messages) => messages.some((msg) => typeof msg.con
2058
2081
  const createChatCompletions = async (config, payload, options = {}) => {
2059
2082
  const client = options.client ?? "generic";
2060
2083
  const requestedModel = options.requestedModel ?? payload.model;
2061
- const requestedThinkEffort = options.requestedThinkEffort ?? payload.reasoning_effort ?? "none";
2084
+ const requestedThinkEffort = options.requestedThinkEffort ?? getRequestReasoningEffort(payload) ?? "none";
2085
+ const reasoningEffort = getRequestedReasoningEffort(payload);
2062
2086
  const requestedThinking = options.requestedThinking ?? "none";
2063
2087
  const signal = createCopilotRequestSignal(options.signal, options.timeoutMs);
2064
2088
  const upstreamModelId = routeModelId(payload.model);
@@ -2069,6 +2093,7 @@ const createChatCompletions = async (config, payload, options = {}) => {
2069
2093
  const upstreamPayload = {
2070
2094
  ...payload,
2071
2095
  model: upstreamModelId,
2096
+ reasoning_effort: reasoningEffort,
2072
2097
  max_tokens: maxTokens,
2073
2098
  stream: bufferResponse ? true : payload.stream
2074
2099
  };
@@ -2325,7 +2350,7 @@ function translateToOpenAI(payload, _settings, toolNameMapper) {
2325
2350
  stream: payload.stream,
2326
2351
  temperature: payload.temperature,
2327
2352
  top_p: payload.top_p,
2328
- reasoning_effort: runtimeState.thinkEffort ?? defaultReasoningEffort,
2353
+ reasoning_effort: resolveReasoningEffort(getRequestReasoningEffort(payload)),
2329
2354
  user: payload.metadata?.user_id,
2330
2355
  tools,
2331
2356
  tool_choice: tools && tools.length > 0 ? translateClaudeToolChoiceToOpenAI(payload.tool_choice, mapper) : void 0
@@ -2770,6 +2795,7 @@ const buildWebSearchRequestPayload = (payload, requestedQuery, model) => ({
2770
2795
  model,
2771
2796
  input: buildSearchInput(payload, requestedQuery),
2772
2797
  tools: [{ type: "web_search_preview" }],
2798
+ reasoning: { effort: resolveReasoningEffort(getRequestReasoningEffort(payload)) },
2773
2799
  max_output_tokens: Math.max(256, Math.min(payload.max_tokens ?? 1024, 1200)),
2774
2800
  temperature: payload.temperature,
2775
2801
  top_p: payload.top_p
@@ -3190,7 +3216,7 @@ const createTokenCountModel = (modelId, tokenizer = "o200k_base") => ({
3190
3216
  id: modelId
3191
3217
  });
3192
3218
  const isNonStreamingResponse = (response) => typeof response === "object" && response !== null && Object.hasOwn(response, "choices");
3193
- const getClaudeRequestedThinkEffort = (payload) => payload.reasoning_effort ?? "none";
3219
+ const getClaudeRequestedThinkEffort = (payload) => getRequestReasoningEffort(payload) ?? "none";
3194
3220
  const getClaudeRequestedThinking = (payload) => {
3195
3221
  if (!payload.thinking) return "none";
3196
3222
  return [`type:${payload.thinking.type}`, `budget:${payload.thinking.budget_tokens ?? "none"}`].join(",");
@@ -3368,7 +3394,10 @@ const handleClaudeMessageRequest = async (config, claudePayload, requestSignal,
3368
3394
  const webSearchToolCall = shouldLetModelDecideWebSearch ? getClaudeWebSearchToolCallFromChatResponse(effectiveResponse, toolNameMapper) : void 0;
3369
3395
  let claudeResponse;
3370
3396
  if (webSearchToolCall) {
3371
- const search = await createClaudeWebSearchExecution(config, claudePayload, webSearchToolCall.query, {
3397
+ const search = await createClaudeWebSearchExecution(config, {
3398
+ ...claudePayload,
3399
+ reasoning_effort: openAIPayload.reasoning_effort
3400
+ }, webSearchToolCall.query, {
3372
3401
  requestId,
3373
3402
  signal: requestSignal,
3374
3403
  timeoutMs: config.upstreamTimeoutMs
@@ -3441,6 +3470,14 @@ claudeRoutes.post("/messages", async (c) => {
3441
3470
  const config = c.get("config");
3442
3471
  const requestId = c.get("requestId");
3443
3472
  const claudePayload = await c.req.json();
3473
+ try {
3474
+ getRequestReasoningEffort(claudePayload);
3475
+ } catch (error) {
3476
+ if (!(error instanceof HTTPError)) throw error;
3477
+ c.set("requestErrorMessage", error.message);
3478
+ log.error(`request_id=${requestId} ${error.message}`);
3479
+ return error.response;
3480
+ }
3444
3481
  const requestSignal = createCopilotRequestSignal(c.req.raw.signal, config.upstreamTimeoutMs);
3445
3482
  log.debug("Full Claude request payload", { payload: claudePayload });
3446
3483
  if (claudePayload.stream) return streamSSE(c, async (stream) => {
@@ -3500,6 +3537,10 @@ claudeRoutes.post("/messages/count_tokens", async (c) => {
3500
3537
  return c.json({ input_tokens: Math.max(1, finalTokenCount) });
3501
3538
  } catch (error) {
3502
3539
  log.error("Error counting tokens:", error);
3540
+ if (error instanceof HTTPError) {
3541
+ c.set("requestErrorMessage", error.message);
3542
+ return error.response;
3543
+ }
3503
3544
  return c.json({ input_tokens: 1 });
3504
3545
  }
3505
3546
  });
@@ -3627,7 +3668,7 @@ async function startRelay(appConfig) {
3627
3668
  };
3628
3669
  applyRuntimeConfig(appConfig);
3629
3670
  log.info(`Log level: ${appConfig.logLevel}`);
3630
- log.info(`Think effort: ${appConfig.thinkEffort}`);
3671
+ log.info(`Default think effort: ${appConfig.thinkEffort}`);
3631
3672
  log.info(`Upstream timeout: ${appConfig.upstreamTimeoutSeconds}s`);
3632
3673
  const authSession = await setupProxyAuth(config);
3633
3674
  try {