betahi-copilot-bridge 0.1.6 → 0.1.7

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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  <h1 align="center">copilot-bridge</h1>
2
2
 
3
3
  <p align="center">
4
- <a href="https://www.npmjs.com/package/betahi-copilot-bridge"><img src="https://img.shields.io/npm/v/betahi-copilot-bridge.svg?v=0.1.6" alt="npm version"></a>
4
+ <a href="https://www.npmjs.com/package/betahi-copilot-bridge"><img src="https://img.shields.io/npm/v/betahi-copilot-bridge.svg?v=0.1.7" alt="npm version"></a>
5
5
  <a href="https://github.com/betahi/copilot-bridge/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/betahi-copilot-bridge.svg" alt="license"></a>
6
6
  </p>
7
7
 
package/dist/main.js CHANGED
@@ -4,7 +4,7 @@ import consola from "consola";
4
4
  import fs from "node:fs/promises";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
- import { randomUUID } from "node:crypto";
7
+ import { createHash, randomUUID } from "node:crypto";
8
8
  import { z } from "zod";
9
9
  import { serve } from "@hono/node-server";
10
10
  import { Hono } from "hono";
@@ -1442,6 +1442,84 @@ function mapOpenAIStopReasonToAnthropic(finishReason) {
1442
1442
  }[finishReason];
1443
1443
  }
1444
1444
 
1445
+ //#endregion
1446
+ //#region src/bridges/claude/tool-names.ts
1447
+ const STRICT_TOOL_NAME_CHARS_PATTERN = /^[A-Za-z0-9_-]+$/;
1448
+ const DOTTED_TOOL_NAME_CHARS_PATTERN = /^[A-Za-z0-9_.-]+$/;
1449
+ const DEFAULT_TOOL_NAME_MAX_LENGTH = 64;
1450
+ const EXTENDED_TOOL_NAME_MAX_LENGTH = 128;
1451
+ const HASH_LENGTH = 10;
1452
+ const getToolNameMapperOptionsForModel = (modelId) => {
1453
+ const normalized = modelId.trim().toLowerCase().replace(/\[1m\]$/, "-1m").replace(/[._]/g, "-");
1454
+ if (/^claude-opus-4-(?:6|7)(?:$|-)/.test(normalized)) return {
1455
+ allowDots: false,
1456
+ maxNameLength: EXTENDED_TOOL_NAME_MAX_LENGTH
1457
+ };
1458
+ if (/^claude-sonnet-4(?:$|-\d{8}$)/.test(normalized)) return {
1459
+ allowDots: false,
1460
+ maxNameLength: EXTENDED_TOOL_NAME_MAX_LENGTH
1461
+ };
1462
+ if (normalized.startsWith("gemini-")) return {
1463
+ allowDots: true,
1464
+ maxNameLength: EXTENDED_TOOL_NAME_MAX_LENGTH
1465
+ };
1466
+ if (/^gpt-5-(?:2|4)(?:$|-)/.test(normalized) || /^gpt-5-(?:2|3)-codex(?:$|-)/.test(normalized) || /^gpt-5-4-mini(?:$|-)/.test(normalized) || /^gpt-5-5(?:$|-)/.test(normalized)) return {
1467
+ allowDots: false,
1468
+ maxNameLength: EXTENDED_TOOL_NAME_MAX_LENGTH
1469
+ };
1470
+ if (normalized.startsWith("gpt-")) return {
1471
+ allowDots: true,
1472
+ maxNameLength: EXTENDED_TOOL_NAME_MAX_LENGTH
1473
+ };
1474
+ return {
1475
+ allowDots: false,
1476
+ maxNameLength: DEFAULT_TOOL_NAME_MAX_LENGTH
1477
+ };
1478
+ };
1479
+ const makeHash = (value) => createHash("sha1").update(value).digest("hex").slice(0, HASH_LENGTH);
1480
+ const getAllowedNamePattern = (allowDots) => allowDots ? DOTTED_TOOL_NAME_CHARS_PATTERN : STRICT_TOOL_NAME_CHARS_PATTERN;
1481
+ const cleanToolName = (name, allowDots) => {
1482
+ const invalidCharsPattern = allowDots ? /[^A-Za-z0-9_.-]/g : /[^A-Za-z0-9_-]/g;
1483
+ return name.replace(invalidCharsPattern, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "") || "tool";
1484
+ };
1485
+ const isValidToolName = (name, maxNameLength, allowDots) => name.length > 0 && name.length <= maxNameLength && getAllowedNamePattern(allowDots).test(name);
1486
+ const makeValidToolName = (name, maxNameLength, allowDots) => {
1487
+ if (isValidToolName(name, maxNameLength, allowDots)) return name;
1488
+ const cleaned = cleanToolName(name, allowDots);
1489
+ if (cleaned.length <= maxNameLength) return cleaned;
1490
+ const hash = makeHash(name);
1491
+ const prefixLength = maxNameLength - hash.length - 1;
1492
+ return `${cleaned.slice(0, prefixLength)}_${hash}`;
1493
+ };
1494
+ const makeUniqueToolName = (name, used, maxNameLength, allowDots) => {
1495
+ const candidate = makeValidToolName(name, maxNameLength, allowDots);
1496
+ if (!used.has(candidate)) return candidate;
1497
+ for (let index = 2;; index++) {
1498
+ const suffix = `_${makeHash(`${name}:${index}`)}`;
1499
+ const prefixLength = maxNameLength - suffix.length;
1500
+ const next = `${cleanToolName(name, allowDots).slice(0, prefixLength)}${suffix}`;
1501
+ if (!used.has(next)) return next;
1502
+ }
1503
+ };
1504
+ const createAnthropicToolNameMapper = (tools, options = {}) => {
1505
+ const maxNameLength = options.maxNameLength ?? DEFAULT_TOOL_NAME_MAX_LENGTH;
1506
+ const allowDots = options.allowDots ?? false;
1507
+ const anthropicToOpenAI = /* @__PURE__ */ new Map();
1508
+ const openAIToAnthropic = /* @__PURE__ */ new Map();
1509
+ const used = /* @__PURE__ */ new Set();
1510
+ for (const tool of tools ?? []) {
1511
+ if (anthropicToOpenAI.has(tool.name)) continue;
1512
+ const openAIName = makeUniqueToolName(tool.name, used, maxNameLength, allowDots);
1513
+ used.add(openAIName);
1514
+ anthropicToOpenAI.set(tool.name, openAIName);
1515
+ openAIToAnthropic.set(openAIName, tool.name);
1516
+ }
1517
+ return {
1518
+ toAnthropic: (name) => openAIToAnthropic.get(name) ?? name,
1519
+ toOpenAI: (name) => anthropicToOpenAI.get(name) ?? makeValidToolName(name, maxNameLength, allowDots)
1520
+ };
1521
+ };
1522
+
1445
1523
  //#endregion
