@dianshuv/copilot-api 0.18.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 +527 -996
- 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
|
|
@@ -1595,6 +1594,7 @@ function captureRequest(params) {
|
|
|
1595
1594
|
};
|
|
1596
1595
|
if (params.reasoningTokens !== void 0) properties.reasoning_tokens = params.reasoningTokens;
|
|
1597
1596
|
if (params.cachedInputTokens !== void 0) properties.cached_input_tokens = params.cachedInputTokens;
|
|
1597
|
+
if (params.cacheCreationInputTokens !== void 0) properties.cache_creation_input_tokens = params.cacheCreationInputTokens;
|
|
1598
1598
|
if (params.totalInputTokens !== void 0) properties.total_input_tokens = params.totalInputTokens;
|
|
1599
1599
|
if (params.stopReason !== void 0) properties.stop_reason = params.stopReason;
|
|
1600
1600
|
if (params.status !== void 0) properties.status = params.status;
|
|
@@ -3002,6 +3002,7 @@ var RequestTracker = class {
|
|
|
3002
3002
|
if (update.outputTokens !== void 0) request.outputTokens = update.outputTokens;
|
|
3003
3003
|
if (update.reasoningTokens !== void 0) request.reasoningTokens = update.reasoningTokens;
|
|
3004
3004
|
if (update.cachedInputTokens !== void 0) request.cachedInputTokens = update.cachedInputTokens;
|
|
3005
|
+
if (update.cacheCreationInputTokens !== void 0) request.cacheCreationInputTokens = update.cacheCreationInputTokens;
|
|
3005
3006
|
if (update.totalInputTokens !== void 0) request.totalInputTokens = update.totalInputTokens;
|
|
3006
3007
|
if (update.error !== void 0) request.error = update.error;
|
|
3007
3008
|
if (update.queuePosition !== void 0) request.queuePosition = update.queuePosition;
|
|
@@ -3565,9 +3566,8 @@ async function autoTruncateOpenAI(payload, model, config = {}) {
|
|
|
3565
3566
|
*
|
|
3566
3567
|
* Pre-flight steps for any request whose final payload is an OpenAI
|
|
3567
3568
|
* ChatCompletionsPayload — auto-truncate decisions, 413 diagnostic logging,
|
|
3568
|
-
* and the non-streaming type guard. Used by
|
|
3569
|
-
* (native OpenAI)
|
|
3570
|
-
* gets translated into OpenAI shape before hitting upstream).
|
|
3569
|
+
* and the non-streaming type guard. Used by `routes/chat-completions`
|
|
3570
|
+
* (native OpenAI).
|
|
3571
3571
|
*/
|
|
3572
3572
|
/** Type guard for non-streaming responses */
|
|
3573
3573
|
function isNonStreaming(response) {
|
|
@@ -3775,89 +3775,6 @@ function createStreamRepetitionChecker(label, config) {
|
|
|
3775
3775
|
};
|
|
3776
3776
|
}
|
|
3777
3777
|
|
|
3778
|
-
//#endregion
|
|
3779
|
-
//#region src/lib/anthropic/beta.ts
|
|
3780
|
-
/**
|
|
3781
|
-
* Vendor-neutral utilities for manipulating the `anthropic-beta` request header.
|
|
3782
|
-
*
|
|
3783
|
-
* Lives in `lib/anthropic/` (not in either transport module) so both the
|
|
3784
|
-
* Anthropic-native and OpenAI-translated transport layers can share these
|
|
3785
|
-
* helpers without introducing cross-transport imports.
|
|
3786
|
-
*/
|
|
3787
|
-
/** Anthropic beta feature that unlocks the 1M context window. */
|
|
3788
|
-
const CONTEXT_1M_BETA_FEATURE = "context-1m-2025-08-07";
|
|
3789
|
-
/**
|
|
3790
|
-
* Merge two comma-separated anthropic-beta header values. Trims whitespace,
|
|
3791
|
-
* drops empty tokens, and dedupes by exact string match. Returns a canonical
|
|
3792
|
-
* comma-joined string with no spaces.
|
|
3793
|
-
*
|
|
3794
|
-
* Either input may be undefined / empty.
|
|
3795
|
-
*/
|
|
3796
|
-
function mergeBetaFeatures(existing, incoming) {
|
|
3797
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3798
|
-
const out = [];
|
|
3799
|
-
for (const raw of [existing, incoming]) {
|
|
3800
|
-
if (!raw) continue;
|
|
3801
|
-
for (const part of raw.split(",")) {
|
|
3802
|
-
const f = part.trim();
|
|
3803
|
-
if (f.length === 0 || seen.has(f)) continue;
|
|
3804
|
-
seen.add(f);
|
|
3805
|
-
out.push(f);
|
|
3806
|
-
}
|
|
3807
|
-
}
|
|
3808
|
-
return out.join(",");
|
|
3809
|
-
}
|
|
3810
|
-
/**
|
|
3811
|
-
* Append the context-1m feature to an anthropic-beta header value, deduping
|
|
3812
|
-
* any prior occurrence. Returns the merged comma-separated string.
|
|
3813
|
-
*/
|
|
3814
|
-
function appendContext1mBeta(existing) {
|
|
3815
|
-
return mergeBetaFeatures(existing, CONTEXT_1M_BETA_FEATURE);
|
|
3816
|
-
}
|
|
3817
|
-
/**
|
|
3818
|
-
* True iff a model id appears to be the suffixed 1M-context variant of an
|
|
3819
|
-
* Anthropic Claude model (e.g. claude-opus-4-8-1m, claude-opus-4.6-1m).
|
|
3820
|
-
*
|
|
3821
|
-
* Used as a state.models-independent signal for whether to inject the
|
|
3822
|
-
* context-1m-2025-08-07 beta header, so the 1M intent survives a stale or
|
|
3823
|
-
* empty model cache (where `resolveAnthropicModelForDirectPath` would return
|
|
3824
|
-
* undefined). Forwarding the beta is harmless to upstreams that ignore it.
|
|
3825
|
-
*/
|
|
3826
|
-
function isOneMillionSuffixedClaudeId(modelId) {
|
|
3827
|
-
return modelId.startsWith("claude-") && modelId.endsWith("-1m");
|
|
3828
|
-
}
|
|
3829
|
-
|
|
3830
|
-
//#endregion
|
|
3831
|
-
//#region src/lib/headers.ts
|
|
3832
|
-
/**
|
|
3833
|
-
* Vendor-neutral header-bag helpers.
|
|
3834
|
-
*
|
|
3835
|
-
* HTTP header names are case-insensitive, but a plain-object header bag is
|
|
3836
|
-
* case-sensitive on its keys. Code that wants to look up "anthropic-beta"
|
|
3837
|
-
* without knowing whether some other producer wrote "Anthropic-Beta" needs
|
|
3838
|
-
* `findHeaderKey`. Code that wants to set a header without creating a
|
|
3839
|
-
* second case variant of the same name needs `setHeader`.
|
|
3840
|
-
*/
|
|
3841
|
-
/** Case-insensitive lookup of a header key in a plain-object header bag. */
|
|
3842
|
-
function findHeaderKey(headers, name) {
|
|
3843
|
-
const lower = name.toLowerCase();
|
|
3844
|
-
return Object.keys(headers).find((k) => k.toLowerCase() === lower);
|
|
3845
|
-
}
|
|
3846
|
-
/** Case-insensitive read of a header value. */
|
|
3847
|
-
function getHeader(headers, name) {
|
|
3848
|
-
const key = findHeaderKey(headers, name);
|
|
3849
|
-
return key === void 0 ? void 0 : headers[key];
|
|
3850
|
-
}
|
|
3851
|
-
/**
|
|
3852
|
-
* Set a header value at the existing case variant if one is present, else at
|
|
3853
|
-
* the supplied canonical name. Prevents a second key (different case) from
|
|
3854
|
-
* being added for the same logical header.
|
|
3855
|
-
*/
|
|
3856
|
-
function setHeader(headers, name, value) {
|
|
3857
|
-
const key = findHeaderKey(headers, name) ?? name;
|
|
3858
|
-
headers[key] = value;
|
|
3859
|
-
}
|
|
3860
|
-
|
|
3861
3778
|
//#endregion
|
|
3862
3779
|
//#region src/services/copilot/create-chat-completions.ts
|
|
3863
3780
|
const GPT_MODEL_PATTERN = /^gpt-/i;
|
|
@@ -3878,21 +3795,15 @@ const createChatCompletions = async (payload, options) => {
|
|
|
3878
3795
|
const enableVision = wire.messages.some((x) => typeof x.content !== "string" && x.content?.some((x) => x.type === "image_url"));
|
|
3879
3796
|
const isAgentCall = wire.messages.some((msg) => ["assistant", "tool"].includes(msg.role));
|
|
3880
3797
|
const modelSupportsVision = options?.resolvedModel?.capabilities?.supports?.vision !== false;
|
|
3881
|
-
const headers = {
|
|
3882
|
-
...copilotHeaders(state, {
|
|
3883
|
-
vision: enableVision && modelSupportsVision,
|
|
3884
|
-
modelRequestHeaders: options?.resolvedModel?.request_headers
|
|
3885
|
-
}),
|
|
3886
|
-
"X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user")
|
|
3887
|
-
};
|
|
3888
|
-
if (options?.anthropicBeta) {
|
|
3889
|
-
const existingKey = findHeaderKey(headers, "anthropic-beta") ?? "anthropic-beta";
|
|
3890
|
-
headers[existingKey] = mergeBetaFeatures(headers[existingKey], options.anthropicBeta);
|
|
3891
|
-
consola.debug(`[ChatCompletions] anthropic-beta after merge: ${headers[existingKey]}`);
|
|
3892
|
-
}
|
|
3893
3798
|
const response = await copilotFetch("/chat/completions", {
|
|
3894
3799
|
method: "POST",
|
|
3895
|
-
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
|
+
},
|
|
3896
3807
|
body: JSON.stringify(wire),
|
|
3897
3808
|
signal: options?.signal
|
|
3898
3809
|
});
|
|
@@ -4178,6 +4089,7 @@ function completeTracking(trackingId, inputTokens, outputTokens, queueWaitMs, re
|
|
|
4178
4089
|
queueWaitMs,
|
|
4179
4090
|
reasoningTokens,
|
|
4180
4091
|
cachedInputTokens: cache?.cachedInputTokens,
|
|
4092
|
+
cacheCreationInputTokens: cache?.cacheCreationInputTokens,
|
|
4181
4093
|
totalInputTokens: cache?.totalInputTokens,
|
|
4182
4094
|
...timingsToUpdate(timings)
|
|
4183
4095
|
});
|
|
@@ -4196,6 +4108,7 @@ function completeTracking(trackingId, inputTokens, outputTokens, queueWaitMs, re
|
|
|
4196
4108
|
toolCount: analytics.toolCount ?? 0,
|
|
4197
4109
|
reasoningTokens,
|
|
4198
4110
|
cachedInputTokens: cache?.cachedInputTokens,
|
|
4111
|
+
cacheCreationInputTokens: cache?.cacheCreationInputTokens,
|
|
4199
4112
|
totalInputTokens: cache?.totalInputTokens,
|
|
4200
4113
|
stopReason: analytics.stopReason
|
|
4201
4114
|
});
|
|
@@ -4343,12 +4256,12 @@ async function executeRequest(opts) {
|
|
|
4343
4256
|
signal: abort.signal
|
|
4344
4257
|
}));
|
|
4345
4258
|
ctx.queueWaitMs = queueWaitMs;
|
|
4346
|
-
if (isNonStreaming(response)) return handleNonStreamingResponse
|
|
4259
|
+
if (isNonStreaming(response)) return handleNonStreamingResponse(c, response, ctx, payload);
|
|
4347
4260
|
consola.debug("Streaming response");
|
|
4348
4261
|
updateTrackerStatus(ctx.trackingId, "streaming");
|
|
4349
4262
|
return streamSSE(c, async (stream) => {
|
|
4350
4263
|
stream.onAbort(() => abort.abort());
|
|
4351
|
-
await handleStreamingResponse
|
|
4264
|
+
await handleStreamingResponse({
|
|
4352
4265
|
stream,
|
|
4353
4266
|
response,
|
|
4354
4267
|
payload,
|
|
@@ -4377,7 +4290,7 @@ async function logTokenCount(payload, selectedModel) {
|
|
|
4377
4290
|
consola.debug("Failed to calculate token count:", error);
|
|
4378
4291
|
}
|
|
4379
4292
|
}
|
|
4380
|
-
function handleNonStreamingResponse
|
|
4293
|
+
function handleNonStreamingResponse(c, originalResponse, ctx, payload) {
|
|
4381
4294
|
consola.debug("Non-streaming response:", JSON.stringify(originalResponse));
|
|
4382
4295
|
let response = originalResponse;
|
|
4383
4296
|
if (state.verbose && ctx.truncateResult?.wasCompacted && response.choices[0]?.message.content) {
|
|
@@ -4410,17 +4323,19 @@ function handleNonStreamingResponse$1(c, originalResponse, ctx, payload) {
|
|
|
4410
4323
|
toolCalls: extractToolCalls(choice)
|
|
4411
4324
|
}, durationMs);
|
|
4412
4325
|
const cachedInputTokens = usage ? getCachedTokensFromOpenAIUsage(usage) : void 0;
|
|
4326
|
+
const freshInputTokens = usage ? usage.prompt_tokens - (cachedInputTokens ?? 0) : 0;
|
|
4413
4327
|
if (ctx.trackingId && usage) requestTracker.updateRequest(ctx.trackingId, {
|
|
4414
|
-
inputTokens:
|
|
4328
|
+
inputTokens: freshInputTokens,
|
|
4415
4329
|
outputTokens: usage.completion_tokens,
|
|
4416
4330
|
queueWaitMs: ctx.queueWaitMs,
|
|
4417
4331
|
reasoningTokens,
|
|
4418
4332
|
cachedInputTokens,
|
|
4333
|
+
cacheCreationInputTokens: 0,
|
|
4419
4334
|
totalInputTokens: usage.prompt_tokens
|
|
4420
4335
|
});
|
|
4421
4336
|
captureRequest({
|
|
4422
4337
|
model: response.model,
|
|
4423
|
-
inputTokens:
|
|
4338
|
+
inputTokens: freshInputTokens,
|
|
4424
4339
|
outputTokens: usage?.completion_tokens ?? 0,
|
|
4425
4340
|
durationMs,
|
|
4426
4341
|
success: true,
|
|
@@ -4428,6 +4343,7 @@ function handleNonStreamingResponse$1(c, originalResponse, ctx, payload) {
|
|
|
4428
4343
|
toolCount: payload.tools?.length ?? 0,
|
|
4429
4344
|
reasoningTokens,
|
|
4430
4345
|
cachedInputTokens,
|
|
4346
|
+
cacheCreationInputTokens: 0,
|
|
4431
4347
|
totalInputTokens: usage?.prompt_tokens,
|
|
4432
4348
|
stopReason: choice.finish_reason
|
|
4433
4349
|
});
|
|
@@ -4467,7 +4383,7 @@ function createStreamAccumulator() {
|
|
|
4467
4383
|
toolCallMap: /* @__PURE__ */ new Map()
|
|
4468
4384
|
};
|
|
4469
4385
|
}
|
|
4470
|
-
async function handleStreamingResponse
|
|
4386
|
+
async function handleStreamingResponse(opts) {
|
|
4471
4387
|
const { stream, response, payload, ctx } = opts;
|
|
4472
4388
|
const acc = createStreamAccumulator();
|
|
4473
4389
|
const checkRepetition = createStreamRepetitionChecker(`openai:${payload.model}`);
|
|
@@ -4497,7 +4413,7 @@ async function handleStreamingResponse$1(opts) {
|
|
|
4497
4413
|
await accumulateAndEchoChunk(chunk, acc, checkRepetition, ctx, stream);
|
|
4498
4414
|
}
|
|
4499
4415
|
recordStreamSuccess(acc, payload.model, ctx);
|
|
4500
|
-
completeTracking(ctx.trackingId, acc.inputTokens, acc.outputTokens, ctx.queueWaitMs, acc.reasoningTokens, {
|
|
4416
|
+
completeTracking(ctx.trackingId, acc.inputTokens - acc.cachedTokens, acc.outputTokens, ctx.queueWaitMs, acc.reasoningTokens, {
|
|
4501
4417
|
model: acc.model || payload.model,
|
|
4502
4418
|
stream: true,
|
|
4503
4419
|
durationMs: Date.now() - ctx.startTime,
|
|
@@ -4505,6 +4421,7 @@ async function handleStreamingResponse$1(opts) {
|
|
|
4505
4421
|
toolCount: payload.tools?.length ?? 0
|
|
4506
4422
|
}, ctx.timings, {
|
|
4507
4423
|
cachedInputTokens: acc.cachedTokens,
|
|
4424
|
+
cacheCreationInputTokens: 0,
|
|
4508
4425
|
totalInputTokens: acc.inputTokens
|
|
4509
4426
|
});
|
|
4510
4427
|
} catch (error) {
|
|
@@ -6600,6 +6517,46 @@ async function checkNeedsCompactionAnthropic(payload, model, config = {}) {
|
|
|
6600
6517
|
};
|
|
6601
6518
|
}
|
|
6602
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
|
+
|
|
6603
6560
|
//#endregion
|
|
6604
6561
|
//#region src/lib/anthropic/features.ts
|
|
6605
6562
|
function normalizeForMatching(modelId) {
|
|
@@ -6750,6 +6707,37 @@ function filterServerToolBlocksFromResponse(response) {
|
|
|
6750
6707
|
};
|
|
6751
6708
|
}
|
|
6752
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
|
+
|
|
6753
6741
|
//#endregion
|
|
6754
6742
|
//#region src/services/copilot/create-anthropic-messages.ts
|
|
6755
6743
|
/**
|
|
@@ -6952,11 +6940,12 @@ function resolveAnthropicModelForDirectPath(modelId) {
|
|
|
6952
6940
|
}
|
|
6953
6941
|
}
|
|
6954
6942
|
/**
|
|
6955
|
-
* Check if a model supports direct Anthropic API.
|
|
6956
|
-
*
|
|
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-...).
|
|
6957
6947
|
*/
|
|
6958
6948
|
function supportsDirectAnthropicApi(modelId) {
|
|
6959
|
-
if (state.redirectAnthropic) return false;
|
|
6960
6949
|
return resolveAnthropicModelForDirectPath(modelId) !== void 0;
|
|
6961
6950
|
}
|
|
6962
6951
|
|
|
@@ -7153,15 +7142,6 @@ function extractToolCallsFromContent(content) {
|
|
|
7153
7142
|
});
|
|
7154
7143
|
return tools.length > 0 ? tools : void 0;
|
|
7155
7144
|
}
|
|
7156
|
-
function mapOpenAIStopReasonToAnthropic(finishReason) {
|
|
7157
|
-
if (finishReason === null) return null;
|
|
7158
|
-
return {
|
|
7159
|
-
stop: "end_turn",
|
|
7160
|
-
length: "max_tokens",
|
|
7161
|
-
tool_calls: "tool_use",
|
|
7162
|
-
content_filter: "end_turn"
|
|
7163
|
-
}[finishReason];
|
|
7164
|
-
}
|
|
7165
7145
|
function prependMarkerToResponse(response, marker) {
|
|
7166
7146
|
if (!marker) return response;
|
|
7167
7147
|
const content = [...response.content];
|
|
@@ -7282,549 +7262,73 @@ function recordAnthropicStreamingResponse(acc, fallbackModel, ctx) {
|
|
|
7282
7262
|
}
|
|
7283
7263
|
|
|
7284
7264
|
//#endregion
|
|
7285
|
-
//#region src/routes/messages/
|
|
7286
|
-
const OPENAI_TOOL_NAME_LIMIT = 64;
|
|
7265
|
+
//#region src/routes/messages/stream-translation.ts
|
|
7287
7266
|
/**
|
|
7288
|
-
*
|
|
7289
|
-
* This handles edge cases where conversation history may be incomplete:
|
|
7290
|
-
* - Session interruptions where tool execution was cut off
|
|
7291
|
-
* - Previous request failures
|
|
7292
|
-
* - Client sending truncated history
|
|
7267
|
+
* Wrap an arbitrary error into an Anthropic-native `error` stream event.
|
|
7293
7268
|
*
|
|
7294
|
-
*
|
|
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).
|
|
7295
7273
|
*/
|
|
7296
|
-
function
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
const foundToolResponses = /* @__PURE__ */ new Set();
|
|
7303
|
-
let j = i + 1;
|
|
7304
|
-
while (j < messages.length && messages[j].role === "tool") {
|
|
7305
|
-
const toolMessage = messages[j];
|
|
7306
|
-
if (toolMessage.tool_call_id) foundToolResponses.add(toolMessage.tool_call_id);
|
|
7307
|
-
j++;
|
|
7308
|
-
}
|
|
7309
|
-
for (const toolCall of message.tool_calls) if (!foundToolResponses.has(toolCall.id)) {
|
|
7310
|
-
consola.debug(`Adding placeholder tool_result for ${toolCall.id}`);
|
|
7311
|
-
fixedMessages.push({
|
|
7312
|
-
role: "tool",
|
|
7313
|
-
tool_call_id: toolCall.id,
|
|
7314
|
-
content: "Tool execution was interrupted or failed."
|
|
7315
|
-
});
|
|
7316
|
-
}
|
|
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."
|
|
7317
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;
|
|
7318
7300
|
}
|
|
7319
|
-
return
|
|
7301
|
+
return raw;
|
|
7320
7302
|
}
|
|
7321
|
-
function
|
|
7322
|
-
const
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
|
|
7341
|
-
};
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
*/
|
|
7351
|
-
function findLatestModel(familyPrefix, fallback) {
|
|
7352
|
-
const models = state.models?.data;
|
|
7353
|
-
if (!models || models.length === 0) return fallback;
|
|
7354
|
-
const candidates = models.filter((m) => m.id.startsWith(familyPrefix));
|
|
7355
|
-
if (candidates.length === 0) return fallback;
|
|
7356
|
-
candidates.sort((a, b) => {
|
|
7357
|
-
const [aMajor, aMinor] = extractVersion(a.id, familyPrefix);
|
|
7358
|
-
const [bMajor, bMinor] = extractVersion(b.id, familyPrefix);
|
|
7359
|
-
if (aMajor !== bMajor) return bMajor - aMajor;
|
|
7360
|
-
return bMinor - aMinor;
|
|
7361
|
-
});
|
|
7362
|
-
return candidates[0].id;
|
|
7363
|
-
}
|
|
7364
|
-
/**
|
|
7365
|
-
* Extract numeric [major, minor] version from a model id.
|
|
7366
|
-
*
|
|
7367
|
-
* Supports both naming conventions Anthropic/Copilot have used:
|
|
7368
|
-
* - dot: "claude-opus-4.5" → [4, 5]
|
|
7369
|
-
* - dash: "claude-opus-4-8" → [4, 8]
|
|
7370
|
-
* - dash double-digit: "claude-opus-4-10" → [4, 10]
|
|
7371
|
-
*
|
|
7372
|
-
* The dash form previously parsed as just the major via the regex
|
|
7373
|
-
* /^(\d+(?:\.\d+)?)/ (the dash stopped the match), which silently
|
|
7374
|
-
* downgraded dash-named candidates against any dot-named candidate in
|
|
7375
|
-
* findLatestModel. Parsing into a tuple also avoids the parseFloat
|
|
7376
|
-
* lossiness on double-digit minors ("4.10" → 4.1).
|
|
7377
|
-
*
|
|
7378
|
-
* Anything after the major/minor segment (date stamps, "-1m") is ignored.
|
|
7379
|
-
* The minor capture is bounded to 1-3 digits so that an 8-digit date suffix
|
|
7380
|
-
* directly after the major (e.g. "claude-opus-4-20250514") is NOT mistaken
|
|
7381
|
-
* for a minor version of 20_250_514 — without that bound, dated ids would
|
|
7382
|
-
* outrank legitimate dotted candidates like "claude-opus-4.8" in
|
|
7383
|
-
* findLatestModel's sort.
|
|
7384
|
-
*
|
|
7385
|
-
* Returns [0, 0] when no version can be extracted.
|
|
7386
|
-
*/
|
|
7387
|
-
function extractVersion(modelId, prefix) {
|
|
7388
|
-
const match = modelId.slice(prefix.length + 1).match(/^(\d+)(?:[.-](\d{1,3}))?/);
|
|
7389
|
-
if (!match) return [0, 0];
|
|
7390
|
-
const major = Number.parseInt(match[1], 10);
|
|
7391
|
-
const rawMinor = match[2];
|
|
7392
|
-
return [major, rawMinor === void 0 ? 0 : Number.parseInt(rawMinor, 10) || 0];
|
|
7393
|
-
}
|
|
7394
|
-
function translateModelName(model) {
|
|
7395
|
-
const aliasMap = {
|
|
7396
|
-
opus: "claude-opus",
|
|
7397
|
-
sonnet: "claude-sonnet",
|
|
7398
|
-
haiku: "claude-haiku"
|
|
7399
|
-
};
|
|
7400
|
-
if (aliasMap[model]) {
|
|
7401
|
-
const familyPrefix = aliasMap[model];
|
|
7402
|
-
return findLatestModel(familyPrefix, `${familyPrefix}-4.5`);
|
|
7403
|
-
}
|
|
7404
|
-
if (/^claude-sonnet-4-5-\d+$/.test(model)) return "claude-sonnet-4.5";
|
|
7405
|
-
if (/^claude-sonnet-4-\d+$/.test(model)) return "claude-sonnet-4";
|
|
7406
|
-
if (model === "claude-opus-4-8-1m") return "claude-opus-4.8";
|
|
7407
|
-
if (model === "claude-opus-4-8") return "claude-opus-4.8";
|
|
7408
|
-
if (model === "claude-opus-4-7-1m") return "claude-opus-4.7";
|
|
7409
|
-
if (/^claude-opus-4-7$/.test(model)) return "claude-opus-4.7";
|
|
7410
|
-
if (model === "claude-opus-4-6-1m") return "claude-opus-4.6-1m";
|
|
7411
|
-
if (/^claude-opus-4-6$/.test(model)) return "claude-opus-4.6";
|
|
7412
|
-
if (/^claude-opus-4-5-\d+$/.test(model)) return "claude-opus-4.5";
|
|
7413
|
-
if (/^claude-opus-4-\d+$/.test(model)) return findLatestModel("claude-opus", "claude-opus-4.5");
|
|
7414
|
-
if (/^claude-haiku-4-5-\d+$/.test(model)) return "claude-haiku-4.5";
|
|
7415
|
-
if (/^claude-haiku-3-5-\d+$/.test(model)) return findLatestModel("claude-haiku", "claude-haiku-4.5");
|
|
7416
|
-
return model;
|
|
7417
|
-
}
|
|
7418
|
-
function translateAnthropicMessagesToOpenAI(anthropicMessages, system, toolNameMapping) {
|
|
7419
|
-
const systemMessages = handleSystemPrompt(system);
|
|
7420
|
-
const otherMessages = anthropicMessages.flatMap((message) => message.role === "user" ? handleUserMessage(message) : handleAssistantMessage(message, toolNameMapping));
|
|
7421
|
-
return [...systemMessages, ...otherMessages];
|
|
7422
|
-
}
|
|
7423
|
-
const RESERVED_KEYWORDS = ["x-anthropic-billing-header", "x-anthropic-billing"];
|
|
7424
|
-
/**
|
|
7425
|
-
* Filter out reserved keywords from system prompt text.
|
|
7426
|
-
* Copilot API rejects requests containing these keywords.
|
|
7427
|
-
* Removes the entire line containing the keyword to keep the prompt clean.
|
|
7428
|
-
*/
|
|
7429
|
-
function filterReservedKeywords(text) {
|
|
7430
|
-
let filtered = text;
|
|
7431
|
-
for (const keyword of RESERVED_KEYWORDS) if (text.includes(keyword)) {
|
|
7432
|
-
consola.debug(`[Reserved Keyword] Removing line containing "${keyword}"`);
|
|
7433
|
-
filtered = filtered.split("\n").filter((line) => !line.includes(keyword)).join("\n");
|
|
7434
|
-
}
|
|
7435
|
-
return filtered;
|
|
7436
|
-
}
|
|
7437
|
-
function handleSystemPrompt(system) {
|
|
7438
|
-
if (!system) return [];
|
|
7439
|
-
if (typeof system === "string") return [{
|
|
7440
|
-
role: "system",
|
|
7441
|
-
content: filterReservedKeywords(system)
|
|
7442
|
-
}];
|
|
7443
|
-
else return [{
|
|
7444
|
-
role: "system",
|
|
7445
|
-
content: filterReservedKeywords(system.map((block) => block.text).join("\n\n"))
|
|
7446
|
-
}];
|
|
7447
|
-
}
|
|
7448
|
-
function handleUserMessage(message) {
|
|
7449
|
-
const newMessages = [];
|
|
7450
|
-
if (Array.isArray(message.content)) {
|
|
7451
|
-
const toolResultBlocks = message.content.filter((block) => block.type === "tool_result");
|
|
7452
|
-
const otherBlocks = message.content.filter((block) => block.type !== "tool_result");
|
|
7453
|
-
for (const block of toolResultBlocks) newMessages.push({
|
|
7454
|
-
role: "tool",
|
|
7455
|
-
tool_call_id: block.tool_use_id,
|
|
7456
|
-
content: mapContent(block.content)
|
|
7457
|
-
});
|
|
7458
|
-
if (otherBlocks.length > 0) newMessages.push({
|
|
7459
|
-
role: "user",
|
|
7460
|
-
content: mapContent(otherBlocks)
|
|
7461
|
-
});
|
|
7462
|
-
} else newMessages.push({
|
|
7463
|
-
role: "user",
|
|
7464
|
-
content: mapContent(message.content)
|
|
7465
|
-
});
|
|
7466
|
-
return newMessages;
|
|
7467
|
-
}
|
|
7468
|
-
function handleAssistantMessage(message, toolNameMapping) {
|
|
7469
|
-
if (!Array.isArray(message.content)) return [{
|
|
7470
|
-
role: "assistant",
|
|
7471
|
-
content: mapContent(message.content)
|
|
7472
|
-
}];
|
|
7473
|
-
const toolUseBlocks = message.content.filter((block) => block.type === "tool_use");
|
|
7474
|
-
const textBlocks = message.content.filter((block) => block.type === "text");
|
|
7475
|
-
const thinkingBlocks = message.content.filter((block) => block.type === "thinking");
|
|
7476
|
-
const allTextContent = [...textBlocks.map((b) => b.text), ...thinkingBlocks.map((b) => b.thinking)].join("\n\n");
|
|
7477
|
-
return toolUseBlocks.length > 0 ? [{
|
|
7478
|
-
role: "assistant",
|
|
7479
|
-
content: allTextContent || null,
|
|
7480
|
-
tool_calls: toolUseBlocks.map((toolUse) => ({
|
|
7481
|
-
id: toolUse.id,
|
|
7482
|
-
type: "function",
|
|
7483
|
-
function: {
|
|
7484
|
-
name: getTruncatedToolName(toolUse.name, toolNameMapping),
|
|
7485
|
-
arguments: JSON.stringify(toolUse.input)
|
|
7486
|
-
}
|
|
7487
|
-
}))
|
|
7488
|
-
}] : [{
|
|
7489
|
-
role: "assistant",
|
|
7490
|
-
content: mapContent(message.content)
|
|
7491
|
-
}];
|
|
7492
|
-
}
|
|
7493
|
-
function mapContent(content) {
|
|
7494
|
-
if (typeof content === "string") return content;
|
|
7495
|
-
if (!Array.isArray(content)) return null;
|
|
7496
|
-
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");
|
|
7497
|
-
const contentParts = [];
|
|
7498
|
-
for (const block of content) switch (block.type) {
|
|
7499
|
-
case "text":
|
|
7500
|
-
contentParts.push({
|
|
7501
|
-
type: "text",
|
|
7502
|
-
text: block.text
|
|
7503
|
-
});
|
|
7504
|
-
break;
|
|
7505
|
-
case "thinking":
|
|
7506
|
-
contentParts.push({
|
|
7507
|
-
type: "text",
|
|
7508
|
-
text: block.thinking
|
|
7509
|
-
});
|
|
7510
|
-
break;
|
|
7511
|
-
case "image":
|
|
7512
|
-
contentParts.push({
|
|
7513
|
-
type: "image_url",
|
|
7514
|
-
image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` }
|
|
7515
|
-
});
|
|
7516
|
-
break;
|
|
7517
|
-
}
|
|
7518
|
-
return contentParts;
|
|
7519
|
-
}
|
|
7520
|
-
function getTruncatedToolName(originalName, toolNameMapping) {
|
|
7521
|
-
if (originalName.length <= OPENAI_TOOL_NAME_LIMIT) return originalName;
|
|
7522
|
-
const existingTruncated = toolNameMapping.originalToTruncated.get(originalName);
|
|
7523
|
-
if (existingTruncated) return existingTruncated;
|
|
7524
|
-
let hash = 0;
|
|
7525
|
-
for (let i = 0; i < originalName.length; i++) {
|
|
7526
|
-
const char = originalName.codePointAt(i) ?? 0;
|
|
7527
|
-
hash = (hash << 5) - hash + char;
|
|
7528
|
-
hash = Math.trunc(hash);
|
|
7529
|
-
}
|
|
7530
|
-
const hashSuffix = Math.abs(hash).toString(36).slice(0, 8);
|
|
7531
|
-
const truncatedName = originalName.slice(0, OPENAI_TOOL_NAME_LIMIT - 9) + "_" + hashSuffix;
|
|
7532
|
-
toolNameMapping.truncatedToOriginal.set(truncatedName, originalName);
|
|
7533
|
-
toolNameMapping.originalToTruncated.set(originalName, truncatedName);
|
|
7534
|
-
consola.debug(`Truncated tool name: "${originalName}" -> "${truncatedName}"`);
|
|
7535
|
-
return truncatedName;
|
|
7536
|
-
}
|
|
7537
|
-
function translateAnthropicToolsToOpenAI(anthropicTools, toolNameMapping) {
|
|
7538
|
-
if (!anthropicTools) return;
|
|
7539
|
-
return anthropicTools.map((tool) => ({
|
|
7540
|
-
type: "function",
|
|
7541
|
-
function: {
|
|
7542
|
-
name: getTruncatedToolName(tool.name, toolNameMapping),
|
|
7543
|
-
description: tool.description,
|
|
7544
|
-
parameters: tool.input_schema ?? {}
|
|
7545
|
-
}
|
|
7546
|
-
}));
|
|
7547
|
-
}
|
|
7548
|
-
function translateAnthropicToolChoiceToOpenAI(anthropicToolChoice, toolNameMapping) {
|
|
7549
|
-
if (!anthropicToolChoice) return;
|
|
7550
|
-
switch (anthropicToolChoice.type) {
|
|
7551
|
-
case "auto": return "auto";
|
|
7552
|
-
case "any": return "required";
|
|
7553
|
-
case "tool":
|
|
7554
|
-
if (anthropicToolChoice.name) return {
|
|
7555
|
-
type: "function",
|
|
7556
|
-
function: { name: getTruncatedToolName(anthropicToolChoice.name, toolNameMapping) }
|
|
7557
|
-
};
|
|
7558
|
-
return;
|
|
7559
|
-
case "none": return "none";
|
|
7560
|
-
default: return;
|
|
7561
|
-
}
|
|
7562
|
-
}
|
|
7563
|
-
/** Create empty response for edge case of no choices */
|
|
7564
|
-
function createEmptyResponse(response) {
|
|
7565
|
-
return {
|
|
7566
|
-
id: response.id,
|
|
7567
|
-
type: "message",
|
|
7568
|
-
role: "assistant",
|
|
7569
|
-
model: response.model,
|
|
7570
|
-
content: [],
|
|
7571
|
-
stop_reason: "end_turn",
|
|
7572
|
-
stop_sequence: null,
|
|
7573
|
-
usage: {
|
|
7574
|
-
input_tokens: response.usage?.prompt_tokens ?? 0,
|
|
7575
|
-
output_tokens: response.usage?.completion_tokens ?? 0
|
|
7576
|
-
}
|
|
7577
|
-
};
|
|
7578
|
-
}
|
|
7579
|
-
/** Build usage object from response */
|
|
7580
|
-
function buildUsageObject(response) {
|
|
7581
|
-
const cachedTokens = response.usage?.prompt_tokens_details?.cached_tokens;
|
|
7582
|
-
return {
|
|
7583
|
-
input_tokens: (response.usage?.prompt_tokens ?? 0) - (cachedTokens ?? 0),
|
|
7584
|
-
output_tokens: response.usage?.completion_tokens ?? 0,
|
|
7585
|
-
...cachedTokens !== void 0 && { cache_read_input_tokens: cachedTokens }
|
|
7586
|
-
};
|
|
7587
|
-
}
|
|
7588
|
-
function translateToAnthropic(response, toolNameMapping) {
|
|
7589
|
-
if (response.choices.length === 0) return createEmptyResponse(response);
|
|
7590
|
-
const allTextBlocks = [];
|
|
7591
|
-
const allToolUseBlocks = [];
|
|
7592
|
-
let stopReason = null;
|
|
7593
|
-
stopReason = response.choices[0]?.finish_reason ?? stopReason;
|
|
7594
|
-
for (const choice of response.choices) {
|
|
7595
|
-
const textBlocks = getAnthropicTextBlocks(choice.message.content);
|
|
7596
|
-
const toolUseBlocks = getAnthropicToolUseBlocks(choice.message.tool_calls, toolNameMapping);
|
|
7597
|
-
allTextBlocks.push(...textBlocks);
|
|
7598
|
-
allToolUseBlocks.push(...toolUseBlocks);
|
|
7599
|
-
if (choice.finish_reason === "tool_calls" || stopReason === "stop") stopReason = choice.finish_reason;
|
|
7600
|
-
}
|
|
7601
|
-
return {
|
|
7602
|
-
id: response.id,
|
|
7603
|
-
type: "message",
|
|
7604
|
-
role: "assistant",
|
|
7605
|
-
model: response.model,
|
|
7606
|
-
content: [...allTextBlocks, ...allToolUseBlocks],
|
|
7607
|
-
stop_reason: mapOpenAIStopReasonToAnthropic(stopReason),
|
|
7608
|
-
stop_sequence: null,
|
|
7609
|
-
usage: buildUsageObject(response)
|
|
7610
|
-
};
|
|
7611
|
-
}
|
|
7612
|
-
function getAnthropicTextBlocks(messageContent) {
|
|
7613
|
-
if (typeof messageContent === "string") return [{
|
|
7614
|
-
type: "text",
|
|
7615
|
-
text: messageContent
|
|
7616
|
-
}];
|
|
7617
|
-
if (Array.isArray(messageContent)) return messageContent.filter((part) => part.type === "text").map((part) => ({
|
|
7618
|
-
type: "text",
|
|
7619
|
-
text: part.text
|
|
7620
|
-
}));
|
|
7621
|
-
return [];
|
|
7622
|
-
}
|
|
7623
|
-
function getAnthropicToolUseBlocks(toolCalls, toolNameMapping) {
|
|
7624
|
-
if (!toolCalls) return [];
|
|
7625
|
-
return toolCalls.map((toolCall) => {
|
|
7626
|
-
let input = {};
|
|
7627
|
-
try {
|
|
7628
|
-
input = JSON.parse(toolCall.function.arguments);
|
|
7629
|
-
} catch (error) {
|
|
7630
|
-
consola.warn(`Failed to parse tool call arguments for ${toolCall.function.name}:`, error);
|
|
7631
|
-
}
|
|
7632
|
-
const originalName = toolNameMapping?.truncatedToOriginal.get(toolCall.function.name) ?? toolCall.function.name;
|
|
7633
|
-
return {
|
|
7634
|
-
type: "tool_use",
|
|
7635
|
-
id: toolCall.id,
|
|
7636
|
-
name: originalName,
|
|
7637
|
-
input
|
|
7638
|
-
};
|
|
7639
|
-
});
|
|
7640
|
-
}
|
|
7641
|
-
|
|
7642
|
-
//#endregion
|
|
7643
|
-
//#region src/routes/messages/stream-translation.ts
|
|
7644
|
-
function isToolBlockOpen(state) {
|
|
7645
|
-
if (!state.contentBlockOpen) return false;
|
|
7646
|
-
return Object.values(state.toolCalls).some((tc) => tc.anthropicBlockIndex === state.contentBlockIndex);
|
|
7647
|
-
}
|
|
7648
|
-
function translateChunkToAnthropicEvents(chunk, state, toolNameMapping) {
|
|
7649
|
-
const events = [];
|
|
7650
|
-
if (chunk.choices.length === 0) {
|
|
7651
|
-
if (chunk.model && !state.model) state.model = chunk.model;
|
|
7652
|
-
return events;
|
|
7653
|
-
}
|
|
7654
|
-
const choice = chunk.choices[0];
|
|
7655
|
-
const { delta } = choice;
|
|
7656
|
-
if (!state.messageStartSent) {
|
|
7657
|
-
const model = chunk.model || state.model || "unknown";
|
|
7658
|
-
events.push({
|
|
7659
|
-
type: "message_start",
|
|
7660
|
-
message: {
|
|
7661
|
-
id: chunk.id || `msg_${Date.now()}`,
|
|
7662
|
-
type: "message",
|
|
7663
|
-
role: "assistant",
|
|
7664
|
-
content: [],
|
|
7665
|
-
model,
|
|
7666
|
-
stop_reason: null,
|
|
7667
|
-
stop_sequence: null,
|
|
7668
|
-
usage: {
|
|
7669
|
-
input_tokens: (chunk.usage?.prompt_tokens ?? 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0),
|
|
7670
|
-
output_tokens: 0,
|
|
7671
|
-
...chunk.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: chunk.usage.prompt_tokens_details.cached_tokens }
|
|
7672
|
-
}
|
|
7673
|
-
}
|
|
7674
|
-
});
|
|
7675
|
-
state.messageStartSent = true;
|
|
7676
|
-
}
|
|
7677
|
-
if (delta.content) {
|
|
7678
|
-
if (isToolBlockOpen(state)) {
|
|
7679
|
-
events.push({
|
|
7680
|
-
type: "content_block_stop",
|
|
7681
|
-
index: state.contentBlockIndex
|
|
7682
|
-
});
|
|
7683
|
-
state.contentBlockIndex++;
|
|
7684
|
-
state.contentBlockOpen = false;
|
|
7685
|
-
}
|
|
7686
|
-
if (!state.contentBlockOpen) {
|
|
7687
|
-
events.push({
|
|
7688
|
-
type: "content_block_start",
|
|
7689
|
-
index: state.contentBlockIndex,
|
|
7690
|
-
content_block: {
|
|
7691
|
-
type: "text",
|
|
7692
|
-
text: ""
|
|
7693
|
-
}
|
|
7694
|
-
});
|
|
7695
|
-
state.contentBlockOpen = true;
|
|
7696
|
-
}
|
|
7697
|
-
events.push({
|
|
7698
|
-
type: "content_block_delta",
|
|
7699
|
-
index: state.contentBlockIndex,
|
|
7700
|
-
delta: {
|
|
7701
|
-
type: "text_delta",
|
|
7702
|
-
text: delta.content
|
|
7703
|
-
}
|
|
7704
|
-
});
|
|
7705
|
-
}
|
|
7706
|
-
if (delta.tool_calls) for (const toolCall of delta.tool_calls) {
|
|
7707
|
-
if (toolCall.id && toolCall.function?.name) {
|
|
7708
|
-
if (state.contentBlockOpen) {
|
|
7709
|
-
events.push({
|
|
7710
|
-
type: "content_block_stop",
|
|
7711
|
-
index: state.contentBlockIndex
|
|
7712
|
-
});
|
|
7713
|
-
state.contentBlockIndex++;
|
|
7714
|
-
state.contentBlockOpen = false;
|
|
7715
|
-
}
|
|
7716
|
-
const originalName = toolNameMapping?.truncatedToOriginal.get(toolCall.function.name) ?? toolCall.function.name;
|
|
7717
|
-
const anthropicBlockIndex = state.contentBlockIndex;
|
|
7718
|
-
state.toolCalls[toolCall.index] = {
|
|
7719
|
-
id: toolCall.id,
|
|
7720
|
-
name: originalName,
|
|
7721
|
-
anthropicBlockIndex
|
|
7722
|
-
};
|
|
7723
|
-
events.push({
|
|
7724
|
-
type: "content_block_start",
|
|
7725
|
-
index: anthropicBlockIndex,
|
|
7726
|
-
content_block: {
|
|
7727
|
-
type: "tool_use",
|
|
7728
|
-
id: toolCall.id,
|
|
7729
|
-
name: originalName,
|
|
7730
|
-
input: {}
|
|
7731
|
-
}
|
|
7732
|
-
});
|
|
7733
|
-
state.contentBlockOpen = true;
|
|
7734
|
-
}
|
|
7735
|
-
if (toolCall.function?.arguments) {
|
|
7736
|
-
const toolCallInfo = state.toolCalls[toolCall.index];
|
|
7737
|
-
if (toolCallInfo) events.push({
|
|
7738
|
-
type: "content_block_delta",
|
|
7739
|
-
index: toolCallInfo.anthropicBlockIndex,
|
|
7740
|
-
delta: {
|
|
7741
|
-
type: "input_json_delta",
|
|
7742
|
-
partial_json: toolCall.function.arguments
|
|
7743
|
-
}
|
|
7744
|
-
});
|
|
7745
|
-
}
|
|
7746
|
-
}
|
|
7747
|
-
if (choice.finish_reason) {
|
|
7748
|
-
if (state.contentBlockOpen) {
|
|
7749
|
-
events.push({
|
|
7750
|
-
type: "content_block_stop",
|
|
7751
|
-
index: state.contentBlockIndex
|
|
7752
|
-
});
|
|
7753
|
-
state.contentBlockOpen = false;
|
|
7754
|
-
}
|
|
7755
|
-
events.push({
|
|
7756
|
-
type: "message_delta",
|
|
7757
|
-
delta: {
|
|
7758
|
-
stop_reason: mapOpenAIStopReasonToAnthropic(choice.finish_reason),
|
|
7759
|
-
stop_sequence: null
|
|
7760
|
-
},
|
|
7761
|
-
usage: {
|
|
7762
|
-
input_tokens: (chunk.usage?.prompt_tokens ?? 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0),
|
|
7763
|
-
output_tokens: chunk.usage?.completion_tokens ?? 0,
|
|
7764
|
-
...chunk.usage?.prompt_tokens_details?.cached_tokens !== void 0 && { cache_read_input_tokens: chunk.usage.prompt_tokens_details.cached_tokens }
|
|
7765
|
-
}
|
|
7766
|
-
}, { type: "message_stop" });
|
|
7767
|
-
}
|
|
7768
|
-
return events;
|
|
7769
|
-
}
|
|
7770
|
-
function translateErrorToAnthropicErrorEvent(error) {
|
|
7771
|
-
return {
|
|
7772
|
-
type: "error",
|
|
7773
|
-
error: {
|
|
7774
|
-
type: "api_error",
|
|
7775
|
-
message: error ? formatError(error) : "An unexpected error occurred during streaming."
|
|
7776
|
-
}
|
|
7777
|
-
};
|
|
7778
|
-
}
|
|
7779
|
-
|
|
7780
|
-
//#endregion
|
|
7781
|
-
//#region src/routes/messages/tool-call-recovery.ts
|
|
7782
|
-
const ENVELOPE = String.raw`(?:<(?:antml:)?function_calls>|call|count|court)`;
|
|
7783
|
-
const INVOKE_BODY = String.raw`<(?:antml:)?invoke\s+name="[^"]+">(?:(?!<(?:antml:)?invoke\b)[\s\S])*?</(?:antml:)?invoke>`;
|
|
7784
|
-
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");
|
|
7785
|
-
const STARTS_WITH_ENVELOPE_RE = new RegExp(String.raw`^[ \t\n]*` + ENVELOPE);
|
|
7786
|
-
const INVOKE_OPENER_RE = new RegExp(String.raw`(?:^|\n)[ \t]*(?:` + ENVELOPE + String.raw`[ \t\n]*)?<(?:antml:)?invoke\b[^\n>]*>?`, "g");
|
|
7787
|
-
const LEAK_OPEN_RE = new RegExp(String.raw`(?:^|\n)[ \t]*(?:` + ENVELOPE + String.raw`[ \t\n]*)?<(?:antml:)?invoke\s+name="`, "g");
|
|
7788
|
-
const INVOKE_RE = /<(?:antml:)?invoke\s+name="([^"]+)">([\s\S]*?)<\/(?:antml:)?invoke>/g;
|
|
7789
|
-
const PARAMETER_RE = /<(?:antml:)?parameter\s+name="([^"]+)">([\s\S]*?)<\/(?:antml:)?parameter>/g;
|
|
7790
|
-
function coerceParamValue(raw) {
|
|
7791
|
-
const trimmed = raw.trim();
|
|
7792
|
-
if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) try {
|
|
7793
|
-
return JSON.parse(trimmed);
|
|
7794
|
-
} catch {
|
|
7795
|
-
return raw;
|
|
7796
|
-
}
|
|
7797
|
-
return raw;
|
|
7798
|
-
}
|
|
7799
|
-
function parseRegionInvokes(region, knownTools) {
|
|
7800
|
-
const calls = [];
|
|
7801
|
-
for (const invokeMatch of region.matchAll(INVOKE_RE)) {
|
|
7802
|
-
const name = invokeMatch[1];
|
|
7803
|
-
if (knownTools && !knownTools.has(name)) continue;
|
|
7804
|
-
const input = {};
|
|
7805
|
-
for (const paramMatch of invokeMatch[2].matchAll(PARAMETER_RE)) input[paramMatch[1]] = coerceParamValue(paramMatch[2]);
|
|
7806
|
-
calls.push({
|
|
7807
|
-
name,
|
|
7808
|
-
input
|
|
7809
|
-
});
|
|
7810
|
-
}
|
|
7811
|
-
return calls;
|
|
7812
|
-
}
|
|
7813
|
-
const FENCE_DELIM_RE = /(?:^|\n)[ \t]*```/g;
|
|
7814
|
-
function insideFence(text, pos) {
|
|
7815
|
-
let openAt = -1;
|
|
7816
|
-
for (const m of text.matchAll(FENCE_DELIM_RE)) if (openAt === -1) {
|
|
7817
|
-
if (m.index >= pos) break;
|
|
7818
|
-
openAt = m.index;
|
|
7819
|
-
} else if (m.index > pos) return true;
|
|
7820
|
-
else openAt = -1;
|
|
7821
|
-
return false;
|
|
7822
|
-
}
|
|
7823
|
-
function regionIsLeak(region, offset, fullText, knownTools) {
|
|
7824
|
-
if (insideFence(fullText, offset)) return false;
|
|
7825
|
-
if (STARTS_WITH_ENVELOPE_RE.test(region)) return true;
|
|
7826
|
-
if (!knownTools) return false;
|
|
7827
|
-
return parseRegionInvokes(region, knownTools).length > 0;
|
|
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;
|
|
7828
7332
|
}
|
|
7829
7333
|
/**
|
|
7830
7334
|
* Split assistant text into ordered segments — dropping leaked envelope markup
|
|
@@ -8462,6 +7966,7 @@ function handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateRes
|
|
|
8462
7966
|
outputTokens: response.usage.output_tokens,
|
|
8463
7967
|
queueWaitMs: ctx.queueWaitMs,
|
|
8464
7968
|
cachedInputTokens: cacheRead,
|
|
7969
|
+
cacheCreationInputTokens: cacheCreation,
|
|
8465
7970
|
totalInputTokens
|
|
8466
7971
|
});
|
|
8467
7972
|
captureRequest({
|
|
@@ -8473,6 +7978,7 @@ function handleDirectAnthropicNonStreamingResponse(c, response, ctx, truncateRes
|
|
|
8473
7978
|
stream: false,
|
|
8474
7979
|
toolCount: payload.tools?.length ?? 0,
|
|
8475
7980
|
cachedInputTokens: cacheRead,
|
|
7981
|
+
cacheCreationInputTokens: cacheCreation,
|
|
8476
7982
|
totalInputTokens,
|
|
8477
7983
|
stopReason: response.stop_reason ?? void 0
|
|
8478
7984
|
});
|
|
@@ -8561,6 +8067,7 @@ async function handleDirectAnthropicStreamingResponse(opts) {
|
|
|
8561
8067
|
toolCount: anthropicPayload.tools?.length ?? 0
|
|
8562
8068
|
}, ctx.timings, {
|
|
8563
8069
|
cachedInputTokens: acc.cacheReadInputTokens,
|
|
8070
|
+
cacheCreationInputTokens: acc.cacheCreationInputTokens,
|
|
8564
8071
|
totalInputTokens: acc.inputTokens + acc.cacheReadInputTokens + acc.cacheCreationInputTokens
|
|
8565
8072
|
});
|
|
8566
8073
|
} catch (error) {
|
|
@@ -8632,356 +8139,350 @@ const parseSubagentMarkerFromSystemReminder = (text) => {
|
|
|
8632
8139
|
};
|
|
8633
8140
|
|
|
8634
8141
|
//#endregion
|
|
8635
|
-
//#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
|
+
}
|
|
8636
8199
|
/**
|
|
8637
|
-
*
|
|
8200
|
+
* Log tool-related information for debugging
|
|
8638
8201
|
*/
|
|
8639
|
-
|
|
8640
|
-
|
|
8641
|
-
|
|
8642
|
-
|
|
8643
|
-
|
|
8644
|
-
|
|
8645
|
-
|
|
8646
|
-
|
|
8647
|
-
const
|
|
8648
|
-
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
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
|
-
updateTrackerStatus(ctx.trackingId, "streaming");
|
|
8685
|
-
return streamSSE(c, async (stream) => {
|
|
8686
|
-
stream.onAbort(() => abort.abort());
|
|
8687
|
-
await handleStreamingResponse({
|
|
8688
|
-
stream,
|
|
8689
|
-
response,
|
|
8690
|
-
toolNameMapping,
|
|
8691
|
-
anthropicPayload,
|
|
8692
|
-
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."
|
|
8693
8247
|
});
|
|
8694
|
-
}
|
|
8695
|
-
}
|
|
8696
|
-
consola.debug("[RateLimiter] Request queued past grace; opening keepalive stream (translated)");
|
|
8697
|
-
updateTrackerStatus(ctx.trackingId, "streaming");
|
|
8698
|
-
return streamSSE(c, async (stream) => {
|
|
8699
|
-
stream.onAbort(() => abort.abort());
|
|
8700
|
-
await runStreamWithKeepalive({
|
|
8701
|
-
stream,
|
|
8702
|
-
settled,
|
|
8703
|
-
pingIntervalMs: KEEPALIVE_PING_INTERVAL_MS,
|
|
8704
|
-
onResponse: async ({ result: response, queueWaitMs }) => {
|
|
8705
|
-
ctx.queueWaitMs = queueWaitMs;
|
|
8706
|
-
if (isNonStreaming(response)) return;
|
|
8707
|
-
await handleStreamingResponse({
|
|
8708
|
-
stream,
|
|
8709
|
-
response,
|
|
8710
|
-
toolNameMapping,
|
|
8711
|
-
anthropicPayload,
|
|
8712
|
-
ctx
|
|
8713
|
-
});
|
|
8714
|
-
},
|
|
8715
|
-
onError: async (error) => {
|
|
8716
|
-
if (isAbortError(error)) {
|
|
8717
|
-
consola.debug("[Translated] client disconnected during keepalive; upstream aborted");
|
|
8718
|
-
failTracking(ctx.trackingId, "client disconnected");
|
|
8719
|
-
return;
|
|
8720
|
-
}
|
|
8721
|
-
recordStreamError({
|
|
8722
|
-
acc: createAnthropicStreamAccumulator(),
|
|
8723
|
-
fallbackModel: anthropicPayload.model,
|
|
8724
|
-
ctx,
|
|
8725
|
-
error,
|
|
8726
|
-
endpoint: "messages"
|
|
8727
|
-
});
|
|
8728
|
-
failTracking(ctx.trackingId, error);
|
|
8729
|
-
const errorEvent = translateErrorToAnthropicErrorEvent(error);
|
|
8730
|
-
await stream.writeSSE({
|
|
8731
|
-
event: errorEvent.type,
|
|
8732
|
-
data: JSON.stringify(errorEvent)
|
|
8733
|
-
});
|
|
8734
|
-
}
|
|
8735
|
-
});
|
|
8736
|
-
});
|
|
8737
|
-
} catch (error) {
|
|
8738
|
-
if (isAbortError(error)) {
|
|
8739
|
-
consola.debug("[Translated] client disconnected before response; upstream aborted");
|
|
8740
|
-
failTracking(ctx.trackingId, "client disconnected");
|
|
8741
|
-
return new Response(null, { status: 499 });
|
|
8248
|
+
}
|
|
8742
8249
|
}
|
|
8743
|
-
if (error instanceof HTTPError && error.status === 413) await logPayloadSizeInfo(openAIPayload, selectedModel);
|
|
8744
|
-
recordErrorResponse(ctx, anthropicPayload.model, error, "messages", anthropicPayload.stream ?? false);
|
|
8745
|
-
throw error;
|
|
8746
8250
|
}
|
|
8251
|
+
return fixedMessages;
|
|
8747
8252
|
}
|
|
8748
|
-
function
|
|
8749
|
-
const {
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
|
|
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;
|
|
8287
|
+
});
|
|
8288
|
+
return candidates[0].id;
|
|
8289
|
+
}
|
|
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];
|
|
8319
|
+
}
|
|
8320
|
+
function translateModelName(model) {
|
|
8321
|
+
const aliasMap = {
|
|
8322
|
+
opus: "claude-opus",
|
|
8323
|
+
sonnet: "claude-sonnet",
|
|
8324
|
+
haiku: "claude-haiku"
|
|
8325
|
+
};
|
|
8326
|
+
if (aliasMap[model]) {
|
|
8327
|
+
const familyPrefix = aliasMap[model];
|
|
8328
|
+
return findLatestModel(familyPrefix, `${familyPrefix}-4.5`);
|
|
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;
|
|
8343
|
+
}
|
|
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];
|
|
8348
|
+
}
|
|
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");
|
|
8756
8360
|
}
|
|
8757
|
-
|
|
8758
|
-
success: true,
|
|
8759
|
-
model: anthropicResponse.model,
|
|
8760
|
-
usage: anthropicResponse.usage,
|
|
8761
|
-
stop_reason: anthropicResponse.stop_reason ?? void 0,
|
|
8762
|
-
content: {
|
|
8763
|
-
role: "assistant",
|
|
8764
|
-
content: anthropicResponse.content.map((block) => {
|
|
8765
|
-
if (block.type === "text") return {
|
|
8766
|
-
type: "text",
|
|
8767
|
-
text: block.text
|
|
8768
|
-
};
|
|
8769
|
-
if (block.type === "tool_use") return {
|
|
8770
|
-
type: "tool_use",
|
|
8771
|
-
id: block.id,
|
|
8772
|
-
name: block.name,
|
|
8773
|
-
input: JSON.stringify(block.input)
|
|
8774
|
-
};
|
|
8775
|
-
return { type: block.type };
|
|
8776
|
-
})
|
|
8777
|
-
},
|
|
8778
|
-
toolCalls: extractToolCallsFromContent(anthropicResponse.content)
|
|
8779
|
-
}, Date.now() - ctx.startTime);
|
|
8780
|
-
const cacheRead = anthropicResponse.usage.cache_read_input_tokens ?? 0;
|
|
8781
|
-
const cacheCreation = anthropicResponse.usage.cache_creation_input_tokens ?? 0;
|
|
8782
|
-
const totalInputTokens = anthropicResponse.usage.input_tokens + cacheRead + cacheCreation;
|
|
8783
|
-
if (ctx.trackingId) requestTracker.updateRequest(ctx.trackingId, {
|
|
8784
|
-
inputTokens: anthropicResponse.usage.input_tokens,
|
|
8785
|
-
outputTokens: anthropicResponse.usage.output_tokens,
|
|
8786
|
-
queueWaitMs: ctx.queueWaitMs,
|
|
8787
|
-
cachedInputTokens: cacheRead,
|
|
8788
|
-
totalInputTokens
|
|
8789
|
-
});
|
|
8790
|
-
captureRequest({
|
|
8791
|
-
model: anthropicResponse.model,
|
|
8792
|
-
inputTokens: anthropicResponse.usage.input_tokens,
|
|
8793
|
-
outputTokens: anthropicResponse.usage.output_tokens,
|
|
8794
|
-
durationMs: Date.now() - ctx.startTime,
|
|
8795
|
-
success: true,
|
|
8796
|
-
stream: false,
|
|
8797
|
-
toolCount: anthropicPayload.tools?.length ?? 0,
|
|
8798
|
-
cachedInputTokens: cacheRead,
|
|
8799
|
-
totalInputTokens,
|
|
8800
|
-
stopReason: anthropicResponse.stop_reason ?? void 0
|
|
8801
|
-
});
|
|
8802
|
-
return c.json(echoResponseBody(anthropicResponse, ctx));
|
|
8361
|
+
return filtered;
|
|
8803
8362
|
}
|
|
8804
|
-
|
|
8805
|
-
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
|
|
8816
|
-
|
|
8817
|
-
|
|
8818
|
-
|
|
8819
|
-
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
|
|
8823
|
-
|
|
8824
|
-
streamState,
|
|
8825
|
-
acc,
|
|
8826
|
-
checkRepetition,
|
|
8827
|
-
ctx
|
|
8828
|
-
});
|
|
8829
|
-
recordAnthropicStreamingResponse(acc, anthropicPayload.model, ctx);
|
|
8830
|
-
completeTracking(ctx.trackingId, acc.inputTokens, acc.outputTokens, ctx.queueWaitMs, void 0, {
|
|
8831
|
-
model: acc.model || anthropicPayload.model,
|
|
8832
|
-
stream: true,
|
|
8833
|
-
durationMs: Date.now() - ctx.startTime,
|
|
8834
|
-
stopReason: acc.stopReason || void 0,
|
|
8835
|
-
toolCount: anthropicPayload.tools?.length ?? 0
|
|
8836
|
-
}, ctx.timings, {
|
|
8837
|
-
cachedInputTokens: acc.cacheReadInputTokens,
|
|
8838
|
-
totalInputTokens: acc.inputTokens + acc.cacheReadInputTokens + acc.cacheCreationInputTokens
|
|
8839
|
-
});
|
|
8840
|
-
} catch (error) {
|
|
8841
|
-
if (isAbortError(error)) {
|
|
8842
|
-
consola.debug("[Translated] client disconnected mid-stream; upstream aborted");
|
|
8843
|
-
failTracking(ctx.trackingId, "client disconnected");
|
|
8844
|
-
return;
|
|
8845
|
-
}
|
|
8846
|
-
consola.error("Stream error:", formatError(error));
|
|
8847
|
-
recordStreamError({
|
|
8848
|
-
acc,
|
|
8849
|
-
fallbackModel: anthropicPayload.model,
|
|
8850
|
-
ctx,
|
|
8851
|
-
error,
|
|
8852
|
-
endpoint: "messages"
|
|
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)
|
|
8853
8383
|
});
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
8857
|
-
event: errorEvent.type,
|
|
8858
|
-
data: JSON.stringify(errorEvent)
|
|
8384
|
+
if (otherBlocks.length > 0) newMessages.push({
|
|
8385
|
+
role: "user",
|
|
8386
|
+
content: mapContent(otherBlocks)
|
|
8859
8387
|
});
|
|
8860
|
-
}
|
|
8861
|
-
|
|
8862
|
-
|
|
8863
|
-
const blockStartEvent = {
|
|
8864
|
-
type: "content_block_start",
|
|
8865
|
-
index: streamState.contentBlockIndex,
|
|
8866
|
-
content_block: {
|
|
8867
|
-
type: "text",
|
|
8868
|
-
text: ""
|
|
8869
|
-
}
|
|
8870
|
-
};
|
|
8871
|
-
await stream.writeSSE({
|
|
8872
|
-
event: "content_block_start",
|
|
8873
|
-
data: JSON.stringify(blockStartEvent)
|
|
8874
|
-
});
|
|
8875
|
-
const deltaEvent = {
|
|
8876
|
-
type: "content_block_delta",
|
|
8877
|
-
index: streamState.contentBlockIndex,
|
|
8878
|
-
delta: {
|
|
8879
|
-
type: "text_delta",
|
|
8880
|
-
text: marker
|
|
8881
|
-
}
|
|
8882
|
-
};
|
|
8883
|
-
await stream.writeSSE({
|
|
8884
|
-
event: "content_block_delta",
|
|
8885
|
-
data: JSON.stringify(deltaEvent)
|
|
8886
|
-
});
|
|
8887
|
-
const blockStopEvent = {
|
|
8888
|
-
type: "content_block_stop",
|
|
8889
|
-
index: streamState.contentBlockIndex
|
|
8890
|
-
};
|
|
8891
|
-
await stream.writeSSE({
|
|
8892
|
-
event: "content_block_stop",
|
|
8893
|
-
data: JSON.stringify(blockStopEvent)
|
|
8388
|
+
} else newMessages.push({
|
|
8389
|
+
role: "user",
|
|
8390
|
+
content: mapContent(message.content)
|
|
8894
8391
|
});
|
|
8895
|
-
|
|
8896
|
-
}
|
|
8897
|
-
async function processStreamChunks(opts) {
|
|
8898
|
-
const { stream, response, toolNameMapping, streamState, acc, checkRepetition, ctx } = opts;
|
|
8899
|
-
for await (const rawEvent of response) {
|
|
8900
|
-
consola.debug("Copilot raw stream event:", JSON.stringify(rawEvent));
|
|
8901
|
-
if (rawEvent.data === "[DONE]") break;
|
|
8902
|
-
if (!rawEvent.data) continue;
|
|
8903
|
-
let chunk;
|
|
8904
|
-
try {
|
|
8905
|
-
chunk = JSON.parse(rawEvent.data);
|
|
8906
|
-
} catch (parseError) {
|
|
8907
|
-
consola.error("Failed to parse stream chunk:", parseError, rawEvent.data);
|
|
8908
|
-
continue;
|
|
8909
|
-
}
|
|
8910
|
-
if (chunk.model && !acc.model) acc.model = chunk.model;
|
|
8911
|
-
const events = translateChunkToAnthropicEvents(chunk, streamState, toolNameMapping);
|
|
8912
|
-
for (const event of events) {
|
|
8913
|
-
consola.debug("Translated Anthropic event:", JSON.stringify(event));
|
|
8914
|
-
processAnthropicEvent(event, acc);
|
|
8915
|
-
if (event.type === "content_block_delta" && event.delta.type === "text_delta") checkRepetition(event.delta.text);
|
|
8916
|
-
const echoed = echoParsedEvent(event, ctx);
|
|
8917
|
-
await stream.writeSSE({
|
|
8918
|
-
event: echoed.type,
|
|
8919
|
-
data: JSON.stringify(echoed)
|
|
8920
|
-
});
|
|
8921
|
-
}
|
|
8922
|
-
}
|
|
8392
|
+
return newMessages;
|
|
8923
8393
|
}
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
const
|
|
8932
|
-
|
|
8933
|
-
return
|
|
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
|
+
}];
|
|
8934
8418
|
}
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
8943
|
-
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
const sanitizedPayload = dePoisonAssistantMessages(anthropicPayload);
|
|
8960
|
-
logToolInfo(sanitizedPayload);
|
|
8961
|
-
const subagentMarker = parseSubagentMarkerFromFirstUser(sanitizedPayload);
|
|
8962
|
-
const initiatorOverride = subagentMarker ? "agent" : void 0;
|
|
8963
|
-
if (subagentMarker) consola.debug("Detected Subagent marker:", JSON.stringify(subagentMarker));
|
|
8964
|
-
if (supportsDirectAnthropicApi(sanitizedPayload.model)) {
|
|
8965
|
-
normalizeSystemPromptDate(sanitizedPayload);
|
|
8966
|
-
injectSystemCacheControl(sanitizedPayload);
|
|
8967
|
-
return handleDirectAnthropicCompletion(c, sanitizedPayload, ctx, initiatorOverride);
|
|
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;
|
|
8968
8443
|
}
|
|
8969
|
-
return
|
|
8444
|
+
return contentParts;
|
|
8970
8445
|
}
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
|
|
8980
|
-
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);
|
|
8981
8455
|
}
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
|
|
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;
|
|
8985
8486
|
}
|
|
8986
8487
|
}
|
|
8987
8488
|
|
|
@@ -9150,6 +8651,26 @@ const createResponses = async (payload, { vision, initiator, resolvedModel, sign
|
|
|
9150
8651
|
return await response.json();
|
|
9151
8652
|
};
|
|
9152
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
|
+
|
|
9153
8674
|
//#endregion
|
|
9154
8675
|
//#region src/routes/responses/stream-id-sync.ts
|
|
9155
8676
|
const createStreamIdTracker = () => ({ outputItems: /* @__PURE__ */ new Map() });
|
|
@@ -9388,7 +8909,12 @@ const handleResponses = async (c) => {
|
|
|
9388
8909
|
rawPayload,
|
|
9389
8910
|
endpoint: "openai",
|
|
9390
8911
|
normalizePayload: (p) => {
|
|
9391
|
-
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;
|
|
9392
8918
|
useFunctionApplyPatch(np);
|
|
9393
8919
|
filterUnsupportedBuiltins(np);
|
|
9394
8920
|
injectPromptCacheKey(np, clientName);
|
|
@@ -9464,13 +8990,15 @@ const handleResponses = async (c) => {
|
|
|
9464
8990
|
if (finalResult) {
|
|
9465
8991
|
recordResponseResult(finalResult, model, historyId, startTime);
|
|
9466
8992
|
const usage = finalResult.usage;
|
|
9467
|
-
|
|
8993
|
+
const cachedInputTokens = usage?.input_tokens_details?.cached_tokens ?? 0;
|
|
8994
|
+
completeTracking(trackingId, (usage?.input_tokens ?? 0) - cachedInputTokens, usage?.output_tokens ?? 0, queueWaitMs, usage?.output_tokens_details?.reasoning_tokens, {
|
|
9468
8995
|
model: finalResult.model || model,
|
|
9469
8996
|
stream: true,
|
|
9470
8997
|
durationMs: Date.now() - startTime,
|
|
9471
8998
|
toolCount: tools.length
|
|
9472
8999
|
}, ctx.timings, {
|
|
9473
|
-
cachedInputTokens
|
|
9000
|
+
cachedInputTokens,
|
|
9001
|
+
cacheCreationInputTokens: 0,
|
|
9474
9002
|
totalInputTokens: usage?.input_tokens ?? 0
|
|
9475
9003
|
});
|
|
9476
9004
|
} else if (streamErrorMessage) {
|
|
@@ -9517,14 +9045,17 @@ const handleResponses = async (c) => {
|
|
|
9517
9045
|
}
|
|
9518
9046
|
const result = response;
|
|
9519
9047
|
const usage = result.usage;
|
|
9048
|
+
const cachedInputTokens = usage?.input_tokens_details?.cached_tokens ?? 0;
|
|
9049
|
+
const freshInputTokens = (usage?.input_tokens ?? 0) - cachedInputTokens;
|
|
9520
9050
|
recordResponseResult(result, model, historyId, startTime);
|
|
9521
|
-
completeTracking(trackingId,
|
|
9051
|
+
completeTracking(trackingId, freshInputTokens, usage?.output_tokens ?? 0, ctx.queueWaitMs, usage?.output_tokens_details?.reasoning_tokens, {
|
|
9522
9052
|
model: result.model || model,
|
|
9523
9053
|
stream: false,
|
|
9524
9054
|
durationMs: Date.now() - startTime,
|
|
9525
9055
|
toolCount: tools.length
|
|
9526
9056
|
}, ctx.timings, {
|
|
9527
|
-
cachedInputTokens
|
|
9057
|
+
cachedInputTokens,
|
|
9058
|
+
cacheCreationInputTokens: 0,
|
|
9528
9059
|
totalInputTokens: usage?.input_tokens ?? 0
|
|
9529
9060
|
});
|
|
9530
9061
|
consola.debug("Forwarding native Responses result:", JSON.stringify(result).slice(-400));
|