copilot-relay 0.4.6 → 0.4.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 +1 -1
- package/dist/main.js +164 -70
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ running daemon. Add `--details` for safe failure evidence and private replay hin
|
|
|
55
55
|
Use `copilot-relay status --deep` for daemon health, and `copilot-relay stop` when finished.
|
|
56
56
|
|
|
57
57
|
Check how well prompt caching works per model and upstream route. The report reads
|
|
58
|
-
only local logs and
|
|
58
|
+
only local logs and shows a hit rate below the goal, 95% by default, in red:
|
|
59
59
|
|
|
60
60
|
```sh
|
|
61
61
|
copilot-relay cache # last 24 hours
|
package/dist/main.js
CHANGED
|
@@ -220,7 +220,7 @@ const formatUpstreamProxyForDisplay = (upstreamProxy) => {
|
|
|
220
220
|
* `status` prints config values directly to stdout. A config string carrying
|
|
221
221
|
* raw control bytes could clear the line and paint status rows the relay never
|
|
222
222
|
* produced. The config file is user-owned, so this is hardening rather than a
|
|
223
|
-
* vulnerability — but it costs nothing to make the output non-forgeable.
|
|
223
|
+
* vulnerability — but it costs nothing to make the output non-forgeable.
|
|
224
224
|
*/
|
|
225
225
|
const sanitizeTerminalString = (text$1) => text$1.replace(ansiEscapePattern, "").replace(controlBytePattern, "");
|
|
226
226
|
/**
|
|
@@ -513,7 +513,7 @@ const cleanupLogsIfDue = async () => {
|
|
|
513
513
|
* Each argument is rendered exactly once and scrubbed once, and the identical
|
|
514
514
|
* array of strings goes to both the file and the console. Scrubbing only on the
|
|
515
515
|
* way to disk would leave the original object on the console - the screen a
|
|
516
|
-
* user screenshots into an issue - so the two sinks must not diverge.
|
|
516
|
+
* user screenshots into an issue - so the two sinks must not diverge.
|
|
517
517
|
*
|
|
518
518
|
* Rendering is skipped only when neither sink would emit, so the cost matches
|
|
519
519
|
* the old behavior at info level. The console gate is read from consola rather
|
|
@@ -595,7 +595,7 @@ var InvalidProxyEnvironmentError = class extends Error {
|
|
|
595
595
|
/**
|
|
596
596
|
* Builds the dispatcher for one resolved upstreamProxy value.
|
|
597
597
|
*
|
|
598
|
-
* Empty connects directly
|
|
598
|
+
* Empty connects directly. "env" hands HTTPS_PROXY, HTTP_PROXY
|
|
599
599
|
* and NO_PROXY, in upper or lower case, to EnvHttpProxyAgent. Anything else is a proxy URL that
|
|
600
600
|
* normalizeUpstreamProxy has already validated.
|
|
601
601
|
*/
|
|
@@ -1373,7 +1373,7 @@ const normalizeApiKey = (value) => {
|
|
|
1373
1373
|
* through. None of them can be found again in arbitrary log text: practical URL
|
|
1374
1374
|
* detection anchors on a literal "://", and loosening that to chase a bare
|
|
1375
1375
|
* "https:" would start matching ordinary prose. Requiring the authority prefix
|
|
1376
|
-
* is the one boundary where this has a definite answer.
|
|
1376
|
+
* is the one boundary where this has a definite answer.
|
|
1377
1377
|
*/
|
|
1378
1378
|
const conventionalHttpUrlPattern = /^https?:\/\//i;
|
|
1379
1379
|
/**
|
|
@@ -1390,7 +1390,7 @@ const conventionalHttpUrlPattern = /^https?:\/\//i;
|
|
|
1390
1390
|
*
|
|
1391
1391
|
* Rejecting the raw byte costs nothing: every one of these has a
|
|
1392
1392
|
* percent-encoded spelling that is unambiguous in log text and is accepted
|
|
1393
|
-
* here unchanged.
|
|
1393
|
+
* here unchanged.
|
|
1394
1394
|
*/
|
|
1395
1395
|
const unsafeRawDelimiterPattern = /['"`<>\s\u0000-\u001F\u007F]/;
|
|
1396
1396
|
/**
|
|
@@ -1404,7 +1404,7 @@ const copilotBaseUrlDelimiterMessage = "Invalid copilotBaseUrl: quotes, backtick
|
|
|
1404
1404
|
/**
|
|
1405
1405
|
* Validates copilotBaseUrl as a conventional HTTP(S) URL without credentials.
|
|
1406
1406
|
*
|
|
1407
|
-
* Four rules
|
|
1407
|
+
* Four rules:
|
|
1408
1408
|
*
|
|
1409
1409
|
* The value is concatenated as `${base}${path}` for every upstream call, so it
|
|
1410
1410
|
* must be something Undici will accept. A relative or non-HTTP value fails at
|
|
@@ -2146,36 +2146,6 @@ const dataColumns = [
|
|
|
2146
2146
|
alignRight: true,
|
|
2147
2147
|
cell: (row) => formatCount(row.requests)
|
|
2148
2148
|
},
|
|
2149
|
-
{
|
|
2150
|
-
header: "UNKNOWN",
|
|
2151
|
-
alignRight: true,
|
|
2152
|
-
cell: (row) => formatCount(row.unknownCacheRequests)
|
|
2153
|
-
},
|
|
2154
|
-
{
|
|
2155
|
-
header: "0-READ",
|
|
2156
|
-
alignRight: true,
|
|
2157
|
-
cell: (row) => formatCount(row.zeroCacheReadRequests)
|
|
2158
|
-
},
|
|
2159
|
-
{
|
|
2160
|
-
header: "INPUT",
|
|
2161
|
-
alignRight: true,
|
|
2162
|
-
cell: (row) => formatCount(row.totalInputTokens)
|
|
2163
|
-
},
|
|
2164
|
-
{
|
|
2165
|
-
header: "CACHE READ",
|
|
2166
|
-
alignRight: true,
|
|
2167
|
-
cell: (row) => formatCount(row.cacheReadTokens)
|
|
2168
|
-
},
|
|
2169
|
-
{
|
|
2170
|
-
header: "UNCACHED",
|
|
2171
|
-
alignRight: true,
|
|
2172
|
-
cell: (row) => formatCount(row.uncachedInputTokens)
|
|
2173
|
-
},
|
|
2174
|
-
{
|
|
2175
|
-
header: "CACHE WRITE",
|
|
2176
|
-
alignRight: true,
|
|
2177
|
-
cell: (row) => row.cacheWriteTokens === null ? "-" : formatCount(row.cacheWriteTokens)
|
|
2178
|
-
},
|
|
2179
2149
|
{
|
|
2180
2150
|
header: "HIT RATE",
|
|
2181
2151
|
alignRight: true,
|
|
@@ -2190,12 +2160,10 @@ const renderTable = (rows, view, color) => {
|
|
|
2190
2160
|
const pad = (text$1, index) => columns[index].alignRight ? text$1.padStart(widths[index]) : text$1.padEnd(widths[index]);
|
|
2191
2161
|
const header = columns.map((column, index) => pad(column.header, index)).join(" ");
|
|
2192
2162
|
const body = rows.map((row, rowIndex) => {
|
|
2193
|
-
|
|
2163
|
+
return ` ${columns.map((column, index) => {
|
|
2194
2164
|
const text$1 = pad(cells[rowIndex][index], index);
|
|
2195
2165
|
return column.tone === void 0 ? text$1 : colorText(text$1, column.tone(row), color);
|
|
2196
|
-
})
|
|
2197
|
-
const marker = row.belowGoal ? ` ${colorText("below goal", "bad", color)}` : "";
|
|
2198
|
-
return ` ${rendered.join(" ")}${marker}`;
|
|
2166
|
+
}).join(" ")}`;
|
|
2199
2167
|
});
|
|
2200
2168
|
return [` ${header}`, ...body];
|
|
2201
2169
|
};
|
|
@@ -2208,7 +2176,7 @@ const formatLocalTime = (date) => `${formatLogDate(date)} ${twoDigits(date.getHo
|
|
|
2208
2176
|
const describeScope = (options) => {
|
|
2209
2177
|
return `${options.since === void 0 ? "across all retained logs" : `since ${formatLocalTime(options.since)} local time`}${options.model === void 0 ? "" : `, for models matching "${terminalText(options.model)}"`}`;
|
|
2210
2178
|
};
|
|
2211
|
-
const unknownNote =
|
|
2179
|
+
const unknownNote = (calls) => `HIT RATE leaves out ${formatCount(calls)} ${calls === 1 ? "call" : "calls"} that logged no cache_read_input_tokens.`;
|
|
2212
2180
|
/** The report as text lines. Kept free of IO, so it is tested without a terminal. */
|
|
2213
2181
|
const renderCacheReport = (rows, options, color = false) => {
|
|
2214
2182
|
if (rows.length === 0) return [
|
|
@@ -2221,7 +2189,8 @@ const renderCacheReport = (rows, options, color = false) => {
|
|
|
2221
2189
|
"",
|
|
2222
2190
|
...renderTable(rows, options.view, color)
|
|
2223
2191
|
];
|
|
2224
|
-
|
|
2192
|
+
const unknownCalls = rows.reduce((total, row) => total + row.unknownCacheRequests, 0);
|
|
2193
|
+
if (unknownCalls > 0) lines.push("", unknownNote(unknownCalls));
|
|
2225
2194
|
return lines;
|
|
2226
2195
|
};
|
|
2227
2196
|
|
|
@@ -2255,7 +2224,7 @@ const cache = defineCommand({
|
|
|
2255
2224
|
},
|
|
2256
2225
|
goal: {
|
|
2257
2226
|
default: String(defaultGoal),
|
|
2258
|
-
description: "Hit-rate goal in percent, with at most two decimals.
|
|
2227
|
+
description: "Hit-rate goal in percent, with at most two decimals. A hit rate below it is shown in red.",
|
|
2259
2228
|
type: "string"
|
|
2260
2229
|
}
|
|
2261
2230
|
},
|
|
@@ -3358,10 +3327,10 @@ const pinCopilotModelCatalog = (config) => {
|
|
|
3358
3327
|
pinnedCatalogs.set(config, config.modelCatalog);
|
|
3359
3328
|
};
|
|
3360
3329
|
const pendingCatalogs = /* @__PURE__ */ new WeakMap();
|
|
3361
|
-
const isRecord$
|
|
3330
|
+
const isRecord$5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3362
3331
|
const isPositiveInteger = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
3363
3332
|
function parseModelTokenLimits(value) {
|
|
3364
|
-
if (!isRecord$
|
|
3333
|
+
if (!isRecord$5(value)) return;
|
|
3365
3334
|
const context = value.max_context_window_tokens;
|
|
3366
3335
|
const prompt = value.max_prompt_tokens;
|
|
3367
3336
|
const output = value.max_output_tokens;
|
|
@@ -3417,13 +3386,13 @@ async function loadCopilotModelCatalog(config) {
|
|
|
3417
3386
|
});
|
|
3418
3387
|
if (!response.ok) throw new HTTPError("Failed to validate upstream models", response);
|
|
3419
3388
|
const payload = await readCopilotJson(response, signal, config.upstreamTimeoutMs);
|
|
3420
|
-
if (!isRecord$
|
|
3389
|
+
if (!isRecord$5(payload) || !Array.isArray(payload.data)) throw new Error("Copilot /models returned an invalid model catalog.");
|
|
3421
3390
|
const models$1 = /* @__PURE__ */ new Map();
|
|
3422
3391
|
for (const model of payload.data) {
|
|
3423
|
-
if (!isRecord$
|
|
3424
|
-
const capabilities = isRecord$
|
|
3392
|
+
if (!isRecord$5(model) || typeof model.id !== "string" || !model.id) continue;
|
|
3393
|
+
const capabilities = isRecord$5(model.capabilities) ? model.capabilities : void 0;
|
|
3425
3394
|
const limits = parseModelTokenLimits(capabilities?.limits);
|
|
3426
|
-
const supports = isRecord$
|
|
3395
|
+
const supports = isRecord$5(capabilities?.supports) ? capabilities.supports : void 0;
|
|
3427
3396
|
const supportedEndpoints = parseStringArray(model.supported_endpoints);
|
|
3428
3397
|
const reasoningEfforts = parseReasoningEfforts(supports);
|
|
3429
3398
|
models$1.set(model.id, {
|
|
@@ -3671,11 +3640,15 @@ const probeHint = (code) => {
|
|
|
3671
3640
|
//#endregion
|
|
3672
3641
|
//#region src/claude/billing-line.ts
|
|
3673
3642
|
const billingLinePrefix = "x-anthropic-billing-header:";
|
|
3674
|
-
const
|
|
3643
|
+
const leadingBillingLine = (text$1) => {
|
|
3675
3644
|
if (!text$1.startsWith(billingLinePrefix)) return;
|
|
3676
3645
|
const lineEnd = text$1.indexOf("\n");
|
|
3677
|
-
|
|
3678
|
-
|
|
3646
|
+
return lineEnd === -1 ? text$1 : text$1.slice(0, lineEnd);
|
|
3647
|
+
};
|
|
3648
|
+
const afterBillingLine = (text$1) => {
|
|
3649
|
+
const line = leadingBillingLine(text$1);
|
|
3650
|
+
if (line === void 0) return;
|
|
3651
|
+
return text$1.slice(line.length).replace(/^(?:\r?\n)+/, "");
|
|
3679
3652
|
};
|
|
3680
3653
|
const isTextBlock = (block) => typeof block === "object" && block !== null && block.type === "text" && typeof block.text === "string";
|
|
3681
3654
|
const withSystem = (payload, system) => {
|
|
@@ -3711,6 +3684,120 @@ const removeBillingLine = (payload) => {
|
|
|
3711
3684
|
if (!changed) return payload;
|
|
3712
3685
|
return withSystem(payload, kept.length > 0 ? kept : void 0);
|
|
3713
3686
|
};
|
|
3687
|
+
const systemTexts$1 = (system) => {
|
|
3688
|
+
if (typeof system === "string") return [system];
|
|
3689
|
+
return Array.isArray(system) ? system.filter(isTextBlock).map((block) => block.text) : [];
|
|
3690
|
+
};
|
|
3691
|
+
const readBillingLineField = (payload, name) => {
|
|
3692
|
+
if (typeof payload !== "object" || payload === null) return;
|
|
3693
|
+
const line = systemTexts$1(payload.system).map((text$1) => leadingBillingLine(text$1)).find((text$1) => text$1 !== void 0);
|
|
3694
|
+
if (line === void 0) return;
|
|
3695
|
+
for (const field of line.slice(27).split(";")) {
|
|
3696
|
+
const separator = field.indexOf("=");
|
|
3697
|
+
if (separator !== -1 && field.slice(0, separator).trim() === name) return field.slice(separator + 1).trim();
|
|
3698
|
+
}
|
|
3699
|
+
};
|
|
3700
|
+
|
|
3701
|
+
//#endregion
|
|
3702
|
+
//#region src/claude/initiator.ts
|
|
3703
|
+
const ownRequestSystemPrompts = ["You are a helpful AI assistant tasked with summarizing conversations.", "You are naming a coding session"];
|
|
3704
|
+
const compactionGuard = "CRITICAL: Respond with TEXT ONLY.";
|
|
3705
|
+
const compactionInstruction = "Your task is to create a detailed summary of the conversation so far";
|
|
3706
|
+
const personDeliveries = [
|
|
3707
|
+
"The user sent a new message while you were working:",
|
|
3708
|
+
"A message arrived in the bound thread while you were working:",
|
|
3709
|
+
"Messages arrived in the bound thread while you were working:"
|
|
3710
|
+
];
|
|
3711
|
+
const claudeCodeTriggers = [
|
|
3712
|
+
"This session is being continued from a previous conversation",
|
|
3713
|
+
"The summarized conversation included Artifact content",
|
|
3714
|
+
"The previous response failed to produce a valid tool call.",
|
|
3715
|
+
"Your tool call was malformed and could not be parsed.",
|
|
3716
|
+
"[Your previous response had no visible output.",
|
|
3717
|
+
"[structured-output-enforce]",
|
|
3718
|
+
"[projects-reply-gate]",
|
|
3719
|
+
"Output token limit hit",
|
|
3720
|
+
"Your response above was cut off mid-stream",
|
|
3721
|
+
"Your response above was stopped by a safety classifier",
|
|
3722
|
+
"Activity in the bound conversation",
|
|
3723
|
+
"Another Claude session sent a message",
|
|
3724
|
+
"Your background observer",
|
|
3725
|
+
"[MESSAGE FROM NON-USER SOURCE",
|
|
3726
|
+
"[SCHEDULED TASK",
|
|
3727
|
+
"<channel source=",
|
|
3728
|
+
"<teammate-message"
|
|
3729
|
+
];
|
|
3730
|
+
const claudeCodeTriggerPatterns = [/^\S+ hook (?:feedback|blocking error from command):/, /^The \S+ plugin sent a message/];
|
|
3731
|
+
const claudeCodeContext = [
|
|
3732
|
+
"[Request interrupted by user",
|
|
3733
|
+
"[Tool call ",
|
|
3734
|
+
"The user doesn't want to take this action right now.",
|
|
3735
|
+
"Base directory for this skill:"
|
|
3736
|
+
];
|
|
3737
|
+
const claudeCodeContextPatterns = [/^\S+ hook additional context:/];
|
|
3738
|
+
const statusProbePrompt = "Reply with the single word: ok";
|
|
3739
|
+
const statusProbeMaxTokens = 16;
|
|
3740
|
+
const isRecord$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3741
|
+
const innermostReminder = /<system-reminder>(?:(?!<system-reminder>)[\s\S])*?<\/system-reminder>/g;
|
|
3742
|
+
const withoutReminders = (text$1) => {
|
|
3743
|
+
let previous = text$1;
|
|
3744
|
+
let remaining = text$1.replace(innermostReminder, "");
|
|
3745
|
+
while (remaining !== previous) {
|
|
3746
|
+
previous = remaining;
|
|
3747
|
+
remaining = remaining.replace(innermostReminder, "");
|
|
3748
|
+
}
|
|
3749
|
+
return remaining.trim();
|
|
3750
|
+
};
|
|
3751
|
+
const startsWithAny = (text$1, prefixes) => prefixes.some((prefix) => text$1.startsWith(prefix));
|
|
3752
|
+
const matchesAny = (text$1, prefixes, patterns) => startsWithAny(text$1, prefixes) || patterns.some((pattern) => pattern.test(text$1));
|
|
3753
|
+
const contentBlocks = (content) => {
|
|
3754
|
+
if (typeof content === "string") return [{
|
|
3755
|
+
type: "text",
|
|
3756
|
+
text: content
|
|
3757
|
+
}];
|
|
3758
|
+
return Array.isArray(content) ? content.filter(isRecord$4) : [];
|
|
3759
|
+
};
|
|
3760
|
+
const originatingTurn = (messages) => {
|
|
3761
|
+
const blocks = [];
|
|
3762
|
+
let sawUser = false;
|
|
3763
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
3764
|
+
const message = messages[index];
|
|
3765
|
+
if (!isRecord$4(message) || message.role === "system") continue;
|
|
3766
|
+
if (message.role !== "user") break;
|
|
3767
|
+
sawUser = true;
|
|
3768
|
+
blocks.unshift(...contentBlocks(message.content));
|
|
3769
|
+
}
|
|
3770
|
+
return sawUser ? blocks : void 0;
|
|
3771
|
+
};
|
|
3772
|
+
const blockOrigin = (block) => {
|
|
3773
|
+
if (block.type === "tool_result") return "agent";
|
|
3774
|
+
if (block.type === "image" || block.type === "document") return "user";
|
|
3775
|
+
if (block.type !== "text" || typeof block.text !== "string") return;
|
|
3776
|
+
const text$1 = withoutReminders(block.text);
|
|
3777
|
+
if (text$1 === "" || matchesAny(text$1, claudeCodeContext, claudeCodeContextPatterns)) return;
|
|
3778
|
+
if (startsWithAny(text$1, personDeliveries)) return "user";
|
|
3779
|
+
if (text$1.includes(compactionGuard) && text$1.includes(compactionInstruction) || matchesAny(text$1, claudeCodeTriggers, claudeCodeTriggerPatterns)) return "agent";
|
|
3780
|
+
return "user";
|
|
3781
|
+
};
|
|
3782
|
+
const isStatusProbe = (body) => {
|
|
3783
|
+
if (Object.keys(body).sort().join(",") !== "max_tokens,messages,model" || body.max_tokens !== statusProbeMaxTokens) return false;
|
|
3784
|
+
const messages = body.messages;
|
|
3785
|
+
return Array.isArray(messages) && messages.length === 1 && isRecord$4(messages[0]) && messages[0].role === "user" && messages[0].content === statusProbePrompt;
|
|
3786
|
+
};
|
|
3787
|
+
const systemTexts = (system) => contentBlocks(system).flatMap((block) => block.type === "text" && typeof block.text === "string" ? [block.text] : []);
|
|
3788
|
+
const getClaudeRequestInitiator = (body) => {
|
|
3789
|
+
if (!isRecord$4(body) || !Array.isArray(body.messages) || isStatusProbe(body)) return "agent";
|
|
3790
|
+
const payload = body;
|
|
3791
|
+
if (readBillingLineField(payload, "cc_is_subagent") === "true" || readBillingLineField(payload, "cc_workload") === "cron") return "agent";
|
|
3792
|
+
if (systemTexts(removeBillingLine(payload).system).some((text$1) => startsWithAny(text$1.trimStart(), ownRequestSystemPrompts))) return "agent";
|
|
3793
|
+
const turn = originatingTurn(body.messages);
|
|
3794
|
+
if (turn === void 0) return "agent";
|
|
3795
|
+
for (let index = turn.length - 1; index >= 0; index--) {
|
|
3796
|
+
const origin = blockOrigin(turn[index]);
|
|
3797
|
+
if (origin !== void 0) return origin;
|
|
3798
|
+
}
|
|
3799
|
+
return "agent";
|
|
3800
|
+
};
|
|
3714
3801
|
|
|
3715
3802
|
//#endregion
|
|
3716
3803
|
//#region src/claude/utils.ts
|
|
@@ -3741,7 +3828,7 @@ function mapOpenAIStopReasonToClaude(finishReason) {
|
|
|
3741
3828
|
}
|
|
3742
3829
|
/**
|
|
3743
3830
|
* Upstream sends an empty `arguments` string for tools that take no
|
|
3744
|
-
* parameters
|
|
3831
|
+
* parameters. Blank text is the empty object; anything else must be a
|
|
3745
3832
|
* JSON object, and a failure names the tool rather than surfacing a bare
|
|
3746
3833
|
* SyntaxError. Argument text is never echoed: it can carry user data.
|
|
3747
3834
|
*/
|
|
@@ -5690,7 +5777,6 @@ const normalizeFinalAssistantPrefill = (payload) => {
|
|
|
5690
5777
|
messages
|
|
5691
5778
|
};
|
|
5692
5779
|
};
|
|
5693
|
-
const isAgentInitiator = (messages) => messages.some((message) => message.role === "assistant" || message.role === "tool") ? "agent" : "user";
|
|
5694
5780
|
const holdsImage = (message) => Array.isArray(message.content) && message.content.some((part) => part.type === "image_url");
|
|
5695
5781
|
const messagesIncludeImage = (messages) => messages.some(holdsImage);
|
|
5696
5782
|
const isToolImageMessage = (message) => message.role === "tool" && holdsImage(message);
|
|
@@ -5767,7 +5853,7 @@ const createChatCompletions = async (config, payload, options = {}) => {
|
|
|
5767
5853
|
const compatiblePayload = useResponsesApi ? upstreamPayload : normalizeFinalAssistantPrefill(moveToolImagesToUserMessages(upstreamPayload));
|
|
5768
5854
|
const provider = getCopilotProviderContext(config);
|
|
5769
5855
|
const enableVision = messagesIncludeImage(compatiblePayload.messages);
|
|
5770
|
-
const initiator =
|
|
5856
|
+
const initiator = options.initiator ?? "agent";
|
|
5771
5857
|
const requestPayload = buildRequestPayload(compatiblePayload);
|
|
5772
5858
|
const effectiveEffortLabel = requestPayload.reasoning_effort ?? "omitted";
|
|
5773
5859
|
log.info(sanitizeTerminalString([
|
|
@@ -5778,7 +5864,8 @@ const createChatCompletions = async (config, payload, options = {}) => {
|
|
|
5778
5864
|
`upstream_model=${compatiblePayload.model}`,
|
|
5779
5865
|
`requested_think_effort=${requestedThinkEffort}`,
|
|
5780
5866
|
`requested_thinking=${requestedThinking}`,
|
|
5781
|
-
`effective_think_effort=${effectiveEffortLabel}
|
|
5867
|
+
`effective_think_effort=${effectiveEffortLabel}`,
|
|
5868
|
+
`initiator=${initiator}`
|
|
5782
5869
|
].join(" ")));
|
|
5783
5870
|
if (useResponsesApi) return completeResponse(await createResponses(provider, compatiblePayload, {
|
|
5784
5871
|
vision: enableVision,
|
|
@@ -6012,7 +6099,8 @@ async function createNativeMessages(config, payload, options) {
|
|
|
6012
6099
|
const beta = options.headers?.get("anthropic-beta");
|
|
6013
6100
|
if (beta) headers["anthropic-beta"] = beta;
|
|
6014
6101
|
const effortLabel = effort === void 0 ? "omitted" : getClaudeTurnEffort(body).effective;
|
|
6015
|
-
|
|
6102
|
+
const initiator = options.initiator ?? "agent";
|
|
6103
|
+
log.info(sanitizeTerminalString(`request_id=${options.requestId} Model request client=claude requested_model=${payload.model} upstream_model=${payload.model} upstream_api=messages effective_think_effort=${effortLabel} initiator=${initiator}`));
|
|
6016
6104
|
const response = await fetchCopilot(getCopilotProviderContext(config), "/v1/messages", {
|
|
6017
6105
|
method: "POST",
|
|
6018
6106
|
headers,
|
|
@@ -6021,7 +6109,7 @@ async function createNativeMessages(config, payload, options) {
|
|
|
6021
6109
|
requestId: options.requestId,
|
|
6022
6110
|
signal: options.signal,
|
|
6023
6111
|
timeoutMs: config.upstreamTimeoutMs,
|
|
6024
|
-
initiator
|
|
6112
|
+
initiator
|
|
6025
6113
|
});
|
|
6026
6114
|
if (!response.ok) {
|
|
6027
6115
|
const detail = await readCopilotText(response, options.signal, config.upstreamTimeoutMs);
|
|
@@ -6294,7 +6382,8 @@ async function handleNativeMessages(config, payload, options, write) {
|
|
|
6294
6382
|
]
|
|
6295
6383
|
}, {
|
|
6296
6384
|
...options,
|
|
6297
|
-
signal
|
|
6385
|
+
signal,
|
|
6386
|
+
initiator: "agent"
|
|
6298
6387
|
});
|
|
6299
6388
|
const offset = combined.length;
|
|
6300
6389
|
let finalHeldFrom;
|
|
@@ -6497,13 +6586,14 @@ const translationEndpoint = (config, model) => {
|
|
|
6497
6586
|
const selection = selectCopilotEndpoint(config, normalizeCopilotModelId(model));
|
|
6498
6587
|
return selection.endpoint && !selection.responsesFallback ? selection.endpoint : void 0;
|
|
6499
6588
|
};
|
|
6500
|
-
const handleClaudeMessageRequest = async (config, claudePayload, requestSignal, requestId, writeEvent, requestHeaders) => {
|
|
6589
|
+
const handleClaudeMessageRequest = async (config, claudePayload, initiator, requestSignal, requestId, writeEvent, requestHeaders) => {
|
|
6501
6590
|
const upstreamModel = translateModelName(claudePayload.model);
|
|
6502
6591
|
if (shouldUseNativeMessages(config, upstreamModel)) return handleNativeMessages(config, {
|
|
6503
6592
|
...claudePayload,
|
|
6504
6593
|
model: upstreamModel
|
|
6505
6594
|
}, {
|
|
6506
6595
|
requestId,
|
|
6596
|
+
initiator,
|
|
6507
6597
|
signal: requestSignal,
|
|
6508
6598
|
headers: requestHeaders
|
|
6509
6599
|
}, writeEvent);
|
|
@@ -6515,6 +6605,7 @@ const handleClaudeMessageRequest = async (config, claudePayload, requestSignal,
|
|
|
6515
6605
|
if (shouldLetModelDecideWebSearch && !canStreamWebSearchDecision) openAIPayload.stream = false;
|
|
6516
6606
|
const response = await createChatCompletions(config, openAIPayload, {
|
|
6517
6607
|
client: "claude",
|
|
6608
|
+
initiator,
|
|
6518
6609
|
requestedModel: claudePayload.model,
|
|
6519
6610
|
requestedThinkEffort: getClaudeRequestedThinkEffort(claudePayload),
|
|
6520
6611
|
requestedThinking: getClaudeRequestedThinking(claudePayload),
|
|
@@ -6588,6 +6679,7 @@ const handleClaudeMessageRequest = async (config, claudePayload, requestSignal,
|
|
|
6588
6679
|
else {
|
|
6589
6680
|
const finalResponse = await createChatCompletions(config, createFinalWebSearchPayload(openAIPayload, search, toolNameMapper), {
|
|
6590
6681
|
client: "claude",
|
|
6682
|
+
initiator: "agent",
|
|
6591
6683
|
requestedModel: claudePayload.model,
|
|
6592
6684
|
requestedThinkEffort: getClaudeRequestedThinkEffort(claudePayload),
|
|
6593
6685
|
requestedThinking: getClaudeRequestedThinking(claudePayload),
|
|
@@ -6665,7 +6757,9 @@ claudeRoutes.get("/models", (c) => c.json({
|
|
|
6665
6757
|
claudeRoutes.post("/messages", async (c) => {
|
|
6666
6758
|
const config = c.get("config");
|
|
6667
6759
|
const requestId = c.get("requestId");
|
|
6668
|
-
const
|
|
6760
|
+
const receivedPayload = await c.req.json();
|
|
6761
|
+
const initiator = c.env?.relayProbe === true ? "agent" : getClaudeRequestInitiator(receivedPayload);
|
|
6762
|
+
const claudePayload = removeBillingLine(receivedPayload);
|
|
6669
6763
|
const requestSignal = createCopilotRequestSignal(c.req.raw.signal, config.upstreamTimeoutMs);
|
|
6670
6764
|
try {
|
|
6671
6765
|
getRequestReasoningEffort(claudePayload);
|
|
@@ -6716,7 +6810,7 @@ claudeRoutes.post("/messages", async (c) => {
|
|
|
6716
6810
|
data: JSON.stringify(event)
|
|
6717
6811
|
}));
|
|
6718
6812
|
try {
|
|
6719
|
-
await handleClaudeMessageRequest(config, claudePayload, requestSignal, requestId, writeEvent, c.req.raw.headers);
|
|
6813
|
+
await handleClaudeMessageRequest(config, claudePayload, initiator, requestSignal, requestId, writeEvent, c.req.raw.headers);
|
|
6720
6814
|
} catch (error) {
|
|
6721
6815
|
log.error(`request_id=${requestId} Error during Claude stream request:`, error);
|
|
6722
6816
|
if (error instanceof UpstreamToolInputError) trace?.recordFailure("invalid-tool-input");
|
|
@@ -6730,7 +6824,7 @@ claudeRoutes.post("/messages", async (c) => {
|
|
|
6730
6824
|
});
|
|
6731
6825
|
}
|
|
6732
6826
|
try {
|
|
6733
|
-
return c.json(await handleClaudeMessageRequest(config, claudePayload, requestSignal, requestId, void 0, c.req.raw.headers));
|
|
6827
|
+
return c.json(await handleClaudeMessageRequest(config, claudePayload, initiator, requestSignal, requestId, void 0, c.req.raw.headers));
|
|
6734
6828
|
} catch (error) {
|
|
6735
6829
|
if (error instanceof ProxyNotImplementedError) {
|
|
6736
6830
|
c.set("requestErrorMessage", error.message);
|
|
@@ -7140,7 +7234,7 @@ async function probeModels(config, entries, options) {
|
|
|
7140
7234
|
content: "Reply with OK only."
|
|
7141
7235
|
}]
|
|
7142
7236
|
})
|
|
7143
|
-
}));
|
|
7237
|
+
}), { relayProbe: true });
|
|
7144
7238
|
return {
|
|
7145
7239
|
response,
|
|
7146
7240
|
body: await response.json().catch(() => void 0)
|
|
@@ -8820,7 +8914,7 @@ const restart = defineCommand({
|
|
|
8820
8914
|
* for different responses - restart the service versus re-authenticate.
|
|
8821
8915
|
*
|
|
8822
8916
|
* A failed health probe is a 2, not a 0. Printing FAILED while exiting 0 would
|
|
8823
|
-
* make every scripted caller treat an unreachable relay as fine.
|
|
8917
|
+
* make every scripted caller treat an unreachable relay as fine.
|
|
8824
8918
|
*/
|
|
8825
8919
|
const exitCodes = {
|
|
8826
8920
|
notRunning: 1,
|
|
@@ -8852,9 +8946,9 @@ const hasVersionMismatch = (status$1) => status$1.running && status$1.daemonVers
|
|
|
8852
8946
|
*
|
|
8853
8947
|
* 0 requires a live process *and* a passing health probe. Printing FAILED while
|
|
8854
8948
|
* exiting 0 would make every scripted caller treat an unreachable relay as
|
|
8855
|
-
* fine.
|
|
8949
|
+
* fine.
|
|
8856
8950
|
*
|
|
8857
|
-
* A version mismatch stays 0 on purpose
|
|
8951
|
+
* A version mismatch stays 0 on purpose. The relay works; it is just not
|
|
8858
8952
|
* the build that was installed. Mapping it to 2 would overstate it and break
|
|
8859
8953
|
* every scripted caller that treats non-zero as broken. It is surfaced in the
|
|
8860
8954
|
* text instead.
|
|
@@ -8994,7 +9088,7 @@ const describeError = (error) => {
|
|
|
8994
9088
|
* The version rides along on a probe `status` already makes, so asking what is
|
|
8995
9089
|
* running costs nothing extra. Absent for a daemon older than v0.3.1, which is
|
|
8996
9090
|
* itself informative, so it stays optional rather than defaulting to the
|
|
8997
|
-
* caller's version - that substitution is the bug.
|
|
9091
|
+
* caller's version - that substitution is the bug.
|
|
8998
9092
|
*/
|
|
8999
9093
|
const checkHealth = async (baseUrl) => {
|
|
9000
9094
|
try {
|
|
@@ -9037,9 +9131,9 @@ const checkDeep = async (baseUrl, model, apiKey) => {
|
|
|
9037
9131
|
try {
|
|
9038
9132
|
const result = await probe(`${baseUrl}/v1/messages`, {
|
|
9039
9133
|
body: JSON.stringify({
|
|
9040
|
-
max_tokens:
|
|
9134
|
+
max_tokens: statusProbeMaxTokens,
|
|
9041
9135
|
messages: [{
|
|
9042
|
-
content:
|
|
9136
|
+
content: statusProbePrompt,
|
|
9043
9137
|
role: "user"
|
|
9044
9138
|
}],
|
|
9045
9139
|
model
|