1446
1524
  //#region src/bridges/claude/non-stream-translation.ts
1447
1525
  const normalizeClaudeModelAlias = (model) => {
@@ -1537,12 +1615,13 @@ function translateReasoningEffort(payload, settings) {
1537
1615
  if (budgetTokens <= 24576) return sanitizeReasoningEffortForModel(modelId, "high");
1538
1616
  return sanitizeReasoningEffortForModel(modelId, "xhigh");
1539
1617
  }
1540
- function translateToOpenAI(payload, settings) {
1618
+ function translateToOpenAI(payload, settings, toolNameMapper) {
1541
1619
  const model = translateModelName(payload.model, settings);
1542
- const tools = translateAnthropicToolsToOpenAI(payload.tools);
1620
+ const mapper = toolNameMapper ?? createAnthropicToolNameMapper(payload.tools, { ...getToolNameMapperOptionsForModel(model) });
1621
+ const tools = translateAnthropicToolsToOpenAI(payload.tools, mapper);
1543
1622
  return {
1544
1623
  model,
1545
- messages: translateAnthropicMessagesToOpenAI(payload.messages, payload.system),
1624
+ messages: translateAnthropicMessagesToOpenAI(payload.messages, payload.system, mapper),
1546
1625
  max_tokens: payload.max_tokens,
1547
1626
  stop: payload.stop_sequences,
1548
1627
  stream: payload.stream,
@@ -1553,12 +1632,12 @@ function translateToOpenAI(payload, settings) {
1553
1632
  reasoning_effort: translateReasoningEffort(payload, settings),
1554
1633
  user: payload.metadata?.user_id,
1555
1634
  tools,
1556
- tool_choice: tools && tools.length > 0 ? translateAnthropicToolChoiceToOpenAI(payload.tool_choice) : void 0
1635
+ tool_choice: tools && tools.length > 0 ? translateAnthropicToolChoiceToOpenAI(payload.tool_choice, mapper) : void 0
1557
1636
  };
1558
1637
  }
1559
- function translateAnthropicMessagesToOpenAI(anthropicMessages, system) {
1638
+ function translateAnthropicMessagesToOpenAI(anthropicMessages, system, toolNameMapper) {
1560
1639
  const systemMessages = handleSystemPrompt(system);
1561
- const otherMessages = anthropicMessages.flatMap((message) => message.role === "user" ? handleUserMessage(message) : handleAssistantMessage(message));
1640
+ const otherMessages = anthropicMessages.flatMap((message) => message.role === "user" ? handleUserMessage(message) : handleAssistantMessage(message, toolNameMapper));
1562
1641
  return [...systemMessages, ...otherMessages];
1563
1642
  }
1564
1643
  function handleSystemPrompt(system) {
@@ -1592,7 +1671,7 @@ function handleUserMessage(message) {
1592
1671
  });
1593
1672
  return newMessages;
1594
1673
  }
1595
- function handleAssistantMessage(message) {
1674
+ function handleAssistantMessage(message, toolNameMapper) {
1596
1675
  if (!Array.isArray(message.content)) return [{
1597
1676
  role: "assistant",
1598
1677
  content: mapContent(message.content)
@@ -1608,7 +1687,7 @@ function handleAssistantMessage(message) {
1608
1687
  id: toolUse.id,
1609
1688
  type: "function",
1610
1689
  function: {
1611
- name: toolUse.name,
1690
+ name: toolNameMapper.toOpenAI(toolUse.name),
1612
1691
  arguments: JSON.stringify(toolUse.input)
1613
1692
  }
1614
1693
  }))
@@ -1644,18 +1723,18 @@ function mapContent(content) {
1644
1723
  }
1645
1724
  return contentParts;
1646
1725
  }
1647
- function translateAnthropicToolsToOpenAI(anthropicTools) {
1726
+ function translateAnthropicToolsToOpenAI(anthropicTools, toolNameMapper) {
1648
1727
  if (!anthropicTools || anthropicTools.length === 0) return;
1649
1728
  return anthropicTools.map((tool) => ({
1650
1729
  type: "function",
1651
1730
  function: {
1652
- name: tool.name,
1731
+ name: toolNameMapper.toOpenAI(tool.name),
1653
1732
  description: tool.description,
1654
1733
  parameters: tool.input_schema
1655
1734
  }
1656
1735
  }));
1657
1736
  }
1658
- function translateAnthropicToolChoiceToOpenAI(anthropicToolChoice) {
1737
+ function translateAnthropicToolChoiceToOpenAI(anthropicToolChoice, toolNameMapper) {
1659
1738
  if (!anthropicToolChoice) return;
1660
1739
  switch (anthropicToolChoice.type) {
1661
1740
  case "auto": return "auto";
@@ -1663,21 +1742,21 @@ function translateAnthropicToolChoiceToOpenAI(anthropicToolChoice) {
1663
1742
  case "tool":
1664
1743
  if (anthropicToolChoice.name) return {
1665
1744
  type: "function",
1666
- function: { name: anthropicToolChoice.name }
1745
+ function: { name: toolNameMapper.toOpenAI(anthropicToolChoice.name) }
1667
1746
  };
1668
1747
  return;
1669
1748
  case "none": return "none";
1670
1749
  default: return;
1671
1750
  }
1672
1751
  }
1673
- function translateToAnthropic(response) {
1752
+ function translateToAnthropic(response, toolNameMapper = createAnthropicToolNameMapper(void 0)) {
1674
1753
  const allTextBlocks = [];
1675
1754
  const allToolUseBlocks = [];
1676
1755
  let stopReason = null;
1677
1756
  stopReason = response.choices[0]?.finish_reason ?? stopReason;
1678
1757
  for (const choice of response.choices) {
1679
1758
  allTextBlocks.push(...getAnthropicTextBlocks(choice.message.content));
1680
- allToolUseBlocks.push(...getAnthropicToolUseBlocks(choice.message.tool_calls));
1759
+ allToolUseBlocks.push(...getAnthropicToolUseBlocks(choice.message.tool_calls, toolNameMapper));
1681
1760
  if (choice.finish_reason === "tool_calls" || stopReason === "stop") stopReason = choice.finish_reason;
1682
1761
  }
1683
1762
  return {
@@ -1706,12 +1785,12 @@ function getAnthropicTextBlocks(messageContent) {
1706
1785
  }));
1707
1786
  return [];
1708
1787
  }
1709
- function getAnthropicToolUseBlocks(toolCalls) {
1788
+ function getAnthropicToolUseBlocks(toolCalls, toolNameMapper) {
1710
1789
  if (!toolCalls) return [];
1711
1790
  return toolCalls.map((toolCall) => ({
1712
1791
  type: "tool_use",
1713
1792
  id: toolCall.id,
1714
- name: toolCall.function.name,
1793
+ name: toolNameMapper.toAnthropic(toolCall.function.name),
1715
1794
  input: safeJsonParse(toolCall.function.arguments)
1716
1795
  }));
1717
1796
  }
@@ -1729,7 +1808,7 @@ function isToolBlockOpen(state) {
1729
1808
  if (!state.contentBlockOpen) return false;
1730
1809
  return Object.values(state.toolCalls).some((tc) => tc.anthropicBlockIndex === state.contentBlockIndex);
1731
1810
  }
1732
- function translateChunkToAnthropicEvents(chunk, state) {
1811
+ function translateChunkToAnthropicEvents(chunk, state, toolNameMapper = createAnthropicToolNameMapper(void 0)) {
1733
1812
  const events$1 = [];
1734
1813
  if (chunk.choices.length === 0) return events$1;
1735
1814
  const choice = chunk.choices[0];
@@ -1793,10 +1872,11 @@ function translateChunkToAnthropicEvents(chunk, state) {
1793
1872
  state.contentBlockIndex++;
1794
1873
  state.contentBlockOpen = false;
1795
1874
  }
1875
+ const toolName = toolNameMapper.toAnthropic(toolCall.function.name);
1796
1876
  const anthropicBlockIndex = state.contentBlockIndex;
1797
1877
  state.toolCalls[toolCall.index] = {
1798
1878
  id: toolCall.id,
1799
- name: toolCall.function.name,
1879
+ name: toolName,
1800
1880
  anthropicBlockIndex
1801
1881
  };
1802
1882
  events$1.push({
@@ -1805,7 +1885,7 @@ function translateChunkToAnthropicEvents(chunk, state) {
1805
1885
  content_block: {
1806
1886
  type: "tool_use",
1807
1887
  id: toolCall.id,
1808
- name: toolCall.function.name,
1888
+ name: toolName,
1809
1889
  input: {}
1810
1890
  }
1811
1891
  });
@@ -2023,10 +2103,14 @@ messageRoutes.post("/", async (c) => {
2023
2103
  if (error instanceof RateLimitError) return c.json({ error: { message: error.message } }, 429);
2024
2104
  throw error;
2025
2105
  }
2026
- const openAIPayload = translateToOpenAI(await c.req.json(), await getClaudeSettings());
2106
+ const anthropicPayload = await c.req.json();
2107
+ const claudeSettings = await getClaudeSettings();
2108
+ const upstreamModel = translateModelName(anthropicPayload.model, claudeSettings);
2109
+ const toolNameMapper = createAnthropicToolNameMapper(anthropicPayload.tools, { ...getToolNameMapperOptionsForModel(upstreamModel) });
2110
+ const openAIPayload = translateToOpenAI(anthropicPayload, claudeSettings, toolNameMapper);
2027
2111
  try {
2028
2112
  const response = await createChatCompletions(config, openAIPayload, { client: "claude" });
2029
- if (isNonStreamingResponse(response)) return c.json(translateToAnthropic(response));
2113
+ if (isNonStreamingResponse(response)) return c.json(translateToAnthropic(response, toolNameMapper));
2030
2114
  return streamSSE(c, async (stream) => {
2031
2115
  const streamState = {
2032
2116
  messageStartSent: false,
@@ -2044,7 +2128,7 @@ messageRoutes.post("/", async (c) => {
2044
2128
  } catch {
2045
2129
  continue;
2046
2130
  }
2047
- const events$1 = translateChunkToAnthropicEvents(chunk, streamState);
2131
+ const events$1 = translateChunkToAnthropicEvents(chunk, streamState, toolNameMapper);
2048
2132
  for (const event of events$1) await stream.writeSSE({
2049
2133
  event: event.type,
2050
2134
  data: JSON.stringify(event)