@dianshuv/copilot-api 0.2.0 → 0.2.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 +518 -506
- package/package.json +1 -1
package/dist/main.mjs
CHANGED
|
@@ -1017,7 +1017,7 @@ const patchClaude = defineCommand({
|
|
|
1017
1017
|
|
|
1018
1018
|
//#endregion
|
|
1019
1019
|
//#region package.json
|
|
1020
|
-
var version = "0.2.
|
|
1020
|
+
var version = "0.2.1";
|
|
1021
1021
|
|
|
1022
1022
|
//#endregion
|
|
1023
1023
|
//#region src/lib/adaptive-rate-limiter.ts
|
|
@@ -4907,6 +4907,211 @@ async function checkNeedsCompactionAnthropic(payload, model, config = {}) {
|
|
|
4907
4907
|
};
|
|
4908
4908
|
}
|
|
4909
4909
|
|
|
4910
|
+
//#endregion
|
|
4911
|
+
//#region src/services/copilot/create-anthropic-messages.ts
|
|
4912
|
+
/**
|
|
4913
|
+
* Direct Anthropic-style message API for Copilot.
|
|
4914
|
+
* Used when the model vendor is Anthropic and supports /v1/messages endpoint.
|
|
4915
|
+
*/
|
|
4916
|
+
/**
|
|
4917
|
+
* Fields that are supported by Copilot's Anthropic API endpoint.
|
|
4918
|
+
* Any other fields in the incoming request will be stripped.
|
|
4919
|
+
*/
|
|
4920
|
+
const COPILOT_SUPPORTED_FIELDS = new Set([
|
|
4921
|
+
"model",
|
|
4922
|
+
"messages",
|
|
4923
|
+
"max_tokens",
|
|
4924
|
+
"system",
|
|
4925
|
+
"metadata",
|
|
4926
|
+
"stop_sequences",
|
|
4927
|
+
"stream",
|
|
4928
|
+
"temperature",
|
|
4929
|
+
"top_p",
|
|
4930
|
+
"top_k",
|
|
4931
|
+
"tools",
|
|
4932
|
+
"tool_choice",
|
|
4933
|
+
"thinking",
|
|
4934
|
+
"service_tier"
|
|
4935
|
+
]);
|
|
4936
|
+
/**
|
|
4937
|
+
* Filter payload to only include fields supported by Copilot's Anthropic API.
|
|
4938
|
+
* This prevents errors like "Extra inputs are not permitted" for unsupported
|
|
4939
|
+
* fields like `output_config`.
|
|
4940
|
+
*
|
|
4941
|
+
* Also converts server-side tools (web_search, etc.) to custom tools.
|
|
4942
|
+
*/
|
|
4943
|
+
function filterPayloadForCopilot(payload) {
|
|
4944
|
+
const filtered = {};
|
|
4945
|
+
const unsupportedFields = [];
|
|
4946
|
+
for (const [key, value] of Object.entries(payload)) if (COPILOT_SUPPORTED_FIELDS.has(key)) filtered[key] = value;
|
|
4947
|
+
else unsupportedFields.push(key);
|
|
4948
|
+
if (unsupportedFields.length > 0) consola.debug(`[DirectAnthropic] Filtered unsupported fields: ${unsupportedFields.join(", ")}`);
|
|
4949
|
+
if (filtered.tools) filtered.tools = convertServerToolsToCustom(filtered.tools);
|
|
4950
|
+
return filtered;
|
|
4951
|
+
}
|
|
4952
|
+
/**
|
|
4953
|
+
* Adjust max_tokens if thinking is enabled.
|
|
4954
|
+
* According to Anthropic docs, max_tokens must be greater than thinking.budget_tokens.
|
|
4955
|
+
* max_tokens = thinking_budget + response_tokens
|
|
4956
|
+
*/
|
|
4957
|
+
function adjustMaxTokensForThinking(payload) {
|
|
4958
|
+
const thinking = payload.thinking;
|
|
4959
|
+
if (!thinking) return payload;
|
|
4960
|
+
const budgetTokens = thinking.budget_tokens;
|
|
4961
|
+
if (!budgetTokens) return payload;
|
|
4962
|
+
if (payload.max_tokens <= budgetTokens) {
|
|
4963
|
+
const newMaxTokens = budgetTokens + Math.min(16384, budgetTokens);
|
|
4964
|
+
consola.debug(`[DirectAnthropic] Adjusted max_tokens: ${payload.max_tokens} → ${newMaxTokens} (thinking.budget_tokens=${budgetTokens})`);
|
|
4965
|
+
return {
|
|
4966
|
+
...payload,
|
|
4967
|
+
max_tokens: newMaxTokens
|
|
4968
|
+
};
|
|
4969
|
+
}
|
|
4970
|
+
return payload;
|
|
4971
|
+
}
|
|
4972
|
+
/**
|
|
4973
|
+
* Create messages using Anthropic-style API directly.
|
|
4974
|
+
* This bypasses the OpenAI translation layer for Anthropic models.
|
|
4975
|
+
*/
|
|
4976
|
+
async function createAnthropicMessages(payload, options) {
|
|
4977
|
+
if (!state.copilotToken) throw new Error("Copilot token not found");
|
|
4978
|
+
let filteredPayload = filterPayloadForCopilot(payload);
|
|
4979
|
+
filteredPayload = adjustMaxTokensForThinking(filteredPayload);
|
|
4980
|
+
const enableVision = filteredPayload.messages.some((msg) => {
|
|
4981
|
+
if (typeof msg.content === "string") return false;
|
|
4982
|
+
return msg.content.some((block) => block.type === "image");
|
|
4983
|
+
});
|
|
4984
|
+
const isAgentCall = filteredPayload.messages.some((msg) => msg.role === "assistant");
|
|
4985
|
+
const headers = {
|
|
4986
|
+
...copilotHeaders(state, enableVision),
|
|
4987
|
+
"X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user"),
|
|
4988
|
+
"anthropic-version": "2023-06-01"
|
|
4989
|
+
};
|
|
4990
|
+
consola.debug("Sending direct Anthropic request to Copilot /v1/messages");
|
|
4991
|
+
const response = await fetch(`${copilotBaseUrl(state)}/v1/messages`, {
|
|
4992
|
+
method: "POST",
|
|
4993
|
+
headers,
|
|
4994
|
+
body: JSON.stringify(filteredPayload)
|
|
4995
|
+
});
|
|
4996
|
+
if (!response.ok) {
|
|
4997
|
+
consola.debug("Request failed:", {
|
|
4998
|
+
model: filteredPayload.model,
|
|
4999
|
+
max_tokens: filteredPayload.max_tokens,
|
|
5000
|
+
stream: filteredPayload.stream,
|
|
5001
|
+
tools: filteredPayload.tools?.map((t) => ({
|
|
5002
|
+
name: t.name,
|
|
5003
|
+
type: t.type
|
|
5004
|
+
})),
|
|
5005
|
+
thinking: filteredPayload.thinking,
|
|
5006
|
+
messageCount: filteredPayload.messages.length
|
|
5007
|
+
});
|
|
5008
|
+
throw await HTTPError.fromResponse("Failed to create Anthropic messages", response, filteredPayload.model);
|
|
5009
|
+
}
|
|
5010
|
+
if (payload.stream) return events(response);
|
|
5011
|
+
return await response.json();
|
|
5012
|
+
}
|
|
5013
|
+
const SERVER_TOOL_CONFIGS = {
|
|
5014
|
+
web_search: {
|
|
5015
|
+
description: "Search the web for current information. Returns web search results that can help answer questions about recent events, current data, or information that may have changed since your knowledge cutoff.",
|
|
5016
|
+
input_schema: {
|
|
5017
|
+
type: "object",
|
|
5018
|
+
properties: { query: {
|
|
5019
|
+
type: "string",
|
|
5020
|
+
description: "The search query"
|
|
5021
|
+
} },
|
|
5022
|
+
required: ["query"]
|
|
5023
|
+
}
|
|
5024
|
+
},
|
|
5025
|
+
web_fetch: {
|
|
5026
|
+
description: "Fetch content from a URL. NOTE: This is a client-side tool - the client must fetch the URL and return the content.",
|
|
5027
|
+
input_schema: {
|
|
5028
|
+
type: "object",
|
|
5029
|
+
properties: { url: {
|
|
5030
|
+
type: "string",
|
|
5031
|
+
description: "The URL to fetch"
|
|
5032
|
+
} },
|
|
5033
|
+
required: ["url"]
|
|
5034
|
+
}
|
|
5035
|
+
},
|
|
5036
|
+
code_execution: {
|
|
5037
|
+
description: "Execute code in a sandbox. NOTE: This is a client-side tool - the client must execute the code.",
|
|
5038
|
+
input_schema: {
|
|
5039
|
+
type: "object",
|
|
5040
|
+
properties: {
|
|
5041
|
+
code: {
|
|
5042
|
+
type: "string",
|
|
5043
|
+
description: "The code to execute"
|
|
5044
|
+
},
|
|
5045
|
+
language: {
|
|
5046
|
+
type: "string",
|
|
5047
|
+
description: "The programming language"
|
|
5048
|
+
}
|
|
5049
|
+
},
|
|
5050
|
+
required: ["code"]
|
|
5051
|
+
}
|
|
5052
|
+
},
|
|
5053
|
+
computer: {
|
|
5054
|
+
description: "Control computer desktop. NOTE: This is a client-side tool - the client must handle computer control.",
|
|
5055
|
+
input_schema: {
|
|
5056
|
+
type: "object",
|
|
5057
|
+
properties: { action: {
|
|
5058
|
+
type: "string",
|
|
5059
|
+
description: "The action to perform"
|
|
5060
|
+
} },
|
|
5061
|
+
required: ["action"]
|
|
5062
|
+
}
|
|
5063
|
+
}
|
|
5064
|
+
};
|
|
5065
|
+
/**
|
|
5066
|
+
* Check if a tool is a server-side tool that needs conversion.
|
|
5067
|
+
*/
|
|
5068
|
+
function getServerToolPrefix(tool) {
|
|
5069
|
+
if (tool.type) {
|
|
5070
|
+
for (const prefix of Object.keys(SERVER_TOOL_CONFIGS)) if (tool.type.startsWith(prefix)) return prefix;
|
|
5071
|
+
}
|
|
5072
|
+
return null;
|
|
5073
|
+
}
|
|
5074
|
+
/**
|
|
5075
|
+
* Convert server-side tools to custom tools, or pass them through unchanged.
|
|
5076
|
+
* This allows them to be passed to the API and handled by the client.
|
|
5077
|
+
*
|
|
5078
|
+
* Note: Server-side tools are only converted if state.rewriteAnthropicTools is enabled.
|
|
5079
|
+
*/
|
|
5080
|
+
function convertServerToolsToCustom(tools) {
|
|
5081
|
+
if (!tools) return;
|
|
5082
|
+
const result = [];
|
|
5083
|
+
for (const tool of tools) {
|
|
5084
|
+
const serverToolPrefix = getServerToolPrefix(tool);
|
|
5085
|
+
if (serverToolPrefix) {
|
|
5086
|
+
const config = SERVER_TOOL_CONFIGS[serverToolPrefix];
|
|
5087
|
+
if (!state.rewriteAnthropicTools) {
|
|
5088
|
+
consola.debug(`[DirectAnthropic] Passing ${serverToolPrefix} through unchanged (use --rewrite-anthropic-tools to convert)`);
|
|
5089
|
+
result.push(tool);
|
|
5090
|
+
continue;
|
|
5091
|
+
}
|
|
5092
|
+
if (config.remove) {
|
|
5093
|
+
consola.warn(`[DirectAnthropic] Removing unsupported server tool: ${tool.name}. Reason: ${config.removalReason}`);
|
|
5094
|
+
continue;
|
|
5095
|
+
}
|
|
5096
|
+
consola.debug(`[DirectAnthropic] Converting server tool to custom: ${tool.name} (type: ${tool.type})`);
|
|
5097
|
+
result.push({
|
|
5098
|
+
name: tool.name,
|
|
5099
|
+
description: config.description,
|
|
5100
|
+
input_schema: config.input_schema
|
|
5101
|
+
});
|
|
5102
|
+
} else result.push(tool);
|
|
5103
|
+
}
|
|
5104
|
+
return result.length > 0 ? result : void 0;
|
|
5105
|
+
}
|
|
5106
|
+
/**
|
|
5107
|
+
* Check if a model supports direct Anthropic API.
|
|
5108
|
+
* Returns true if redirect is disabled (direct API is on) and the model is from Anthropic vendor.
|
|
5109
|
+
*/
|
|
5110
|
+
function supportsDirectAnthropicApi(modelId) {
|
|
5111
|
+
if (state.redirectAnthropic) return false;
|
|
5112
|
+
return (state.models?.data.find((m) => m.id === modelId))?.vendor === "Anthropic";
|
|
5113
|
+
}
|
|
5114
|
+
|
|
4910
5115
|
//#endregion
|
|
4911
5116
|
//#region src/routes/messages/message-utils.ts
|
|
4912
5117
|
function convertAnthropicMessages(messages) {
|
|
@@ -4976,56 +5181,111 @@ function mapOpenAIStopReasonToAnthropic(finishReason) {
|
|
|
4976
5181
|
}
|
|
4977
5182
|
|
|
4978
5183
|
//#endregion
|
|
4979
|
-
//#region src/routes/messages/
|
|
4980
|
-
|
|
4981
|
-
/**
|
|
4982
|
-
* Ensure all tool_use blocks have corresponding tool_result responses.
|
|
4983
|
-
* This handles edge cases where conversation history may be incomplete:
|
|
4984
|
-
* - Session interruptions where tool execution was cut off
|
|
4985
|
-
* - Previous request failures
|
|
4986
|
-
* - Client sending truncated history
|
|
4987
|
-
*
|
|
4988
|
-
* Adding placeholder responses prevents API errors and maintains protocol compliance.
|
|
4989
|
-
*/
|
|
4990
|
-
function fixMessageSequence(messages) {
|
|
4991
|
-
const fixedMessages = [];
|
|
4992
|
-
for (let i = 0; i < messages.length; i++) {
|
|
4993
|
-
const message = messages[i];
|
|
4994
|
-
fixedMessages.push(message);
|
|
4995
|
-
if (message.role === "assistant" && message.tool_calls && message.tool_calls.length > 0) {
|
|
4996
|
-
const foundToolResponses = /* @__PURE__ */ new Set();
|
|
4997
|
-
let j = i + 1;
|
|
4998
|
-
while (j < messages.length && messages[j].role === "tool") {
|
|
4999
|
-
const toolMessage = messages[j];
|
|
5000
|
-
if (toolMessage.tool_call_id) foundToolResponses.add(toolMessage.tool_call_id);
|
|
5001
|
-
j++;
|
|
5002
|
-
}
|
|
5003
|
-
for (const toolCall of message.tool_calls) if (!foundToolResponses.has(toolCall.id)) {
|
|
5004
|
-
consola.debug(`Adding placeholder tool_result for ${toolCall.id}`);
|
|
5005
|
-
fixedMessages.push({
|
|
5006
|
-
role: "tool",
|
|
5007
|
-
tool_call_id: toolCall.id,
|
|
5008
|
-
content: "Tool execution was interrupted or failed."
|
|
5009
|
-
});
|
|
5010
|
-
}
|
|
5011
|
-
}
|
|
5012
|
-
}
|
|
5013
|
-
return fixedMessages;
|
|
5014
|
-
}
|
|
5015
|
-
function translateToOpenAI(payload) {
|
|
5016
|
-
const toolNameMapping = {
|
|
5017
|
-
truncatedToOriginal: /* @__PURE__ */ new Map(),
|
|
5018
|
-
originalToTruncated: /* @__PURE__ */ new Map()
|
|
5019
|
-
};
|
|
5020
|
-
const messages = translateAnthropicMessagesToOpenAI(payload.messages, payload.system, toolNameMapping);
|
|
5184
|
+
//#region src/routes/messages/stream-accumulator.ts
|
|
5185
|
+
function createAnthropicStreamAccumulator() {
|
|
5021
5186
|
return {
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5187
|
+
model: "",
|
|
5188
|
+
inputTokens: 0,
|
|
5189
|
+
outputTokens: 0,
|
|
5190
|
+
stopReason: "",
|
|
5191
|
+
content: "",
|
|
5192
|
+
toolCalls: [],
|
|
5193
|
+
currentToolCall: null
|
|
5194
|
+
};
|
|
5195
|
+
}
|
|
5196
|
+
function processAnthropicEvent(event, acc) {
|
|
5197
|
+
switch (event.type) {
|
|
5198
|
+
case "content_block_delta":
|
|
5199
|
+
handleContentBlockDelta(event.delta, acc);
|
|
5200
|
+
break;
|
|
5201
|
+
case "content_block_start":
|
|
5202
|
+
handleContentBlockStart(event.content_block, acc);
|
|
5203
|
+
break;
|
|
5204
|
+
case "content_block_stop":
|
|
5205
|
+
handleContentBlockStop(acc);
|
|
5206
|
+
break;
|
|
5207
|
+
case "message_delta":
|
|
5208
|
+
handleMessageDelta(event.delta, event.usage, acc);
|
|
5209
|
+
break;
|
|
5210
|
+
default: break;
|
|
5211
|
+
}
|
|
5212
|
+
}
|
|
5213
|
+
function handleContentBlockDelta(delta, acc) {
|
|
5214
|
+
if (delta.type === "text_delta") acc.content += delta.text;
|
|
5215
|
+
else if (delta.type === "input_json_delta" && acc.currentToolCall) acc.currentToolCall.input += delta.partial_json;
|
|
5216
|
+
}
|
|
5217
|
+
function handleContentBlockStart(block, acc) {
|
|
5218
|
+
if (block.type === "tool_use") acc.currentToolCall = {
|
|
5219
|
+
id: block.id,
|
|
5220
|
+
name: block.name,
|
|
5221
|
+
input: ""
|
|
5222
|
+
};
|
|
5223
|
+
}
|
|
5224
|
+
function handleContentBlockStop(acc) {
|
|
5225
|
+
if (acc.currentToolCall) {
|
|
5226
|
+
acc.toolCalls.push(acc.currentToolCall);
|
|
5227
|
+
acc.currentToolCall = null;
|
|
5228
|
+
}
|
|
5229
|
+
}
|
|
5230
|
+
function handleMessageDelta(delta, usage, acc) {
|
|
5231
|
+
if (delta.stop_reason) acc.stopReason = delta.stop_reason;
|
|
5232
|
+
if (usage) {
|
|
5233
|
+
acc.inputTokens = usage.input_tokens ?? 0;
|
|
5234
|
+
acc.outputTokens = usage.output_tokens;
|
|
5235
|
+
}
|
|
5236
|
+
}
|
|
5237
|
+
|
|
5238
|
+
//#endregion
|
|
5239
|
+
//#region src/routes/messages/non-stream-translation.ts
|
|
5240
|
+
const OPENAI_TOOL_NAME_LIMIT = 64;
|
|
5241
|
+
/**
|
|
5242
|
+
* Ensure all tool_use blocks have corresponding tool_result responses.
|
|
5243
|
+
* This handles edge cases where conversation history may be incomplete:
|
|
5244
|
+
* - Session interruptions where tool execution was cut off
|
|
5245
|
+
* - Previous request failures
|
|
5246
|
+
* - Client sending truncated history
|
|
5247
|
+
*
|
|
5248
|
+
* Adding placeholder responses prevents API errors and maintains protocol compliance.
|
|
5249
|
+
*/
|
|
5250
|
+
function fixMessageSequence(messages) {
|
|
5251
|
+
const fixedMessages = [];
|
|
5252
|
+
for (let i = 0; i < messages.length; i++) {
|
|
5253
|
+
const message = messages[i];
|
|
5254
|
+
fixedMessages.push(message);
|
|
5255
|
+
if (message.role === "assistant" && message.tool_calls && message.tool_calls.length > 0) {
|
|
5256
|
+
const foundToolResponses = /* @__PURE__ */ new Set();
|
|
5257
|
+
let j = i + 1;
|
|
5258
|
+
while (j < messages.length && messages[j].role === "tool") {
|
|
5259
|
+
const toolMessage = messages[j];
|
|
5260
|
+
if (toolMessage.tool_call_id) foundToolResponses.add(toolMessage.tool_call_id);
|
|
5261
|
+
j++;
|
|
5262
|
+
}
|
|
5263
|
+
for (const toolCall of message.tool_calls) if (!foundToolResponses.has(toolCall.id)) {
|
|
5264
|
+
consola.debug(`Adding placeholder tool_result for ${toolCall.id}`);
|
|
5265
|
+
fixedMessages.push({
|
|
5266
|
+
role: "tool",
|
|
5267
|
+
tool_call_id: toolCall.id,
|
|
5268
|
+
content: "Tool execution was interrupted or failed."
|
|
5269
|
+
});
|
|
5270
|
+
}
|
|
5271
|
+
}
|
|
5272
|
+
}
|
|
5273
|
+
return fixedMessages;
|
|
5274
|
+
}
|
|
5275
|
+
function translateToOpenAI(payload) {
|
|
5276
|
+
const toolNameMapping = {
|
|
5277
|
+
truncatedToOriginal: /* @__PURE__ */ new Map(),
|
|
5278
|
+
originalToTruncated: /* @__PURE__ */ new Map()
|
|
5279
|
+
};
|
|
5280
|
+
const messages = translateAnthropicMessagesToOpenAI(payload.messages, payload.system, toolNameMapping);
|
|
5281
|
+
return {
|
|
5282
|
+
payload: {
|
|
5283
|
+
model: translateModelName(payload.model),
|
|
5284
|
+
messages: fixMessageSequence(messages),
|
|
5285
|
+
max_tokens: payload.max_tokens,
|
|
5286
|
+
stop: payload.stop_sequences,
|
|
5287
|
+
stream: payload.stream,
|
|
5288
|
+
temperature: payload.temperature,
|
|
5029
5289
|
top_p: payload.top_p,
|
|
5030
5290
|
user: payload.metadata?.user_id,
|
|
5031
5291
|
tools: translateAnthropicToolsToOpenAI(payload.tools, toolNameMapping),
|
|
@@ -5073,7 +5333,8 @@ function translateModelName(model) {
|
|
|
5073
5333
|
}
|
|
5074
5334
|
if (/^claude-sonnet-4-5-\d+$/.test(model)) return "claude-sonnet-4.5";
|
|
5075
5335
|
if (/^claude-sonnet-4-\d+$/.test(model)) return "claude-sonnet-4";
|
|
5076
|
-
if (
|
|
5336
|
+
if (model === "claude-opus-4-6-1m") return "claude-opus-4.6-1m";
|
|
5337
|
+
if (/^claude-opus-4-6$/.test(model)) return "claude-opus-4.6";
|
|
5077
5338
|
if (/^claude-opus-4-5-\d+$/.test(model)) return "claude-opus-4.5";
|
|
5078
5339
|
if (/^claude-opus-4-\d+$/.test(model)) return findLatestModel("claude-opus", "claude-opus-4.5");
|
|
5079
5340
|
if (/^claude-haiku-4-5-\d+$/.test(model)) return "claude-haiku-4.5";
|
|
@@ -5144,474 +5405,164 @@ function handleAssistantMessage(message, toolNameMapping) {
|
|
|
5144
5405
|
content: allTextContent || null,
|
|
5145
5406
|
tool_calls: toolUseBlocks.map((toolUse) => ({
|
|
5146
5407
|
id: toolUse.id,
|
|
5147
|
-
type: "function",
|
|
5148
|
-
function: {
|
|
5149
|
-
name: getTruncatedToolName(toolUse.name, toolNameMapping),
|
|
5150
|
-
arguments: JSON.stringify(toolUse.input)
|
|
5151
|
-
}
|
|
5152
|
-
}))
|
|
5153
|
-
}] : [{
|
|
5154
|
-
role: "assistant",
|
|
5155
|
-
content: mapContent(message.content)
|
|
5156
|
-
}];
|
|
5157
|
-
}
|
|
5158
|
-
function mapContent(content) {
|
|
5159
|
-
if (typeof content === "string") return content;
|
|
5160
|
-
if (!Array.isArray(content)) return null;
|
|
5161
|
-
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");
|
|
5162
|
-
const contentParts = [];
|
|
5163
|
-
for (const block of content) switch (block.type) {
|
|
5164
|
-
case "text":
|
|
5165
|
-
contentParts.push({
|
|
5166
|
-
type: "text",
|
|
5167
|
-
text: block.text
|
|
5168
|
-
});
|
|
5169
|
-
break;
|
|
5170
|
-
case "thinking":
|
|
5171
|
-
contentParts.push({
|
|
5172
|
-
type: "text",
|
|
5173
|
-
text: block.thinking
|
|
5174
|
-
});
|
|
5175
|
-
break;
|
|
5176
|
-
case "image":
|
|
5177
|
-
contentParts.push({
|
|
5178
|
-
type: "image_url",
|
|
5179
|
-
image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` }
|
|
5180
|
-
});
|
|
5181
|
-
break;
|
|
5182
|
-
}
|
|
5183
|
-
return contentParts;
|
|
5184
|
-
}
|
|
5185
|
-
function getTruncatedToolName(originalName, toolNameMapping) {
|
|
5186
|
-
if (originalName.length <= OPENAI_TOOL_NAME_LIMIT) return originalName;
|
|
5187
|
-
const existingTruncated = toolNameMapping.originalToTruncated.get(originalName);
|
|
5188
|
-
if (existingTruncated) return existingTruncated;
|
|
5189
|
-
let hash = 0;
|
|
5190
|
-
for (let i = 0; i < originalName.length; i++) {
|
|
5191
|
-
const char = originalName.codePointAt(i) ?? 0;
|
|
5192
|
-
hash = (hash << 5) - hash + char;
|
|
5193
|
-
hash = Math.trunc(hash);
|
|
5194
|
-
}
|
|
5195
|
-
const hashSuffix = Math.abs(hash).toString(36).slice(0, 8);
|
|
5196
|
-
const truncatedName = originalName.slice(0, OPENAI_TOOL_NAME_LIMIT - 9) + "_" + hashSuffix;
|
|
5197
|
-
toolNameMapping.truncatedToOriginal.set(truncatedName, originalName);
|
|
5198
|
-
toolNameMapping.originalToTruncated.set(originalName, truncatedName);
|
|
5199
|
-
consola.debug(`Truncated tool name: "${originalName}" -> "${truncatedName}"`);
|
|
5200
|
-
return truncatedName;
|
|
5201
|
-
}
|
|
5202
|
-
function translateAnthropicToolsToOpenAI(anthropicTools, toolNameMapping) {
|
|
5203
|
-
if (!anthropicTools) return;
|
|
5204
|
-
return anthropicTools.map((tool) => ({
|
|
5205
|
-
type: "function",
|
|
5206
|
-
function: {
|
|
5207
|
-
name: getTruncatedToolName(tool.name, toolNameMapping),
|
|
5208
|
-
description: tool.description,
|
|
5209
|
-
parameters: tool.input_schema ?? {}
|
|
5210
|
-
}
|
|
5211
|
-
}));
|
|
5212
|
-
}
|
|
5213
|
-
function translateAnthropicToolChoiceToOpenAI(anthropicToolChoice, toolNameMapping) {
|
|
5214
|
-
if (!anthropicToolChoice) return;
|
|
5215
|
-
switch (anthropicToolChoice.type) {
|
|
5216
|
-
case "auto": return "auto";
|
|
5217
|
-
case "any": return "required";
|
|
5218
|
-
case "tool":
|
|
5219
|
-
if (anthropicToolChoice.name) return {
|
|
5220
|
-
type: "function",
|
|
5221
|
-
function: { name: getTruncatedToolName(anthropicToolChoice.name, toolNameMapping) }
|
|
5222
|
-
};
|
|
5223
|
-
return;
|
|
5224
|
-
case "none": return "none";
|
|
5225
|
-
default: return;
|
|
5226
|
-
}
|
|
5227
|
-
}
|
|
5228
|
-
/** Create empty response for edge case of no choices */
|
|
5229
|
-
function createEmptyResponse(response) {
|
|
5230
|
-
return {
|
|
5231
|
-
id: response.id,
|
|
5232
|
-
type: "message",
|
|
5233
|
-
role: "assistant",
|
|
5234
|
-
model: response.model,
|
|
5235
|
-
content: [],
|
|
5236
|
-
stop_reason: "end_turn",
|
|
5237
|
-
stop_sequence: null,
|
|
5238
|
-
usage: {
|
|
5239
|
-
input_tokens: response.usage?.prompt_tokens ?? 0,
|
|
5240
|
-
output_tokens: response.usage?.completion_tokens ?? 0
|
|
5241
|
-
}
|
|
5242
|
-
};
|
|
5243
|
-
}
|
|
5244
|
-
/** Build usage object from response */
|
|
5245
|
-
function buildUsageObject(response) {
|
|
5246
|
-
const cachedTokens = response.usage?.prompt_tokens_details?.cached_tokens;
|
|
5247
|
-
return {
|
|
5248
|
-
input_tokens: (response.usage?.prompt_tokens ?? 0) - (cachedTokens ?? 0),
|
|
5249
|
-
output_tokens: response.usage?.completion_tokens ?? 0,
|
|
5250
|
-
...cachedTokens !== void 0 && { cache_read_input_tokens: cachedTokens }
|
|
5251
|
-
};
|
|
5252
|
-
}
|
|
5253
|
-
function translateToAnthropic(response, toolNameMapping) {
|
|
5254
|
-
if (response.choices.length === 0) return createEmptyResponse(response);
|
|
5255
|
-
const allTextBlocks = [];
|
|
5256
|
-
const allToolUseBlocks = [];
|
|
5257
|
-
let stopReason = null;
|
|
5258
|
-
stopReason = response.choices[0]?.finish_reason ?? stopReason;
|
|
5259
|
-
for (const choice of response.choices) {
|
|
5260
|
-
const textBlocks = getAnthropicTextBlocks(choice.message.content);
|
|
5261
|
-
const toolUseBlocks = getAnthropicToolUseBlocks(choice.message.tool_calls, toolNameMapping);
|
|
5262
|
-
allTextBlocks.push(...textBlocks);
|
|
5263
|
-
allToolUseBlocks.push(...toolUseBlocks);
|
|
5264
|
-
if (choice.finish_reason === "tool_calls" || stopReason === "stop") stopReason = choice.finish_reason;
|
|
5265
|
-
}
|
|
5266
|
-
return {
|
|
5267
|
-
id: response.id,
|
|
5268
|
-
type: "message",
|
|
5269
|
-
role: "assistant",
|
|
5270
|
-
model: response.model,
|
|
5271
|
-
content: [...allTextBlocks, ...allToolUseBlocks],
|
|
5272
|
-
stop_reason: mapOpenAIStopReasonToAnthropic(stopReason),
|
|
5273
|
-
stop_sequence: null,
|
|
5274
|
-
usage: buildUsageObject(response)
|
|
5275
|
-
};
|
|
5276
|
-
}
|
|
5277
|
-
function getAnthropicTextBlocks(messageContent) {
|
|
5278
|
-
if (typeof messageContent === "string") return [{
|
|
5279
|
-
type: "text",
|
|
5280
|
-
text: messageContent
|
|
5281
|
-
}];
|
|
5282
|
-
if (Array.isArray(messageContent)) return messageContent.filter((part) => part.type === "text").map((part) => ({
|
|
5283
|
-
type: "text",
|
|
5284
|
-
text: part.text
|
|
5285
|
-
}));
|
|
5286
|
-
return [];
|
|
5287
|
-
}
|
|
5288
|
-
function getAnthropicToolUseBlocks(toolCalls, toolNameMapping) {
|
|
5289
|
-
if (!toolCalls) return [];
|
|
5290
|
-
return toolCalls.map((toolCall) => {
|
|
5291
|
-
let input = {};
|
|
5292
|
-
try {
|
|
5293
|
-
input = JSON.parse(toolCall.function.arguments);
|
|
5294
|
-
} catch (error) {
|
|
5295
|
-
consola.warn(`Failed to parse tool call arguments for ${toolCall.function.name}:`, error);
|
|
5296
|
-
}
|
|
5297
|
-
const originalName = toolNameMapping?.truncatedToOriginal.get(toolCall.function.name) ?? toolCall.function.name;
|
|
5298
|
-
return {
|
|
5299
|
-
type: "tool_use",
|
|
5300
|
-
id: toolCall.id,
|
|
5301
|
-
name: originalName,
|
|
5302
|
-
input
|
|
5303
|
-
};
|
|
5304
|
-
});
|
|
5305
|
-
}
|
|
5306
|
-
|
|
5307
|
-
//#endregion
|
|
5308
|
-
//#region src/routes/messages/count-tokens-handler.ts
|
|
5309
|
-
/**
|
|
5310
|
-
* Handles token counting for Anthropic messages.
|
|
5311
|
-
*
|
|
5312
|
-
* For Anthropic models (vendor === "Anthropic"), uses the official Anthropic tokenizer.
|
|
5313
|
-
* For other models, uses GPT tokenizers with appropriate buffers.
|
|
5314
|
-
*
|
|
5315
|
-
* When auto-truncate is enabled and the request would exceed limits,
|
|
5316
|
-
* returns an inflated token count to trigger Claude Code's auto-compact mechanism.
|
|
5317
|
-
*/
|
|
5318
|
-
async function handleCountTokens(c) {
|
|
5319
|
-
try {
|
|
5320
|
-
const anthropicBeta = c.req.header("anthropic-beta");
|
|
5321
|
-
const anthropicPayload = await c.req.json();
|
|
5322
|
-
const { payload: openAIPayload } = translateToOpenAI(anthropicPayload);
|
|
5323
|
-
const selectedModel = state.models?.data.find((model) => model.id === openAIPayload.model);
|
|
5324
|
-
if (!selectedModel) {
|
|
5325
|
-
consola.warn("Model not found, returning default token count");
|
|
5326
|
-
return c.json({ input_tokens: 1 });
|
|
5327
|
-
}
|
|
5328
|
-
if (state.autoTruncate) {
|
|
5329
|
-
const truncateCheck = await checkNeedsCompactionAnthropic(anthropicPayload, selectedModel);
|
|
5330
|
-
if (truncateCheck.needed) {
|
|
5331
|
-
const contextWindow = selectedModel.capabilities?.limits?.max_context_window_tokens ?? 2e5;
|
|
5332
|
-
const inflatedTokens = Math.floor(contextWindow * .95);
|
|
5333
|
-
consola.debug(`[count_tokens] Would trigger auto-truncate: ${truncateCheck.currentTokens} tokens > ${truncateCheck.tokenLimit}, returning inflated count: ${inflatedTokens}`);
|
|
5334
|
-
return c.json({ input_tokens: inflatedTokens });
|
|
5335
|
-
}
|
|
5336
|
-
}
|
|
5337
|
-
const tokenizerName = selectedModel.capabilities?.tokenizer ?? "o200k_base";
|
|
5338
|
-
const tokenCount = await getTokenCount(openAIPayload, selectedModel);
|
|
5339
|
-
if (anthropicPayload.tools && anthropicPayload.tools.length > 0) {
|
|
5340
|
-
let mcpToolExist = false;
|
|
5341
|
-
if (anthropicBeta?.startsWith("claude-code")) mcpToolExist = anthropicPayload.tools.some((tool) => tool.name.startsWith("mcp__"));
|
|
5342
|
-
if (!mcpToolExist) {
|
|
5343
|
-
if (anthropicPayload.model.startsWith("claude")) tokenCount.input = tokenCount.input + 346;
|
|
5344
|
-
else if (anthropicPayload.model.startsWith("grok")) tokenCount.input = tokenCount.input + 480;
|
|
5345
|
-
}
|
|
5346
|
-
}
|
|
5347
|
-
let finalTokenCount = tokenCount.input + tokenCount.output;
|
|
5348
|
-
if (!(selectedModel.vendor === "Anthropic")) finalTokenCount = anthropicPayload.model.startsWith("grok") ? Math.round(finalTokenCount * 1.03) : Math.round(finalTokenCount * 1.05);
|
|
5349
|
-
consola.debug(`Token count: ${finalTokenCount} (tokenizer: ${tokenizerName})`);
|
|
5350
|
-
return c.json({ input_tokens: finalTokenCount });
|
|
5351
|
-
} catch (error) {
|
|
5352
|
-
consola.error("Error counting tokens:", error);
|
|
5353
|
-
return c.json({ input_tokens: 1 });
|
|
5354
|
-
}
|
|
5355
|
-
}
|
|
5356
|
-
|
|
5357
|
-
//#endregion
|
|
5358
|
-
//#region src/services/copilot/create-anthropic-messages.ts
|
|
5359
|
-
/**
|
|
5360
|
-
* Direct Anthropic-style message API for Copilot.
|
|
5361
|
-
* Used when the model vendor is Anthropic and supports /v1/messages endpoint.
|
|
5362
|
-
*/
|
|
5363
|
-
/**
|
|
5364
|
-
* Fields that are supported by Copilot's Anthropic API endpoint.
|
|
5365
|
-
* Any other fields in the incoming request will be stripped.
|
|
5366
|
-
*/
|
|
5367
|
-
const COPILOT_SUPPORTED_FIELDS = new Set([
|
|
5368
|
-
"model",
|
|
5369
|
-
"messages",
|
|
5370
|
-
"max_tokens",
|
|
5371
|
-
"system",
|
|
5372
|
-
"metadata",
|
|
5373
|
-
"stop_sequences",
|
|
5374
|
-
"stream",
|
|
5375
|
-
"temperature",
|
|
5376
|
-
"top_p",
|
|
5377
|
-
"top_k",
|
|
5378
|
-
"tools",
|
|
5379
|
-
"tool_choice",
|
|
5380
|
-
"thinking",
|
|
5381
|
-
"service_tier"
|
|
5382
|
-
]);
|
|
5383
|
-
/**
|
|
5384
|
-
* Filter payload to only include fields supported by Copilot's Anthropic API.
|
|
5385
|
-
* This prevents errors like "Extra inputs are not permitted" for unsupported
|
|
5386
|
-
* fields like `output_config`.
|
|
5387
|
-
*
|
|
5388
|
-
* Also converts server-side tools (web_search, etc.) to custom tools.
|
|
5389
|
-
*/
|
|
5390
|
-
function filterPayloadForCopilot(payload) {
|
|
5391
|
-
const filtered = {};
|
|
5392
|
-
const unsupportedFields = [];
|
|
5393
|
-
for (const [key, value] of Object.entries(payload)) if (COPILOT_SUPPORTED_FIELDS.has(key)) filtered[key] = value;
|
|
5394
|
-
else unsupportedFields.push(key);
|
|
5395
|
-
if (unsupportedFields.length > 0) consola.debug(`[DirectAnthropic] Filtered unsupported fields: ${unsupportedFields.join(", ")}`);
|
|
5396
|
-
if (filtered.tools) filtered.tools = convertServerToolsToCustom(filtered.tools);
|
|
5397
|
-
return filtered;
|
|
5398
|
-
}
|
|
5399
|
-
/**
|
|
5400
|
-
* Adjust max_tokens if thinking is enabled.
|
|
5401
|
-
* According to Anthropic docs, max_tokens must be greater than thinking.budget_tokens.
|
|
5402
|
-
* max_tokens = thinking_budget + response_tokens
|
|
5403
|
-
*/
|
|
5404
|
-
function adjustMaxTokensForThinking(payload) {
|
|
5405
|
-
const thinking = payload.thinking;
|
|
5406
|
-
if (!thinking) return payload;
|
|
5407
|
-
const budgetTokens = thinking.budget_tokens;
|
|
5408
|
-
if (!budgetTokens) return payload;
|
|
5409
|
-
if (payload.max_tokens <= budgetTokens) {
|
|
5410
|
-
const newMaxTokens = budgetTokens + Math.min(16384, budgetTokens);
|
|
5411
|
-
consola.debug(`[DirectAnthropic] Adjusted max_tokens: ${payload.max_tokens} → ${newMaxTokens} (thinking.budget_tokens=${budgetTokens})`);
|
|
5412
|
-
return {
|
|
5413
|
-
...payload,
|
|
5414
|
-
max_tokens: newMaxTokens
|
|
5415
|
-
};
|
|
5416
|
-
}
|
|
5417
|
-
return payload;
|
|
5418
|
-
}
|
|
5419
|
-
/**
|
|
5420
|
-
* Create messages using Anthropic-style API directly.
|
|
5421
|
-
* This bypasses the OpenAI translation layer for Anthropic models.
|
|
5422
|
-
*/
|
|
5423
|
-
async function createAnthropicMessages(payload, options) {
|
|
5424
|
-
if (!state.copilotToken) throw new Error("Copilot token not found");
|
|
5425
|
-
let filteredPayload = filterPayloadForCopilot(payload);
|
|
5426
|
-
filteredPayload = adjustMaxTokensForThinking(filteredPayload);
|
|
5427
|
-
const enableVision = filteredPayload.messages.some((msg) => {
|
|
5428
|
-
if (typeof msg.content === "string") return false;
|
|
5429
|
-
return msg.content.some((block) => block.type === "image");
|
|
5430
|
-
});
|
|
5431
|
-
const isAgentCall = filteredPayload.messages.some((msg) => msg.role === "assistant");
|
|
5432
|
-
const headers = {
|
|
5433
|
-
...copilotHeaders(state, enableVision),
|
|
5434
|
-
"X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user"),
|
|
5435
|
-
"anthropic-version": "2023-06-01"
|
|
5436
|
-
};
|
|
5437
|
-
consola.debug("Sending direct Anthropic request to Copilot /v1/messages");
|
|
5438
|
-
const response = await fetch(`${copilotBaseUrl(state)}/v1/messages`, {
|
|
5439
|
-
method: "POST",
|
|
5440
|
-
headers,
|
|
5441
|
-
body: JSON.stringify(filteredPayload)
|
|
5442
|
-
});
|
|
5443
|
-
if (!response.ok) {
|
|
5444
|
-
consola.debug("Request failed:", {
|
|
5445
|
-
model: filteredPayload.model,
|
|
5446
|
-
max_tokens: filteredPayload.max_tokens,
|
|
5447
|
-
stream: filteredPayload.stream,
|
|
5448
|
-
tools: filteredPayload.tools?.map((t) => ({
|
|
5449
|
-
name: t.name,
|
|
5450
|
-
type: t.type
|
|
5451
|
-
})),
|
|
5452
|
-
thinking: filteredPayload.thinking,
|
|
5453
|
-
messageCount: filteredPayload.messages.length
|
|
5454
|
-
});
|
|
5455
|
-
throw await HTTPError.fromResponse("Failed to create Anthropic messages", response, filteredPayload.model);
|
|
5456
|
-
}
|
|
5457
|
-
if (payload.stream) return events(response);
|
|
5458
|
-
return await response.json();
|
|
5459
|
-
}
|
|
5460
|
-
const SERVER_TOOL_CONFIGS = {
|
|
5461
|
-
web_search: {
|
|
5462
|
-
description: "Search the web for current information. Returns web search results that can help answer questions about recent events, current data, or information that may have changed since your knowledge cutoff.",
|
|
5463
|
-
input_schema: {
|
|
5464
|
-
type: "object",
|
|
5465
|
-
properties: { query: {
|
|
5466
|
-
type: "string",
|
|
5467
|
-
description: "The search query"
|
|
5468
|
-
} },
|
|
5469
|
-
required: ["query"]
|
|
5470
|
-
}
|
|
5471
|
-
},
|
|
5472
|
-
web_fetch: {
|
|
5473
|
-
description: "Fetch content from a URL. NOTE: This is a client-side tool - the client must fetch the URL and return the content.",
|
|
5474
|
-
input_schema: {
|
|
5475
|
-
type: "object",
|
|
5476
|
-
properties: { url: {
|
|
5477
|
-
type: "string",
|
|
5478
|
-
description: "The URL to fetch"
|
|
5479
|
-
} },
|
|
5480
|
-
required: ["url"]
|
|
5481
|
-
}
|
|
5482
|
-
},
|
|
5483
|
-
code_execution: {
|
|
5484
|
-
description: "Execute code in a sandbox. NOTE: This is a client-side tool - the client must execute the code.",
|
|
5485
|
-
input_schema: {
|
|
5486
|
-
type: "object",
|
|
5487
|
-
properties: {
|
|
5488
|
-
code: {
|
|
5489
|
-
type: "string",
|
|
5490
|
-
description: "The code to execute"
|
|
5491
|
-
},
|
|
5492
|
-
language: {
|
|
5493
|
-
type: "string",
|
|
5494
|
-
description: "The programming language"
|
|
5495
|
-
}
|
|
5496
|
-
},
|
|
5497
|
-
required: ["code"]
|
|
5498
|
-
}
|
|
5499
|
-
},
|
|
5500
|
-
computer: {
|
|
5501
|
-
description: "Control computer desktop. NOTE: This is a client-side tool - the client must handle computer control.",
|
|
5502
|
-
input_schema: {
|
|
5503
|
-
type: "object",
|
|
5504
|
-
properties: { action: {
|
|
5505
|
-
type: "string",
|
|
5506
|
-
description: "The action to perform"
|
|
5507
|
-
} },
|
|
5508
|
-
required: ["action"]
|
|
5509
|
-
}
|
|
5510
|
-
}
|
|
5511
|
-
};
|
|
5512
|
-
/**
|
|
5513
|
-
* Check if a tool is a server-side tool that needs conversion.
|
|
5514
|
-
*/
|
|
5515
|
-
function getServerToolPrefix(tool) {
|
|
5516
|
-
if (tool.type) {
|
|
5517
|
-
for (const prefix of Object.keys(SERVER_TOOL_CONFIGS)) if (tool.type.startsWith(prefix)) return prefix;
|
|
5518
|
-
}
|
|
5519
|
-
return null;
|
|
5520
|
-
}
|
|
5521
|
-
/**
|
|
5522
|
-
* Convert server-side tools to custom tools, or pass them through unchanged.
|
|
5523
|
-
* This allows them to be passed to the API and handled by the client.
|
|
5524
|
-
*
|
|
5525
|
-
* Note: Server-side tools are only converted if state.rewriteAnthropicTools is enabled.
|
|
5526
|
-
*/
|
|
5527
|
-
function convertServerToolsToCustom(tools) {
|
|
5528
|
-
if (!tools) return;
|
|
5529
|
-
const result = [];
|
|
5530
|
-
for (const tool of tools) {
|
|
5531
|
-
const serverToolPrefix = getServerToolPrefix(tool);
|
|
5532
|
-
if (serverToolPrefix) {
|
|
5533
|
-
const config = SERVER_TOOL_CONFIGS[serverToolPrefix];
|
|
5534
|
-
if (!state.rewriteAnthropicTools) {
|
|
5535
|
-
consola.debug(`[DirectAnthropic] Passing ${serverToolPrefix} through unchanged (use --rewrite-anthropic-tools to convert)`);
|
|
5536
|
-
result.push(tool);
|
|
5537
|
-
continue;
|
|
5538
|
-
}
|
|
5539
|
-
if (config.remove) {
|
|
5540
|
-
consola.warn(`[DirectAnthropic] Removing unsupported server tool: ${tool.name}. Reason: ${config.removalReason}`);
|
|
5541
|
-
continue;
|
|
5408
|
+
type: "function",
|
|
5409
|
+
function: {
|
|
5410
|
+
name: getTruncatedToolName(toolUse.name, toolNameMapping),
|
|
5411
|
+
arguments: JSON.stringify(toolUse.input)
|
|
5542
5412
|
}
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5413
|
+
}))
|
|
5414
|
+
}] : [{
|
|
5415
|
+
role: "assistant",
|
|
5416
|
+
content: mapContent(message.content)
|
|
5417
|
+
}];
|
|
5418
|
+
}
|
|
5419
|
+
function mapContent(content) {
|
|
5420
|
+
if (typeof content === "string") return content;
|
|
5421
|
+
if (!Array.isArray(content)) return null;
|
|
5422
|
+
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");
|
|
5423
|
+
const contentParts = [];
|
|
5424
|
+
for (const block of content) switch (block.type) {
|
|
5425
|
+
case "text":
|
|
5426
|
+
contentParts.push({
|
|
5427
|
+
type: "text",
|
|
5428
|
+
text: block.text
|
|
5548
5429
|
});
|
|
5549
|
-
|
|
5430
|
+
break;
|
|
5431
|
+
case "thinking":
|
|
5432
|
+
contentParts.push({
|
|
5433
|
+
type: "text",
|
|
5434
|
+
text: block.thinking
|
|
5435
|
+
});
|
|
5436
|
+
break;
|
|
5437
|
+
case "image":
|
|
5438
|
+
contentParts.push({
|
|
5439
|
+
type: "image_url",
|
|
5440
|
+
image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` }
|
|
5441
|
+
});
|
|
5442
|
+
break;
|
|
5550
5443
|
}
|
|
5551
|
-
return
|
|
5444
|
+
return contentParts;
|
|
5552
5445
|
}
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5446
|
+
function getTruncatedToolName(originalName, toolNameMapping) {
|
|
5447
|
+
if (originalName.length <= OPENAI_TOOL_NAME_LIMIT) return originalName;
|
|
5448
|
+
const existingTruncated = toolNameMapping.originalToTruncated.get(originalName);
|
|
5449
|
+
if (existingTruncated) return existingTruncated;
|
|
5450
|
+
let hash = 0;
|
|
5451
|
+
for (let i = 0; i < originalName.length; i++) {
|
|
5452
|
+
const char = originalName.codePointAt(i) ?? 0;
|
|
5453
|
+
hash = (hash << 5) - hash + char;
|
|
5454
|
+
hash = Math.trunc(hash);
|
|
5455
|
+
}
|
|
5456
|
+
const hashSuffix = Math.abs(hash).toString(36).slice(0, 8);
|
|
5457
|
+
const truncatedName = originalName.slice(0, OPENAI_TOOL_NAME_LIMIT - 9) + "_" + hashSuffix;
|
|
5458
|
+
toolNameMapping.truncatedToOriginal.set(truncatedName, originalName);
|
|
5459
|
+
toolNameMapping.originalToTruncated.set(originalName, truncatedName);
|
|
5460
|
+
consola.debug(`Truncated tool name: "${originalName}" -> "${truncatedName}"`);
|
|
5461
|
+
return truncatedName;
|
|
5560
5462
|
}
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
function
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
toolCalls: [],
|
|
5572
|
-
currentToolCall: null
|
|
5573
|
-
};
|
|
5463
|
+
function translateAnthropicToolsToOpenAI(anthropicTools, toolNameMapping) {
|
|
5464
|
+
if (!anthropicTools) return;
|
|
5465
|
+
return anthropicTools.map((tool) => ({
|
|
5466
|
+
type: "function",
|
|
5467
|
+
function: {
|
|
5468
|
+
name: getTruncatedToolName(tool.name, toolNameMapping),
|
|
5469
|
+
description: tool.description,
|
|
5470
|
+
parameters: tool.input_schema ?? {}
|
|
5471
|
+
}
|
|
5472
|
+
}));
|
|
5574
5473
|
}
|
|
5575
|
-
function
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
case "
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
case "
|
|
5587
|
-
|
|
5588
|
-
break;
|
|
5589
|
-
default: break;
|
|
5474
|
+
function translateAnthropicToolChoiceToOpenAI(anthropicToolChoice, toolNameMapping) {
|
|
5475
|
+
if (!anthropicToolChoice) return;
|
|
5476
|
+
switch (anthropicToolChoice.type) {
|
|
5477
|
+
case "auto": return "auto";
|
|
5478
|
+
case "any": return "required";
|
|
5479
|
+
case "tool":
|
|
5480
|
+
if (anthropicToolChoice.name) return {
|
|
5481
|
+
type: "function",
|
|
5482
|
+
function: { name: getTruncatedToolName(anthropicToolChoice.name, toolNameMapping) }
|
|
5483
|
+
};
|
|
5484
|
+
return;
|
|
5485
|
+
case "none": return "none";
|
|
5486
|
+
default: return;
|
|
5590
5487
|
}
|
|
5591
5488
|
}
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5489
|
+
/** Create empty response for edge case of no choices */
|
|
5490
|
+
function createEmptyResponse(response) {
|
|
5491
|
+
return {
|
|
5492
|
+
id: response.id,
|
|
5493
|
+
type: "message",
|
|
5494
|
+
role: "assistant",
|
|
5495
|
+
model: response.model,
|
|
5496
|
+
content: [],
|
|
5497
|
+
stop_reason: "end_turn",
|
|
5498
|
+
stop_sequence: null,
|
|
5499
|
+
usage: {
|
|
5500
|
+
input_tokens: response.usage?.prompt_tokens ?? 0,
|
|
5501
|
+
output_tokens: response.usage?.completion_tokens ?? 0
|
|
5502
|
+
}
|
|
5503
|
+
};
|
|
5595
5504
|
}
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5505
|
+
/** Build usage object from response */
|
|
5506
|
+
function buildUsageObject(response) {
|
|
5507
|
+
const cachedTokens = response.usage?.prompt_tokens_details?.cached_tokens;
|
|
5508
|
+
return {
|
|
5509
|
+
input_tokens: (response.usage?.prompt_tokens ?? 0) - (cachedTokens ?? 0),
|
|
5510
|
+
output_tokens: response.usage?.completion_tokens ?? 0,
|
|
5511
|
+
...cachedTokens !== void 0 && { cache_read_input_tokens: cachedTokens }
|
|
5601
5512
|
};
|
|
5602
5513
|
}
|
|
5603
|
-
function
|
|
5604
|
-
if (
|
|
5605
|
-
|
|
5606
|
-
|
|
5514
|
+
function translateToAnthropic(response, toolNameMapping) {
|
|
5515
|
+
if (response.choices.length === 0) return createEmptyResponse(response);
|
|
5516
|
+
const allTextBlocks = [];
|
|
5517
|
+
const allToolUseBlocks = [];
|
|
5518
|
+
let stopReason = null;
|
|
5519
|
+
stopReason = response.choices[0]?.finish_reason ?? stopReason;
|
|
5520
|
+
for (const choice of response.choices) {
|
|
5521
|
+
const textBlocks = getAnthropicTextBlocks(choice.message.content);
|
|
5522
|
+
const toolUseBlocks = getAnthropicToolUseBlocks(choice.message.tool_calls, toolNameMapping);
|
|
5523
|
+
allTextBlocks.push(...textBlocks);
|
|
5524
|
+
allToolUseBlocks.push(...toolUseBlocks);
|
|
5525
|
+
if (choice.finish_reason === "tool_calls" || stopReason === "stop") stopReason = choice.finish_reason;
|
|
5607
5526
|
}
|
|
5527
|
+
return {
|
|
5528
|
+
id: response.id,
|
|
5529
|
+
type: "message",
|
|
5530
|
+
role: "assistant",
|
|
5531
|
+
model: response.model,
|
|
5532
|
+
content: [...allTextBlocks, ...allToolUseBlocks],
|
|
5533
|
+
stop_reason: mapOpenAIStopReasonToAnthropic(stopReason),
|
|
5534
|
+
stop_sequence: null,
|
|
5535
|
+
usage: buildUsageObject(response)
|
|
5536
|
+
};
|
|
5608
5537
|
}
|
|
5609
|
-
function
|
|
5610
|
-
if (
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5538
|
+
function getAnthropicTextBlocks(messageContent) {
|
|
5539
|
+
if (typeof messageContent === "string") return [{
|
|
5540
|
+
type: "text",
|
|
5541
|
+
text: messageContent
|
|
5542
|
+
}];
|
|
5543
|
+
if (Array.isArray(messageContent)) return messageContent.filter((part) => part.type === "text").map((part) => ({
|
|
5544
|
+
type: "text",
|
|
5545
|
+
text: part.text
|
|
5546
|
+
}));
|
|
5547
|
+
return [];
|
|
5548
|
+
}
|
|
5549
|
+
function getAnthropicToolUseBlocks(toolCalls, toolNameMapping) {
|
|
5550
|
+
if (!toolCalls) return [];
|
|
5551
|
+
return toolCalls.map((toolCall) => {
|
|
5552
|
+
let input = {};
|
|
5553
|
+
try {
|
|
5554
|
+
input = JSON.parse(toolCall.function.arguments);
|
|
5555
|
+
} catch (error) {
|
|
5556
|
+
consola.warn(`Failed to parse tool call arguments for ${toolCall.function.name}:`, error);
|
|
5557
|
+
}
|
|
5558
|
+
const originalName = toolNameMapping?.truncatedToOriginal.get(toolCall.function.name) ?? toolCall.function.name;
|
|
5559
|
+
return {
|
|
5560
|
+
type: "tool_use",
|
|
5561
|
+
id: toolCall.id,
|
|
5562
|
+
name: originalName,
|
|
5563
|
+
input
|
|
5564
|
+
};
|
|
5565
|
+
});
|
|
5615
5566
|
}
|
|
5616
5567
|
|
|
5617
5568
|
//#endregion
|
|
@@ -6212,9 +6163,19 @@ function recordStreamingResponse(acc, fallbackModel, ctx) {
|
|
|
6212
6163
|
|
|
6213
6164
|
//#endregion
|
|
6214
6165
|
//#region src/routes/messages/handler.ts
|
|
6166
|
+
function resolveModelFromBetaHeader(model, betaHeader) {
|
|
6167
|
+
if (!betaHeader || !/\bcontext-1m\b/.test(betaHeader)) return model;
|
|
6168
|
+
if (!model.startsWith("claude-")) return model;
|
|
6169
|
+
if (model.endsWith("-1m")) return model;
|
|
6170
|
+
const resolved = `${model}-1m`;
|
|
6171
|
+
consola.debug(`Detected context-1m in anthropic-beta header, resolving model: ${model} → ${resolved}`);
|
|
6172
|
+
return resolved;
|
|
6173
|
+
}
|
|
6215
6174
|
async function handleCompletion(c) {
|
|
6216
6175
|
const anthropicPayload = await c.req.json();
|
|
6217
6176
|
consola.debug("Anthropic request payload:", JSON.stringify(anthropicPayload));
|
|
6177
|
+
const betaHeader = c.req.header("anthropic-beta");
|
|
6178
|
+
anthropicPayload.model = resolveModelFromBetaHeader(anthropicPayload.model, betaHeader);
|
|
6218
6179
|
logToolInfo(anthropicPayload);
|
|
6219
6180
|
const subagentMarker = parseSubagentMarkerFromFirstUser(anthropicPayload);
|
|
6220
6181
|
const initiatorOverride = subagentMarker ? "agent" : void 0;
|
|
@@ -6259,6 +6220,57 @@ function logToolInfo(anthropicPayload) {
|
|
|
6259
6220
|
}
|
|
6260
6221
|
}
|
|
6261
6222
|
|
|
6223
|
+
//#endregion
|
|
6224
|
+
//#region src/routes/messages/count-tokens-handler.ts
|
|
6225
|
+
/**
|
|
6226
|
+
* Handles token counting for Anthropic messages.
|
|
6227
|
+
*
|
|
6228
|
+
* For Anthropic models (vendor === "Anthropic"), uses the official Anthropic tokenizer.
|
|
6229
|
+
* For other models, uses GPT tokenizers with appropriate buffers.
|
|
6230
|
+
*
|
|
6231
|
+
* When auto-truncate is enabled and the request would exceed limits,
|
|
6232
|
+
* returns an inflated token count to trigger Claude Code's auto-compact mechanism.
|
|
6233
|
+
*/
|
|
6234
|
+
async function handleCountTokens(c) {
|
|
6235
|
+
try {
|
|
6236
|
+
const anthropicBeta = c.req.header("anthropic-beta");
|
|
6237
|
+
const anthropicPayload = await c.req.json();
|
|
6238
|
+
anthropicPayload.model = resolveModelFromBetaHeader(anthropicPayload.model, anthropicBeta);
|
|
6239
|
+
const { payload: openAIPayload } = translateToOpenAI(anthropicPayload);
|
|
6240
|
+
const selectedModel = state.models?.data.find((model) => model.id === openAIPayload.model);
|
|
6241
|
+
if (!selectedModel) {
|
|
6242
|
+
consola.warn("Model not found, returning default token count");
|
|
6243
|
+
return c.json({ input_tokens: 1 });
|
|
6244
|
+
}
|
|
6245
|
+
if (state.autoTruncate) {
|
|
6246
|
+
const truncateCheck = await checkNeedsCompactionAnthropic(anthropicPayload, selectedModel);
|
|
6247
|
+
if (truncateCheck.needed) {
|
|
6248
|
+
const contextWindow = selectedModel.capabilities?.limits?.max_context_window_tokens ?? 2e5;
|
|
6249
|
+
const inflatedTokens = Math.floor(contextWindow * .95);
|
|
6250
|
+
consola.debug(`[count_tokens] Would trigger auto-truncate: ${truncateCheck.currentTokens} tokens > ${truncateCheck.tokenLimit}, returning inflated count: ${inflatedTokens}`);
|
|
6251
|
+
return c.json({ input_tokens: inflatedTokens });
|
|
6252
|
+
}
|
|
6253
|
+
}
|
|
6254
|
+
const tokenizerName = selectedModel.capabilities?.tokenizer ?? "o200k_base";
|
|
6255
|
+
const tokenCount = await getTokenCount(openAIPayload, selectedModel);
|
|
6256
|
+
if (anthropicPayload.tools && anthropicPayload.tools.length > 0) {
|
|
6257
|
+
let mcpToolExist = false;
|
|
6258
|
+
if (anthropicBeta?.startsWith("claude-code")) mcpToolExist = anthropicPayload.tools.some((tool) => tool.name.startsWith("mcp__"));
|
|
6259
|
+
if (!mcpToolExist) {
|
|
6260
|
+
if (anthropicPayload.model.startsWith("claude")) tokenCount.input = tokenCount.input + 346;
|
|
6261
|
+
else if (anthropicPayload.model.startsWith("grok")) tokenCount.input = tokenCount.input + 480;
|
|
6262
|
+
}
|
|
6263
|
+
}
|
|
6264
|
+
let finalTokenCount = tokenCount.input + tokenCount.output;
|
|
6265
|
+
if (!(selectedModel.vendor === "Anthropic")) finalTokenCount = anthropicPayload.model.startsWith("grok") ? Math.round(finalTokenCount * 1.03) : Math.round(finalTokenCount * 1.05);
|
|
6266
|
+
consola.debug(`Token count: ${finalTokenCount} (tokenizer: ${tokenizerName})`);
|
|
6267
|
+
return c.json({ input_tokens: finalTokenCount });
|
|
6268
|
+
} catch (error) {
|
|
6269
|
+
consola.error("Error counting tokens:", error);
|
|
6270
|
+
return c.json({ input_tokens: 1 });
|
|
6271
|
+
}
|
|
6272
|
+
}
|
|
6273
|
+
|
|
6262
6274
|
//#endregion
|
|
6263
6275
|
//#region src/routes/messages/route.ts
|
|
6264
6276
|
const messageRoutes = new Hono();
|