betahi-copilot-bridge 0.1.6 → 0.1.8
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 +1 -1
- package/dist/main.js +107 -23
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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.8" 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-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
|
|
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:
|
|
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:
|
|
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
|
|
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)
|