@dianshuv/copilot-api 0.19.0 → 0.20.0
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/dist/main.mjs +507 -995
- package/package.json +1 -1
package/dist/main.mjs
CHANGED
|
@@ -143,7 +143,6 @@ const state = {
|
|
|
143
143
|
allowTokenEndpoint: false,
|
|
144
144
|
autoTruncate: true,
|
|
145
145
|
compressToolResults: false,
|
|
146
|
-
redirectAnthropic: false,
|
|
147
146
|
stripServerTools: false,
|
|
148
147
|
contextEditingMode: "off",
|
|
149
148
|
normalizeResponsesCallIds: true,
|
|
@@ -1012,7 +1011,7 @@ const logout = defineCommand({
|
|
|
1012
1011
|
|
|
1013
1012
|
//#endregion
|
|
1014
1013
|
//#region package.json
|
|
1015
|
-
var version = "0.
|
|
1014
|
+
var version = "0.20.0";
|
|
1016
1015
|
|
|
1017
1016
|
//#endregion
|
|
1018
1017
|
//#region src/lib/event-loop-lag.ts
|
|
@@ -3567,9 +3566,8 @@ async function autoTruncateOpenAI(payload, model, config = {}) {
|
|
|
3567
3566
|
*
|
|
3568
3567
|
* Pre-flight steps for any request whose final payload is an OpenAI
|
|
3569
3568
|
* ChatCompletionsPayload — auto-truncate decisions, 413 diagnostic logging,
|
|
3570
|
-
* and the non-streaming type guard. Used by
|
|
3571
|
-
* (native OpenAI)
|
|
3572
|
-
* gets translated into OpenAI shape before hitting upstream).
|
|
3569
|
+
* and the non-streaming type guard. Used by `routes/chat-completions`
|
|
3570
|
+
* (native OpenAI).
|
|
3573
3571
|
*/
|
|
3574
3572
|
/** Type guard for non-streaming responses */
|
|
3575
3573
|
function isNonStreaming(response) {
|
|
@@ -3777,89 +3775,6 @@ function createStreamRepetitionChecker(label, config) {
|
|
|
3777
3775
|
};
|
|
3778
3776
|
}
|
|
3779
3777
|
|
|
3780
|
-
//#endregion
|
|
3781
|
-
//#region src/lib/anthropic/beta.ts
|
|
3782
|
-
/**
|
|
3783
|
-
* Vendor-neutral utilities for manipulating the `anthropic-beta` request header.
|
|
3784
|
-
*
|
|
3785
|
-
* Lives in `lib/anthropic/` (not in either transport module) so both the
|
|
3786
|
-
* Anthropic-native and OpenAI-translated transport layers can share these
|
|
3787
|
-
* helpers without introducing cross-transport imports.
|
|
3788
|
-
*/
|
|
3789
|
-
/** Anthropic beta feature that unlocks the 1M context window. */
|
|
3790
|
-
const CONTEXT_1M_BETA_FEATURE = "context-1m-2025-08-07";
|
|
3791
|
-
/**
|
|
3792
|
-
* Merge two comma-separated anthropic-beta header values. Trims whitespace,
|
|
3793
|
-
* drops empty tokens, and dedupes by exact string match. Returns a canonical
|
|
3794
|
-
* comma-joined string with no spaces.
|
|
3795
|
-
*
|
|
3796
|
-
* Either input may be undefined / empty.
|
|
3797
|
-
*/
|
|
3798
|
-
function mergeBetaFeatures(existing, incoming) {
|
|
3799
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3800
|
-
const out = [];
|
|
3801
|
-
for (const raw of [existing, incoming]) {
|
|
3802
|
-
if (!raw) continue;
|
|
3803
|
-
for (const part of raw.split(",")) {
|
|
3804
|
-
const f = part.trim();
|
|
3805
|
-
if (f.length === 0 || seen.has(f)) continue;
|
|
3806
|
-
seen.add(f);
|
|
3807
|
-
out.push(f);
|
|
3808
|
-
}
|
|
3809
|
-
}
|
|
3810
|
-
return out.join(",");
|
|
3811
|
-
}
|
|
3812
|
-
/**
|
|
3813
|
-
* Append the context-1m feature to an anthropic-beta header value, deduping
|
|
3814
|
-
* any prior occurrence. Returns the merged comma-separated string.
|
|
3815
|
-
*/
|
|
3816
|
-
function appendContext1mBeta(existing) {
|
|
3817
|
-
return mergeBetaFeatures(existing, CONTEXT_1M_BETA_FEATURE);
|
|
3818
|
-
}
|
|
3819
|
-
/**
|
|
3820
|
-
* True iff a model id appears to be the suffixed 1M-context variant of an
|
|
3821
|
-
* Anthropic Claude model (e.g. claude-opus-4-8-1m, claude-opus-4.6-1m).
|
|
3822
|
-
*
|
|
3823
|
-
* Used as a state.models-independent signal for whether to inject the
|
|
3824
|
-
* context-1m-2025-08-07 beta header, so the 1M intent survives a stale or
|
|
3825
|
-
* empty model cache (where `resolveAnthropicModelForDirectPath` would return
|
|
3826
|
-
* undefined). Forwarding the beta is harmless to upstreams that ignore it.
|
|
3827
|
-
*/
|
|
3828
|
-
function isOneMillionSuffixedClaudeId(modelId) {
|
|
3829
|
-
return modelId.startsWith("claude-") && modelId.endsWith("-1m");
|
|
3830
|
-
}
|
|
3831
|
-
|
|
3832
|
-
//#endregion
|
|
3833
|
-
//#region src/lib/headers.ts
|
|
3834
|
-
/**
|
|
3835
|
-
* Vendor-neutral header-bag helpers.
|
|
3836
|
-
*
|
|
3837
|
-
* HTTP header names are case-insensitive, but a plain-object header bag is
|
|
3838
|
-
* case-sensitive on its keys. Code that wants to look up "anthropic-beta"
|
|
3839
|
-
* without knowing whether some other producer wrote "Anthropic-Beta" needs
|
|
3840
|
-
* `findHeaderKey`. Code that wants to set a header without creating a
|
|
3841
|
-
* second case variant of the same name needs `setHeader`.
|
|
3842
|
-
*/
|
|
3843
|
-
/** Case-insensitive lookup of a header key in a plain-object header bag. */
|
|
3844
|
-
function findHeaderKey(headers, name) {
|
|
3845
|
-
const lower = name.toLowerCase();
|
|
3846
|
-
return Object.keys(headers).find((k) => k.toLowerCase() === lower);
|
|
3847
|
-
}
|
|
3848
|
-
/** Case-insensitive read of a header value. */
|
|
3849
|
-
function getHeader(headers, name) {
|
|
3850
|
-
const key = findHeaderKey(headers, name);
|
|
3851
|
-
return key === void 0 ? void 0 : headers[key];
|
|
3852
|
-
}
|
|
3853
|
-
/**
|
|
3854
|
-
* Set a header value at the existing case variant if one is present, else at
|
|
3855
|
-
* the supplied canonical name. Prevents a second key (different case) from
|
|
3856
|
-
* being added for the same logical header.
|
|
3857
|
-
*/
|
|
3858
|
-
function setHeader(headers, name, value) {
|
|
3859
|
-
const key = findHeaderKey(headers, name) ?? name;
|
|
3860
|
-
headers[key] = value;
|
|
3861
|
-
}
|
|
3862
|
-
|
|
3863
3778
|
//#endregion
|
|
3864
3779
|
//#region src/services/copilot/create-chat-completions.ts
|
|
3865
3780
|
const GPT_MODEL_PATTERN = /^gpt-/i;
|
|
@@ -3880,21 +3795,15 @@ const createChatCompletions = async (payload, options) => {
|
|
|
3880
3795
|
const enableVision = wire.messages.some((x) => typeof x.content !== "string" && x.content?.some((x) => x.type === "image_url"));
|
|
3881
3796
|
const isAgentCall = wire.messages.some((msg) => ["assistant", "tool"].includes(msg.role));
|
|
3882
3797
|
const modelSupportsVision = options?.resolvedModel?.capabilities?.supports?.vision !== false;
|
|
3883
|
-
const headers = {
|
|
3884
|
-
...copilotHeaders(state, {
|
|
3885
|
-
vision: enableVision && modelSupportsVision,
|
|
3886
|
-
modelRequestHeaders: options?.resolvedModel?.request_headers
|
|
3887
|
-
}),
|
|
3888
|
-
"X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user")
|
|
3889
|
-
};
|
|
3890
|
-
if (options?.anthropicBeta) {
|
|
3891
|
-
const existingKey = findHeaderKey(headers, "anthropic-beta") ?? "anthropic-beta";
|
|
3892
|
-
headers[existingKey] = mergeBetaFeatures(headers[existingKey], options.anthropicBeta);
|
|
3893
|
-
consola.debug(`[ChatCompletions] anthropic-beta after merge: ${headers[existingKey]}`);
|
|
3894
|
-
}
|
|
3895
3798
|
const response = await copilotFetch("/chat/completions", {
|
|
3896
3799
|
method: "POST",
|
|
3897
|
-
headers
|
|
3800
|
+
headers: {
|
|
3801
|
+
...copilotHeaders(state, {
|
|
3802
|
+
vision: enableVision && modelSupportsVision,
|
|
3803
|
+
modelRequestHeaders: options?.resolvedModel?.request_headers
|
|
3804
|
+
}),
|
|
3805
|
+
"X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user")
|
|
3806
|
+
},
|
|
3898
3807
|
body: JSON.stringify(wire),
|
|
3899
3808
|
signal: options?.signal
|
|
3900
3809
|
});
|
|
@@ -4347,12 +4256,12 @@ async function executeRequest(opts) {
|
|
|
4347
4256
|
signal: abort.signal
|
|
4348
4257
|
}));
|
|
4349
4258
|
ctx.queueWaitMs = queueWaitMs;
|
|
4350
|
-
if (isNonStreaming(response)) return handleNonStreamingResponse
|
|
4259
|
+
if (isNonStreaming(response)) return handleNonStreamingResponse(c, response, ctx, payload);
|
|
4351
4260
|
consola.debug("Streaming response");
|
|
4352
4261
|
updateTrackerStatus(ctx.trackingId, "streaming");
|
|
4353
4262
|
return streamSSE(c, async (stream) => {
|
|
4354
4263
|
stream.onAbort(() => abort.abort());
|
|
4355
|
-
await handleStreamingResponse
|
|
4264
|
+
await handleStreamingResponse({
|
|
4356
4265
|
stream,
|
|
4357
4266
|
response,
|
|
4358
4267
|
payload,
|
|
@@ -4381,7 +4290,7 @@ async function logTokenCount(payload, selectedModel) {
|
|
|
4381
4290
|
consola.debug("Failed to calculate token count:", error);
|
|
4382
4291
|
}
|
|
4383
4292
|
}
|
|
4384
|
-
function handleNonStreamingResponse
|
|
4293
|
+
function handleNonStreamingResponse(c, originalResponse, ctx, payload) {
|
|
4385
4294
|
consola.debug("Non-streaming response:", JSON.stringify(originalResponse));
|
|
4386
4295
|
let response = originalResponse;
|
|
4387
4296
|
if (state.verbose && ctx.truncateResult?.wasCompacted && response.choices[0]?.message.content) {
|
|
@@ -4474,7 +4383,7 @@ function createStreamAccumulator() {
|
|
|
4474
4383
|
toolCallMap: /* @__PURE__ */ new Map()
|
|
4475
4384
|
};
|
|
4476
4385
|
}
|
|
4477
|
-
async function handleStreamingResponse
|
|
4386
|
+
async function handleStreamingResponse(opts) {
|
|
4478
4387
|
const { stream, response, payload, ctx } = opts;
|
|
4479
4388
|
const acc = createStreamAccumulator();
|
|
4480
4389
|
const checkRepetition = createStreamRepetitionChecker(`openai:${payload.model}`);
|
|
@@ -6608,6 +6517,46 @@ async function checkNeedsCompactionAnthropic(payload, model, config = {}) {
|
|
|
6608
6517
|
};
|
|
6609
6518
|
}
|
|
6610
6519
|
|
|
6520
|
+
//#endregion
|
|
6521
|
+
//#region src/lib/anthropic/beta.ts
|
|
6522
|
+
/**
|
|
6523
|
+
* Vendor-neutral utilities for manipulating the `anthropic-beta` request header.
|
|
6524
|
+
*
|
|
6525
|
+
* Lives in `lib/anthropic/` (not in either transport module) so both the
|
|
6526
|
+
* Anthropic-native and OpenAI-translated transport layers can share these
|
|
6527
|
+
* helpers without introducing cross-transport imports.
|
|
6528
|
+
*/
|
|
6529
|
+
/** Anthropic beta feature that unlocks the 1M context window. */
|
|
6530
|
+
const CONTEXT_1M_BETA_FEATURE = "context-1m-2025-08-07";
|
|
6531
|
+
/**
|
|
6532
|
+
* Merge two comma-separated anthropic-beta header values. Trims whitespace,
|
|
6533
|
+
* drops empty tokens, and dedupes by exact string match. Returns a canonical
|
|
6534
|
+
* comma-joined string with no spaces.
|
|
6535
|
+
*
|
|
6536
|
+
* Either input may be undefined / empty.
|
|
6537
|
+
*/
|
|
6538
|
+
function mergeBetaFeatures(existing, incoming) {
|
|
6539
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6540
|
+
const out = [];
|
|
6541
|
+
for (const raw of [existing, incoming]) {
|
|
6542
|
+
if (!raw) continue;
|
|
6543
|
+
for (const part of raw.split(",")) {
|
|
6544
|
+
const f = part.trim();
|
|
6545
|
+
if (f.length === 0 || seen.has(f)) continue;
|
|
6546
|
+
seen.add(f);
|
|
6547
|
+
out.push(f);
|
|
6548
|
+
}
|
|
6549
|
+
}
|
|
6550
|
+
return out.join(",");
|
|
6551
|
+
}
|
|
6552
|
+
/**
|
|
6553
|
+
* Append the context-1m feature to an anthropic-beta header value, deduping
|
|
6554
|
+
* any prior occurrence. Returns the merged comma-separated string.
|
|
6555
|
+
*/
|
|
6556
|
+
function appendContext1mBeta(existing) {
|
|
6557
|
+
return mergeBetaFeatures(existing, CONTEXT_1M_BETA_FEATURE);
|
|
6558
|
+
}
|
|
6559
|
+
|
|
6611
6560
|
//#endregion
|
|
6612
6561
|
//#region src/lib/anthropic/features.ts
|
|
6613
6562
|
function normalizeForMatching(modelId) {
|
|
@@ -6758,6 +6707,37 @@ function filterServerToolBlocksFromResponse(response) {
|
|
|
6758
6707
|
};
|
|
6759
6708
|
}
|
|
6760
6709
|
|
|
6710
|
+
//#endregion
|
|
6711
|
+
//#region src/lib/headers.ts
|
|
6712
|
+
/**
|
|
6713
|
+
* Vendor-neutral header-bag helpers.
|
|
6714
|
+
*
|
|
6715
|
+
* HTTP header names are case-insensitive, but a plain-object header bag is
|
|
6716
|
+
* case-sensitive on its keys. Code that wants to look up "anthropic-beta"
|
|
6717
|
+
* without knowing whether some other producer wrote "Anthropic-Beta" needs
|
|
6718
|
+
* `findHeaderKey`. Code that wants to set a header without creating a
|
|
6719
|
+
* second case variant of the same name needs `setHeader`.
|
|
6720
|
+
*/
|
|
6721
|
+
/** Case-insensitive lookup of a header key in a plain-object header bag. */
|
|
6722
|
+
function findHeaderKey(headers, name) {
|
|
6723
|
+
const lower = name.toLowerCase();
|
|
6724
|
+
return Object.keys(headers).find((k) => k.toLowerCase() === lower);
|
|
6725
|
+
}
|
|
6726
|
+
/** Case-insensitive read of a header value. */
|
|
6727
|
+
function getHeader(headers, name) {
|
|
6728
|
+
const key = findHeaderKey(headers, name);
|
|
6729
|
+
return key === void 0 ? void 0 : headers[key];
|
|
6730
|
+
}
|
|
6731
|
+
/**
|
|
6732
|
+
* Set a header value at the existing case variant if one is present, else at
|
|
6733
|
+
* the supplied canonical name. Prevents a second key (different case) from
|
|
6734
|
+
* being added for the same logical header.
|
|
6735
|
+
*/
|
|
6736
|
+
function setHeader(headers, name, value) {
|
|
6737
|
+
const key = findHeaderKey(headers, name) ?? name;
|
|
6738
|
+
headers[key] = value;
|
|
6739
|
+
}
|
|
6740
|
+
|
|
6761
6741
|
//#endregion
|
|
6762
6742
|
//#region src/services/copilot/create-anthropic-messages.ts
|
|
6763
6743
|
/**
|
|
@@ -6960,11 +6940,12 @@ function resolveAnthropicModelForDirectPath(modelId) {
|
|
|
6960
6940
|
}
|
|
6961
6941
|
}
|
|
6962
6942
|
/**
|
|
6963
|
-
* Check if a model supports direct Anthropic API.
|
|
6964
|
-
*
|
|
6943
|
+
* Check if a model supports the native direct Anthropic API on Copilot.
|
|
6944
|
+
* True iff the model resolves to an Anthropic-vendor model (see
|
|
6945
|
+
* resolveAnthropicModelForDirectPath). `/v1/messages` serves these only;
|
|
6946
|
+
* the OpenAI-translation fallback was removed (docs/adr/0004-...).
|
|
6965
6947
|
*/
|
|
6966
6948
|
function supportsDirectAnthropicApi(modelId) {
|
|
6967
|
-
if (state.redirectAnthropic) return false;
|
|
6968
6949
|
return resolveAnthropicModelForDirectPath(modelId) !== void 0;
|
|
6969
6950
|
}
|
|
6970
6951
|
|
|
@@ -7161,15 +7142,6 @@ function extractToolCallsFromContent(content) {
|
|
|
7161
7142
|
});
|
|
7162
7143
|
return tools.length > 0 ? tools : void 0;
|
|
7163
7144
|
}
|
|
7164
|
-
function mapOpenAIStopReasonToAnthropic(finishReason) {
|
|
7165
|
-
if (finishReason === null) return null;
|
|
7166
|
-
return {
|
|
7167
|
-
stop: "end_turn",
|
|
7168
|
-
length: "max_tokens",
|
|
7169
|
-
tool_calls: "tool_use",
|
|
7170
|
-
content_filter: "end_turn"
|
|
7171
|
-
}[finishReason];
|
|
7172
|
-
}
|
|
7173
7145
|
function prependMarkerToResponse(response, marker) {
|
|
7174
7146
|
if (!marker) return response;
|
|
7175
7147
|
const content = [...response.content];
|
|
@@ -7290,555 +7262,79 @@ function recordAnthropicStreamingResponse(acc, fallbackModel, ctx) {
|
|
|
7290
7262
|
}
|
|
7291
7263
|
|
|
7292
7264
|
//#endregion
|
|
7293
|
-
//#region src/routes/messages/
|
|
7294
|
-
const OPENAI_TOOL_NAME_LIMIT = 64;
|
|
7265
|
+
//#region src/routes/messages/stream-translation.ts
|
|
7295
7266
|
/**
|
|
7296
|
-
*
|
|
7297
|
-
* This handles edge cases where conversation history may be incomplete:
|
|
7298
|
-
* - Session interruptions where tool execution was cut off
|
|
7299
|
-
* - Previous request failures
|
|
7300
|
-
* - Client sending truncated history
|
|
7267
|
+
* Wrap an arbitrary error into an Anthropic-native `error` stream event.
|
|
7301
7268
|
*
|
|
7302
|
-
*
|
|
7269
|
+
* Shared by the direct Anthropic path (`direct-anthropic-handler.ts`) to emit a
|
|
7270
|
+
* client-facing error frame mid-stream. The OpenAI→Anthropic response
|
|
7271
|
+
* translation that once also lived here was removed with the translation
|
|
7272
|
+
* fallback (see docs/adr/0004-drop-openai-translation-fallback-for-messages.md).
|
|
7303
7273
|
*/
|
|
7304
|
-
function
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
const foundToolResponses = /* @__PURE__ */ new Set();
|
|
7311
|
-
let j = i + 1;
|
|
7312
|
-
while (j < messages.length && messages[j].role === "tool") {
|
|
7313
|
-
const toolMessage = messages[j];
|
|
7314
|
-
if (toolMessage.tool_call_id) foundToolResponses.add(toolMessage.tool_call_id);
|
|
7315
|
-
j++;
|
|
7316
|
-
}
|
|
7317
|
-
for (const toolCall of message.tool_calls) if (!foundToolResponses.has(toolCall.id)) {
|
|
7318
|
-
consola.debug(`Adding placeholder tool_result for ${toolCall.id}`);
|
|
7319
|
-
fixedMessages.push({
|
|
7320
|
-
role: "tool",
|
|
7321
|
-
tool_call_id: toolCall.id,
|
|
7322
|
-
content: "Tool execution was interrupted or failed."
|
|
7323
|
-
});
|
|
7324
|
-
}
|
|
7274
|
+
function translateErrorToAnthropicErrorEvent(error) {
|
|
7275
|
+
return {
|
|
7276
|
+
type: "error",
|
|
7277
|
+
error: {
|
|
7278
|
+
type: "api_error",
|
|
7279
|
+
message: error ? formatError(error) : "An unexpected error occurred during streaming."
|
|
7325
7280
|
}
|
|
7281
|
+
};
|
|
7282
|
+
}
|
|
7283
|
+
|
|
7284
|
+
//#endregion
|
|
7285
|
+
//#region src/routes/messages/tool-call-recovery.ts
|
|
7286
|
+
const ENVELOPE = String.raw`(?:<(?:antml:)?function_calls>|call|count|court)`;
|
|
7287
|
+
const INVOKE_BODY = String.raw`<(?:antml:)?invoke\s+name="[^"]+">(?:(?!<(?:antml:)?invoke\b)[\s\S])*?</(?:antml:)?invoke>`;
|
|
7288
|
+
const LEAKED_REGION_RE = new RegExp(String.raw`(?:^|\n)[ \t]*(?:` + ENVELOPE + String.raw`[ \t\n]*(?:` + INVOKE_BODY + String.raw`\s*)+(?:</(?:antml:)?function_calls>)?|` + INVOKE_BODY + String.raw`)`, "g");
|
|
7289
|
+
const STARTS_WITH_ENVELOPE_RE = new RegExp(String.raw`^[ \t\n]*` + ENVELOPE);
|
|
7290
|
+
const INVOKE_OPENER_RE = new RegExp(String.raw`(?:^|\n)[ \t]*(?:` + ENVELOPE + String.raw`[ \t\n]*)?<(?:antml:)?invoke\b[^\n>]*>?`, "g");
|
|
7291
|
+
const LEAK_OPEN_RE = new RegExp(String.raw`(?:^|\n)[ \t]*(?:` + ENVELOPE + String.raw`[ \t\n]*)?<(?:antml:)?invoke\s+name="`, "g");
|
|
7292
|
+
const INVOKE_RE = /<(?:antml:)?invoke\s+name="([^"]+)">([\s\S]*?)<\/(?:antml:)?invoke>/g;
|
|
7293
|
+
const PARAMETER_RE = /<(?:antml:)?parameter\s+name="([^"]+)">([\s\S]*?)<\/(?:antml:)?parameter>/g;
|
|
7294
|
+
function coerceParamValue(raw) {
|
|
7295
|
+
const trimmed = raw.trim();
|
|
7296
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) try {
|
|
7297
|
+
return JSON.parse(trimmed);
|
|
7298
|
+
} catch {
|
|
7299
|
+
return raw;
|
|
7326
7300
|
}
|
|
7327
|
-
return
|
|
7301
|
+
return raw;
|
|
7328
7302
|
}
|
|
7329
|
-
function
|
|
7330
|
-
const
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
};
|
|
7303
|
+
function parseRegionInvokes(region, knownTools) {
|
|
7304
|
+
const calls = [];
|
|
7305
|
+
for (const invokeMatch of region.matchAll(INVOKE_RE)) {
|
|
7306
|
+
const name = invokeMatch[1];
|
|
7307
|
+
if (knownTools && !knownTools.has(name)) continue;
|
|
7308
|
+
const input = {};
|
|
7309
|
+
for (const paramMatch of invokeMatch[2].matchAll(PARAMETER_RE)) input[paramMatch[1]] = coerceParamValue(paramMatch[2]);
|
|
7310
|
+
calls.push({
|
|
7311
|
+
name,
|
|
7312
|
+
input
|
|
7313
|
+
});
|
|
7314
|
+
}
|
|
7315
|
+
return calls;
|
|
7316
|
+
}
|
|
7317
|
+
const FENCE_DELIM_RE = /(?:^|\n)[ \t]*```/g;
|
|
7318
|
+
function insideFence(text, pos) {
|
|
7319
|
+
let openAt = -1;
|
|
7320
|
+
for (const m of text.matchAll(FENCE_DELIM_RE)) if (openAt === -1) {
|
|
7321
|
+
if (m.index >= pos) break;
|
|
7322
|
+
openAt = m.index;
|
|
7323
|
+
} else if (m.index > pos) return true;
|
|
7324
|
+
else openAt = -1;
|
|
7325
|
+
return false;
|
|
7326
|
+
}
|
|
7327
|
+
function regionIsLeak(region, offset, fullText, knownTools) {
|
|
7328
|
+
if (insideFence(fullText, offset)) return false;
|
|
7329
|
+
if (STARTS_WITH_ENVELOPE_RE.test(region)) return true;
|
|
7330
|
+
if (!knownTools) return false;
|
|
7331
|
+
return parseRegionInvokes(region, knownTools).length > 0;
|
|
7350
7332
|
}
|
|
7351
7333
|
/**
|
|
7352
|
-
*
|
|
7353
|
-
*
|
|
7354
|
-
* and
|
|
7355
|
-
*
|
|
7356
|
-
* @param familyPrefix - e.g., "claude-opus", "claude-sonnet", "claude-haiku"
|
|
7357
|
-
* @param fallback - fallback model ID if no match found
|
|
7358
|
-
*/
|
|
7359
|
-
function findLatestModel(familyPrefix, fallback) {
|
|
7360
|
-
const models = state.models?.data;
|
|
7361
|
-
if (!models || models.length === 0) return fallback;
|
|
7362
|
-
const candidates = models.filter((m) => m.id.startsWith(familyPrefix));
|
|
7363
|
-
if (candidates.length === 0) return fallback;
|
|
7364
|
-
candidates.sort((a, b) => {
|
|
7365
|
-
const [aMajor, aMinor] = extractVersion(a.id, familyPrefix);
|
|
7366
|
-
const [bMajor, bMinor] = extractVersion(b.id, familyPrefix);
|
|
7367
|
-
if (aMajor !== bMajor) return bMajor - aMajor;
|
|
7368
|
-
return bMinor - aMinor;
|
|
7369
|
-
});
|
|
7370
|
-
return candidates[0].id;
|
|
7371
|
-
}
|
|
7372
|
-
/**
|
|
7373
|
-
* Extract numeric [major, minor] version from a model id.
|
|
7374
|
-
*
|
|
7375
|
-
* Supports both naming conventions Anthropic/Copilot have used:
|
|
7376
|
-
* - dot: "claude-opus-4.5" → [4, 5]
|
|
7377
|
-
* - dash: "claude-opus-4-8" → [4, 8]
|
|
7378
|
-
* - dash double-digit: "claude-opus-4-10" → [4, 10]
|
|
7379
|
-
*
|
|
7380
|
-
* The dash form previously parsed as just the major via the regex
|
|
7381
|
-
* /^(\d+(?:\.\d+)?)/ (the dash stopped the match), which silently
|
|
7382
|
-
* downgraded dash-named candidates against any dot-named candidate in
|
|
7383
|
-
* findLatestModel. Parsing into a tuple also avoids the parseFloat
|
|
7384
|
-
* lossiness on double-digit minors ("4.10" → 4.1).
|
|
7385
|
-
*
|
|
7386
|
-
* Anything after the major/minor segment (date stamps, "-1m") is ignored.
|
|
7387
|
-
* The minor capture is bounded to 1-3 digits so that an 8-digit date suffix
|
|
7388
|
-
* directly after the major (e.g. "claude-opus-4-20250514") is NOT mistaken
|
|
7389
|
-
* for a minor version of 20_250_514 — without that bound, dated ids would
|
|
7390
|
-
* outrank legitimate dotted candidates like "claude-opus-4.8" in
|
|
7391
|
-
* findLatestModel's sort.
|
|
7392
|
-
*
|
|
7393
|
-
* Returns [0, 0] when no version can be extracted.
|
|
7394
|
-
*/
|
|
7395
|
-
function extractVersion(modelId, prefix) {
|
|
7396
|
-
const match = modelId.slice(prefix.length + 1).match(/^(\d+)(?:[.-](\d{1,3}))?/);
|
|
7397
|
-
if (!match) return [0, 0];
|
|
7398
|
-
const major = Number.parseInt(match[1], 10);
|
|
7399
|
-
const rawMinor = match[2];
|
|
7400
|
-
return [major, rawMinor === void 0 ? 0 : Number.parseInt(rawMinor, 10) || 0];
|
|
7401
|
-
}
|
|
7402
|
-
function translateModelName(model) {
|
|
7403
|
-
const aliasMap = {
|
|
7404
|
-
opus: "claude-opus",
|
|
7405
|
-
sonnet: "claude-sonnet",
|
|
7406
|
-
haiku: "claude-haiku"
|
|
7407
|
-
};
|
|
7408
|
-
if (aliasMap[model]) {
|
|
7409
|
-
const familyPrefix = aliasMap[model];
|
|
7410
|
-
return findLatestModel(familyPrefix, `${familyPrefix}-4.5`);
|
|
7411
|
-
}
|
|
7412
|
-
if (/^claude-sonnet-4-5-\d+$/.test(model)) return "claude-sonnet-4.5";
|
|
7413
|
-
if (/^claude-sonnet-4-\d+$/.test(model)) return "claude-sonnet-4";
|
|
7414
|
-
if (model === "claude-opus-4-8-1m") return "claude-opus-4.8";
|
|
7415
|
-
if (model === "claude-opus-4-8") return "claude-opus-4.8";
|
|
7416
|
-
if (model === "claude-opus-4-7-1m") return "claude-opus-4.7";
|
|
7417
|
-
if (/^claude-opus-4-7$/.test(model)) return "claude-opus-4.7";
|
|
7418
|
-
if (model === "claude-opus-4-6-1m") return "claude-opus-4.6-1m";
|
|
7419
|
-
if (/^claude-opus-4-6$/.test(model)) return "claude-opus-4.6";
|
|
7420
|
-
if (/^claude-opus-4-5-\d+$/.test(model)) return "claude-opus-4.5";
|
|
7421
|
-
if (/^claude-opus-4-\d+$/.test(model)) return findLatestModel("claude-opus", "claude-opus-4.5");
|
|
7422
|
-
if (/^claude-haiku-4-5-\d+$/.test(model)) return "claude-haiku-4.5";
|
|
7423
|
-
if (/^claude-haiku-3-5-\d+$/.test(model)) return findLatestModel("claude-haiku", "claude-haiku-4.5");
|
|
7424
|
-
return model;
|
|
7425
|
-
}
|
|
7426
|
-
function translateAnthropicMessagesToOpenAI(anthropicMessages, system, toolNameMapping) {
|
|
7427
|
-
const systemMessages = handleSystemPrompt(system);
|
|
7428
|
-
const otherMessages = anthropicMessages.flatMap((message) => message.role === "user" ? handleUserMessage(message) : handleAssistantMessage(message, toolNameMapping));
|
|
7429
|
-
return [...systemMessages, ...otherMessages];
|
|
7430
|
-
}
|
|
7431
|
-
const RESERVED_KEYWORDS = ["x-anthropic-billing-header", "x-anthropic-billing"];
|
|
7432
|
-
/**
|
|
7433
|
-
* Filter out reserved keywords from system prompt text.
|
|
7434
|
-
* Copilot API rejects requests containing these keywords.
|
|
7435
|
-
* Removes the entire line containing the keyword to keep the prompt clean.
|
|
7436
|
-
*/
|
|
7437
|
-
function filterReservedKeywords(text) {
|
|
7438
|
-
let filtered = text;
|
|
7439
|
-
for (const keyword of RESERVED_KEYWORDS) if (text.includes(keyword)) {
|
|
7440
|
-
consola.debug(`[Reserved Keyword] Removing line containing "${keyword}"`);
|
|
7441
|
-
filtered = filtered.split("\n").filter((line) => !line.includes(keyword)).join("\n");
|
|
7442
|
-
}
|
|
7443
|
-
return filtered;
|
|
7444
|
-
}
|
|
7445
|
-
function handleSystemPrompt(system) {
|
|
7446
|
-
if (!system) return [];
|
|
7447
|
-
if (typeof system === "string") return [{
|
|
7448
|
-
role: "system",
|
|
7449
|
-
content: filterReservedKeywords(system)
|
|
7450
|
-
}];
|
|
7451
|
-
else return [{
|
|
7452
|
-
role: "system",
|
|
7453
|
-
content: filterReservedKeywords(system.map((block) => block.text).join("\n\n"))
|
|
7454
|
-
}];
|
|
7455
|
-
}
|
|
7456
|
-
function handleUserMessage(message) {
|
|
7457
|
-
const newMessages = [];
|
|
7458
|
-
if (Array.isArray(message.content)) {
|
|
7459
|
-
const toolResultBlocks = message.content.filter((block) => block.type === "tool_result");
|
|
7460
|
-
const otherBlocks = message.content.filter((block) => block.type !== "tool_result");
|
|
7461
|
-
for (const block of toolResultBlocks) newMessages.push({
|
|
7462
|
-
role: "tool",
|
|
7463
|
-
tool_call_id: block.tool_use_id,
|
|
7464
|
-
content: mapContent(block.content)
|
|
7465
|
-
});
|
|
7466
|
-
if (otherBlocks.length > 0) newMessages.push({
|
|
7467
|
-
role: "user",
|
|
7468
|
-
content: mapContent(otherBlocks)
|
|
7469
|
-
});
|
|
7470
|
-
} else newMessages.push({
|
|
7471
|
-
role: "user",
|
|
7472
|
-
content: mapContent(message.content)
|
|
7473
|
-
});
|
|
7474
|
-
return newMessages;
|
|
7475
|
-
}
|
|
7476
|
-
function handleAssistantMessage(message, toolNameMapping) {
|
|
7477
|
-
if (!Array.isArray(message.content)) return [{
|
|
7478
|
-
role: "assistant",
|
|
7479
|
-
content: mapContent(message.content)
|
|
7480
|
-
}];
|
|
7481
|
-
const toolUseBlocks = message.content.filter((block) => block.type === "tool_use");
|
|
7482
|
-
const textBlocks = message.content.filter((block) => block.type === "text");
|
|
7483
|
-
const thinkingBlocks = message.content.filter((block) => block.type === "thinking");
|
|
7484
|
-
const allTextContent = [...textBlocks.map((b) => b.text), ...thinkingBlocks.map((b) => b.thinking)].join("\n\n");
|
|
7485
|
-
return toolUseBlocks.length > 0 ? [{
|
|
7486
|
-
role: "assistant",
|
|
7487
|
-
content: allTextContent || null,
|
|
7488
|
-
tool_calls: toolUseBlocks.map((toolUse) => ({
|
|
7489
|
-
id: toolUse.id,
|
|
7490
|
-
type: "function",
|
|
7491
|
-
function: {
|
|
7492
|
-
name: getTruncatedToolName(toolUse.name, toolNameMapping),
|
|
7493
|
-
arguments: JSON.stringify(toolUse.input)
|
|
7494
|
-
}
|
|
7495
|
-
}))
|
|
7496
|
-
}] : [{
|
|
7497
|
-
role: "assistant",
|
|
7498
|
-
content: mapContent(message.content)
|
|
7499
|
-
}];
|
|
7500
|
-
}
|
|
7501
|
-
function mapContent(content) {
|
|
7502
|
-
if (typeof content === "string") return content;
|
|
7503
|
-
if (!Array.isArray(content)) return null;
|
|
7504
|
-
if (!content.some((block) => block.type === "image")) return content.filter((block) => block.type === "text" || block.type === "thinking").map((block) => block.type === "text" ? block.text : block.thinking).join("\n\n");
|
|
7505
|
-
const contentParts = [];
|
|
7506
|
-
for (const block of content) switch (block.type) {
|
|
7507
|
-
case "text":
|
|
7508
|
-
contentParts.push({
|
|
7509
|
-
type: "text",
|
|
7510
|
-
text: block.text
|
|
7511
|
-
});
|
|
7512
|
-
break;
|
|
7513
|
-
case "thinking":
|
|
7514
|
-
contentParts.push({
|
|
7515
|
-
type: "text",
|
|
7516
|
-
text: block.thinking
|
|
7517
|
-
});
|
|
7518
|
-
break;
|
|
7519
|
-
case "image":
|
|
7520
|
-
contentParts.push({
|
|
7521
|
-
type: "image_url",
|
|
7522
|
-
image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` }
|
|
7523
|
-
});
|
|
7524
|
-
break;
|
|
7525
|
-
}
|
|
7526
|
-
return contentParts;
|
|
7527
|
-
}
|
|
7528
|
-
function getTruncatedToolName(originalName, toolNameMapping) {
|
|
7529
|
-
if (originalName.length <= OPENAI_TOOL_NAME_LIMIT) return originalName;
|
|
7530
|
-
const existingTruncated = toolNameMapping.originalToTruncated.get(originalName);
|
|
7531
|
-
if (existingTruncated) return existingTruncated;
|
|
7532
|
-
let hash = 0;
|
|
7533
|
-
for (let i = 0; i < originalName.length; i++) {
|
|
7534
|
-
const char = originalName.codePointAt(i) ?? 0;
|
|
7535
|
-
hash = (hash << 5) - hash + char;
|
|
7536
|
-
hash = Math.trunc(hash);
|
|
7537
|
-
}
|
|
7538
|
-
const hashSuffix = Math.abs(hash).toString(36).slice(0, 8);
|
|
7539
|
-
const truncatedName = originalName.slice(0, OPENAI_TOOL_NAME_LIMIT - 9) + "_" + hashSuffix;
|
|
7540
|
-
toolNameMapping.truncatedToOriginal.set(truncatedName, originalName);
|
|
7541
|
-
toolNameMapping.originalToTruncated.set(originalName, truncatedName);
|
|
7542
|
-
consola.debug(`Truncated tool name: "${originalName}" -> "${truncatedName}"`);
|
|
7543
|
-
return truncatedName;
|
|
7544
|
-
}
|
|
7545
|
-
function translateAnthropicToolsToOpenAI(anthropicTools, toolNameMapping) {
|
|
7546
|
-
if (!anthropicTools) return;
|
|
7547
|
-
return anthropicTools.map((tool) => ({
|
|
7548
|
-
type: "function",
|
|
7549
|
-
function: {
|
|
7550
|
-
name: getTruncatedToolName(tool.name, toolNameMapping),
|
|
7551
|
-
description: tool.description,
|
|
7552
|
-
parameters: tool.input_schema ?? {}
|
|
7553
|
-
}
|
|
7554
|
-
}));
|
|
7555
|
-
}
|
|
7556
|
-
function translateAnthropicToolChoiceToOpenAI(anthropicToolChoice, toolNameMapping) {
|
|
7557
|
-
if (!anthropicToolChoice) return;
|
|
7558
|
-
switch (anthropicToolChoice.type) {
|
|
7559
|
-
case "auto": return "auto";
|
|
7560
|
-
case "any": return "required";
|
|
7561
|
-
case "tool":
|
|
7562
|
-
if (anthropicToolChoice.name) return {
|
|
7563
|
-
type: "function",
|
|
7564
|
-
function: { name: getTruncatedToolName(anthropicToolChoice.name, toolNameMapping) }
|
|
7565
|
-
};
|
|
7566
|
-
return;
|
|
7567
|
-
case "none": return "none";
|
|
7568
|
-
default: return;
|
|
7569
|
-
}
|
|
7570
|
-
}
|
|
7571
|
-
/** Create empty response for edge case of no choices */
|
|
7572
|
-
function createEmptyResponse(response) {
|
|
7573
|
-
return {
|
|
7574
|
-
id: response.id,
|
|
7575
|
-
type: "message",
|
|
7576
|
-
role: "assistant",
|
|
7577
|
-
model: response.model,
|
|
7578
|
-
content: [],
|
|
7579
|
-
stop_reason: "end_turn",
|
|
7580
|
-
stop_sequence: null,
|
|
7581
|
-
usage: {
|
|
7582
|
-
input_tokens: response.usage?.prompt_tokens ?? 0,
|
|
7583
|
-
output_tokens: response.usage?.completion_tokens ?? 0
|
|
7584
|
-
}
|
|
7585
|
-
};
|
|
7586
|
-
}
|
|
7587
|
-
/** Build usage object from response */
|
|
7588
|
-
function buildUsageObject(response) {
|
|
7589
|
-
const cachedTokens = response.usage?.prompt_tokens_details?.cached_tokens;
|
|
7590
|
-
return {
|
|
7591
|
-
input_tokens: (response.usage?.prompt_tokens ?? 0) - (cachedTokens ?? 0),
|
|
7592
|
-
output_tokens: response.usage?.completion_tokens ?? 0,
|
|
7593
|
-
...cachedTokens !== void 0 && { cache_read_input_tokens: cachedTokens }
|
|
7594
|
-
};
|
|
7595
|
-
}
|
|
7596
|
-
function translateToAnthropic(response, toolNameMapping) {
|
|
7597
|
-
if (response.choices.length === 0) return createEmptyResponse(response);
|
|
7598
|
-
const allTextBlocks = [];
|
|
7599
|
-
const allToolUseBlocks = [];
|
|
7600
|
-
let stopReason = null;
|
|
7601
|
-
stopReason = response.choices[0]?.finish_reason ?? stopReason;
|
|
7602
|
-
for (const choice of response.choices) {
|
|
7603
|
-
const textBlocks = getAnthropicTextBlocks(choice.message.content);
|
|
7604
|
-
const toolUseBlocks = getAnthropicToolUseBlocks(choice.message.tool_calls, toolNameMapping);
|
|
7605
|
-
allTextBlocks.push(...textBlocks);
|
|
7606
|
-
allToolUseBlocks.push(...toolUseBlocks);
|
|
7607
|
-
if (choice.finish_reason === "tool_calls" || stopReason === "stop") stopReason = choice.finish_reason;
|
|
7608
|
-
}
|
|
7609
|
-
return {
|
|
7610
|
-
id: response.id,
|
|
7611
|
-
type: "message",
|
|
7612
|
-
role: "assistant",
|
|
7613
|
-
model: response.model,
|
|
7614
|
-
content: [...allTextBlocks, ...allToolUseBlocks],
|
|
7615
|
-
stop_reason: mapOpenAIStopReasonToAnthropic(stopReason),
|
|
7616
|
-
stop_sequence: null,
|
|
7617
|
-
usage: buildUsageObject(response)
|
|
7618
|
-
};
|
|
7619
|
-
}
|
|
7620
|
-
function getAnthropicTextBlocks(messageContent) {
|
|
7621
|
-
if (typeof messageContent === "string") return [{
|
|
7622
|
-
type: "text",
|
|
7623
|
-
text: messageContent
|
|
7624
|
-
}];
|
|
7625
|
-
if (Array.isArray(messageContent)) return messageContent.filter((part) => part.type === "text").map((part) => ({
|
|
7626
|
-
type: "text",
|
|
7627
|
-
text: part.text
|
|
7628
|
-
}));
|
|
7629
|
-
return [];
|
|
7630
|
-
}
|
|
7631
|
-
function getAnthropicToolUseBlocks(toolCalls, toolNameMapping) {
|
|
7632
|
-
if (!toolCalls) return [];
|
|
7633
|
-
return toolCalls.map((toolCall) => {
|
|
7634
|
-
let input = {};
|
|
7635
|
-
try {
|
|
7636
|
-
input = JSON.parse(toolCall.function.arguments);
|
|
7637
|
-
} catch (error) {
|
|
7638
|
-
consola.warn(`Failed to parse tool call arguments for ${toolCall.function.name}:`, error);
|
|
7639
|
-
}
|
|
7640
|
-
const originalName = toolNameMapping?.truncatedToOriginal.get(toolCall.function.name) ?? toolCall.function.name;
|
|
7641
|
-
return {
|
|
7642
|
-
type: "tool_use",
|
|
7643
|
-
id: toolCall.id,
|
|
7644
|
-
name: originalName,
|
|
7645
|
-
input
|
|
7646
|
-
};
|
|
7647
|
-
});
|
|
7648
|
-
}
|
|
7649
|
-
|
|
7650
|
-
//#endregion
|
|
7651
|
-
//#region src/routes/messages/stream-translation.ts
|
|
7652
|
-
function isToolBlockOpen(state) {
|
|
7653
|
-
if (!state.contentBlockOpen) return false;
|
|
7654
|
-
return Object.values(state.toolCalls).some((tc) => tc.anthropicBlockIndex === state.contentBlockIndex);
|
|
7655
|
-
}
|
|
7656
|
-
function translateChunkToAnthropicEvents(chunk, state, toolNameMapping) {
|
|
7657
|
-
const events = [];
|
|
7658
|
-
if (chunk.choices.length === 0) {
|
|
7659
|
-
if (chunk.model && !state.model) state.model = chunk.model;
|
|
7660
|
-
return events;
|
|
7661
|
-
}
|
|
7662
|
-
const choice = chunk.choices[0];
|
|
7663
|
-
const { delta } = choice;
|
|
7664
|
-
if (!state.messageStartSent) {
|
|
7665
|
-
const model = chunk.model || state.model || "unknown";
|
|
7666
|
-
events.push({
|
|
7667
|
-
type: "message_start",
|
|
7668
|
-
message: {
|
|
7669
|
-
id: chunk.id || `msg_${Date.now()}`,
|
|
7670
|
-
type: "message",
|
|
7671
|
-
role: "assistant",
|
|
7672
|
-
content: [],
|
|
7673
|
-
model,
|
|
7674
|
-
stop_reason: null,
|
|
7675
|
-
stop_sequence: null,
|
|
7676
|
-
usage: {
|
|
7677
|
-
input_tokens: (chunk.usage?.prompt_tokens ?? 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0),
|
|
7678
|
-
output_tokens: 0,
|
|
7679
|
-
...chunk.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: chunk.usage.prompt_tokens_details.cached_tokens }
|
|
7680
|
-
}
|
|
7681
|
-
}
|
|
7682
|
-
});
|
|
7683
|
-
state.messageStartSent = true;
|
|
7684
|
-
}
|
|
7685
|
-
if (delta.content) {
|
|
7686
|
-
if (isToolBlockOpen(state)) {
|
|
7687
|
-
events.push({
|
|
7688
|
-
type: "content_block_stop",
|
|
7689
|
-
index: state.contentBlockIndex
|
|
7690
|
-
});
|
|
7691
|
-
state.contentBlockIndex++;
|
|
7692
|
-
state.contentBlockOpen = false;
|
|
7693
|
-
}
|
|
7694
|
-
if (!state.contentBlockOpen) {
|
|
7695
|
-
events.push({
|
|
7696
|
-
type: "content_block_start",
|
|
7697
|
-
index: state.contentBlockIndex,
|
|
7698
|
-
content_block: {
|
|
7699
|
-
type: "text",
|
|
7700
|
-
text: ""
|
|
7701
|
-
}
|
|
7702
|
-
});
|
|
7703
|
-
state.contentBlockOpen = true;
|
|
7704
|
-
}
|
|
7705
|
-
events.push({
|
|
7706
|
-
type: "content_block_delta",
|
|
7707
|
-
index: state.contentBlockIndex,
|
|
7708
|
-
delta: {
|
|
7709
|
-
type: "text_delta",
|
|
7710
|
-
text: delta.content
|
|
7711
|
-
}
|
|
7712
|
-
});
|
|
7713
|
-
}
|
|
7714
|
-
if (delta.tool_calls) for (const toolCall of delta.tool_calls) {
|
|
7715
|
-
if (toolCall.id && toolCall.function?.name) {
|
|
7716
|
-
if (state.contentBlockOpen) {
|
|
7717
|
-
events.push({
|
|
7718
|
-
type: "content_block_stop",
|
|
7719
|
-
index: state.contentBlockIndex
|
|
7720
|
-
});
|
|
7721
|
-
state.contentBlockIndex++;
|
|
7722
|
-
state.contentBlockOpen = false;
|
|
7723
|
-
}
|
|
7724
|
-
const originalName = toolNameMapping?.truncatedToOriginal.get(toolCall.function.name) ?? toolCall.function.name;
|
|
7725
|
-
const anthropicBlockIndex = state.contentBlockIndex;
|
|
7726
|
-
state.toolCalls[toolCall.index] = {
|
|
7727
|
-
id: toolCall.id,
|
|
7728
|
-
name: originalName,
|
|
7729
|
-
anthropicBlockIndex
|
|
7730
|
-
};
|
|
7731
|
-
events.push({
|
|
7732
|
-
type: "content_block_start",
|
|
7733
|
-
index: anthropicBlockIndex,
|
|
7734
|
-
content_block: {
|
|
7735
|
-
type: "tool_use",
|
|
7736
|
-
id: toolCall.id,
|
|
7737
|
-
name: originalName,
|
|
7738
|
-
input: {}
|
|
7739
|
-
}
|
|
7740
|
-
});
|
|
7741
|
-
state.contentBlockOpen = true;
|
|
7742
|
-
}
|
|
7743
|
-
if (toolCall.function?.arguments) {
|
|
7744
|
-
const toolCallInfo = state.toolCalls[toolCall.index];
|
|
7745
|
-
if (toolCallInfo) events.push({
|
|
7746
|
-
type: "content_block_delta",
|
|
7747
|
-
index: toolCallInfo.anthropicBlockIndex,
|
|
7748
|
-
delta: {
|
|
7749
|
-
type: "input_json_delta",
|
|
7750
|
-
partial_json: toolCall.function.arguments
|
|
7751
|
-
}
|
|
7752
|
-
});
|
|
7753
|
-
}
|
|
7754
|
-
}
|
|
7755
|
-
if (choice.finish_reason) {
|
|
7756
|
-
if (state.contentBlockOpen) {
|
|
7757
|
-
events.push({
|
|
7758
|
-
type: "content_block_stop",
|
|
7759
|
-
index: state.contentBlockIndex
|
|
7760
|
-
});
|
|
7761
|
-
state.contentBlockOpen = false;
|
|
7762
|
-
}
|
|
7763
|
-
events.push({
|
|
7764
|
-
type: "message_delta",
|
|
7765
|
-
delta: {
|
|
7766
|
-
stop_reason: mapOpenAIStopReasonToAnthropic(choice.finish_reason),
|
|
7767
|
-
stop_sequence: null
|
|
7768
|
-
},
|
|
7769
|
-
usage: {
|
|
7770
|
-
input_tokens: (chunk.usage?.prompt_tokens ?? 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0),
|
|
7771
|
-
output_tokens: chunk.usage?.completion_tokens ?? 0,
|
|
7772
|
-
...chunk.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: chunk.usage.prompt_tokens_details.cached_tokens }
|
|
7773
|
-
}
|
|
7774
|
-
}, { type: "message_stop" });
|
|
7775
|
-
}
|
|
7776
|
-
return events;
|
|
7777
|
-
}
|
|
7778
|
-
function translateErrorToAnthropicErrorEvent(error) {
|
|
7779
|
-
return {
|
|
7780
|
-
type: "error",
|
|
7781
|
-
error: {
|
|
7782
|
-
type: "api_error",
|
|
7783
|
-
message: error ? formatError(error) : "An unexpected error occurred during streaming."
|
|
7784
|
-
}
|
|
7785
|
-
};
|
|
7786
|
-
}
|
|
7787
|
-
|
|
7788
|
-
//#endregion
|
|
7789
|
-
//#region src/routes/messages/tool-call-recovery.ts
|
|
7790
|
-
const ENVELOPE = String.raw`(?:<(?:antml:)?function_calls>|call|count|court)`;
|
|
7791
|
-
const INVOKE_BODY = String.raw`<(?:antml:)?invoke\s+name="[^"]+">(?:(?!<(?:antml:)?invoke\b)[\s\S])*?</(?:antml:)?invoke>`;
|
|
7792
|
-
const LEAKED_REGION_RE = new RegExp(String.raw`(?:^|\n)[ \t]*(?:` + ENVELOPE + String.raw`[ \t\n]*(?:` + INVOKE_BODY + String.raw`\s*)+(?:</(?:antml:)?function_calls>)?|` + INVOKE_BODY + String.raw`)`, "g");
|
|
7793
|
-
const STARTS_WITH_ENVELOPE_RE = new RegExp(String.raw`^[ \t\n]*` + ENVELOPE);
|
|
7794
|
-
const INVOKE_OPENER_RE = new RegExp(String.raw`(?:^|\n)[ \t]*(?:` + ENVELOPE + String.raw`[ \t\n]*)?<(?:antml:)?invoke\b[^\n>]*>?`, "g");
|
|
7795
|
-
const LEAK_OPEN_RE = new RegExp(String.raw`(?:^|\n)[ \t]*(?:` + ENVELOPE + String.raw`[ \t\n]*)?<(?:antml:)?invoke\s+name="`, "g");
|
|
7796
|
-
const INVOKE_RE = /<(?:antml:)?invoke\s+name="([^"]+)">([\s\S]*?)<\/(?:antml:)?invoke>/g;
|
|
7797
|
-
const PARAMETER_RE = /<(?:antml:)?parameter\s+name="([^"]+)">([\s\S]*?)<\/(?:antml:)?parameter>/g;
|
|
7798
|
-
function coerceParamValue(raw) {
|
|
7799
|
-
const trimmed = raw.trim();
|
|
7800
|
-
if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) try {
|
|
7801
|
-
return JSON.parse(trimmed);
|
|
7802
|
-
} catch {
|
|
7803
|
-
return raw;
|
|
7804
|
-
}
|
|
7805
|
-
return raw;
|
|
7806
|
-
}
|
|
7807
|
-
function parseRegionInvokes(region, knownTools) {
|
|
7808
|
-
const calls = [];
|
|
7809
|
-
for (const invokeMatch of region.matchAll(INVOKE_RE)) {
|
|
7810
|
-
const name = invokeMatch[1];
|
|
7811
|
-
if (knownTools && !knownTools.has(name)) continue;
|
|
7812
|
-
const input = {};
|
|
7813
|
-
for (const paramMatch of invokeMatch[2].matchAll(PARAMETER_RE)) input[paramMatch[1]] = coerceParamValue(paramMatch[2]);
|
|
7814
|
-
calls.push({
|
|
7815
|
-
name,
|
|
7816
|
-
input
|
|
7817
|
-
});
|
|
7818
|
-
}
|
|
7819
|
-
return calls;
|
|
7820
|
-
}
|
|
7821
|
-
const FENCE_DELIM_RE = /(?:^|\n)[ \t]*```/g;
|
|
7822
|
-
function insideFence(text, pos) {
|
|
7823
|
-
let openAt = -1;
|
|
7824
|
-
for (const m of text.matchAll(FENCE_DELIM_RE)) if (openAt === -1) {
|
|
7825
|
-
if (m.index >= pos) break;
|
|
7826
|
-
openAt = m.index;
|
|
7827
|
-
} else if (m.index > pos) return true;
|
|
7828
|
-
else openAt = -1;
|
|
7829
|
-
return false;
|
|
7830
|
-
}
|
|
7831
|
-
function regionIsLeak(region, offset, fullText, knownTools) {
|
|
7832
|
-
if (insideFence(fullText, offset)) return false;
|
|
7833
|
-
if (STARTS_WITH_ENVELOPE_RE.test(region)) return true;
|
|
7834
|
-
if (!knownTools) return false;
|
|
7835
|
-
return parseRegionInvokes(region, knownTools).length > 0;
|
|
7836
|
-
}
|
|
7837
|
-
/**
|
|
7838
|
-
* Split assistant text into ordered segments — dropping leaked envelope markup
|
|
7839
|
-
* (and undeclared-tool invokes) while preserving the natural-language on either
|
|
7840
|
-
* side and the pre/tool/post ordering. Returns a single text segment when there
|
|
7841
|
-
* is no leak. Shared by both response recovery paths so they cannot diverge.
|
|
7334
|
+
* Split assistant text into ordered segments — dropping leaked envelope markup
|
|
7335
|
+
* (and undeclared-tool invokes) while preserving the natural-language on either
|
|
7336
|
+
* side and the pre/tool/post ordering. Returns a single text segment when there
|
|
7337
|
+
* is no leak. Shared by both response recovery paths so they cannot diverge.
|
|
7842
7338
|
*
|
|
7843
7339
|
* `from` restricts the EMITTED window to `text.slice(from)` while still
|
|
7844
7340
|
* classifying against the FULL `text` — so a streaming capture that started just
|
|
@@ -8643,359 +8139,350 @@ const parseSubagentMarkerFromSystemReminder = (text) => {
|
|
|
8643
8139
|
};
|
|
8644
8140
|
|
|
8645
8141
|
//#endregion
|
|
8646
|
-
//#region src/routes/messages/
|
|
8142
|
+
//#region src/routes/messages/handler.ts
|
|
8143
|
+
function resolveModelFromBetaHeader(model, betaHeader) {
|
|
8144
|
+
if (!betaHeader || !/\bcontext-1m\b/.test(betaHeader)) return model;
|
|
8145
|
+
if (!model.startsWith("claude-")) return model;
|
|
8146
|
+
if (model.endsWith("-1m")) return model;
|
|
8147
|
+
const resolved = `${model}-1m`;
|
|
8148
|
+
consola.debug(`Detected context-1m in anthropic-beta header, resolving model: ${model} → ${resolved}`);
|
|
8149
|
+
return resolved;
|
|
8150
|
+
}
|
|
8151
|
+
async function handleCompletion(c) {
|
|
8152
|
+
const rawPayload = await c.req.json();
|
|
8153
|
+
consola.debug("Anthropic request payload:", JSON.stringify(rawPayload));
|
|
8154
|
+
if (rawPayload === null || typeof rawPayload.model !== "string" || rawPayload.model.length === 0) return c.json({
|
|
8155
|
+
type: "error",
|
|
8156
|
+
error: {
|
|
8157
|
+
type: "invalid_request_error",
|
|
8158
|
+
message: "model is required and must be a non-empty string"
|
|
8159
|
+
}
|
|
8160
|
+
}, 400);
|
|
8161
|
+
const normalizedModel = resolveModelFromBetaHeader(rawPayload.model, c.req.header("anthropic-beta"));
|
|
8162
|
+
if (!supportsDirectAnthropicApi(normalizedModel)) return c.json({
|
|
8163
|
+
type: "error",
|
|
8164
|
+
error: {
|
|
8165
|
+
type: "invalid_request_error",
|
|
8166
|
+
message: `model \`${normalizedModel}\` is not an Anthropic model available on /v1/messages`
|
|
8167
|
+
}
|
|
8168
|
+
}, 400);
|
|
8169
|
+
const { ctx, payload: anthropicPayload } = createEntryContext({
|
|
8170
|
+
c,
|
|
8171
|
+
rawPayload,
|
|
8172
|
+
endpoint: "anthropic",
|
|
8173
|
+
normalizePayload: (p) => ({
|
|
8174
|
+
...p,
|
|
8175
|
+
model: resolveModelFromBetaHeader(p.model, c.req.header("anthropic-beta"))
|
|
8176
|
+
}),
|
|
8177
|
+
buildHistoryRequest: (p) => ({
|
|
8178
|
+
model: p.model,
|
|
8179
|
+
messages: convertAnthropicMessages(p.messages),
|
|
8180
|
+
stream: p.stream ?? false,
|
|
8181
|
+
tools: p.tools?.map((t) => ({
|
|
8182
|
+
name: t.name,
|
|
8183
|
+
description: t.description
|
|
8184
|
+
})),
|
|
8185
|
+
max_tokens: p.max_tokens,
|
|
8186
|
+
temperature: p.temperature,
|
|
8187
|
+
system: extractSystemPrompt(p.system)
|
|
8188
|
+
})
|
|
8189
|
+
});
|
|
8190
|
+
const sanitizedPayload = dePoisonAssistantMessages(anthropicPayload);
|
|
8191
|
+
logToolInfo(sanitizedPayload);
|
|
8192
|
+
const subagentMarker = parseSubagentMarkerFromFirstUser(sanitizedPayload);
|
|
8193
|
+
const initiatorOverride = subagentMarker ? "agent" : void 0;
|
|
8194
|
+
if (subagentMarker) consola.debug("Detected Subagent marker:", JSON.stringify(subagentMarker));
|
|
8195
|
+
normalizeSystemPromptDate(sanitizedPayload);
|
|
8196
|
+
injectSystemCacheControl(sanitizedPayload);
|
|
8197
|
+
return handleDirectAnthropicCompletion(c, sanitizedPayload, ctx, initiatorOverride);
|
|
8198
|
+
}
|
|
8647
8199
|
/**
|
|
8648
|
-
*
|
|
8200
|
+
* Log tool-related information for debugging
|
|
8649
8201
|
*/
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
const
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
8662
|
-
|
|
8663
|
-
|
|
8664
|
-
|
|
8665
|
-
|
|
8666
|
-
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
|
|
8682
|
-
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
8688
|
-
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
updateTrackerStatus(ctx.trackingId, "streaming");
|
|
8696
|
-
return streamSSE(c, async (stream) => {
|
|
8697
|
-
stream.onAbort(() => abort.abort());
|
|
8698
|
-
await handleStreamingResponse({
|
|
8699
|
-
stream,
|
|
8700
|
-
response,
|
|
8701
|
-
toolNameMapping,
|
|
8702
|
-
anthropicPayload,
|
|
8703
|
-
ctx
|
|
8202
|
+
function logToolInfo(anthropicPayload) {
|
|
8203
|
+
if (anthropicPayload.tools?.length) {
|
|
8204
|
+
const toolInfo = anthropicPayload.tools.map((t) => ({
|
|
8205
|
+
name: t.name,
|
|
8206
|
+
type: t.type ?? "(custom)"
|
|
8207
|
+
}));
|
|
8208
|
+
consola.debug(`[Tools] Defined tools:`, JSON.stringify(toolInfo));
|
|
8209
|
+
}
|
|
8210
|
+
for (const msg of anthropicPayload.messages) if (typeof msg.content !== "string") for (const block of msg.content) {
|
|
8211
|
+
if (block.type === "tool_use") consola.debug(`[Tools] tool_use in message: ${block.name} (id: ${block.id})`);
|
|
8212
|
+
if (block.type === "tool_result") consola.debug(`[Tools] tool_result in message: id=${block.tool_use_id}, is_error=${block.is_error ?? false}`);
|
|
8213
|
+
}
|
|
8214
|
+
}
|
|
8215
|
+
|
|
8216
|
+
//#endregion
|
|
8217
|
+
//#region src/routes/messages/non-stream-translation.ts
|
|
8218
|
+
const OPENAI_TOOL_NAME_LIMIT = 64;
|
|
8219
|
+
/**
|
|
8220
|
+
* Ensure all tool_use blocks have corresponding tool_result responses.
|
|
8221
|
+
* This handles edge cases where conversation history may be incomplete:
|
|
8222
|
+
* - Session interruptions where tool execution was cut off
|
|
8223
|
+
* - Previous request failures
|
|
8224
|
+
* - Client sending truncated history
|
|
8225
|
+
*
|
|
8226
|
+
* Adding placeholder responses prevents API errors and maintains protocol compliance.
|
|
8227
|
+
*/
|
|
8228
|
+
function fixMessageSequence(messages) {
|
|
8229
|
+
const fixedMessages = [];
|
|
8230
|
+
for (let i = 0; i < messages.length; i++) {
|
|
8231
|
+
const message = messages[i];
|
|
8232
|
+
fixedMessages.push(message);
|
|
8233
|
+
if (message.role === "assistant" && message.tool_calls && message.tool_calls.length > 0) {
|
|
8234
|
+
const foundToolResponses = /* @__PURE__ */ new Set();
|
|
8235
|
+
let j = i + 1;
|
|
8236
|
+
while (j < messages.length && messages[j].role === "tool") {
|
|
8237
|
+
const toolMessage = messages[j];
|
|
8238
|
+
if (toolMessage.tool_call_id) foundToolResponses.add(toolMessage.tool_call_id);
|
|
8239
|
+
j++;
|
|
8240
|
+
}
|
|
8241
|
+
for (const toolCall of message.tool_calls) if (!foundToolResponses.has(toolCall.id)) {
|
|
8242
|
+
consola.debug(`Adding placeholder tool_result for ${toolCall.id}`);
|
|
8243
|
+
fixedMessages.push({
|
|
8244
|
+
role: "tool",
|
|
8245
|
+
tool_call_id: toolCall.id,
|
|
8246
|
+
content: "Tool execution was interrupted or failed."
|
|
8704
8247
|
});
|
|
8705
|
-
}
|
|
8706
|
-
}
|
|
8707
|
-
consola.debug("[RateLimiter] Request queued past grace; opening keepalive stream (translated)");
|
|
8708
|
-
updateTrackerStatus(ctx.trackingId, "streaming");
|
|
8709
|
-
return streamSSE(c, async (stream) => {
|
|
8710
|
-
stream.onAbort(() => abort.abort());
|
|
8711
|
-
await runStreamWithKeepalive({
|
|
8712
|
-
stream,
|
|
8713
|
-
settled,
|
|
8714
|
-
pingIntervalMs: KEEPALIVE_PING_INTERVAL_MS,
|
|
8715
|
-
onResponse: async ({ result: response, queueWaitMs }) => {
|
|
8716
|
-
ctx.queueWaitMs = queueWaitMs;
|
|
8717
|
-
if (isNonStreaming(response)) return;
|
|
8718
|
-
await handleStreamingResponse({
|
|
8719
|
-
stream,
|
|
8720
|
-
response,
|
|
8721
|
-
toolNameMapping,
|
|
8722
|
-
anthropicPayload,
|
|
8723
|
-
ctx
|
|
8724
|
-
});
|
|
8725
|
-
},
|
|
8726
|
-
onError: async (error) => {
|
|
8727
|
-
if (isAbortError(error)) {
|
|
8728
|
-
consola.debug("[Translated] client disconnected during keepalive; upstream aborted");
|
|
8729
|
-
failTracking(ctx.trackingId, "client disconnected");
|
|
8730
|
-
return;
|
|
8731
|
-
}
|
|
8732
|
-
recordStreamError({
|
|
8733
|
-
acc: createAnthropicStreamAccumulator(),
|
|
8734
|
-
fallbackModel: anthropicPayload.model,
|
|
8735
|
-
ctx,
|
|
8736
|
-
error,
|
|
8737
|
-
endpoint: "messages"
|
|
8738
|
-
});
|
|
8739
|
-
failTracking(ctx.trackingId, error);
|
|
8740
|
-
const errorEvent = translateErrorToAnthropicErrorEvent(error);
|
|
8741
|
-
await stream.writeSSE({
|
|
8742
|
-
event: errorEvent.type,
|
|
8743
|
-
data: JSON.stringify(errorEvent)
|
|
8744
|
-
});
|
|
8745
|
-
}
|
|
8746
|
-
});
|
|
8747
|
-
});
|
|
8748
|
-
} catch (error) {
|
|
8749
|
-
if (isAbortError(error)) {
|
|
8750
|
-
consola.debug("[Translated] client disconnected before response; upstream aborted");
|
|
8751
|
-
failTracking(ctx.trackingId, "client disconnected");
|
|
8752
|
-
return new Response(null, { status: 499 });
|
|
8248
|
+
}
|
|
8753
8249
|
}
|
|
8754
|
-
if (error instanceof HTTPError && error.status === 413) await logPayloadSizeInfo(openAIPayload, selectedModel);
|
|
8755
|
-
recordErrorResponse(ctx, anthropicPayload.model, error, "messages", anthropicPayload.stream ?? false);
|
|
8756
|
-
throw error;
|
|
8757
8250
|
}
|
|
8251
|
+
return fixedMessages;
|
|
8758
8252
|
}
|
|
8759
|
-
function
|
|
8760
|
-
const {
|
|
8761
|
-
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
|
|
8777
|
-
|
|
8778
|
-
|
|
8779
|
-
|
|
8780
|
-
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
|
|
8784
|
-
|
|
8785
|
-
|
|
8786
|
-
|
|
8787
|
-
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
|
|
8793
|
-
const totalInputTokens = anthropicResponse.usage.input_tokens + cacheRead + cacheCreation;
|
|
8794
|
-
if (ctx.trackingId) requestTracker.updateRequest(ctx.trackingId, {
|
|
8795
|
-
inputTokens: anthropicResponse.usage.input_tokens,
|
|
8796
|
-
outputTokens: anthropicResponse.usage.output_tokens,
|
|
8797
|
-
queueWaitMs: ctx.queueWaitMs,
|
|
8798
|
-
cachedInputTokens: cacheRead,
|
|
8799
|
-
cacheCreationInputTokens: cacheCreation,
|
|
8800
|
-
totalInputTokens
|
|
8801
|
-
});
|
|
8802
|
-
captureRequest({
|
|
8803
|
-
model: anthropicResponse.model,
|
|
8804
|
-
inputTokens: anthropicResponse.usage.input_tokens,
|
|
8805
|
-
outputTokens: anthropicResponse.usage.output_tokens,
|
|
8806
|
-
durationMs: Date.now() - ctx.startTime,
|
|
8807
|
-
success: true,
|
|
8808
|
-
stream: false,
|
|
8809
|
-
toolCount: anthropicPayload.tools?.length ?? 0,
|
|
8810
|
-
cachedInputTokens: cacheRead,
|
|
8811
|
-
cacheCreationInputTokens: cacheCreation,
|
|
8812
|
-
totalInputTokens,
|
|
8813
|
-
stopReason: anthropicResponse.stop_reason ?? void 0
|
|
8253
|
+
function translateToOpenAI(payload) {
|
|
8254
|
+
const toolNameMapping = { originalToTruncated: /* @__PURE__ */ new Map() };
|
|
8255
|
+
const messages = translateAnthropicMessagesToOpenAI(payload.messages, payload.system, toolNameMapping);
|
|
8256
|
+
return { payload: {
|
|
8257
|
+
model: translateModelName(payload.model),
|
|
8258
|
+
messages: fixMessageSequence(messages),
|
|
8259
|
+
max_tokens: payload.max_tokens,
|
|
8260
|
+
stop: payload.stop_sequences,
|
|
8261
|
+
stream: payload.stream,
|
|
8262
|
+
temperature: payload.temperature,
|
|
8263
|
+
top_p: payload.top_p,
|
|
8264
|
+
user: payload.metadata?.user_id,
|
|
8265
|
+
tools: translateAnthropicToolsToOpenAI(payload.tools, toolNameMapping),
|
|
8266
|
+
tool_choice: translateAnthropicToolChoiceToOpenAI(payload.tool_choice, toolNameMapping)
|
|
8267
|
+
} };
|
|
8268
|
+
}
|
|
8269
|
+
/**
|
|
8270
|
+
* Find the latest available model matching a family prefix.
|
|
8271
|
+
* Searches state.models for models starting with the given prefix
|
|
8272
|
+
* and returns the one with the highest version number.
|
|
8273
|
+
*
|
|
8274
|
+
* @param familyPrefix - e.g., "claude-opus", "claude-sonnet", "claude-haiku"
|
|
8275
|
+
* @param fallback - fallback model ID if no match found
|
|
8276
|
+
*/
|
|
8277
|
+
function findLatestModel(familyPrefix, fallback) {
|
|
8278
|
+
const models = state.models?.data;
|
|
8279
|
+
if (!models || models.length === 0) return fallback;
|
|
8280
|
+
const candidates = models.filter((m) => m.id.startsWith(familyPrefix));
|
|
8281
|
+
if (candidates.length === 0) return fallback;
|
|
8282
|
+
candidates.sort((a, b) => {
|
|
8283
|
+
const [aMajor, aMinor] = extractVersion(a.id, familyPrefix);
|
|
8284
|
+
const [bMajor, bMinor] = extractVersion(b.id, familyPrefix);
|
|
8285
|
+
if (aMajor !== bMajor) return bMajor - aMajor;
|
|
8286
|
+
return bMinor - aMinor;
|
|
8814
8287
|
});
|
|
8815
|
-
return
|
|
8288
|
+
return candidates[0].id;
|
|
8816
8289
|
}
|
|
8817
|
-
|
|
8818
|
-
|
|
8819
|
-
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
|
|
8823
|
-
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
|
|
8828
|
-
|
|
8829
|
-
|
|
8830
|
-
|
|
8831
|
-
|
|
8832
|
-
|
|
8833
|
-
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
|
|
8841
|
-
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
durationMs: Date.now() - ctx.startTime,
|
|
8847
|
-
stopReason: acc.stopReason || void 0,
|
|
8848
|
-
toolCount: anthropicPayload.tools?.length ?? 0
|
|
8849
|
-
}, ctx.timings, {
|
|
8850
|
-
cachedInputTokens: acc.cacheReadInputTokens,
|
|
8851
|
-
cacheCreationInputTokens: acc.cacheCreationInputTokens,
|
|
8852
|
-
totalInputTokens: acc.inputTokens + acc.cacheReadInputTokens + acc.cacheCreationInputTokens
|
|
8853
|
-
});
|
|
8854
|
-
} catch (error) {
|
|
8855
|
-
if (isAbortError(error)) {
|
|
8856
|
-
consola.debug("[Translated] client disconnected mid-stream; upstream aborted");
|
|
8857
|
-
failTracking(ctx.trackingId, "client disconnected");
|
|
8858
|
-
return;
|
|
8859
|
-
}
|
|
8860
|
-
consola.error("Stream error:", formatError(error));
|
|
8861
|
-
recordStreamError({
|
|
8862
|
-
acc,
|
|
8863
|
-
fallbackModel: anthropicPayload.model,
|
|
8864
|
-
ctx,
|
|
8865
|
-
error,
|
|
8866
|
-
endpoint: "messages"
|
|
8867
|
-
});
|
|
8868
|
-
failTracking(ctx.trackingId, error);
|
|
8869
|
-
const errorEvent = translateErrorToAnthropicErrorEvent(error);
|
|
8870
|
-
await stream.writeSSE({
|
|
8871
|
-
event: errorEvent.type,
|
|
8872
|
-
data: JSON.stringify(errorEvent)
|
|
8873
|
-
});
|
|
8874
|
-
}
|
|
8290
|
+
/**
|
|
8291
|
+
* Extract numeric [major, minor] version from a model id.
|
|
8292
|
+
*
|
|
8293
|
+
* Supports both naming conventions Anthropic/Copilot have used:
|
|
8294
|
+
* - dot: "claude-opus-4.5" → [4, 5]
|
|
8295
|
+
* - dash: "claude-opus-4-8" → [4, 8]
|
|
8296
|
+
* - dash double-digit: "claude-opus-4-10" → [4, 10]
|
|
8297
|
+
*
|
|
8298
|
+
* The dash form previously parsed as just the major via the regex
|
|
8299
|
+
* /^(\d+(?:\.\d+)?)/ (the dash stopped the match), which silently
|
|
8300
|
+
* downgraded dash-named candidates against any dot-named candidate in
|
|
8301
|
+
* findLatestModel. Parsing into a tuple also avoids the parseFloat
|
|
8302
|
+
* lossiness on double-digit minors ("4.10" → 4.1).
|
|
8303
|
+
*
|
|
8304
|
+
* Anything after the major/minor segment (date stamps, "-1m") is ignored.
|
|
8305
|
+
* The minor capture is bounded to 1-3 digits so that an 8-digit date suffix
|
|
8306
|
+
* directly after the major (e.g. "claude-opus-4-20250514") is NOT mistaken
|
|
8307
|
+
* for a minor version of 20_250_514 — without that bound, dated ids would
|
|
8308
|
+
* outrank legitimate dotted candidates like "claude-opus-4.8" in
|
|
8309
|
+
* findLatestModel's sort.
|
|
8310
|
+
*
|
|
8311
|
+
* Returns [0, 0] when no version can be extracted.
|
|
8312
|
+
*/
|
|
8313
|
+
function extractVersion(modelId, prefix) {
|
|
8314
|
+
const match = modelId.slice(prefix.length + 1).match(/^(\d+)(?:[.-](\d{1,3}))?/);
|
|
8315
|
+
if (!match) return [0, 0];
|
|
8316
|
+
const major = Number.parseInt(match[1], 10);
|
|
8317
|
+
const rawMinor = match[2];
|
|
8318
|
+
return [major, rawMinor === void 0 ? 0 : Number.parseInt(rawMinor, 10) || 0];
|
|
8875
8319
|
}
|
|
8876
|
-
|
|
8877
|
-
const
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
type: "text",
|
|
8882
|
-
text: ""
|
|
8883
|
-
}
|
|
8884
|
-
};
|
|
8885
|
-
await stream.writeSSE({
|
|
8886
|
-
event: "content_block_start",
|
|
8887
|
-
data: JSON.stringify(blockStartEvent)
|
|
8888
|
-
});
|
|
8889
|
-
const deltaEvent = {
|
|
8890
|
-
type: "content_block_delta",
|
|
8891
|
-
index: streamState.contentBlockIndex,
|
|
8892
|
-
delta: {
|
|
8893
|
-
type: "text_delta",
|
|
8894
|
-
text: marker
|
|
8895
|
-
}
|
|
8896
|
-
};
|
|
8897
|
-
await stream.writeSSE({
|
|
8898
|
-
event: "content_block_delta",
|
|
8899
|
-
data: JSON.stringify(deltaEvent)
|
|
8900
|
-
});
|
|
8901
|
-
const blockStopEvent = {
|
|
8902
|
-
type: "content_block_stop",
|
|
8903
|
-
index: streamState.contentBlockIndex
|
|
8320
|
+
function translateModelName(model) {
|
|
8321
|
+
const aliasMap = {
|
|
8322
|
+
opus: "claude-opus",
|
|
8323
|
+
sonnet: "claude-sonnet",
|
|
8324
|
+
haiku: "claude-haiku"
|
|
8904
8325
|
};
|
|
8905
|
-
|
|
8906
|
-
|
|
8907
|
-
|
|
8908
|
-
});
|
|
8909
|
-
streamState.contentBlockIndex++;
|
|
8910
|
-
}
|
|
8911
|
-
async function processStreamChunks(opts) {
|
|
8912
|
-
const { stream, response, toolNameMapping, streamState, acc, checkRepetition, ctx } = opts;
|
|
8913
|
-
for await (const rawEvent of response) {
|
|
8914
|
-
consola.debug("Copilot raw stream event:", JSON.stringify(rawEvent));
|
|
8915
|
-
if (rawEvent.data === "[DONE]") break;
|
|
8916
|
-
if (!rawEvent.data) continue;
|
|
8917
|
-
let chunk;
|
|
8918
|
-
try {
|
|
8919
|
-
chunk = JSON.parse(rawEvent.data);
|
|
8920
|
-
} catch (parseError) {
|
|
8921
|
-
consola.error("Failed to parse stream chunk:", parseError, rawEvent.data);
|
|
8922
|
-
continue;
|
|
8923
|
-
}
|
|
8924
|
-
if (chunk.model && !acc.model) acc.model = chunk.model;
|
|
8925
|
-
const events = translateChunkToAnthropicEvents(chunk, streamState, toolNameMapping);
|
|
8926
|
-
for (const event of events) {
|
|
8927
|
-
consola.debug("Translated Anthropic event:", JSON.stringify(event));
|
|
8928
|
-
processAnthropicEvent(event, acc);
|
|
8929
|
-
if (event.type === "content_block_delta" && event.delta.type === "text_delta") checkRepetition(event.delta.text);
|
|
8930
|
-
const echoed = echoParsedEvent(event, ctx);
|
|
8931
|
-
await stream.writeSSE({
|
|
8932
|
-
event: echoed.type,
|
|
8933
|
-
data: JSON.stringify(echoed)
|
|
8934
|
-
});
|
|
8935
|
-
}
|
|
8326
|
+
if (aliasMap[model]) {
|
|
8327
|
+
const familyPrefix = aliasMap[model];
|
|
8328
|
+
return findLatestModel(familyPrefix, `${familyPrefix}-4.5`);
|
|
8936
8329
|
}
|
|
8330
|
+
if (/^claude-sonnet-4-5-\d+$/.test(model)) return "claude-sonnet-4.5";
|
|
8331
|
+
if (/^claude-sonnet-4-\d+$/.test(model)) return "claude-sonnet-4";
|
|
8332
|
+
if (model === "claude-opus-4-8-1m") return "claude-opus-4.8";
|
|
8333
|
+
if (model === "claude-opus-4-8") return "claude-opus-4.8";
|
|
8334
|
+
if (model === "claude-opus-4-7-1m") return "claude-opus-4.7";
|
|
8335
|
+
if (/^claude-opus-4-7$/.test(model)) return "claude-opus-4.7";
|
|
8336
|
+
if (model === "claude-opus-4-6-1m") return "claude-opus-4.6-1m";
|
|
8337
|
+
if (/^claude-opus-4-6$/.test(model)) return "claude-opus-4.6";
|
|
8338
|
+
if (/^claude-opus-4-5-\d+$/.test(model)) return "claude-opus-4.5";
|
|
8339
|
+
if (/^claude-opus-4-\d+$/.test(model)) return findLatestModel("claude-opus", "claude-opus-4.5");
|
|
8340
|
+
if (/^claude-haiku-4-5-\d+$/.test(model)) return "claude-haiku-4.5";
|
|
8341
|
+
if (/^claude-haiku-3-5-\d+$/.test(model)) return findLatestModel("claude-haiku", "claude-haiku-4.5");
|
|
8342
|
+
return model;
|
|
8937
8343
|
}
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
if (!betaHeader || !/\bcontext-1m\b/.test(betaHeader)) return model;
|
|
8943
|
-
if (!model.startsWith("claude-")) return model;
|
|
8944
|
-
if (model.endsWith("-1m")) return model;
|
|
8945
|
-
const resolved = `${model}-1m`;
|
|
8946
|
-
consola.debug(`Detected context-1m in anthropic-beta header, resolving model: ${model} → ${resolved}`);
|
|
8947
|
-
return resolved;
|
|
8344
|
+
function translateAnthropicMessagesToOpenAI(anthropicMessages, system, toolNameMapping) {
|
|
8345
|
+
const systemMessages = handleSystemPrompt(system);
|
|
8346
|
+
const otherMessages = anthropicMessages.flatMap((message) => message.role === "user" ? handleUserMessage(message) : handleAssistantMessage(message, toolNameMapping));
|
|
8347
|
+
return [...systemMessages, ...otherMessages];
|
|
8948
8348
|
}
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
|
|
8963
|
-
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
8968
|
-
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
8349
|
+
const RESERVED_KEYWORDS = ["x-anthropic-billing-header", "x-anthropic-billing"];
|
|
8350
|
+
/**
|
|
8351
|
+
* Filter out reserved keywords from system prompt text.
|
|
8352
|
+
* Copilot API rejects requests containing these keywords.
|
|
8353
|
+
* Removes the entire line containing the keyword to keep the prompt clean.
|
|
8354
|
+
*/
|
|
8355
|
+
function filterReservedKeywords(text) {
|
|
8356
|
+
let filtered = text;
|
|
8357
|
+
for (const keyword of RESERVED_KEYWORDS) if (text.includes(keyword)) {
|
|
8358
|
+
consola.debug(`[Reserved Keyword] Removing line containing "${keyword}"`);
|
|
8359
|
+
filtered = filtered.split("\n").filter((line) => !line.includes(keyword)).join("\n");
|
|
8360
|
+
}
|
|
8361
|
+
return filtered;
|
|
8362
|
+
}
|
|
8363
|
+
function handleSystemPrompt(system) {
|
|
8364
|
+
if (!system) return [];
|
|
8365
|
+
if (typeof system === "string") return [{
|
|
8366
|
+
role: "system",
|
|
8367
|
+
content: filterReservedKeywords(system)
|
|
8368
|
+
}];
|
|
8369
|
+
else return [{
|
|
8370
|
+
role: "system",
|
|
8371
|
+
content: filterReservedKeywords(system.map((block) => block.text).join("\n\n"))
|
|
8372
|
+
}];
|
|
8373
|
+
}
|
|
8374
|
+
function handleUserMessage(message) {
|
|
8375
|
+
const newMessages = [];
|
|
8376
|
+
if (Array.isArray(message.content)) {
|
|
8377
|
+
const toolResultBlocks = message.content.filter((block) => block.type === "tool_result");
|
|
8378
|
+
const otherBlocks = message.content.filter((block) => block.type !== "tool_result");
|
|
8379
|
+
for (const block of toolResultBlocks) newMessages.push({
|
|
8380
|
+
role: "tool",
|
|
8381
|
+
tool_call_id: block.tool_use_id,
|
|
8382
|
+
content: mapContent(block.content)
|
|
8383
|
+
});
|
|
8384
|
+
if (otherBlocks.length > 0) newMessages.push({
|
|
8385
|
+
role: "user",
|
|
8386
|
+
content: mapContent(otherBlocks)
|
|
8387
|
+
});
|
|
8388
|
+
} else newMessages.push({
|
|
8389
|
+
role: "user",
|
|
8390
|
+
content: mapContent(message.content)
|
|
8972
8391
|
});
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8392
|
+
return newMessages;
|
|
8393
|
+
}
|
|
8394
|
+
function handleAssistantMessage(message, toolNameMapping) {
|
|
8395
|
+
if (!Array.isArray(message.content)) return [{
|
|
8396
|
+
role: "assistant",
|
|
8397
|
+
content: mapContent(message.content)
|
|
8398
|
+
}];
|
|
8399
|
+
const toolUseBlocks = message.content.filter((block) => block.type === "tool_use");
|
|
8400
|
+
const textBlocks = message.content.filter((block) => block.type === "text");
|
|
8401
|
+
const thinkingBlocks = message.content.filter((block) => block.type === "thinking");
|
|
8402
|
+
const allTextContent = [...textBlocks.map((b) => b.text), ...thinkingBlocks.map((b) => b.thinking)].join("\n\n");
|
|
8403
|
+
return toolUseBlocks.length > 0 ? [{
|
|
8404
|
+
role: "assistant",
|
|
8405
|
+
content: allTextContent || null,
|
|
8406
|
+
tool_calls: toolUseBlocks.map((toolUse) => ({
|
|
8407
|
+
id: toolUse.id,
|
|
8408
|
+
type: "function",
|
|
8409
|
+
function: {
|
|
8410
|
+
name: getTruncatedToolName(toolUse.name, toolNameMapping),
|
|
8411
|
+
arguments: JSON.stringify(toolUse.input)
|
|
8412
|
+
}
|
|
8413
|
+
}))
|
|
8414
|
+
}] : [{
|
|
8415
|
+
role: "assistant",
|
|
8416
|
+
content: mapContent(message.content)
|
|
8417
|
+
}];
|
|
8418
|
+
}
|
|
8419
|
+
function mapContent(content) {
|
|
8420
|
+
if (typeof content === "string") return content;
|
|
8421
|
+
if (!Array.isArray(content)) return null;
|
|
8422
|
+
if (!content.some((block) => block.type === "image")) return content.filter((block) => block.type === "text" || block.type === "thinking").map((block) => block.type === "text" ? block.text : block.thinking).join("\n\n");
|
|
8423
|
+
const contentParts = [];
|
|
8424
|
+
for (const block of content) switch (block.type) {
|
|
8425
|
+
case "text":
|
|
8426
|
+
contentParts.push({
|
|
8427
|
+
type: "text",
|
|
8428
|
+
text: block.text
|
|
8429
|
+
});
|
|
8430
|
+
break;
|
|
8431
|
+
case "thinking":
|
|
8432
|
+
contentParts.push({
|
|
8433
|
+
type: "text",
|
|
8434
|
+
text: block.thinking
|
|
8435
|
+
});
|
|
8436
|
+
break;
|
|
8437
|
+
case "image":
|
|
8438
|
+
contentParts.push({
|
|
8439
|
+
type: "image_url",
|
|
8440
|
+
image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` }
|
|
8441
|
+
});
|
|
8442
|
+
break;
|
|
8982
8443
|
}
|
|
8983
|
-
return
|
|
8444
|
+
return contentParts;
|
|
8984
8445
|
}
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
consola.debug(`[Tools] Defined tools:`, JSON.stringify(toolInfo));
|
|
8446
|
+
function getTruncatedToolName(originalName, toolNameMapping) {
|
|
8447
|
+
if (originalName.length <= OPENAI_TOOL_NAME_LIMIT) return originalName;
|
|
8448
|
+
const existingTruncated = toolNameMapping.originalToTruncated.get(originalName);
|
|
8449
|
+
if (existingTruncated) return existingTruncated;
|
|
8450
|
+
let hash = 0;
|
|
8451
|
+
for (let i = 0; i < originalName.length; i++) {
|
|
8452
|
+
const char = originalName.codePointAt(i) ?? 0;
|
|
8453
|
+
hash = (hash << 5) - hash + char;
|
|
8454
|
+
hash = Math.trunc(hash);
|
|
8995
8455
|
}
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8456
|
+
const hashSuffix = Math.abs(hash).toString(36).slice(0, 8);
|
|
8457
|
+
const truncatedName = originalName.slice(0, OPENAI_TOOL_NAME_LIMIT - 9) + "_" + hashSuffix;
|
|
8458
|
+
toolNameMapping.originalToTruncated.set(originalName, truncatedName);
|
|
8459
|
+
consola.debug(`Truncated tool name: "${originalName}" -> "${truncatedName}"`);
|
|
8460
|
+
return truncatedName;
|
|
8461
|
+
}
|
|
8462
|
+
function translateAnthropicToolsToOpenAI(anthropicTools, toolNameMapping) {
|
|
8463
|
+
if (!anthropicTools) return;
|
|
8464
|
+
return anthropicTools.map((tool) => ({
|
|
8465
|
+
type: "function",
|
|
8466
|
+
function: {
|
|
8467
|
+
name: getTruncatedToolName(tool.name, toolNameMapping),
|
|
8468
|
+
description: tool.description,
|
|
8469
|
+
parameters: tool.input_schema ?? {}
|
|
8470
|
+
}
|
|
8471
|
+
}));
|
|
8472
|
+
}
|
|
8473
|
+
function translateAnthropicToolChoiceToOpenAI(anthropicToolChoice, toolNameMapping) {
|
|
8474
|
+
if (!anthropicToolChoice) return;
|
|
8475
|
+
switch (anthropicToolChoice.type) {
|
|
8476
|
+
case "auto": return "auto";
|
|
8477
|
+
case "any": return "required";
|
|
8478
|
+
case "tool":
|
|
8479
|
+
if (anthropicToolChoice.name) return {
|
|
8480
|
+
type: "function",
|
|
8481
|
+
function: { name: getTruncatedToolName(anthropicToolChoice.name, toolNameMapping) }
|
|
8482
|
+
};
|
|
8483
|
+
return;
|
|
8484
|
+
case "none": return "none";
|
|
8485
|
+
default: return;
|
|
8999
8486
|
}
|
|
9000
8487
|
}
|
|
9001
8488
|
|
|
@@ -9164,6 +8651,26 @@ const createResponses = async (payload, { vision, initiator, resolvedModel, sign
|
|
|
9164
8651
|
return await response.json();
|
|
9165
8652
|
};
|
|
9166
8653
|
|
|
8654
|
+
//#endregion
|
|
8655
|
+
//#region src/routes/responses/model-shortcut.ts
|
|
8656
|
+
/**
|
|
8657
|
+
* Client-facing shortcut aliases for /responses model ids.
|
|
8658
|
+
*
|
|
8659
|
+
* Copilot exposes gpt-5.6 only as three named variants — Luna (lightweight),
|
|
8660
|
+
* Sol (powerful), Terra (versatile). A client sending a bare "gpt-5.6" would
|
|
8661
|
+
* otherwise fail findModelById. Map the shortcut to Sol because upstream tags
|
|
8662
|
+
* it `model_picker_category: "powerful"` — the closest match for an
|
|
8663
|
+
* unqualified "GPT-5.6" ask.
|
|
8664
|
+
*
|
|
8665
|
+
* Applied inside `normalizePayload`, i.e. AFTER `captureRequestedModel`, so
|
|
8666
|
+
* the client-facing echo (ADR-0001) still shows the original "gpt-5.6".
|
|
8667
|
+
*/
|
|
8668
|
+
const SHORTCUT_MAP = Object.assign(Object.create(null), { "gpt-5.6": "gpt-5.6-sol" });
|
|
8669
|
+
function resolveResponsesModelShortcut(model) {
|
|
8670
|
+
if (typeof model !== "string") return model;
|
|
8671
|
+
return SHORTCUT_MAP[model.toLowerCase()] ?? model;
|
|
8672
|
+
}
|
|
8673
|
+
|
|
9167
8674
|
//#endregion
|
|
9168
8675
|
//#region src/routes/responses/stream-id-sync.ts
|
|
9169
8676
|
const createStreamIdTracker = () => ({ outputItems: /* @__PURE__ */ new Map() });
|
|
@@ -9402,7 +8909,12 @@ const handleResponses = async (c) => {
|
|
|
9402
8909
|
rawPayload,
|
|
9403
8910
|
endpoint: "openai",
|
|
9404
8911
|
normalizePayload: (p) => {
|
|
9405
|
-
const
|
|
8912
|
+
const resolvedModel = resolveResponsesModelShortcut(p.model);
|
|
8913
|
+
const withModel = resolvedModel === p.model ? p : {
|
|
8914
|
+
...p,
|
|
8915
|
+
model: resolvedModel
|
|
8916
|
+
};
|
|
8917
|
+
const np = state.normalizeResponsesCallIds ? normalizeCallIds(withModel) : withModel;
|
|
9406
8918
|
useFunctionApplyPatch(np);
|
|
9407
8919
|
filterUnsupportedBuiltins(np);
|
|
9408
8920
|
injectPromptCacheKey(np, clientName);
|