@dianshuv/copilot-api 0.1.2 → 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.
Files changed (2) hide show
  1. package/dist/main.mjs +390 -359
  2. 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.1.2";
1020
+ var version = "0.2.1";
1021
1021
 
1022
1022
  //#endregion
1023
1023
  //#region src/lib/adaptive-rate-limiter.ts
@@ -1832,6 +1832,15 @@ var ConsoleRenderer = class {
1832
1832
  process.stdout.write(message + "\n");
1833
1833
  this.renderFooter();
1834
1834
  }
1835
+ /**
1836
+ * Format model display string, showing resolved model when different.
1837
+ * e.g., "claude-opus-4-6 -> claude-opus-4.6-1m"
1838
+ */
1839
+ formatModelDisplay(request) {
1840
+ if (!request.model) return void 0;
1841
+ if (request.resolvedModel) return `${request.model} -> ${request.resolvedModel}`;
1842
+ return request.model;
1843
+ }
1835
1844
  onRequestStart(request) {
1836
1845
  this.activeRequests.set(request.id, request);
1837
1846
  if (this.showActive && consola.level >= 5) {
@@ -1840,7 +1849,7 @@ var ConsoleRenderer = class {
1840
1849
  time: formatTime(),
1841
1850
  method: request.method,
1842
1851
  path: request.path,
1843
- model: request.model,
1852
+ model: this.formatModelDisplay(request),
1844
1853
  extra: request.queuePosition !== void 0 && request.queuePosition > 0 ? `[q#${request.queuePosition}]` : void 0,
1845
1854
  isDim: true
1846
1855
  });
@@ -1857,7 +1866,7 @@ var ConsoleRenderer = class {
1857
1866
  time: formatTime(),
1858
1867
  method: request.method,
1859
1868
  path: request.path,
1860
- model: request.model,
1869
+ model: this.formatModelDisplay(request),
1861
1870
  extra: "streaming...",
1862
1871
  isDim: true
1863
1872
  });
