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 +14 -4
- package/config.default.yaml +6 -6
- package/dist/main.js +83 -19
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
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
|
|
121
|
-
| `debug` | Info plus
|
|
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
|
|
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 `
|
|
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
|
|
package/config.default.yaml
CHANGED
|
@@ -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,
|
|
30
|
-
# status codes
|
|
31
|
-
# debug - info logs plus
|
|
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
|
-
#
|
|
43
|
-
#
|
|
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
|
|
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 (
|
|
718
|
-
|
|
719
|
-
|
|
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,
|
|
839
|
-
"# status codes",
|
|
840
|
-
"# debug - info logs plus
|
|
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
|
-
|
|
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,
|
|
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
|
|
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.
|
|
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 ?? "
|
|
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:
|
|
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
|
|
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,
|
|
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(`
|
|
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 {
|