@dianshuv/copilot-api 0.19.0 → 0.20.1
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 +511 -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.1";
|
|
1016
1015
|
|
|
1017
1016
|
//#endregion
|
|
1018
1017
|
//#region src/lib/event-loop-lag.ts
|
|
@@ -2450,6 +2449,8 @@ const HIDDEN_MODEL_IDS = new Set([
|
|
|
2450
2449
|
"gpt-41-copilot",
|
|
2451
2450
|
"gpt-5-mini",
|
|
2452
2451
|
"gpt-5.3-codex",
|
|
2452
|
+
"gpt-5.4",
|
|
2453
|
+
"gpt-5.4-nano",
|
|
2453
2454
|
"text-embedding-ada-002",
|
|
2454
2455
|
"text-embedding-3-small",
|
|
2455
2456
|
"text-embedding-3-small-inference",
|
|
@@ -2460,7 +2461,9 @@ const HIDDEN_MODEL_IDS = new Set([
|
|
|
2460
2461
|
"claude-opus-4.7-high",
|
|
2461
2462
|
"claude-opus-4.7-xhigh",
|
|
2462
2463
|
"claude-sonnet-4.5",
|
|
2464
|
+
"claude-sonnet-4.6",
|
|
2463
2465
|
"mai-code-1-flash-internal",
|
|
2466
|
+
"mai-code-1-flash-picker",
|
|
2464
2467
|
"trajectory-compaction"
|
|
2465
2468
|
]);
|
|
2466
2469
|
function isHiddenModel(id, showAll) {
|
|
@@ -3567,9 +3570,8 @@ async function autoTruncateOpenAI(payload, model, config = {}) {
|
|
|
3567
3570
|
*
|
|
3568
3571
|
* Pre-flight steps for any request whose final payload is an OpenAI
|
|
3569
3572
|
* 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).
|
|
3573
|
+
* and the non-streaming type guard. Used by `routes/chat-completions`
|
|
3574
|
+
* (native OpenAI).
|
|
3573
3575
|
*/
|
|
3574
3576
|
/** Type guard for non-streaming responses */
|
|
3575
3577
|
function isNonStreaming(response) {
|
|
@@ -3777,89 +3779,6 @@ function createStreamRepetitionChecker(label, config) {
|
|
|
3777
3779
|
};
|
|
3778
3780
|
}
|
|
3779
3781
|
|
|
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
3782
|
//#endregion
|
|
3864
3783
|
//#region src/services/copilot/create-chat-completions.ts
|
|
3865
3784
|
const GPT_MODEL_PATTERN = /^gpt-/i;
|
|
@@ -3880,21 +3799,15 @@ const createChatCompletions = async (payload, options) => {
|
|
|
3880
3799
|
const enableVision = wire.messages.some((x) => typeof x.content !== "string" && x.content?.some((x) => x.type === "image_url"));
|
|
3881
3800
|
const isAgentCall = wire.messages.some((msg) => ["assistant", "tool"].includes(msg.role));
|
|
3882
3801
|
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
3802
|
const response = await copilotFetch("/chat/completions", {
|
|
3896
3803
|
method: "POST",
|
|
3897
|
-
headers
|
|
3804
|
+
headers: {
|
|
3805
|
+
...copilotHeaders(state, {
|
|
3806
|
+
vision: enableVision && modelSupportsVision,
|
|
3807
|
+
modelRequestHeaders: options?.resolvedModel?.request_headers
|
|
3808
|
+
}),
|
|
3809
|
+
"X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user")
|
|
3810
|
+
},
|
|
3898
3811
|
body: JSON.stringify(wire),
|
|
3899
3812
|
signal: options?.signal
|
|
3900
3813
|
});
|
|
@@ -4347,12 +4260,12 @@ async function executeRequest(opts) {
|
|
|
4347
4260
|
signal: abort.signal
|
|
4348
4261
|
}));
|
|
4349
4262
|
ctx.queueWaitMs = queueWaitMs;
|
|
4350
|
-
if (isNonStreaming(response)) return handleNonStreamingResponse
|
|
4263
|
+
if (isNonStreaming(response)) return handleNonStreamingResponse(c, response, ctx, payload);
|
|
4351
4264
|
consola.debug("Streaming response");
|
|
4352
4265
|
updateTrackerStatus(ctx.trackingId, "streaming");
|
|
4353
4266
|
return streamSSE(c, async (stream) => {
|
|
4354
4267
|
stream.onAbort(() => abort.abort());
|
|
4355
|
-
await handleStreamingResponse
|
|
4268
|
+
await handleStreamingResponse({
|
|
4356
4269
|
stream,
|
|
4357
4270
|
response,
|
|
4358
4271
|
payload,
|
|
@@ -4381,7 +4294,7 @@ async function logTokenCount(payload, selectedModel) {
|
|
|
4381
4294
|
consola.debug("Failed to calculate token count:", error);
|
|
4382
4295
|
}
|
|
4383
4296
|
}
|
|
4384
|
-
function handleNonStreamingResponse
|
|
4297
|
+
function handleNonStreamingResponse(c, originalResponse, ctx, payload) {
|
|
4385
4298
|
consola.debug("Non-streaming response:", JSON.stringify(originalResponse));
|
|
4386
4299
|
let response = originalResponse;
|
|
4387
4300
|
if (state.verbose && ctx.truncateResult?.wasCompacted && response.choices[0]?.message.content) {
|
|
@@ -4474,7 +4387,7 @@ function createStreamAccumulator() {
|
|
|
4474
4387
|
toolCallMap: /* @__PURE__ */ new Map()
|
|
4475
4388
|
};
|
|
4476
4389
|
}
|
|
4477
|
-
async function handleStreamingResponse
|
|
4390
|
+
async function handleStreamingResponse(opts) {
|
|
4478
4391
|
const { stream, response, payload, ctx } = opts;
|
|
4479
4392
|
const acc = createStreamAccumulator();
|
|
4480
4393
|
const checkRepetition = createStreamRepetitionChecker(`openai:${payload.model}`);
|
|
@@ -6608,6 +6521,46 @@ async function checkNeedsCompactionAnthropic(payload, model, config = {}) {
|
|
|
6608
6521
|
};
|
|
6609
6522
|
}
|
|
6610
6523
|
|
|
6524
|
+
//#endregion
|
|
6525
|
+
//#region src/lib/anthropic/beta.ts
|
|
6526
|
+
/**
|
|
6527
|
+
* Vendor-neutral utilities for manipulating the `anthropic-beta` request header.
|
|
6528
|
+
*
|
|
6529
|
+
* Lives in `lib/anthropic/` (not in either transport module) so both the
|
|
6530
|
+
* Anthropic-native and OpenAI-translated transport layers can share these
|
|
6531
|
+
* helpers without introducing cross-transport imports.
|
|
6532
|
+
*/
|
|
6533
|
+
/** Anthropic beta feature that unlocks the 1M context window. */
|
|
6534
|
+
const CONTEXT_1M_BETA_FEATURE = "context-1m-2025-08-07";
|
|
6535
|
+
/**
|
|
6536
|
+
* Merge two comma-separated anthropic-beta header values. Trims whitespace,
|
|
6537
|
+
* drops empty tokens, and dedupes by exact string match. Returns a canonical
|
|
6538
|
+
* comma-joined string with no spaces.
|
|
6539
|
+
*
|
|
6540
|
+
* Either input may be undefined / empty.
|
|
6541
|
+
*/
|
|
6542
|
+
function mergeBetaFeatures(existing, incoming) {
|
|
6543
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6544
|
+
const out = [];
|
|
6545
|
+
for (const raw of [existing, incoming]) {
|
|
6546
|
+
if (!raw) continue;
|
|
6547
|
+
for (const part of raw.split(",")) {
|
|
6548
|
+
const f = part.trim();
|
|
6549
|
+
if (f.length === 0 || seen.has(f)) continue;
|
|
6550
|
+
seen.add(f);
|
|
6551
|
+
out.push(f);
|
|
6552
|
+
}
|
|
6553
|
+
}
|
|
6554
|
+
return out.join(",");
|
|
6555
|
+
}
|
|
6556
|
+
/**
|
|
6557
|
+
* Append the context-1m feature to an anthropic-beta header value, deduping
|
|
6558
|
+
* any prior occurrence. Returns the merged comma-separated string.
|
|
6559
|
+
*/
|
|
6560
|
+
function appendContext1mBeta(existing) {
|
|
6561
|
+
return mergeBetaFeatures(existing, CONTEXT_1M_BETA_FEATURE);
|
|
6562
|
+
}
|
|
6563
|
+
|
|
6611
6564
|
//#endregion
|
|
6612
6565
|
//#region src/lib/anthropic/features.ts
|
|
6613
6566
|
function normalizeForMatching(modelId) {
|
|
@@ -6758,6 +6711,37 @@ function filterServerToolBlocksFromResponse(response) {
|
|
|
6758
6711
|
};
|
|
6759
6712
|
}
|
|
6760
6713
|
|
|
6714
|
+
//#endregion
|
|
6715
|
+
//#region src/lib/headers.ts
|
|
6716
|
+
/**
|
|
6717
|
+
* Vendor-neutral header-bag helpers.
|
|
6718
|
+
*
|
|
6719
|
+
* HTTP header names are case-insensitive, but a plain-object header bag is
|
|
6720
|
+
* case-sensitive on its keys. Code that wants to look up "anthropic-beta"
|
|
6721
|
+
* without knowing whether some other producer wrote "Anthropic-Beta" needs
|
|
6722
|
+
* `findHeaderKey`. Code that wants to set a header without creating a
|
|
6723
|
+
* second case variant of the same name needs `setHeader`.
|
|
6724
|
+
*/
|
|
6725
|
+
/** Case-insensitive lookup of a header key in a plain-object header bag. */
|
|
6726
|
+
function findHeaderKey(headers, name) {
|
|
6727
|
+
const lower = name.toLowerCase();
|
|
6728
|
+
return Object.keys(headers).find((k) => k.toLowerCase() === lower);
|
|
6729
|
+
}
|
|
6730
|
+
/** Case-insensitive read of a header value. */
|
|
6731
|
+
function getHeader(headers, name) {
|
|
6732
|
+
const key = findHeaderKey(headers, name);
|
|
6733
|
+
return key === void 0 ? void 0 : headers[key];
|
|
6734
|
+
}
|
|
6735
|
+
/**
|
|
6736
|
+
* Set a header value at the existing case variant if one is present, else at
|
|
6737
|
+
* the supplied canonical name. Prevents a second key (different case) from
|
|
6738
|
+
* being added for the same logical header.
|
|
6739
|
+
*/
|
|
6740
|
+
function setHeader(headers, name, value) {
|
|
6741
|
+
const key = findHeaderKey(headers, name) ?? name;
|
|
6742
|
+
headers[key] = value;
|
|
6743
|
+
}
|
|
6744
|
+
|
|
6761
6745
|
//#endregion
|
|
6762
6746
|
//#region src/services/copilot/create-anthropic-messages.ts
|
|
6763
6747
|
/**
|
|
@@ -6960,11 +6944,12 @@ function resolveAnthropicModelForDirectPath(modelId) {
|
|
|
6960
6944
|
}
|
|
6961
6945
|
}
|
|
6962
6946
|
/**
|
|
6963
|
-
* Check if a model supports direct Anthropic API.
|
|
6964
|
-
*
|
|
6947
|
+
* Check if a model supports the native direct Anthropic API on Copilot.
|
|
6948
|
+
* True iff the model resolves to an Anthropic-vendor model (see
|
|
6949
|
+
* resolveAnthropicModelForDirectPath). `/v1/messages` serves these only;
|
|
6950
|
+
* the OpenAI-translation fallback was removed (docs/adr/0004-...).
|
|
6965
6951
|
*/
|
|
6966
6952
|
function supportsDirectAnthropicApi(modelId) {
|
|
6967
|
-
if (state.redirectAnthropic) return false;
|
|
6968
6953
|
return resolveAnthropicModelForDirectPath(modelId) !== void 0;
|
|
6969
6954
|
}
|
|
6970
6955
|
|
|
@@ -7161,15 +7146,6 @@ function extractToolCallsFromContent(content) {
|
|
|
7161
7146
|
});
|
|
7162
7147
|
return tools.length > 0 ? tools : void 0;
|
|
7163
7148
|
}
|
|
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
7149
|
function prependMarkerToResponse(response, marker) {
|
|
7174
7150
|
if (!marker) return response;
|
|
7175
7151
|
const content = [...response.content];
|
|
@@ -7290,555 +7266,79 @@ function recordAnthropicStreamingResponse(acc, fallbackModel, ctx) {
|
|
|
7290
7266
|
}
|
|
7291
7267
|
|
|
7292
7268
|
//#endregion
|
|
7293
|
-
//#region src/routes/messages/
|
|
7294
|
-
const OPENAI_TOOL_NAME_LIMIT = 64;
|
|
7269
|
+
//#region src/routes/messages/stream-translation.ts
|
|
7295
7270
|
/**
|
|
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
|
|
7271
|
+
* Wrap an arbitrary error into an Anthropic-native `error` stream event.
|
|
7301
7272
|
*
|
|
7302
|
-
*
|
|
7273
|
+
* Shared by the direct Anthropic path (`direct-anthropic-handler.ts`) to emit a
|
|
7274
|
+
* client-facing error frame mid-stream. The OpenAI→Anthropic response
|
|
7275
|
+
* translation that once also lived here was removed with the translation
|
|
7276
|
+
* fallback (see docs/adr/0004-drop-openai-translation-fallback-for-messages.md).
|
|
7303
7277
|
*/
|
|
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
|
-
}
|
|
7278
|
+
function translateErrorToAnthropicErrorEvent(error) {
|
|
7279
|
+
return {
|
|
7280
|
+
type: "error",
|
|
7281
|
+
error: {
|
|
7282
|
+
type: "api_error",
|
|
7283
|
+
message: error ? formatError(error) : "An unexpected error occurred during streaming."
|
|
7325
7284
|
}
|
|
7285
|
+
};
|
|
7286
|
+
}
|
|
7287
|
+
|
|
7288
|
+
//#endregion
|
|
7289
|
+
//#region src/routes/messages/tool-call-recovery.ts
|
|
7290
|
+
const ENVELOPE = String.raw`(?:<(?:antml:)?function_calls>|call|count|court)`;
|
|
7291
|
+
const INVOKE_BODY = String.raw`<(?:antml:)?invoke\s+name="[^"]+">(?:(?!<(?:antml:)?invoke\b)[\s\S])*?</(?:antml:)?invoke>`;
|
|
7292
|
+
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");
|
|
7293
|
+
const STARTS_WITH_ENVELOPE_RE = new RegExp(String.raw`^[ \t\n]*` + ENVELOPE);
|
|
7294
|
+
const INVOKE_OPENER_RE = new RegExp(String.raw`(?:^|\n)[ \t]*(?:` + ENVELOPE + String.raw`[ \t\n]*)?<(?:antml:)?invoke\b[^\n>]*>?`, "g");
|
|
7295
|
+
const LEAK_OPEN_RE = new RegExp(String.raw`(?:^|\n)[ \t]*(?:` + ENVELOPE + String.raw`[ \t\n]*)?<(?:antml:)?invoke\s+name="`, "g");
|
|
7296
|
+
const INVOKE_RE = /<(?:antml:)?invoke\s+name="([^"]+)">([\s\S]*?)<\/(?:antml:)?invoke>/g;
|
|
7297
|
+
const PARAMETER_RE = /<(?:antml:)?parameter\s+name="([^"]+)">([\s\S]*?)<\/(?:antml:)?parameter>/g;
|
|
7298
|
+
function coerceParamValue(raw) {
|
|
7299
|
+
const trimmed = raw.trim();
|
|
7300
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) try {
|
|
7301
|
+
return JSON.parse(trimmed);
|
|
7302
|
+
} catch {
|
|
7303
|
+
return raw;
|
|
7326
7304
|
}
|
|
7327
|
-
return
|
|
7305
|
+
return raw;
|
|
7328
7306
|
}
|
|
7329
|
-
function
|
|
7330
|
-
const
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
};
|
|
7307
|
+
function parseRegionInvokes(region, knownTools) {
|
|
7308
|
+
const calls = [];
|
|
7309
|
+
for (const invokeMatch of region.matchAll(INVOKE_RE)) {
|
|
7310
|
+
const name = invokeMatch[1];
|
|
7311
|
+
if (knownTools && !knownTools.has(name)) continue;
|
|
7312
|
+
const input = {};
|
|
7313
|
+
for (const paramMatch of invokeMatch[2].matchAll(PARAMETER_RE)) input[paramMatch[1]] = coerceParamValue(paramMatch[2]);
|
|
7314
|
+
calls.push({
|
|
7315
|
+
name,
|
|
7316
|
+
input
|
|
7317
|
+
});
|
|
7318
|
+
}
|
|
7319
|
+
return calls;
|
|
7320
|
+
}
|
|
7321
|
+
const FENCE_DELIM_RE = /(?:^|\n)[ \t]*```/g;
|
|
7322
|
+
function insideFence(text, pos) {
|
|
7323
|
+
let openAt = -1;
|
|
7324
|
+
for (const m of text.matchAll(FENCE_DELIM_RE)) if (openAt === -1) {
|
|
7325
|
+
if (m.index >= pos) break;
|
|
7326
|
+
openAt = m.index;
|
|
7327
|
+
} else if (m.index > pos) return true;
|
|
7328
|
+
else openAt = -1;
|
|
7329
|
+
return false;
|
|
7330
|
+
}
|
|
7331
|
+
function regionIsLeak(region, offset, fullText, knownTools) {
|
|
7332
|
+
if (insideFence(fullText, offset)) return false;
|
|
7333
|
+
if (STARTS_WITH_ENVELOPE_RE.test(region)) return true;
|
|
7334
|
+
if (!knownTools) return false;
|
|
7335
|
+
return parseRegionInvokes(region, knownTools).length > 0;
|
|
7350
7336
|
}
|
|
7351
7337
|
/**
|
|
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.
|
|
7338
|
+
* Split assistant text into ordered segments — dropping leaked envelope markup
|
|
7339
|
+
* (and undeclared-tool invokes) while preserving the natural-language on either
|
|
7340
|
+
* side and the pre/tool/post ordering. Returns a single text segment when there
|
|
7341
|
+
* is no leak. Shared by both response recovery paths so they cannot diverge.
|
|
7842
7342
|
*
|
|
7843
7343
|
* `from` restricts the EMITTED window to `text.slice(from)` while still
|
|
7844
7344
|
* classifying against the FULL `text` — so a streaming capture that started just
|
|
@@ -8643,359 +8143,350 @@ const parseSubagentMarkerFromSystemReminder = (text) => {
|
|
|
8643
8143
|
};
|
|
8644
8144
|
|
|
8645
8145
|
//#endregion
|
|
8646
|
-
//#region src/routes/messages/
|
|
8146
|
+
//#region src/routes/messages/handler.ts
|
|
8147
|
+
function resolveModelFromBetaHeader(model, betaHeader) {
|
|
8148
|
+
if (!betaHeader || !/\bcontext-1m\b/.test(betaHeader)) return model;
|
|
8149
|
+
if (!model.startsWith("claude-")) return model;
|
|
8150
|
+
if (model.endsWith("-1m")) return model;
|
|
8151
|
+
const resolved = `${model}-1m`;
|
|
8152
|
+
consola.debug(`Detected context-1m in anthropic-beta header, resolving model: ${model} → ${resolved}`);
|
|
8153
|
+
return resolved;
|
|
8154
|
+
}
|
|
8155
|
+
async function handleCompletion(c) {
|
|
8156
|
+
const rawPayload = await c.req.json();
|
|
8157
|
+
consola.debug("Anthropic request payload:", JSON.stringify(rawPayload));
|
|
8158
|
+
if (rawPayload === null || typeof rawPayload.model !== "string" || rawPayload.model.length === 0) return c.json({
|
|
8159
|
+
type: "error",
|
|
8160
|
+
error: {
|
|
8161
|
+
type: "invalid_request_error",
|
|
8162
|
+
message: "model is required and must be a non-empty string"
|
|
8163
|
+
}
|
|
8164
|
+
}, 400);
|
|
8165
|
+
const normalizedModel = resolveModelFromBetaHeader(rawPayload.model, c.req.header("anthropic-beta"));
|
|
8166
|
+
if (!supportsDirectAnthropicApi(normalizedModel)) return c.json({
|
|
8167
|
+
type: "error",
|
|
8168
|
+
error: {
|
|
8169
|
+
type: "invalid_request_error",
|
|
8170
|
+
message: `model \`${normalizedModel}\` is not an Anthropic model available on /v1/messages`
|
|
8171
|
+
}
|
|
8172
|
+
}, 400);
|
|
8173
|
+
const { ctx, payload: anthropicPayload } = createEntryContext({
|
|
8174
|
+
c,
|
|
8175
|
+
rawPayload,
|
|
8176
|
+
endpoint: "anthropic",
|
|
8177
|
+
normalizePayload: (p) => ({
|
|
8178
|
+
...p,
|
|
8179
|
+
model: resolveModelFromBetaHeader(p.model, c.req.header("anthropic-beta"))
|
|
8180
|
+
}),
|
|
8181
|
+
buildHistoryRequest: (p) => ({
|
|
8182
|
+
model: p.model,
|
|
8183
|
+
messages: convertAnthropicMessages(p.messages),
|
|
8184
|
+
stream: p.stream ?? false,
|
|
8185
|
+
tools: p.tools?.map((t) => ({
|
|
8186
|
+
name: t.name,
|
|
8187
|
+
description: t.description
|
|
8188
|
+
})),
|
|
8189
|
+
max_tokens: p.max_tokens,
|
|
8190
|
+
temperature: p.temperature,
|
|
8191
|
+
system: extractSystemPrompt(p.system)
|
|
8192
|
+
})
|
|
8193
|
+
});
|
|
8194
|
+
const sanitizedPayload = dePoisonAssistantMessages(anthropicPayload);
|
|
8195
|
+
logToolInfo(sanitizedPayload);
|
|
8196
|
+
const subagentMarker = parseSubagentMarkerFromFirstUser(sanitizedPayload);
|
|
8197
|
+
const initiatorOverride = subagentMarker ? "agent" : void 0;
|
|
8198
|
+
if (subagentMarker) consola.debug("Detected Subagent marker:", JSON.stringify(subagentMarker));
|
|
8199
|
+
normalizeSystemPromptDate(sanitizedPayload);
|
|
8200
|
+
injectSystemCacheControl(sanitizedPayload);
|
|
8201
|
+
return handleDirectAnthropicCompletion(c, sanitizedPayload, ctx, initiatorOverride);
|
|
8202
|
+
}
|
|
8647
8203
|
/**
|
|
8648
|
-
*
|
|
8204
|
+
* Log tool-related information for debugging
|
|
8649
8205
|
*/
|
|
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
|
|
8206
|
+
function logToolInfo(anthropicPayload) {
|
|
8207
|
+
if (anthropicPayload.tools?.length) {
|
|
8208
|
+
const toolInfo = anthropicPayload.tools.map((t) => ({
|
|
8209
|
+
name: t.name,
|
|
8210
|
+
type: t.type ?? "(custom)"
|
|
8211
|
+
}));
|
|
8212
|
+
consola.debug(`[Tools] Defined tools:`, JSON.stringify(toolInfo));
|
|
8213
|
+
}
|
|
8214
|
+
for (const msg of anthropicPayload.messages) if (typeof msg.content !== "string") for (const block of msg.content) {
|
|
8215
|
+
if (block.type === "tool_use") consola.debug(`[Tools] tool_use in message: ${block.name} (id: ${block.id})`);
|
|
8216
|
+
if (block.type === "tool_result") consola.debug(`[Tools] tool_result in message: id=${block.tool_use_id}, is_error=${block.is_error ?? false}`);
|
|
8217
|
+
}
|
|
8218
|
+
}
|
|
8219
|
+
|
|
8220
|
+
//#endregion
|
|
8221
|
+
//#region src/routes/messages/non-stream-translation.ts
|
|
8222
|
+
const OPENAI_TOOL_NAME_LIMIT = 64;
|
|
8223
|
+
/**
|
|
8224
|
+
* Ensure all tool_use blocks have corresponding tool_result responses.
|
|
8225
|
+
* This handles edge cases where conversation history may be incomplete:
|
|
8226
|
+
* - Session interruptions where tool execution was cut off
|
|
8227
|
+
* - Previous request failures
|
|
8228
|
+
* - Client sending truncated history
|
|
8229
|
+
*
|
|
8230
|
+
* Adding placeholder responses prevents API errors and maintains protocol compliance.
|
|
8231
|
+
*/
|
|
8232
|
+
function fixMessageSequence(messages) {
|
|
8233
|
+
const fixedMessages = [];
|
|
8234
|
+
for (let i = 0; i < messages.length; i++) {
|
|
8235
|
+
const message = messages[i];
|
|
8236
|
+
fixedMessages.push(message);
|
|
8237
|
+
if (message.role === "assistant" && message.tool_calls && message.tool_calls.length > 0) {
|
|
8238
|
+
const foundToolResponses = /* @__PURE__ */ new Set();
|
|
8239
|
+
let j = i + 1;
|
|
8240
|
+
while (j < messages.length && messages[j].role === "tool") {
|
|
8241
|
+
const toolMessage = messages[j];
|
|
8242
|
+
if (toolMessage.tool_call_id) foundToolResponses.add(toolMessage.tool_call_id);
|
|
8243
|
+
j++;
|
|
8244
|
+
}
|
|
8245
|
+
for (const toolCall of message.tool_calls) if (!foundToolResponses.has(toolCall.id)) {
|
|
8246
|
+
consola.debug(`Adding placeholder tool_result for ${toolCall.id}`);
|
|
8247
|
+
fixedMessages.push({
|
|
8248
|
+
role: "tool",
|
|
8249
|
+
tool_call_id: toolCall.id,
|
|
8250
|
+
content: "Tool execution was interrupted or failed."
|
|
8704
8251
|
});
|
|
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 });
|
|
8252
|
+
}
|
|
8753
8253
|
}
|
|
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
8254
|
}
|
|
8255
|
+
return fixedMessages;
|
|
8758
8256
|
}
|
|
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
|
|
8257
|
+
function translateToOpenAI(payload) {
|
|
8258
|
+
const toolNameMapping = { originalToTruncated: /* @__PURE__ */ new Map() };
|
|
8259
|
+
const messages = translateAnthropicMessagesToOpenAI(payload.messages, payload.system, toolNameMapping);
|
|
8260
|
+
return { payload: {
|
|
8261
|
+
model: translateModelName(payload.model),
|
|
8262
|
+
messages: fixMessageSequence(messages),
|
|
8263
|
+
max_tokens: payload.max_tokens,
|
|
8264
|
+
stop: payload.stop_sequences,
|
|
8265
|
+
stream: payload.stream,
|
|
8266
|
+
temperature: payload.temperature,
|
|
8267
|
+
top_p: payload.top_p,
|
|
8268
|
+
user: payload.metadata?.user_id,
|
|
8269
|
+
tools: translateAnthropicToolsToOpenAI(payload.tools, toolNameMapping),
|
|
8270
|
+
tool_choice: translateAnthropicToolChoiceToOpenAI(payload.tool_choice, toolNameMapping)
|
|
8271
|
+
} };
|
|
8272
|
+
}
|
|
8273
|
+
/**
|
|
8274
|
+
* Find the latest available model matching a family prefix.
|
|
8275
|
+
* Searches state.models for models starting with the given prefix
|
|
8276
|
+
* and returns the one with the highest version number.
|
|
8277
|
+
*
|
|
8278
|
+
* @param familyPrefix - e.g., "claude-opus", "claude-sonnet", "claude-haiku"
|
|
8279
|
+
* @param fallback - fallback model ID if no match found
|
|
8280
|
+
*/
|
|
8281
|
+
function findLatestModel(familyPrefix, fallback) {
|
|
8282
|
+
const models = state.models?.data;
|
|
8283
|
+
if (!models || models.length === 0) return fallback;
|
|
8284
|
+
const candidates = models.filter((m) => m.id.startsWith(familyPrefix));
|
|
8285
|
+
if (candidates.length === 0) return fallback;
|
|
8286
|
+
candidates.sort((a, b) => {
|
|
8287
|
+
const [aMajor, aMinor] = extractVersion(a.id, familyPrefix);
|
|
8288
|
+
const [bMajor, bMinor] = extractVersion(b.id, familyPrefix);
|
|
8289
|
+
if (aMajor !== bMajor) return bMajor - aMajor;
|
|
8290
|
+
return bMinor - aMinor;
|
|
8814
8291
|
});
|
|
8815
|
-
return
|
|
8292
|
+
return candidates[0].id;
|
|
8816
8293
|
}
|
|
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
|
-
}
|
|
8294
|
+
/**
|
|
8295
|
+
* Extract numeric [major, minor] version from a model id.
|
|
8296
|
+
*
|
|
8297
|
+
* Supports both naming conventions Anthropic/Copilot have used:
|
|
8298
|
+
* - dot: "claude-opus-4.5" → [4, 5]
|
|
8299
|
+
* - dash: "claude-opus-4-8" → [4, 8]
|
|
8300
|
+
* - dash double-digit: "claude-opus-4-10" → [4, 10]
|
|
8301
|
+
*
|
|
8302
|
+
* The dash form previously parsed as just the major via the regex
|
|
8303
|
+
* /^(\d+(?:\.\d+)?)/ (the dash stopped the match), which silently
|
|
8304
|
+
* downgraded dash-named candidates against any dot-named candidate in
|
|
8305
|
+
* findLatestModel. Parsing into a tuple also avoids the parseFloat
|
|
8306
|
+
* lossiness on double-digit minors ("4.10" → 4.1).
|
|
8307
|
+
*
|
|
8308
|
+
* Anything after the major/minor segment (date stamps, "-1m") is ignored.
|
|
8309
|
+
* The minor capture is bounded to 1-3 digits so that an 8-digit date suffix
|
|
8310
|
+
* directly after the major (e.g. "claude-opus-4-20250514") is NOT mistaken
|
|
8311
|
+
* for a minor version of 20_250_514 — without that bound, dated ids would
|
|
8312
|
+
* outrank legitimate dotted candidates like "claude-opus-4.8" in
|
|
8313
|
+
* findLatestModel's sort.
|
|
8314
|
+
*
|
|
8315
|
+
* Returns [0, 0] when no version can be extracted.
|
|
8316
|
+
*/
|
|
8317
|
+
function extractVersion(modelId, prefix) {
|
|
8318
|
+
const match = modelId.slice(prefix.length + 1).match(/^(\d+)(?:[.-](\d{1,3}))?/);
|
|
8319
|
+
if (!match) return [0, 0];
|
|
8320
|
+
const major = Number.parseInt(match[1], 10);
|
|
8321
|
+
const rawMinor = match[2];
|
|
8322
|
+
return [major, rawMinor === void 0 ? 0 : Number.parseInt(rawMinor, 10) || 0];
|
|
8875
8323
|
}
|
|
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
|
|
8324
|
+
function translateModelName(model) {
|
|
8325
|
+
const aliasMap = {
|
|
8326
|
+
opus: "claude-opus",
|
|
8327
|
+
sonnet: "claude-sonnet",
|
|
8328
|
+
haiku: "claude-haiku"
|
|
8904
8329
|
};
|
|
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
|
-
}
|
|
8330
|
+
if (aliasMap[model]) {
|
|
8331
|
+
const familyPrefix = aliasMap[model];
|
|
8332
|
+
return findLatestModel(familyPrefix, `${familyPrefix}-4.5`);
|
|
8936
8333
|
}
|
|
8334
|
+
if (/^claude-sonnet-4-5-\d+$/.test(model)) return "claude-sonnet-4.5";
|
|
8335
|
+
if (/^claude-sonnet-4-\d+$/.test(model)) return "claude-sonnet-4";
|
|
8336
|
+
if (model === "claude-opus-4-8-1m") return "claude-opus-4.8";
|
|
8337
|
+
if (model === "claude-opus-4-8") return "claude-opus-4.8";
|
|
8338
|
+
if (model === "claude-opus-4-7-1m") return "claude-opus-4.7";
|
|
8339
|
+
if (/^claude-opus-4-7$/.test(model)) return "claude-opus-4.7";
|
|
8340
|
+
if (model === "claude-opus-4-6-1m") return "claude-opus-4.6-1m";
|
|
8341
|
+
if (/^claude-opus-4-6$/.test(model)) return "claude-opus-4.6";
|
|
8342
|
+
if (/^claude-opus-4-5-\d+$/.test(model)) return "claude-opus-4.5";
|
|
8343
|
+
if (/^claude-opus-4-\d+$/.test(model)) return findLatestModel("claude-opus", "claude-opus-4.5");
|
|
8344
|
+
if (/^claude-haiku-4-5-\d+$/.test(model)) return "claude-haiku-4.5";
|
|
8345
|
+
if (/^claude-haiku-3-5-\d+$/.test(model)) return findLatestModel("claude-haiku", "claude-haiku-4.5");
|
|
8346
|
+
return model;
|
|
8937
8347
|
}
|
|
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;
|
|
8348
|
+
function translateAnthropicMessagesToOpenAI(anthropicMessages, system, toolNameMapping) {
|
|
8349
|
+
const systemMessages = handleSystemPrompt(system);
|
|
8350
|
+
const otherMessages = anthropicMessages.flatMap((message) => message.role === "user" ? handleUserMessage(message) : handleAssistantMessage(message, toolNameMapping));
|
|
8351
|
+
return [...systemMessages, ...otherMessages];
|
|
8948
8352
|
}
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
|
|
8963
|
-
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
8968
|
-
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
8353
|
+
const RESERVED_KEYWORDS = ["x-anthropic-billing-header", "x-anthropic-billing"];
|
|
8354
|
+
/**
|
|
8355
|
+
* Filter out reserved keywords from system prompt text.
|
|
8356
|
+
* Copilot API rejects requests containing these keywords.
|
|
8357
|
+
* Removes the entire line containing the keyword to keep the prompt clean.
|
|
8358
|
+
*/
|
|
8359
|
+
function filterReservedKeywords(text) {
|
|
8360
|
+
let filtered = text;
|
|
8361
|
+
for (const keyword of RESERVED_KEYWORDS) if (text.includes(keyword)) {
|
|
8362
|
+
consola.debug(`[Reserved Keyword] Removing line containing "${keyword}"`);
|
|
8363
|
+
filtered = filtered.split("\n").filter((line) => !line.includes(keyword)).join("\n");
|
|
8364
|
+
}
|
|
8365
|
+
return filtered;
|
|
8366
|
+
}
|
|
8367
|
+
function handleSystemPrompt(system) {
|
|
8368
|
+
if (!system) return [];
|
|
8369
|
+
if (typeof system === "string") return [{
|
|
8370
|
+
role: "system",
|
|
8371
|
+
content: filterReservedKeywords(system)
|
|
8372
|
+
}];
|
|
8373
|
+
else return [{
|
|
8374
|
+
role: "system",
|
|
8375
|
+
content: filterReservedKeywords(system.map((block) => block.text).join("\n\n"))
|
|
8376
|
+
}];
|
|
8377
|
+
}
|
|
8378
|
+
function handleUserMessage(message) {
|
|
8379
|
+
const newMessages = [];
|
|
8380
|
+
if (Array.isArray(message.content)) {
|
|
8381
|
+
const toolResultBlocks = message.content.filter((block) => block.type === "tool_result");
|
|
8382
|
+
const otherBlocks = message.content.filter((block) => block.type !== "tool_result");
|
|
8383
|
+
for (const block of toolResultBlocks) newMessages.push({
|
|
8384
|
+
role: "tool",
|
|
8385
|
+
tool_call_id: block.tool_use_id,
|
|
8386
|
+
content: mapContent(block.content)
|
|
8387
|
+
});
|
|
8388
|
+
if (otherBlocks.length > 0) newMessages.push({
|
|
8389
|
+
role: "user",
|
|
8390
|
+
content: mapContent(otherBlocks)
|
|
8391
|
+
});
|
|
8392
|
+
} else newMessages.push({
|
|
8393
|
+
role: "user",
|
|
8394
|
+
content: mapContent(message.content)
|
|
8972
8395
|
});
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8396
|
+
return newMessages;
|
|
8397
|
+
}
|
|
8398
|
+
function handleAssistantMessage(message, toolNameMapping) {
|
|
8399
|
+
if (!Array.isArray(message.content)) return [{
|
|
8400
|
+
role: "assistant",
|
|
8401
|
+
content: mapContent(message.content)
|
|
8402
|
+
}];
|
|
8403
|
+
const toolUseBlocks = message.content.filter((block) => block.type === "tool_use");
|
|
8404
|
+
const textBlocks = message.content.filter((block) => block.type === "text");
|
|
8405
|
+
const thinkingBlocks = message.content.filter((block) => block.type === "thinking");
|
|
8406
|
+
const allTextContent = [...textBlocks.map((b) => b.text), ...thinkingBlocks.map((b) => b.thinking)].join("\n\n");
|
|
8407
|
+
return toolUseBlocks.length > 0 ? [{
|
|
8408
|
+
role: "assistant",
|
|
8409
|
+
content: allTextContent || null,
|
|
8410
|
+
tool_calls: toolUseBlocks.map((toolUse) => ({
|
|
8411
|
+
id: toolUse.id,
|
|
8412
|
+
type: "function",
|
|
8413
|
+
function: {
|
|
8414
|
+
name: getTruncatedToolName(toolUse.name, toolNameMapping),
|
|
8415
|
+
arguments: JSON.stringify(toolUse.input)
|
|
8416
|
+
}
|
|
8417
|
+
}))
|
|
8418
|
+
}] : [{
|
|
8419
|
+
role: "assistant",
|
|
8420
|
+
content: mapContent(message.content)
|
|
8421
|
+
}];
|
|
8422
|
+
}
|
|
8423
|
+
function mapContent(content) {
|
|
8424
|
+
if (typeof content === "string") return content;
|
|
8425
|
+
if (!Array.isArray(content)) return null;
|
|
8426
|
+
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");
|
|
8427
|
+
const contentParts = [];
|
|
8428
|
+
for (const block of content) switch (block.type) {
|
|
8429
|
+
case "text":
|
|
8430
|
+
contentParts.push({
|
|
8431
|
+
type: "text",
|
|
8432
|
+
text: block.text
|
|
8433
|
+
});
|
|
8434
|
+
break;
|
|
8435
|
+
case "thinking":
|
|
8436
|
+
contentParts.push({
|
|
8437
|
+
type: "text",
|
|
8438
|
+
text: block.thinking
|
|
8439
|
+
});
|
|
8440
|
+
break;
|
|
8441
|
+
case "image":
|
|
8442
|
+
contentParts.push({
|
|
8443
|
+
type: "image_url",
|
|
8444
|
+
image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` }
|
|
8445
|
+
});
|
|
8446
|
+
break;
|
|
8982
8447
|
}
|
|
8983
|
-
return
|
|
8448
|
+
return contentParts;
|
|
8984
8449
|
}
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
consola.debug(`[Tools] Defined tools:`, JSON.stringify(toolInfo));
|
|
8450
|
+
function getTruncatedToolName(originalName, toolNameMapping) {
|
|
8451
|
+
if (originalName.length <= OPENAI_TOOL_NAME_LIMIT) return originalName;
|
|
8452
|
+
const existingTruncated = toolNameMapping.originalToTruncated.get(originalName);
|
|
8453
|
+
if (existingTruncated) return existingTruncated;
|
|
8454
|
+
let hash = 0;
|
|
8455
|
+
for (let i = 0; i < originalName.length; i++) {
|
|
8456
|
+
const char = originalName.codePointAt(i) ?? 0;
|
|
8457
|
+
hash = (hash << 5) - hash + char;
|
|
8458
|
+
hash = Math.trunc(hash);
|
|
8995
8459
|
}
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8460
|
+
const hashSuffix = Math.abs(hash).toString(36).slice(0, 8);
|
|
8461
|
+
const truncatedName = originalName.slice(0, OPENAI_TOOL_NAME_LIMIT - 9) + "_" + hashSuffix;
|
|
8462
|
+
toolNameMapping.originalToTruncated.set(originalName, truncatedName);
|
|
8463
|
+
consola.debug(`Truncated tool name: "${originalName}" -> "${truncatedName}"`);
|
|
8464
|
+
return truncatedName;
|
|
8465
|
+
}
|
|
8466
|
+
function translateAnthropicToolsToOpenAI(anthropicTools, toolNameMapping) {
|
|
8467
|
+
if (!anthropicTools) return;
|
|
8468
|
+
return anthropicTools.map((tool) => ({
|
|
8469
|
+
type: "function",
|
|
8470
|
+
function: {
|
|
8471
|
+
name: getTruncatedToolName(tool.name, toolNameMapping),
|
|
8472
|
+
description: tool.description,
|
|
8473
|
+
parameters: tool.input_schema ?? {}
|
|
8474
|
+
}
|
|
8475
|
+
}));
|
|
8476
|
+
}
|
|
8477
|
+
function translateAnthropicToolChoiceToOpenAI(anthropicToolChoice, toolNameMapping) {
|
|
8478
|
+
if (!anthropicToolChoice) return;
|
|
8479
|
+
switch (anthropicToolChoice.type) {
|
|
8480
|
+
case "auto": return "auto";
|
|
8481
|
+
case "any": return "required";
|
|
8482
|
+
case "tool":
|
|
8483
|
+
if (anthropicToolChoice.name) return {
|
|
8484
|
+
type: "function",
|
|
8485
|
+
function: { name: getTruncatedToolName(anthropicToolChoice.name, toolNameMapping) }
|
|
8486
|
+
};
|
|
8487
|
+
return;
|
|
8488
|
+
case "none": return "none";
|
|
8489
|
+
default: return;
|
|
8999
8490
|
}
|
|
9000
8491
|
}
|
|
9001
8492
|
|
|
@@ -9164,6 +8655,26 @@ const createResponses = async (payload, { vision, initiator, resolvedModel, sign
|
|
|
9164
8655
|
return await response.json();
|
|
9165
8656
|
};
|
|
9166
8657
|
|
|
8658
|
+
//#endregion
|
|
8659
|
+
//#region src/routes/responses/model-shortcut.ts
|
|
8660
|
+
/**
|
|
8661
|
+
* Client-facing shortcut aliases for /responses model ids.
|
|
8662
|
+
*
|
|
8663
|
+
* Copilot exposes gpt-5.6 only as three named variants — Luna (lightweight),
|
|
8664
|
+
* Sol (powerful), Terra (versatile). A client sending a bare "gpt-5.6" would
|
|
8665
|
+
* otherwise fail findModelById. Map the shortcut to Sol because upstream tags
|
|
8666
|
+
* it `model_picker_category: "powerful"` — the closest match for an
|
|
8667
|
+
* unqualified "GPT-5.6" ask.
|
|
8668
|
+
*
|
|
8669
|
+
* Applied inside `normalizePayload`, i.e. AFTER `captureRequestedModel`, so
|
|
8670
|
+
* the client-facing echo (ADR-0001) still shows the original "gpt-5.6".
|
|
8671
|
+
*/
|
|
8672
|
+
const SHORTCUT_MAP = Object.assign(Object.create(null), { "gpt-5.6": "gpt-5.6-sol" });
|
|
8673
|
+
function resolveResponsesModelShortcut(model) {
|
|
8674
|
+
if (typeof model !== "string") return model;
|
|
8675
|
+
return SHORTCUT_MAP[model.toLowerCase()] ?? model;
|
|
8676
|
+
}
|
|
8677
|
+
|
|
9167
8678
|
//#endregion
|
|
9168
8679
|
//#region src/routes/responses/stream-id-sync.ts
|
|
9169
8680
|
const createStreamIdTracker = () => ({ outputItems: /* @__PURE__ */ new Map() });
|
|
@@ -9402,7 +8913,12 @@ const handleResponses = async (c) => {
|
|
|
9402
8913
|
rawPayload,
|
|
9403
8914
|
endpoint: "openai",
|
|
9404
8915
|
normalizePayload: (p) => {
|
|
9405
|
-
const
|
|
8916
|
+
const resolvedModel = resolveResponsesModelShortcut(p.model);
|
|
8917
|
+
const withModel = resolvedModel === p.model ? p : {
|
|
8918
|
+
...p,
|
|
8919
|
+
model: resolvedModel
|
|
8920
|
+
};
|
|
8921
|
+
const np = state.normalizeResponsesCallIds ? normalizeCallIds(withModel) : withModel;
|
|
9406
8922
|
useFunctionApplyPatch(np);
|
|
9407
8923
|
filterUnsupportedBuiltins(np);
|
|
9408
8924
|
injectPromptCacheKey(np, clientName);
|