@@ -1875,7 +1884,7 @@ var ConsoleRenderer = class {
1875
1884
  time: formatTime(),
1876
1885
  method: request.method,
1877
1886
  path: request.path,
1878
- model: request.model,
1887
+ model: this.formatModelDisplay(request),
1879
1888
  status,
1880
1889
  duration: formatDuration(request.durationMs ?? 0),
1881
1890
  queueWait,
@@ -2790,10 +2799,18 @@ const createChatCompletions = async (payload, options) => {
2790
2799
  * Contains common functions used by both OpenAI and Anthropic message handlers.
2791
2800
  */
2792
2801
  /** Helper to update tracker model */
2793
- function updateTrackerModel(trackingId, model) {
2802
+ function updateTrackerModel(trackingId, model, resolvedModel) {
2794
2803
  if (!trackingId) return;
2795
2804
  const request = requestTracker.getRequest(trackingId);
2796
- if (request) request.model = model;
2805
+ if (!request) return;
2806
+ request.model = model;
2807
+ request.resolvedModel = resolvedModel ?? model;
2808
+ }
2809
+ /** Helper to update only the resolved (post-translation) model */
2810
+ function updateTrackerResolvedModel(trackingId, resolvedModel) {
2811
+ if (!trackingId) return;
2812
+ const request = requestTracker.getRequest(trackingId);
2813
+ if (request) request.resolvedModel = resolvedModel;
2797
2814
  }
2798
2815
  /** Helper to update tracker status */
2799
2816
  function updateTrackerStatus(trackingId, status) {
@@ -4890,6 +4907,211 @@ async function checkNeedsCompactionAnthropic(payload, model, config = {}) {
4890
4907
  };
4891
4908
  }
4892
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
+
4893
5115
  //#endregion
4894
5116
  //#region src/routes/messages/message-utils.ts
4895
5117
  function convertAnthropicMessages(messages) {
@@ -4959,48 +5181,103 @@ function mapOpenAIStopReasonToAnthropic(finishReason) {
4959
5181
  }
4960
5182
 
4961
5183
  //#endregion
4962
- //#region src/routes/messages/non-stream-translation.ts
4963
- const OPENAI_TOOL_NAME_LIMIT = 64;
4964
- /**
4965
- * Ensure all tool_use blocks have corresponding tool_result responses.
4966
- * This handles edge cases where conversation history may be incomplete:
4967
- * - Session interruptions where tool execution was cut off
4968
- * - Previous request failures
4969
- * - Client sending truncated history
4970
- *
4971
- * Adding placeholder responses prevents API errors and maintains protocol compliance.
4972
- */
4973
- function fixMessageSequence(messages) {
4974
- const fixedMessages = [];
4975
- for (let i = 0; i < messages.length; i++) {
4976
- const message = messages[i];
4977
- fixedMessages.push(message);
4978
- if (message.role === "assistant" && message.tool_calls && message.tool_calls.length > 0) {
4979
- const foundToolResponses = /* @__PURE__ */ new Set();
4980
- let j = i + 1;
4981
- while (j < messages.length && messages[j].role === "tool") {
4982
- const toolMessage = messages[j];
4983
- if (toolMessage.tool_call_id) foundToolResponses.add(toolMessage.tool_call_id);
4984
- j++;
4985
- }
4986
- for (const toolCall of message.tool_calls) if (!foundToolResponses.has(toolCall.id)) {
4987
- consola.debug(`Adding placeholder tool_result for ${toolCall.id}`);
4988
- fixedMessages.push({
4989
- role: "tool",
4990
- tool_call_id: toolCall.id,
4991
- content: "Tool execution was interrupted or failed."
4992
- });
4993
- }
4994
- }
4995
- }
4996
- return fixedMessages;
4997
- }
4998
- function translateToOpenAI(payload) {
4999
- const toolNameMapping = {
5000
- truncatedToOriginal: /* @__PURE__ */ new Map(),
5001
- originalToTruncated: /* @__PURE__ */ new Map()
5002
- };
5003
- const messages = translateAnthropicMessagesToOpenAI(payload.messages, payload.system, toolNameMapping);
5184
+ //#region src/routes/messages/stream-accumulator.ts
5185
+ function createAnthropicStreamAccumulator() {
5186
+ return {
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);
5004
5281
  return {
5005
5282
  payload: {
5006
5283
  model: translateModelName(payload.model),
@@ -5056,7 +5333,8 @@ function translateModelName(model) {
5056
5333
  }
5057
5334
  if (/^claude-sonnet-4-5-\d+$/.test(model)) return "claude-sonnet-4.5";
5058
5335
  if (/^claude-sonnet-4-\d+$/.test(model)) return "claude-sonnet-4";
5059
- if (/^claude-opus-4-6$/.test(model)) return "claude-opus-4.6-1m";
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";
5060
5338
  if (/^claude-opus-4-5-\d+$/.test(model)) return "claude-opus-4.5";
5061
5339
  if (/^claude-opus-4-\d+$/.test(model)) return findLatestModel("claude-opus", "claude-opus-4.5");
5062
5340
  if (/^claude-haiku-4-5-\d+$/.test(model)) return "claude-haiku-4.5";
@@ -5287,316 +5565,6 @@ function getAnthropicToolUseBlocks(toolCalls, toolNameMapping) {
5287
5565
  });
5288
5566
  }
5289
5567
 
5290
- //#endregion
5291
- //#region src/routes/messages/count-tokens-handler.ts
5292
- /**
5293
- * Handles token counting for Anthropic messages.
5294
- *
5295
- * For Anthropic models (vendor === "Anthropic"), uses the official Anthropic tokenizer.
5296
- * For other models, uses GPT tokenizers with appropriate buffers.
5297
- *
5298
- * When auto-truncate is enabled and the request would exceed limits,
5299
- * returns an inflated token count to trigger Claude Code's auto-compact mechanism.
5300
- */
5301
- async function handleCountTokens(c) {
5302
- try {
5303
- const anthropicBeta = c.req.header("anthropic-beta");
5304
- const anthropicPayload = await c.req.json();
5305
- const { payload: openAIPayload } = translateToOpenAI(anthropicPayload);
5306
- const selectedModel = state.models?.data.find((model) => model.id === openAIPayload.model);
5307
- if (!selectedModel) {
5308
- consola.warn("Model not found, returning default token count");
5309
- return c.json({ input_tokens: 1 });
5310
- }
5311
- if (state.autoTruncate) {
5312
- const truncateCheck = await checkNeedsCompactionAnthropic(anthropicPayload, selectedModel);
5313
- if (truncateCheck.needed) {
5314
- const contextWindow = selectedModel.capabilities?.limits?.max_context_window_tokens ?? 2e5;
5315
- const inflatedTokens = Math.floor(contextWindow * .95);
5316
- consola.debug(`[count_tokens] Would trigger auto-truncate: ${truncateCheck.currentTokens} tokens > ${truncateCheck.tokenLimit}, returning inflated count: ${inflatedTokens}`);
5317
- return c.json({ input_tokens: inflatedTokens });
5318
- }
5319
- }
5320
- const tokenizerName = selectedModel.capabilities?.tokenizer ?? "o200k_base";
5321
- const tokenCount = await getTokenCount(openAIPayload, selectedModel);
5322
- if (anthropicPayload.tools && anthropicPayload.tools.length > 0) {
5323
- let mcpToolExist = false;
5324
- if (anthropicBeta?.startsWith("claude-code")) mcpToolExist = anthropicPayload.tools.some((tool) => tool.name.startsWith("mcp__"));
5325
- if (!mcpToolExist) {
5326
- if (anthropicPayload.model.startsWith("claude")) tokenCount.input = tokenCount.input + 346;
5327
- else if (anthropicPayload.model.startsWith("grok")) tokenCount.input = tokenCount.input + 480;
5328
- }
5329
- }
5330
- let finalTokenCount = tokenCount.input + tokenCount.output;
5331
- if (!(selectedModel.vendor === "Anthropic")) finalTokenCount = anthropicPayload.model.startsWith("grok") ? Math.round(finalTokenCount * 1.03) : Math.round(finalTokenCount * 1.05);
5332
- consola.debug(`Token count: ${finalTokenCount} (tokenizer: ${tokenizerName})`);
5333
- return c.json({ input_tokens: finalTokenCount });
5334
- } catch (error) {
5335
- consola.error("Error counting tokens:", error);
5336
- return c.json({ input_tokens: 1 });
5337
- }
5338
- }
5339
-
5340
- //#endregion
5341
- //#region src/services/copilot/create-anthropic-messages.ts
5342
- /**
5343
- * Direct Anthropic-style message API for Copilot.
5344
- * Used when the model vendor is Anthropic and supports /v1/messages endpoint.
5345
- */
5346
- /**
5347
- * Fields that are supported by Copilot's Anthropic API endpoint.
5348
- * Any other fields in the incoming request will be stripped.
5349
- */
5350
- const COPILOT_SUPPORTED_FIELDS = new Set([
5351
- "model",
5352
- "messages",
5353
- "max_tokens",
5354
- "system",
5355
- "metadata",
5356
- "stop_sequences",
5357
- "stream",
5358
- "temperature",
5359
- "top_p",
5360
- "top_k",
5361
- "tools",
5362
- "tool_choice",
5363
- "thinking",
5364
- "service_tier"
5365
- ]);
5366
- /**
5367
- * Filter payload to only include fields supported by Copilot's Anthropic API.
5368
- * This prevents errors like "Extra inputs are not permitted" for unsupported
5369
- * fields like `output_config`.
5370
- *
5371
- * Also converts server-side tools (web_search, etc.) to custom tools.
5372
- */
5373
- function filterPayloadForCopilot(payload) {
5374
- const filtered = {};
5375
- const unsupportedFields = [];
5376
- for (const [key, value] of Object.entries(payload)) if (COPILOT_SUPPORTED_FIELDS.has(key)) filtered[key] = value;
5377
- else unsupportedFields.push(key);
5378
- if (unsupportedFields.length > 0) consola.debug(`[DirectAnthropic] Filtered unsupported fields: ${unsupportedFields.join(", ")}`);
5379
- if (filtered.tools) filtered.tools = convertServerToolsToCustom(filtered.tools);
5380
- return filtered;
5381
- }
5382
- /**
5383
- * Adjust max_tokens if thinking is enabled.
5384
- * According to Anthropic docs, max_tokens must be greater than thinking.budget_tokens.
5385
- * max_tokens = thinking_budget + response_tokens
5386
- */
5387
- function adjustMaxTokensForThinking(payload) {
5388
- const thinking = payload.thinking;
5389
- if (!thinking) return payload;
5390
- const budgetTokens = thinking.budget_tokens;
5391
- if (!budgetTokens) return payload;
5392
- if (payload.max_tokens <= budgetTokens) {
5393
- const newMaxTokens = budgetTokens + Math.min(16384, budgetTokens);
5394
- consola.debug(`[DirectAnthropic] Adjusted max_tokens: ${payload.max_tokens} → ${newMaxTokens} (thinking.budget_tokens=${budgetTokens})`);
5395
- return {
5396
- ...payload,
5397
- max_tokens: newMaxTokens
5398
- };
5399
- }
5400
- return payload;
5401
- }
5402
- /**
5403
- * Create messages using Anthropic-style API directly.
5404
- * This bypasses the OpenAI translation layer for Anthropic models.
5405
- */
5406
- async function createAnthropicMessages(payload, options) {
5407
- if (!state.copilotToken) throw new Error("Copilot token not found");
5408
- let filteredPayload = filterPayloadForCopilot(payload);
5409
- filteredPayload = adjustMaxTokensForThinking(filteredPayload);
5410
- const enableVision = filteredPayload.messages.some((msg) => {
5411
- if (typeof msg.content === "string") return false;
5412
- return msg.content.some((block) => block.type === "image");
5413
- });
5414
- const isAgentCall = filteredPayload.messages.some((msg) => msg.role === "assistant");
5415
- const headers = {
5416
- ...copilotHeaders(state, enableVision),
5417
- "X-Initiator": options?.initiator ?? (isAgentCall ? "agent" : "user"),
5418
- "anthropic-version": "2023-06-01"
5419
- };
5420
- consola.debug("Sending direct Anthropic request to Copilot /v1/messages");
5421
- const response = await fetch(`${copilotBaseUrl(state)}/v1/messages`, {
5422
- method: "POST",
5423
- headers,
5424
- body: JSON.stringify(filteredPayload)
5425
- });
5426
- if (!response.ok) {
5427
- consola.debug("Request failed:", {
5428
- model: filteredPayload.model,
5429
- max_tokens: filteredPayload.max_tokens,
5430
- stream: filteredPayload.stream,
5431
- tools: filteredPayload.tools?.map((t) => ({
5432
- name: t.name,
5433
- type: t.type
5434
- })),
5435
- thinking: filteredPayload.thinking,
5436
- messageCount: filteredPayload.messages.length
5437
- });
5438
- throw await HTTPError.fromResponse("Failed to create Anthropic messages", response, filteredPayload.model);
5439
- }
5440
- if (payload.stream) return events(response);
5441
- return await response.json();
5442
- }
5443
- const SERVER_TOOL_CONFIGS = {
5444
- web_search: {
5445
- 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.",
5446
- input_schema: {
5447
- type: "object",
5448
- properties: { query: {
5449
- type: "string",
5450
- description: "The search query"
5451
- } },
5452
- required: ["query"]
5453
- }
5454
- },
5455
- web_fetch: {
5456
- description: "Fetch content from a URL. NOTE: This is a client-side tool - the client must fetch the URL and return the content.",
5457
- input_schema: {
5458
- type: "object",
5459
- properties: { url: {
5460
- type: "string",
5461
- description: "The URL to fetch"
5462
- } },
5463
- required: ["url"]
5464
- }
5465
- },
5466
- code_execution: {
5467
- description: "Execute code in a sandbox. NOTE: This is a client-side tool - the client must execute the code.",
5468
- input_schema: {
5469
- type: "object",
5470
- properties: {
5471
- code: {
5472
- type: "string",
5473
- description: "The code to execute"
5474
- },
5475
- language: {
5476
- type: "string",
5477
- description: "The programming language"
5478
- }
5479
- },
5480
- required: ["code"]
5481
- }
5482
- },
5483
- computer: {
5484
- description: "Control computer desktop. NOTE: This is a client-side tool - the client must handle computer control.",
5485
- input_schema: {
5486
- type: "object",
5487
- properties: { action: {
5488
- type: "string",
5489
- description: "The action to perform"
5490
- } },
5491
- required: ["action"]
5492
- }
5493
- }
5494
- };
5495
- /**
5496
- * Check if a tool is a server-side tool that needs conversion.
5497
- */
5498
- function getServerToolPrefix(tool) {
5499
- if (tool.type) {
5500
- for (const prefix of Object.keys(SERVER_TOOL_CONFIGS)) if (tool.type.startsWith(prefix)) return prefix;
5501
- }
5502
- return null;
5503
- }
5504
- /**
5505
- * Convert server-side tools to custom tools, or pass them through unchanged.
5506
- * This allows them to be passed to the API and handled by the client.
5507
- *
5508
- * Note: Server-side tools are only converted if state.rewriteAnthropicTools is enabled.
5509
- */
5510
- function convertServerToolsToCustom(tools) {
5511
- if (!tools) return;
5512
- const result = [];
5513
- for (const tool of tools) {
5514
- const serverToolPrefix = getServerToolPrefix(tool);
5515
- if (serverToolPrefix) {
5516
- const config = SERVER_TOOL_CONFIGS[serverToolPrefix];
5517
- if (!state.rewriteAnthropicTools) {
5518
- consola.debug(`[DirectAnthropic] Passing ${serverToolPrefix} through unchanged (use --rewrite-anthropic-tools to convert)`);
5519
- result.push(tool);
5520
- continue;
5521
- }
5522
- if (config.remove) {
5523
- consola.warn(`[DirectAnthropic] Removing unsupported server tool: ${tool.name}. Reason: ${config.removalReason}`);
5524
- continue;
5525
- }
5526
- consola.debug(`[DirectAnthropic] Converting server tool to custom: ${tool.name} (type: ${tool.type})`);
5527
- result.push({
5528
- name: tool.name,
5529
- description: config.description,
5530
- input_schema: config.input_schema
5531
- });
5532
- } else result.push(tool);
5533
- }
5534
- return result.length > 0 ? result : void 0;
5535
- }
5536
- /**
5537
- * Check if a model supports direct Anthropic API.
5538
- * Returns true if redirect is disabled (direct API is on) and the model is from Anthropic vendor.
5539
- */
5540
- function supportsDirectAnthropicApi(modelId) {
5541
- if (state.redirectAnthropic) return false;
5542
- return (state.models?.data.find((m) => m.id === modelId))?.vendor === "Anthropic";
5543
- }
5544
-
5545
- //#endregion
5546
- //#region src/routes/messages/stream-accumulator.ts
5547
- function createAnthropicStreamAccumulator() {
5548
- return {
5549
- model: "",
5550
- inputTokens: 0,
5551
- outputTokens: 0,
5552
- stopReason: "",
5553
- content: "",
5554
- toolCalls: [],
5555
- currentToolCall: null
5556
- };
5557
- }
5558
- function processAnthropicEvent(event, acc) {
5559
- switch (event.type) {
5560
- case "content_block_delta":
5561
- handleContentBlockDelta(event.delta, acc);
5562
- break;
5563
- case "content_block_start":
5564
- handleContentBlockStart(event.content_block, acc);
5565
- break;
5566
- case "content_block_stop":
5567
- handleContentBlockStop(acc);
5568
- break;
5569
- case "message_delta":
5570
- handleMessageDelta(event.delta, event.usage, acc);
5571
- break;
5572
- default: break;
5573
- }
5574
- }
5575
- function handleContentBlockDelta(delta, acc) {
5576
- if (delta.type === "text_delta") acc.content += delta.text;
5577
- else if (delta.type === "input_json_delta" && acc.currentToolCall) acc.currentToolCall.input += delta.partial_json;
5578
- }
5579
- function handleContentBlockStart(block, acc) {
5580
- if (block.type === "tool_use") acc.currentToolCall = {
5581
- id: block.id,
5582
- name: block.name,
5583
- input: ""
5584
- };
5585
- }
5586
- function handleContentBlockStop(acc) {
5587
- if (acc.currentToolCall) {
5588
- acc.toolCalls.push(acc.currentToolCall);
5589
- acc.currentToolCall = null;
5590
- }
5591
- }
5592
- function handleMessageDelta(delta, usage, acc) {
5593
- if (delta.stop_reason) acc.stopReason = delta.stop_reason;
5594
- if (usage) {
5595
- acc.inputTokens = usage.input_tokens ?? 0;
5596
- acc.outputTokens = usage.output_tokens;
5597
- }
5598
- }
5599
-
5600
5568
  //#endregion
5601
5569
  //#region src/routes/messages/stream-translation.ts
5602
5570
  function isToolBlockOpen(state) {
@@ -5979,6 +5947,7 @@ const parseSubagentMarkerFromSystemReminder = (text) => {
5979
5947
  async function handleTranslatedCompletion(c, anthropicPayload, ctx, initiatorOverride) {
5980
5948
  const { payload: translatedPayload, toolNameMapping } = translateToOpenAI(anthropicPayload);
5981
5949
  consola.debug("Translated OpenAI request payload:", JSON.stringify(translatedPayload));
5950
+ updateTrackerResolvedModel(ctx.trackingId, translatedPayload.model);
5982
5951
  const selectedModel = state.models?.data.find((model) => model.id === translatedPayload.model);
5983
5952
  const { finalPayload: openAIPayload, truncateResult } = await buildFinalPayload(translatedPayload, selectedModel);
5984
5953
  if (truncateResult) ctx.truncateResult = truncateResult;
@@ -6194,9 +6163,19 @@ function recordStreamingResponse(acc, fallbackModel, ctx) {
6194
6163
 
6195
6164
  //#endregion
6196
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
+ }
6197
6174
  async function handleCompletion(c) {
6198
6175
  const anthropicPayload = await c.req.json();
6199
6176
  consola.debug("Anthropic request payload:", JSON.stringify(anthropicPayload));
6177
+ const betaHeader = c.req.header("anthropic-beta");
6178
+ anthropicPayload.model = resolveModelFromBetaHeader(anthropicPayload.model, betaHeader);
6200
6179
  logToolInfo(anthropicPayload);
6201
6180
  const subagentMarker = parseSubagentMarkerFromFirstUser(anthropicPayload);
6202
6181
  const initiatorOverride = subagentMarker ? "agent" : void 0;
@@ -6241,6 +6220,57 @@ function logToolInfo(anthropicPayload) {
6241
6220
  }
6242
6221
  }
6243
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
+
6244
6274
  //#endregion
6245
6275
  //#region src/routes/messages/route.ts
6246
6276
  const messageRoutes = new Hono();
@@ -6396,6 +6426,7 @@ const RESPONSES_ENDPOINT = "/responses";
6396
6426
  const handleResponses = async (c) => {
6397
6427
  const payload = await c.req.json();
6398
6428
  consola.debug("Responses request payload:", JSON.stringify(payload));
6429
+ updateTrackerModel(c.get("trackingId"), payload.model);
6399
6430
  useFunctionApplyPatch(payload);
6400
6431
  removeWebSearchTool(payload);
6401
6432
  if (!((state.models?.data.find((model) => model.id === payload.model))?.supported_endpoints?.includes(RESPONSES_ENDPOINT) ?? false)) return c.json({ error: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dianshuv/copilot-api",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "Turn GitHub Copilot into OpenAI/Anthropic API compatible server. Usable with Claude Code!",
5
5
  "author": "dianshuv",
6
6
  "type": "module",