dsh-router-core 0.4.6 → 0.4.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.
Files changed (63) hide show
  1. package/README.md +152 -12
  2. package/docs/ext.md +1 -1
  3. package/docs/protocols.md +198 -0
  4. package/lib/client-registry.js +146 -106
  5. package/lib/client.js +146 -106
  6. package/lib/ext-prompt/index.js +182 -17
  7. package/lib/index.js +2732 -55
  8. package/lib/types/ext/contract.d.ts +20 -1
  9. package/lib/types/ext-prompt/content.d.ts +7 -1
  10. package/lib/types/ext-prompt/mount.d.ts +12 -1
  11. package/lib/types/ext-prompt/plugin.d.ts +10 -0
  12. package/lib/types/ext-prompt/render.d.ts +14 -3
  13. package/lib/types/ext-prompt/sections.d.ts +95 -0
  14. package/lib/types/protocol/anthropic.d.ts +35 -0
  15. package/lib/types/protocol/chat.d.ts +25 -0
  16. package/lib/types/protocol/entry.d.ts +137 -0
  17. package/lib/types/protocol/gemini.d.ts +57 -0
  18. package/lib/types/protocol/index.d.ts +13 -0
  19. package/lib/types/protocol/responses.d.ts +41 -0
  20. package/lib/types/protocol/sse.d.ts +41 -0
  21. package/lib/types/protocol/stream.d.ts +92 -0
  22. package/lib/types/protocol/types.d.ts +224 -0
  23. package/lib/types/router/index.d.ts +42 -1
  24. package/lib/types/router/types.d.ts +19 -0
  25. package/lib/types/shared.d.ts +11 -0
  26. package/lib/types/suppliers/contract.d.ts +8 -0
  27. package/package.json +6 -6
  28. package/src/client/ExtDetail.tsx +25 -5
  29. package/src/client/group-controls.test.ts +53 -0
  30. package/src/client/group-controls.ts +43 -0
  31. package/src/client/router.css +20 -1
  32. package/src/ext/contract.ts +20 -1
  33. package/src/ext-prompt/assembly.test.ts +63 -0
  34. package/src/ext-prompt/content.ts +8 -10
  35. package/src/ext-prompt/ext-prompt.test.ts +38 -28
  36. package/src/ext-prompt/index.ts +50 -2
  37. package/src/ext-prompt/mount.test.ts +18 -1
  38. package/src/ext-prompt/mount.ts +22 -7
  39. package/src/ext-prompt/plugin.ts +64 -1
  40. package/src/ext-prompt/render.test.ts +1 -1
  41. package/src/ext-prompt/render.ts +17 -6
  42. package/src/ext-prompt/section-controls.test.ts +148 -0
  43. package/src/ext-prompt/section-filter.test.ts +140 -0
  44. package/src/ext-prompt/sections.test.ts +123 -0
  45. package/src/ext-prompt/sections.ts +193 -0
  46. package/src/ext-route.test.ts +16 -0
  47. package/src/index.ts +109 -1
  48. package/src/protocol/anthropic.ts +662 -0
  49. package/src/protocol/chat.ts +272 -0
  50. package/src/protocol/entry.ts +440 -0
  51. package/src/protocol/gemini.ts +548 -0
  52. package/src/protocol/index.ts +31 -0
  53. package/src/protocol/protocol.test.ts +728 -0
  54. package/src/protocol/responses.ts +551 -0
  55. package/src/protocol/sse.ts +94 -0
  56. package/src/protocol/stream.ts +195 -0
  57. package/src/protocol/types.ts +249 -0
  58. package/src/protocol/wire-e2e.test.ts +208 -0
  59. package/src/protocol/wiring.test.ts +225 -0
  60. package/src/router/index.ts +142 -23
  61. package/src/router/types.ts +19 -0
  62. package/src/shared.ts +11 -0
  63. package/src/suppliers/contract.ts +8 -0
package/lib/index.js CHANGED
@@ -1207,7 +1207,7 @@ function top(map) {
1207
1207
  //#endregion
1208
1208
  //#region src/router/usage-tokens.ts
1209
1209
  /** 取有限数,其余当 0(NaN 会毒穿整个累加:`Math.max(x, NaN) === NaN`)。 */
1210
- function num(v) {
1210
+ function num$1(v) {
1211
1211
  const n = Number(v);
1212
1212
  return Number.isFinite(n) ? n : 0;
1213
1213
  }
@@ -1231,10 +1231,10 @@ function num(v) {
1231
1231
  function normalizeUsage(raw) {
1232
1232
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
1233
1233
  const u = raw;
1234
- const cached = num(u.cached_tokens) + num(u.cache_read_input_tokens) + num(u.cachedContentTokenCount) + num(u.prompt_tokens_details?.cached_tokens);
1235
- let prompt = num(u.prompt_tokens) + num(u.input_tokens) + num(u.promptTokenCount);
1236
- if (u.cache_read_input_tokens !== void 0 && u.prompt_tokens === void 0) prompt += num(u.cache_read_input_tokens);
1237
- const completion = num(u.completion_tokens) + num(u.output_tokens) + num(u.candidatesTokenCount);
1234
+ const cached = num$1(u.cached_tokens) + num$1(u.cache_read_input_tokens) + num$1(u.cachedContentTokenCount) + num$1(u.prompt_tokens_details?.cached_tokens);
1235
+ let prompt = num$1(u.prompt_tokens) + num$1(u.input_tokens) + num$1(u.promptTokenCount);
1236
+ if (u.cache_read_input_tokens !== void 0 && u.prompt_tokens === void 0) prompt += num$1(u.cache_read_input_tokens);
1237
+ const completion = num$1(u.completion_tokens) + num$1(u.output_tokens) + num$1(u.candidatesTokenCount);
1238
1238
  if (prompt === 0 && completion === 0 && cached === 0) return null;
1239
1239
  return {
1240
1240
  promptTokens: prompt,
@@ -1445,23 +1445,2209 @@ function tapStreamUsage(source, startedAt) {
1445
1445
  };
1446
1446
  }
1447
1447
  /**
1448
- * 补齐估算:上游没给的字段用字符数估,并打标记。
1448
+ * 补齐估算:上游没给的字段用字符数估,并打标记。
1449
+ *
1450
+ * `requestChars` = 请求体字符数(估输入),`outputChars` = 输出字符数(估输出)。
1451
+ * 上游给了的字段**不覆盖**——估算只填空,不抢真数据。
1452
+ */
1453
+ function withEstimates(usage, requestChars, outputChars) {
1454
+ const prompt = usage?.promptTokens ?? 0;
1455
+ const completion = usage?.completionTokens ?? 0;
1456
+ const inputEstimated = prompt === 0;
1457
+ const outputEstimated = completion === 0;
1458
+ return {
1459
+ promptTokens: inputEstimated ? estimateTokens(requestChars) : prompt,
1460
+ completionTokens: outputEstimated ? estimateTokens(outputChars) : completion,
1461
+ cachedTokens: usage?.cachedTokens ?? 0,
1462
+ inputEstimated,
1463
+ outputEstimated
1464
+ };
1465
+ }
1466
+ //#endregion
1467
+ //#region src/protocol/types.ts
1468
+ /** 取对象字段(不是对象则 undefined)。 */
1469
+ function obj(v) {
1470
+ return typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
1471
+ }
1472
+ /** 取数组字段(不是数组则 undefined)。 */
1473
+ function arr(v) {
1474
+ return Array.isArray(v) ? v : void 0;
1475
+ }
1476
+ /** 取非空字符串字段。 */
1477
+ function str(v) {
1478
+ return typeof v === "string" && v !== "" ? v : void 0;
1479
+ }
1480
+ /** 取有限数字字段。 */
1481
+ function num(v) {
1482
+ return typeof v === "number" && Number.isFinite(v) ? v : void 0;
1483
+ }
1484
+ /** 把 `finish` 原因在各协议间对齐(各家词汇不同,这里是唯一的对照表)。 */
1485
+ function finishOf(v) {
1486
+ switch (v) {
1487
+ case "length":
1488
+ case "max_tokens":
1489
+ case "MAX_TOKENS": return "length";
1490
+ case "tool_calls":
1491
+ case "tool_use":
1492
+ case "function_call": return "tool-calls";
1493
+ case "content_filter":
1494
+ case "SAFETY":
1495
+ case "safety": return "content-filter";
1496
+ case "error": return "error";
1497
+ default: return "stop";
1498
+ }
1499
+ }
1500
+ //#endregion
1501
+ //#region src/protocol/sse.ts
1502
+ /**
1503
+ * 增量分帧器:喂字节进去,吐出完整帧。
1504
+ *
1505
+ * 用类而不是纯函数,是因为「半个帧」必须留在实例里 —— 每次调用都是无状态的
1506
+ * 函数做不到这件事,而每个流都要一个独立缓冲。
1507
+ */
1508
+ var SseSplitter = class {
1509
+ buf = "";
1510
+ /** 喂入一块字节,返回其中包含的完整帧(可能 0..n 个)。 */
1511
+ push(bytes) {
1512
+ this.buf += new TextDecoder().decode(bytes, { stream: true });
1513
+ return this.drain();
1514
+ }
1515
+ /** 流结束时冲刷:把残留的、不以空行结尾的尾帧也吐出来(上游不发尾空行是有的)。 */
1516
+ end() {
1517
+ this.buf += new TextDecoder().decode();
1518
+ const out = this.drain();
1519
+ if (this.buf.trim() !== "") {
1520
+ const frame = parseFrame(this.buf);
1521
+ this.buf = "";
1522
+ if (frame.data !== "") out.push(frame);
1523
+ }
1524
+ return out;
1525
+ }
1526
+ /** 把已缓冲的完整帧全部切出来。 */
1527
+ drain() {
1528
+ const out = [];
1529
+ let sep;
1530
+ while ((sep = this.buf.search(/\r?\n\r?\n/)) >= 0) {
1531
+ const match = /\r?\n\r?\n/.exec(this.buf);
1532
+ const raw = this.buf.slice(0, sep);
1533
+ this.buf = this.buf.slice(sep + (match?.[0].length ?? 2));
1534
+ if (raw.trim() !== "") out.push(parseFrame(raw));
1535
+ }
1536
+ return out;
1537
+ }
1538
+ };
1539
+ /** 把一帧原文拆成 `event` + `data`。空 data 的帧(注释/keep-alive)返回空 data。 */
1540
+ function parseFrame(raw) {
1541
+ let event = "";
1542
+ const dataLines = [];
1543
+ for (const line of raw.split(/\r?\n/)) {
1544
+ if (line === "" || line.startsWith(":")) continue;
1545
+ const colon = line.indexOf(":");
1546
+ const field = colon < 0 ? line : line.slice(0, colon);
1547
+ let value = colon < 0 ? "" : line.slice(colon + 1);
1548
+ if (value.startsWith(" ")) value = value.slice(1);
1549
+ if (field === "event") event = value;
1550
+ else if (field === "data") dataLines.push(value);
1551
+ }
1552
+ return {
1553
+ event,
1554
+ data: dataLines.join("\n")
1555
+ };
1556
+ }
1557
+ /** 拼一帧字节(`data:` 帧;不带 `event:`)。 */
1558
+ function dataFrame(data) {
1559
+ return `data: ${data}\n\n`;
1560
+ }
1561
+ /** 拼一帧字节(带 `event:`)。 */
1562
+ function eventFrame(event, data) {
1563
+ return `event: ${event}\ndata: ${data}\n\n`;
1564
+ }
1565
+ /** 把若干帧原文拼成字节。 */
1566
+ function framesToBytes(frames) {
1567
+ return new TextEncoder().encode(frames.join(""));
1568
+ }
1569
+ //#endregion
1570
+ //#region src/protocol/chat.ts
1571
+ /**
1572
+ * OpenAI Chat Completions 协议 —— **规范形态本身**,所以编解码是恒等变换。
1573
+ *
1574
+ * 这个文件承担两件别处不做的事:
1575
+ *
1576
+ * 1. **请求侧恒等**:`toChat`/`fromChat` 原样返回。供应商契约
1577
+ * (`suppliers/contract.ts` 的 `chatOnce`)吃的就是 Chat Completions 请求体,
1578
+ * 多一层拷贝只会让「哪一层改的」变难查。
1579
+ *
1580
+ * 2. **响应侧的规范事件翻译**:OpenAI SSE → `ChatEvent`,以及反向。这是**所有
1581
+ * 协议共用的脊柱** —— Anthropic/Gemini 的 `decodeFrame` 拿到自己的帧后,
1582
+ * 也要靠这里的逻辑把内容映射成同一组事件,才能让「Anthropic 客户端问、
1583
+ * Gemini 供应商答」这条路只有两个模块参与,而不是 12 条路径。
1584
+ *
1585
+ * ⚠️ 这里的映射规则与 `llm/adapter.ts` 的 `translateSse` 是**同一套语义的两种
1586
+ * 落点**:那边翻译成 DSH 的 `StreamChunk`(给宿主 agent loop 用),这边翻译
1587
+ * 成 `ChatEvent`(给协议矩阵用)。两处都留着,因为消费方不同、且
1588
+ * `translateSse` 里那些踩过坑的处理(残缺 tool_call 收尾、空串 name 剥除)
1589
+ * 只对 DSH 有意义;但**改任何一处规则都必须看另一处**,否则同一个上游在
1590
+ * 两个入口表现不一致。
1591
+ */
1592
+ const chatCodec = {
1593
+ id: "chat",
1594
+ toChat: (body) => body,
1595
+ fromChat: (body) => body,
1596
+ decodeFrame(frame) {
1597
+ let chunk;
1598
+ try {
1599
+ chunk = JSON.parse(frame);
1600
+ } catch {
1601
+ return [];
1602
+ }
1603
+ return chatBodyEvents(chunk);
1604
+ },
1605
+ /**
1606
+ * 非流式响应体 → 规范事件。
1607
+ *
1608
+ * 复用同一个 `chatBodyEvents`:一个非流式 body 就是一帧(内容在
1609
+ * `choices[0].message` 里、`finish_reason` 已给、`usage` 在外层)。单独实现
1610
+ * 一份只会让「流式与非流式行为不一致」有地方可长。
1611
+ */
1612
+ decodeJson(body) {
1613
+ return chatBodyEvents(body);
1614
+ },
1615
+ encodeFrame(event) {
1616
+ switch (event.type) {
1617
+ case "text": return [dataFrame(JSON.stringify(choiceDelta({ content: event.text })))];
1618
+ case "reasoning": return [dataFrame(JSON.stringify(choiceDelta({ reasoning_content: event.text })))];
1619
+ case "tool-start": return [dataFrame(JSON.stringify(choiceDelta({ tool_calls: [{
1620
+ index: event.index,
1621
+ ...event.id === void 0 ? {} : { id: event.id },
1622
+ type: "function",
1623
+ function: {
1624
+ name: event.name ?? "",
1625
+ arguments: ""
1626
+ }
1627
+ }] })))];
1628
+ case "tool-args": return [dataFrame(JSON.stringify(choiceDelta({ tool_calls: [{
1629
+ index: event.index,
1630
+ function: { arguments: event.delta }
1631
+ }] })))];
1632
+ case "usage":
1633
+ if (event.promptTokens === 0 && event.completionTokens === 0 && event.cachedTokens === 0) return [];
1634
+ return [dataFrame(JSON.stringify({
1635
+ choices: [],
1636
+ usage: {
1637
+ prompt_tokens: event.promptTokens,
1638
+ completion_tokens: event.completionTokens,
1639
+ total_tokens: event.promptTokens + event.completionTokens
1640
+ }
1641
+ }))];
1642
+ case "error": return [dataFrame(JSON.stringify({ error: {
1643
+ message: event.message,
1644
+ type: "api_error"
1645
+ } }))];
1646
+ case "finish": return [dataFrame(JSON.stringify(choiceDelta({}, finishToOpenAi(event.reason)))), "data: [DONE]\n\n"];
1647
+ }
1648
+ },
1649
+ startFrames: () => [],
1650
+ endFrames: () => [],
1651
+ /** 规范事件 → 非流式 body。 */
1652
+ encodeJson(events, ctx) {
1653
+ let content = "";
1654
+ let reasoning = "";
1655
+ let finish = "stop";
1656
+ let usage;
1657
+ const calls = /* @__PURE__ */ new Map();
1658
+ for (const ev of events) switch (ev.type) {
1659
+ case "text":
1660
+ content += ev.text;
1661
+ break;
1662
+ case "reasoning":
1663
+ reasoning += ev.text;
1664
+ break;
1665
+ case "tool-start":
1666
+ calls.set(ev.index, {
1667
+ ...ev.id === void 0 ? {} : { id: ev.id },
1668
+ name: ev.name ?? "",
1669
+ args: ""
1670
+ });
1671
+ break;
1672
+ case "tool-args": {
1673
+ const c = calls.get(ev.index) ?? {
1674
+ name: "",
1675
+ args: ""
1676
+ };
1677
+ c.args += ev.delta;
1678
+ calls.set(ev.index, c);
1679
+ break;
1680
+ }
1681
+ case "finish":
1682
+ finish = ev.reason;
1683
+ break;
1684
+ case "usage": usage = ev;
1685
+ }
1686
+ const message = {
1687
+ role: "assistant",
1688
+ content
1689
+ };
1690
+ if (reasoning !== "") message.reasoning_content = reasoning;
1691
+ const toolCalls = [...calls.entries()].sort((a, b) => a[0] - b[0]).filter(([, c]) => c.name !== "").map(([i, c]) => ({
1692
+ id: c.id ?? `call_${i}`,
1693
+ type: "function",
1694
+ function: {
1695
+ name: c.name,
1696
+ arguments: c.args
1697
+ }
1698
+ }));
1699
+ if (toolCalls.length > 0) message.tool_calls = toolCalls;
1700
+ const body = {
1701
+ id: `chatcmpl-${Date.now().toString(36)}`,
1702
+ object: "chat.completion",
1703
+ created: Math.floor(Date.now() / 1e3),
1704
+ model: ctx.model,
1705
+ choices: [{
1706
+ index: 0,
1707
+ message,
1708
+ finish_reason: finishToOpenAi(finish)
1709
+ }]
1710
+ };
1711
+ if (usage !== void 0) body.usage = {
1712
+ prompt_tokens: usage.promptTokens,
1713
+ completion_tokens: usage.completionTokens,
1714
+ total_tokens: usage.promptTokens + usage.completionTokens
1715
+ };
1716
+ return body;
1717
+ }
1718
+ };
1719
+ /** OpenAI 的一帧 `choices[0].delta`。 */
1720
+ function choiceDelta(delta, finish) {
1721
+ return {
1722
+ id: "chatcmpl-stream",
1723
+ object: "chat.completion.chunk",
1724
+ choices: [{
1725
+ index: 0,
1726
+ delta,
1727
+ finish_reason: finish ?? null
1728
+ }]
1729
+ };
1730
+ }
1731
+ /**
1732
+ * 一个 OpenAI 响应体(流式帧或非流式 body,两者形状一致)→ 规范事件。
1733
+ *
1734
+ * 流式与非流式共用:非流式 body 就是「一帧」(`choices[0].message` 带全量
1735
+ * 内容、`finish_reason` 已给、`usage` 在外层)。分开写两份只会让两边行为
1736
+ * 漂移 —— 而漂移的表现是「同一个上游在流式下工具名对、非流式下为空」。
1737
+ */
1738
+ function chatBodyEvents(chunk) {
1739
+ const out = [];
1740
+ const usage = decodeUsage(chunk);
1741
+ if (usage !== void 0) out.push(usage);
1742
+ const err = obj(chunk.error);
1743
+ if (err !== void 0) {
1744
+ out.push({
1745
+ type: "error",
1746
+ message: str(err.message) ?? "upstream error"
1747
+ });
1748
+ return out;
1749
+ }
1750
+ const ch = obj(arr(chunk.choices)?.[0]);
1751
+ if (ch === void 0) return out;
1752
+ const delta = obj(ch.delta) ?? obj(ch.message);
1753
+ if (delta !== void 0) {
1754
+ const text = str(delta.content);
1755
+ if (text !== void 0) out.push({
1756
+ type: "text",
1757
+ text
1758
+ });
1759
+ const reasoning = str(delta.reasoning_content) ?? str(obj(delta.reasoning)?.text);
1760
+ if (reasoning !== void 0) out.push({
1761
+ type: "reasoning",
1762
+ text: reasoning
1763
+ });
1764
+ for (const ev of decodeToolCalls(delta.tool_calls)) out.push(ev);
1765
+ }
1766
+ const finish = ch.finish_reason;
1767
+ if (finish !== void 0 && finish !== null) out.push({
1768
+ type: "finish",
1769
+ reason: finishOf(finish)
1770
+ });
1771
+ return out;
1772
+ }
1773
+ function finishToOpenAi(reason) {
1774
+ switch (reason) {
1775
+ case "length": return "length";
1776
+ case "tool-calls": return "tool_calls";
1777
+ case "content-filter": return "content_filter";
1778
+ case "error": return "stop";
1779
+ default: return "stop";
1780
+ }
1781
+ }
1782
+ /** OpenAI SSE 帧 → 工具调用增量。 */
1783
+ function decodeToolCalls(v) {
1784
+ const list = arr(v);
1785
+ if (list === void 0) return [];
1786
+ const out = [];
1787
+ for (const item of list) {
1788
+ const tc = obj(item);
1789
+ if (tc === void 0) continue;
1790
+ const index = num(tc.index) ?? 0;
1791
+ const fn = obj(tc.function);
1792
+ const name = fn === void 0 ? void 0 : str(fn.name);
1793
+ const id = str(tc.id);
1794
+ if (name !== void 0 || id !== void 0) out.push({
1795
+ type: "tool-start",
1796
+ index,
1797
+ ...id === void 0 ? {} : { id },
1798
+ ...name === void 0 ? {} : { name }
1799
+ });
1800
+ const args = fn === void 0 ? void 0 : str(fn.arguments);
1801
+ if (args !== void 0 && args !== "") out.push({
1802
+ type: "tool-args",
1803
+ index,
1804
+ delta: args
1805
+ });
1806
+ }
1807
+ return out;
1808
+ }
1809
+ /**
1810
+ * 从一帧 OpenAI 响应里取用量。
1811
+ *
1812
+ * 口径与 `router/usage-tokens.ts` 的 `normalizeUsage` 一致:`prompt_tokens`
1813
+ * **含**缓存(OpenAI 系如此),`cachedTokens` 是它的子集。
1814
+ */
1815
+ function decodeUsage(chunk) {
1816
+ const u = obj(chunk.usage);
1817
+ if (u === void 0) return void 0;
1818
+ return {
1819
+ type: "usage",
1820
+ promptTokens: num(u.prompt_tokens) ?? num(u.input_tokens) ?? 0,
1821
+ completionTokens: num(u.completion_tokens) ?? num(u.output_tokens) ?? 0,
1822
+ cachedTokens: num(u.cached_tokens) ?? num(obj(u.prompt_tokens_details)?.cached_tokens) ?? 0
1823
+ };
1824
+ }
1825
+ //#endregion
1826
+ //#region src/protocol/anthropic.ts
1827
+ /**
1828
+ * Anthropic Messages 协议。
1829
+ *
1830
+ * ════════════════════════════════════════════════════════════════════
1831
+ * 为什么第一个做它
1832
+ * ════════════════════════════════════════════════════════════════════
1833
+ * Claude Code 只会说这一种协议,而它对 `ANTHROPIC_BASE_URL` 的支持是现成的:
1834
+ * 把它指到我们的端点,就**直接**能用上账号池/组合/用量看板。其余三种协议
1835
+ * (Responses/Gemini)没有这样的「一个客户端在等」的理由。
1836
+ *
1837
+ * ════════════════════════════════════════════════════════════════════
1838
+ * 翻译表(唯一的事实来源)
1839
+ * ════════════════════════════════════════════════════════════════════
1840
+ * 请求 system → messages[0] 的 system-role 消息(字符串或块数组都要吃)
1841
+ * tools[].input_schema → tools[].function.parameters
1842
+ * tool_use / tool_result 块 → assistant.tool_calls / role:"tool"
1843
+ * max_tokens / temperature / stop_sequences → 同名(下划线差异两处)
1844
+ * 响应 message_start → usage(input_tokens 在这帧)
1845
+ * content_block_start(text) → text 增量开始
1846
+ * content_block_delta(text_delta) → text 增量
1847
+ * content_block_delta(input_json_delta) → tool-args 增量
1848
+ * content_block_delta(thinking_delta) → reasoning 增量
1849
+ * content_block_stop → 该 index 结束(无对应规范事件,吃掉)
1850
+ * message_delta → usage + finish_reason
1851
+ * message_stop → 无对应事件(finish 已由 message_delta 给)
1852
+ *
1853
+ * 丢弃(明确不做翻译,见下):
1854
+ * - `cache_control`:Anthropic 的提示缓存标记。跨到 Chat 后失去意义
1855
+ * (Chat 的缓存是上游自动的),硬造一个 `prompt_cache_key` 反而是猜。
1856
+ * - `thinking.budget_tokens`:只在响应侧保留为 reasoning 增量。
1857
+ * - `mcp_servers` / `container` / `context_management`:客户端侧特性,
1858
+ * 转发到只懂 Chat 的上游只会得到 400。
1859
+ */
1860
+ const anthropicCodec = {
1861
+ id: "anthropic",
1862
+ /** Anthropic 请求 → Chat Completions 请求。 */
1863
+ toChat(body, ctx) {
1864
+ const out = { ...body };
1865
+ delete out.system;
1866
+ delete out.tools;
1867
+ delete out.max_tokens;
1868
+ delete out.stop_sequences;
1869
+ const messages = [];
1870
+ const system = body.system;
1871
+ if (system !== void 0) {
1872
+ const text = systemText$1(system);
1873
+ if (text !== "") messages.push({
1874
+ role: "system",
1875
+ content: text
1876
+ });
1877
+ }
1878
+ for (const m of messagePairs(body.messages)) messages.push(m);
1879
+ out.messages = messages;
1880
+ if (body.max_tokens !== void 0) out.max_tokens = body.max_tokens;
1881
+ if (body.stop_sequences !== void 0) out.stop = body.stop_sequences;
1882
+ if (body.temperature !== void 0) out.temperature = body.temperature;
1883
+ if (body.top_p !== void 0) out.top_p = body.top_p;
1884
+ const tools = convertTools$2(body.tools);
1885
+ if (tools.length > 0) out.tools = tools;
1886
+ if (ctx.stream) out.stream = true;
1887
+ return out;
1888
+ },
1889
+ /** Chat Completions 请求 → Anthropic 请求(供应商说 Anthropic 时用)。 */
1890
+ fromChat(body, ctx) {
1891
+ const out = { ...body };
1892
+ delete out.messages;
1893
+ delete out.tools;
1894
+ delete out.stop;
1895
+ delete out.stream;
1896
+ const systemParts = [];
1897
+ const messages = [];
1898
+ for (const m of body.messages) {
1899
+ const msg = obj(m);
1900
+ if (msg === void 0) continue;
1901
+ const role = str(msg.role);
1902
+ if (role === "system") {
1903
+ const t = contentText$2(msg.content);
1904
+ if (t !== "") systemParts.push(t);
1905
+ continue;
1906
+ }
1907
+ if (role === "assistant") {
1908
+ messages.push(assistantMessage(msg));
1909
+ continue;
1910
+ }
1911
+ if (role === "tool") {
1912
+ messages.push({
1913
+ role: "user",
1914
+ content: [{
1915
+ type: "tool_result",
1916
+ tool_use_id: str(msg.tool_call_id) ?? "",
1917
+ content: contentText$2(msg.content)
1918
+ }]
1919
+ });
1920
+ continue;
1921
+ }
1922
+ messages.push({
1923
+ role: "user",
1924
+ content: contentText$2(msg.content)
1925
+ });
1926
+ }
1927
+ if (systemParts.length > 0) out.system = systemParts.join("\n\n");
1928
+ out.messages = messages;
1929
+ out.max_tokens = num(body.max_tokens) ?? 4096;
1930
+ if (body.stop !== void 0) out.stop_sequences = body.stop;
1931
+ const tools = body.tools?.map(chatToolToAnthropic).filter((t) => t !== void 0);
1932
+ if (tools !== void 0 && tools.length > 0) out.tools = tools;
1933
+ if (ctx.stream) out.stream = true;
1934
+ return out;
1935
+ },
1936
+ decodeFrame(frame, event) {
1937
+ let payload;
1938
+ try {
1939
+ payload = JSON.parse(frame);
1940
+ } catch {
1941
+ return [];
1942
+ }
1943
+ switch (event) {
1944
+ case "message_start": return messageStartEvents(payload);
1945
+ case "content_block_start": return contentBlockStartEvents(payload);
1946
+ case "content_block_delta": return contentDeltaEvents(payload);
1947
+ case "message_delta": return messageDeltaEvents(payload);
1948
+ case "error": return [{
1949
+ type: "error",
1950
+ message: str(obj(payload.error)?.message) ?? "upstream error"
1951
+ }];
1952
+ default: return [];
1953
+ }
1954
+ },
1955
+ encodeFrame(event, session) {
1956
+ switch (event.type) {
1957
+ case "text": return [eventFrame("content_block_delta", JSON.stringify({
1958
+ type: "content_block_delta",
1959
+ index: 0,
1960
+ delta: {
1961
+ type: "text_delta",
1962
+ text: event.text
1963
+ }
1964
+ }))];
1965
+ case "reasoning": return [eventFrame("content_block_delta", JSON.stringify({
1966
+ type: "content_block_delta",
1967
+ index: 0,
1968
+ delta: {
1969
+ type: "thinking_delta",
1970
+ thinking: event.text
1971
+ }
1972
+ }))];
1973
+ case "tool-start": return [eventFrame("content_block_start", JSON.stringify({
1974
+ type: "content_block_start",
1975
+ index: event.index,
1976
+ content_block: {
1977
+ type: "tool_use",
1978
+ id: event.id ?? `toolu_${event.index}`,
1979
+ name: event.name ?? "",
1980
+ input: {}
1981
+ }
1982
+ }))];
1983
+ case "tool-args": return [eventFrame("content_block_delta", JSON.stringify({
1984
+ type: "content_block_delta",
1985
+ index: event.index,
1986
+ delta: {
1987
+ type: "input_json_delta",
1988
+ partial_json: event.delta
1989
+ }
1990
+ }))];
1991
+ case "usage": return [];
1992
+ case "error": return [eventFrame("error", JSON.stringify({
1993
+ type: "error",
1994
+ error: {
1995
+ type: "api_error",
1996
+ message: event.message
1997
+ }
1998
+ }))];
1999
+ case "finish": {
2000
+ const tokens = session?.usage?.completionTokens ?? 0;
2001
+ return [eventFrame("message_delta", JSON.stringify({
2002
+ type: "message_delta",
2003
+ delta: {
2004
+ stop_reason: finishToAnthropic(event.reason),
2005
+ stop_sequence: null
2006
+ },
2007
+ usage: { output_tokens: tokens }
2008
+ }))];
2009
+ }
2010
+ }
2011
+ },
2012
+ startFrames(ctx) {
2013
+ return [eventFrame("message_start", JSON.stringify({
2014
+ type: "message_start",
2015
+ message: {
2016
+ id: `msg_${Date.now().toString(36)}`,
2017
+ type: "message",
2018
+ role: "assistant",
2019
+ content: [],
2020
+ model: ctx.model,
2021
+ stop_reason: null,
2022
+ stop_sequence: null,
2023
+ usage: {
2024
+ input_tokens: 0,
2025
+ output_tokens: 0
2026
+ }
2027
+ }
2028
+ }))];
2029
+ },
2030
+ endFrames() {
2031
+ return [eventFrame("message_stop", JSON.stringify({ type: "message_stop" }))];
2032
+ },
2033
+ /**
2034
+ * 非流式响应体 → 规范事件。
2035
+ *
2036
+ * Anthropic 非流式 body 形状:`{content:[{type:'text',text}|{type:'tool_use',...}],
2037
+ * stop_reason, usage:{input_tokens,output_tokens,cache_read_input_tokens}}`。
2038
+ * 与流式共用同一组 block→事件 的映射,见 `blockEvents`。
2039
+ */
2040
+ decodeJson(body) {
2041
+ const out = [];
2042
+ let index = 0;
2043
+ let text = "";
2044
+ for (const b of arr(body.content) ?? []) {
2045
+ const block = obj(b);
2046
+ if (block === void 0) continue;
2047
+ if (block.type === "text") {
2048
+ const t = str(block.text);
2049
+ if (t !== void 0) {
2050
+ text += t;
2051
+ index += 1;
2052
+ }
2053
+ continue;
2054
+ }
2055
+ if (block.type === "thinking") {
2056
+ const t = str(block.thinking);
2057
+ if (t !== void 0) {
2058
+ out.push({
2059
+ type: "reasoning",
2060
+ text: t
2061
+ });
2062
+ index += 1;
2063
+ }
2064
+ continue;
2065
+ }
2066
+ if (block.type === "tool_use") {
2067
+ out.push({
2068
+ type: "tool-start",
2069
+ index,
2070
+ ...str(block.id) === void 0 ? {} : { id: str(block.id) },
2071
+ name: str(block.name) ?? ""
2072
+ });
2073
+ out.push({
2074
+ type: "tool-args",
2075
+ index,
2076
+ delta: JSON.stringify(block.input ?? {})
2077
+ });
2078
+ index += 1;
2079
+ }
2080
+ }
2081
+ if (text !== "") out.unshift({
2082
+ type: "text",
2083
+ text
2084
+ });
2085
+ const usage = obj(body.usage);
2086
+ if (usage !== void 0) out.push({
2087
+ type: "usage",
2088
+ promptTokens: num(usage.input_tokens) ?? 0,
2089
+ completionTokens: num(usage.output_tokens) ?? 0,
2090
+ cachedTokens: num(usage.cache_read_input_tokens) ?? 0
2091
+ });
2092
+ const stop = body.stop_reason;
2093
+ if (stop !== void 0 && stop !== null) out.push({
2094
+ type: "finish",
2095
+ reason: finishOf(stop === "max_tokens" ? "length" : stop)
2096
+ });
2097
+ return out;
2098
+ },
2099
+ /** 规范事件 → 非流式 body。 */
2100
+ encodeJson(events, ctx) {
2101
+ const content = [];
2102
+ let stop = "end_turn";
2103
+ let usage = {
2104
+ input: 0,
2105
+ output: 0,
2106
+ cached: 0
2107
+ };
2108
+ for (const ev of events) switch (ev.type) {
2109
+ case "text":
2110
+ if (content.length > 0 && content[content.length - 1]?.type === "text") content[content.length - 1].text += ev.text;
2111
+ else content.push({
2112
+ type: "text",
2113
+ text: ev.text
2114
+ });
2115
+ break;
2116
+ case "reasoning":
2117
+ if (content.length > 0 && content[content.length - 1]?.type === "thinking") content[content.length - 1].thinking += ev.text;
2118
+ else content.push({
2119
+ type: "thinking",
2120
+ thinking: ev.text
2121
+ });
2122
+ break;
2123
+ case "tool-start":
2124
+ content.push({
2125
+ type: "tool_use",
2126
+ id: ev.id ?? `toolu_${ev.index}`,
2127
+ name: ev.name ?? "",
2128
+ input: {}
2129
+ });
2130
+ break;
2131
+ case "tool-args": {
2132
+ const last = content[content.length - 1];
2133
+ if (last?.type === "tool_use") last.input = parseArgs$1((str(last.input) ?? "") + ev.delta);
2134
+ break;
2135
+ }
2136
+ case "finish":
2137
+ stop = finishToAnthropic(ev.reason);
2138
+ break;
2139
+ case "usage": usage = {
2140
+ input: ev.promptTokens,
2141
+ output: ev.completionTokens,
2142
+ cached: ev.cachedTokens
2143
+ };
2144
+ }
2145
+ return {
2146
+ id: `msg_${Date.now().toString(36)}`,
2147
+ type: "message",
2148
+ role: "assistant",
2149
+ content,
2150
+ model: ctx.model,
2151
+ stop_reason: stop,
2152
+ stop_sequence: null,
2153
+ usage: {
2154
+ input_tokens: usage.input,
2155
+ output_tokens: usage.output,
2156
+ ...usage.cached > 0 ? { cache_read_input_tokens: usage.cached } : {}
2157
+ }
2158
+ };
2159
+ }
2160
+ };
2161
+ /** Anthropic 的 system 字段:字符串或 [{type:'text',text}] 数组。 */
2162
+ function systemText$1(system) {
2163
+ if (typeof system === "string") return system;
2164
+ const parts = [];
2165
+ for (const b of arr(system) ?? []) {
2166
+ const t = str(obj(b)?.text);
2167
+ if (t !== void 0) parts.push(t);
2168
+ }
2169
+ return parts.join("\n\n");
2170
+ }
2171
+ /**
2172
+ * Anthropic messages → Chat messages。
2173
+ *
2174
+ * 关键差异:Anthropic 把「模型发起的工具调用」与「用户回传的工具结果」都放在
2175
+ * content 块里,而 Chat 把它们拆成 `assistant.tool_calls` 与 `role:'tool'` 消息。
2176
+ * 且 Anthropic 允许 tool_result 单独成一条 user 消息,Chat 也允许 —— 但**连续
2177
+ * 多条 tool_result 必须各自一条**,合并不了(每个 tool_use_id 要能对上)。
2178
+ */
2179
+ function messagePairs(messages) {
2180
+ const out = [];
2181
+ for (const m of arr(messages) ?? []) {
2182
+ const msg = obj(m);
2183
+ if (msg === void 0) continue;
2184
+ const role = str(msg.role);
2185
+ if (role !== "user" && role !== "assistant") continue;
2186
+ const blocks = arr(msg.content);
2187
+ if (blocks === void 0) {
2188
+ out.push({
2189
+ role,
2190
+ content: typeof msg.content === "string" ? msg.content : ""
2191
+ });
2192
+ continue;
2193
+ }
2194
+ const texts = [];
2195
+ const toolCalls = [];
2196
+ const toolResults = [];
2197
+ for (const b of blocks) {
2198
+ const block = obj(b);
2199
+ if (block === void 0) continue;
2200
+ switch (block.type) {
2201
+ case "text": {
2202
+ const t = str(block.text);
2203
+ if (t !== void 0) texts.push(t);
2204
+ break;
2205
+ }
2206
+ case "tool_use":
2207
+ toolCalls.push({
2208
+ id: str(block.id) ?? "",
2209
+ type: "function",
2210
+ function: {
2211
+ name: str(block.name) ?? "",
2212
+ arguments: JSON.stringify(block.input ?? {})
2213
+ }
2214
+ });
2215
+ break;
2216
+ case "tool_result": toolResults.push({
2217
+ role: "tool",
2218
+ tool_call_id: str(block.tool_use_id) ?? "",
2219
+ content: blockContentText(block.content)
2220
+ });
2221
+ }
2222
+ }
2223
+ if (role === "assistant") {
2224
+ const msg2 = {
2225
+ role: "assistant",
2226
+ content: texts.join("\n\n")
2227
+ };
2228
+ if (toolCalls.length > 0) msg2.tool_calls = toolCalls;
2229
+ out.push(msg2);
2230
+ } else {
2231
+ if (texts.length > 0) out.push({
2232
+ role: "user",
2233
+ content: texts.join("\n\n")
2234
+ });
2235
+ for (const tr of toolResults) out.push(tr);
2236
+ }
2237
+ }
2238
+ return out;
2239
+ }
2240
+ /** tool_result 的 content:字符串或 [{type:'text'}] 数组。 */
2241
+ function blockContentText(content) {
2242
+ if (typeof content === "string") return content;
2243
+ const parts = [];
2244
+ for (const b of arr(content) ?? []) {
2245
+ const t = str(obj(b)?.text);
2246
+ if (t !== void 0) parts.push(t);
2247
+ }
2248
+ return parts.join("\n\n");
2249
+ }
2250
+ /** Chat 的 content(字符串或 parts 数组)→ 纯文本。 */
2251
+ function contentText$2(content) {
2252
+ if (typeof content === "string") return content;
2253
+ const parts = [];
2254
+ for (const p of arr(content) ?? []) {
2255
+ const part = obj(p);
2256
+ if (part === void 0) continue;
2257
+ const t = str(part.text) ?? str(part.content);
2258
+ if (t !== void 0) parts.push(t);
2259
+ }
2260
+ return parts.join("\n\n");
2261
+ }
2262
+ /** Chat assistant 消息 → Anthropic assistant 消息(tool_calls → tool_use 块)。 */
2263
+ function assistantMessage(msg) {
2264
+ const content = [];
2265
+ const text = contentText$2(msg.content);
2266
+ if (text !== "") content.push({
2267
+ type: "text",
2268
+ text
2269
+ });
2270
+ for (const tc of arr(msg.tool_calls) ?? []) {
2271
+ const call = obj(tc);
2272
+ if (call === void 0) continue;
2273
+ const fn = obj(call.function);
2274
+ content.push({
2275
+ type: "tool_use",
2276
+ id: str(call.id) ?? "",
2277
+ name: str(fn?.name) ?? "",
2278
+ input: parseArgs$1(str(fn?.arguments))
2279
+ });
2280
+ }
2281
+ return {
2282
+ role: "assistant",
2283
+ content
2284
+ };
2285
+ }
2286
+ /** 解析工具参数字符串;坏 JSON 返回空对象(上游偶发半截参数,别让整条消息失败)。 */
2287
+ function parseArgs$1(s) {
2288
+ if (s === void 0 || s === "") return {};
2289
+ try {
2290
+ return obj(JSON.parse(s)) ?? {};
2291
+ } catch {
2292
+ return {};
2293
+ }
2294
+ }
2295
+ /**
2296
+ * Anthropic tool → Chat tool(`toChat` 方向)。
2297
+ *
2298
+ * Anthropic 的工具是**扁平**的 `{ name, description, input_schema }`,而 Chat
2299
+ * 要多包一层 `function`、并把 `input_schema` 改名为 `parameters`。两件事都要做。
2300
+ *
2301
+ * ⚠️ 曾经这里读的是 `tool.function.*`(Chat 的形状)—— 方向搞反了,于是从
2302
+ * Anthropic 客户端进来的工具**被静默丢光**,Claude Code 接上来后模型会说
2303
+ * 「我没有这个工具」(2026-10-21 在 3080 实测)。现按 Anthropic 结构读,
2304
+ * 并兼容已经带 `function` 包装的输入(中间层可能提前转过)。
2305
+ */
2306
+ function convertTools$2(tools) {
2307
+ const out = [];
2308
+ for (const t of arr(tools) ?? []) {
2309
+ const tool = obj(t);
2310
+ if (tool === void 0) continue;
2311
+ const fn = obj(tool.function) ?? tool;
2312
+ const name = str(fn.name);
2313
+ if (name === void 0) continue;
2314
+ out.push({
2315
+ type: "function",
2316
+ function: {
2317
+ name,
2318
+ description: str(fn.description) ?? "",
2319
+ parameters: obj(fn.input_schema) ?? obj(fn.parameters) ?? {
2320
+ type: "object",
2321
+ properties: {}
2322
+ }
2323
+ }
2324
+ });
2325
+ }
2326
+ return out;
2327
+ }
2328
+ /**
2329
+ * Chat tool → Anthropic tool(`fromChat` 方向,与上面的 `convertTools` 反着走)。
2330
+ *
2331
+ * Chat 的具名工具是 `{ type:'function', function:{ name, description, parameters } }`
2332
+ * —— **多包了一层** `function`。直接读 `tool.name` 会永远 undefined,于是所有
2333
+ * 工具被静默丢掉(Claude Code 接上来就看不到任何 tool)。所以这里先解包,
2334
+ * 再兼容已经扁平化的输入(有些中间层会提前拍平)。
2335
+ */
2336
+ function chatToolToAnthropic(t) {
2337
+ const tool = obj(t);
2338
+ if (tool === void 0) return void 0;
2339
+ const fn = obj(tool.function) ?? tool;
2340
+ const name = str(fn.name);
2341
+ if (name === void 0) return void 0;
2342
+ return {
2343
+ name,
2344
+ description: str(fn.description) ?? "",
2345
+ input_schema: obj(fn.parameters) ?? {
2346
+ type: "object",
2347
+ properties: {}
2348
+ }
2349
+ };
2350
+ }
2351
+ /** message_start:把 input_tokens 带出来(Anthropic 只在这帧给输入用量)。 */
2352
+ function messageStartEvents(payload) {
2353
+ const usage = obj(obj(payload.message)?.usage);
2354
+ return [{
2355
+ type: "usage",
2356
+ promptTokens: num(usage?.input_tokens) ?? 0,
2357
+ completionTokens: 0,
2358
+ cachedTokens: num(usage?.cache_read_input_tokens) ?? 0
2359
+ }];
2360
+ }
2361
+ /**
2362
+ * content_block_start:`tool_use` 块的**名字**只在这一帧给,后面的
2363
+ * `input_json_delta` 只有参数增量。所以这里必须产出 `tool-start`,否则
2364
+ * tool-start 永远丢失,客户端收到一堆没有名字的参数。
2365
+ *
2366
+ * `text` 块与 `thinking` 块不产出事件:它们的内容全在后续 delta 里,
2367
+ * 这里发空文本只会造成一个多余的空增量。
2368
+ */
2369
+ function contentBlockStartEvents(payload) {
2370
+ const block = obj(payload.content_block);
2371
+ if (block === void 0 || block.type !== "tool_use") return [];
2372
+ const index = num(payload.index) ?? 0;
2373
+ const name = str(block.name);
2374
+ const id = str(block.id);
2375
+ return [{
2376
+ type: "tool-start",
2377
+ index,
2378
+ ...id === void 0 ? {} : { id },
2379
+ ...name === void 0 ? {} : { name }
2380
+ }];
2381
+ }
2382
+ /** content_block_delta:四种 delta 各对应一种规范事件。 */
2383
+ function contentDeltaEvents(payload) {
2384
+ const delta = obj(payload.delta);
2385
+ if (delta === void 0) return [];
2386
+ const index = num(payload.index) ?? 0;
2387
+ switch (delta.type) {
2388
+ case "text_delta": {
2389
+ const text = str(delta.text);
2390
+ return text === void 0 ? [] : [{
2391
+ type: "text",
2392
+ text
2393
+ }];
2394
+ }
2395
+ case "thinking_delta":
2396
+ case "signature_delta": {
2397
+ const text = str(delta.thinking);
2398
+ return text === void 0 ? [] : [{
2399
+ type: "reasoning",
2400
+ text
2401
+ }];
2402
+ }
2403
+ case "input_json_delta": {
2404
+ const partial = str(delta.partial_json);
2405
+ return partial === void 0 || partial === "" ? [] : [{
2406
+ type: "tool-args",
2407
+ index,
2408
+ delta: partial
2409
+ }];
2410
+ }
2411
+ default: return [];
2412
+ }
2413
+ }
2414
+ /** message_delta:finish_reason + 输出用量。 */
2415
+ function messageDeltaEvents(payload) {
2416
+ const out = [];
2417
+ const stop = obj(payload.delta)?.stop_reason;
2418
+ if (stop !== void 0 && stop !== null) out.push({
2419
+ type: "finish",
2420
+ reason: finishOf(stop === "max_tokens" ? "length" : stop)
2421
+ });
2422
+ const usage = obj(payload.usage);
2423
+ if (usage !== void 0) {
2424
+ const prompt = num(usage.input_tokens);
2425
+ const completion = num(usage.output_tokens);
2426
+ if (prompt !== void 0 || completion !== void 0) out.push({
2427
+ type: "usage",
2428
+ promptTokens: prompt ?? 0,
2429
+ completionTokens: completion ?? 0,
2430
+ cachedTokens: num(usage.cache_read_input_tokens) ?? 0
2431
+ });
2432
+ }
2433
+ return out;
2434
+ }
2435
+ function finishToAnthropic(reason) {
2436
+ switch (reason) {
2437
+ case "length": return "max_tokens";
2438
+ case "tool-calls": return "tool_use";
2439
+ case "content-filter": return "refusal";
2440
+ default: return "end_turn";
2441
+ }
2442
+ }
2443
+ //#endregion
2444
+ //#region src/protocol/responses.ts
2445
+ /**
2446
+ * OpenAI Responses 协议(Codex 用的是它)。
2447
+ *
2448
+ * ════════════════════════════════════════════════════════════════════
2449
+ * 与 Chat Completions 的关键差异
2450
+ * ════════════════════════════════════════════════════════════════════
2451
+ * 请求 `input` 是**消息数组**(与 messages 同构)而不是 messages 本身
2452
+ * `instructions` 替代 `system` 消息
2453
+ * `max_output_tokens` 替代 `max_tokens`
2454
+ * `tools` 项是扁平的 `{type:'function', name, parameters}`,
2455
+ * 没有 `function` 这一层包装
2456
+ * 响应 事件名带语义:`response.output_text.delta` / `response.function_call_arguments.delta`
2457
+ * / `response.reasoning_summary_text.delta`,全部带 `sequence_number`
2458
+ * 收尾是 `response.completed`(带完整 response 对象),不是 finish_reason
2459
+ *
2460
+ * 丢弃(明确不做):
2461
+ * - `previous_response_id` / `store`:服务端会话态。我们的供应商契约是
2462
+ * 无状态的(`chatOnce` 一次一请求),转发过去只会拿到错误。
2463
+ * - `reasoning.effort` 之外的 `reasoning.summary`:摘要粒度由上游定,
2464
+ * 我们只透传 reasoning 增量。
2465
+ * - `include` / `text.format`(结构化输出):跨协议没有对应物。
2466
+ */
2467
+ const responsesCodec = {
2468
+ id: "responses",
2469
+ /** Responses 请求 → Chat Completions 请求。 */
2470
+ toChat(body, ctx) {
2471
+ const out = { ...body };
2472
+ delete out.input;
2473
+ delete out.instructions;
2474
+ delete out.tools;
2475
+ delete out.max_output_tokens;
2476
+ delete out.previous_response_id;
2477
+ delete out.store;
2478
+ delete out.include;
2479
+ delete out.reasoning;
2480
+ const messages = [];
2481
+ const instructions = str(body.instructions);
2482
+ if (instructions !== void 0) messages.push({
2483
+ role: "system",
2484
+ content: instructions
2485
+ });
2486
+ for (const m of arr(body.input) ?? []) messages.push(inputItem(m));
2487
+ out.messages = messages;
2488
+ if (body.max_output_tokens !== void 0) out.max_tokens = body.max_output_tokens;
2489
+ if (body.temperature !== void 0) out.temperature = body.temperature;
2490
+ if (body.top_p !== void 0) out.top_p = body.top_p;
2491
+ const tools = convertTools$1(body.tools);
2492
+ if (tools.length > 0) out.tools = tools;
2493
+ if (ctx.stream) out.stream = true;
2494
+ return out;
2495
+ },
2496
+ /** Chat Completions 请求 → Responses 请求。 */
2497
+ fromChat(body, ctx) {
2498
+ const out = { ...body };
2499
+ delete out.messages;
2500
+ delete out.tools;
2501
+ delete out.stream;
2502
+ delete out.max_tokens;
2503
+ const input = [];
2504
+ const instructions = [];
2505
+ for (const m of arr(body.messages) ?? []) {
2506
+ const msg = obj(m);
2507
+ if (msg === void 0) continue;
2508
+ const role = str(msg.role);
2509
+ if (role === "system" || role === "developer") {
2510
+ const t = contentText$1(msg.content);
2511
+ if (t !== "") instructions.push(t);
2512
+ continue;
2513
+ }
2514
+ if (role === "tool") {
2515
+ input.push({
2516
+ type: "function_call_output",
2517
+ call_id: str(msg.tool_call_id) ?? "",
2518
+ output: contentText$1(msg.content)
2519
+ });
2520
+ continue;
2521
+ }
2522
+ const text = contentText$1(msg.content);
2523
+ if (text !== "") input.push({
2524
+ type: "message",
2525
+ role: role === "assistant" ? "assistant" : "user",
2526
+ content: [{
2527
+ type: role === "assistant" ? "output_text" : "input_text",
2528
+ text
2529
+ }]
2530
+ });
2531
+ if (role === "assistant") for (const tc of arr(msg.tool_calls) ?? []) {
2532
+ const call = obj(tc);
2533
+ const fn = obj(call?.function);
2534
+ if (call === void 0 || fn === void 0) continue;
2535
+ const name = str(fn.name);
2536
+ if (name === void 0) continue;
2537
+ input.push({
2538
+ type: "function_call",
2539
+ call_id: str(call.id) ?? "",
2540
+ name,
2541
+ arguments: str(fn.arguments) ?? "{}"
2542
+ });
2543
+ }
2544
+ }
2545
+ if (instructions.length > 0) out.instructions = instructions.join("\n\n");
2546
+ out.input = input;
2547
+ if (body.max_tokens !== void 0) out.max_output_tokens = body.max_tokens;
2548
+ const tools = body.tools?.map(chatToolToResponses).filter((t) => t !== void 0);
2549
+ if (tools !== void 0 && tools.length > 0) out.tools = tools;
2550
+ if (ctx.stream) out.stream = true;
2551
+ return out;
2552
+ },
2553
+ decodeFrame(frame, event) {
2554
+ let payload;
2555
+ try {
2556
+ payload = JSON.parse(frame);
2557
+ } catch {
2558
+ return [];
2559
+ }
2560
+ switch (event) {
2561
+ case "response.output_text.delta": {
2562
+ const delta = str(payload.delta);
2563
+ return delta === void 0 ? [] : [{
2564
+ type: "text",
2565
+ text: delta
2566
+ }];
2567
+ }
2568
+ case "response.reasoning_summary_text.delta":
2569
+ case "response.reasoning_text.delta": {
2570
+ const delta = str(payload.delta);
2571
+ return delta === void 0 ? [] : [{
2572
+ type: "reasoning",
2573
+ text: delta
2574
+ }];
2575
+ }
2576
+ case "response.function_call_arguments.delta": {
2577
+ const delta = str(payload.delta);
2578
+ if (delta === void 0 || delta === "") return [];
2579
+ return [{
2580
+ type: "tool-args",
2581
+ index: num(payload.output_index) ?? 0,
2582
+ delta
2583
+ }];
2584
+ }
2585
+ case "response.output_item.added": return outputItemEvents(payload);
2586
+ case "response.completed": return completedEvents(payload);
2587
+ case "response.failed":
2588
+ case "error": return [{
2589
+ type: "error",
2590
+ message: str((obj(obj(payload.response)?.error) ?? obj(payload.error))?.message) ?? str(payload.message) ?? "upstream error"
2591
+ }];
2592
+ default: return [];
2593
+ }
2594
+ },
2595
+ encodeFrame(event) {
2596
+ switch (event.type) {
2597
+ case "text": return [eventFrame("response.output_text.delta", JSON.stringify({
2598
+ type: "response.output_text.delta",
2599
+ output_index: 0,
2600
+ delta: event.text
2601
+ }))];
2602
+ case "reasoning": return [eventFrame("response.reasoning_summary_text.delta", JSON.stringify({
2603
+ type: "response.reasoning_summary_text.delta",
2604
+ output_index: 0,
2605
+ delta: event.text
2606
+ }))];
2607
+ case "tool-start": return [eventFrame("response.output_item.added", JSON.stringify({
2608
+ type: "response.output_item.added",
2609
+ output_index: event.index,
2610
+ item: {
2611
+ type: "function_call",
2612
+ id: event.id ?? `fc_${event.index}`,
2613
+ call_id: event.id ?? `call_${event.index}`,
2614
+ name: event.name ?? "",
2615
+ arguments: ""
2616
+ }
2617
+ }))];
2618
+ case "tool-args": return [eventFrame("response.function_call_arguments.delta", JSON.stringify({
2619
+ type: "response.function_call_arguments.delta",
2620
+ item_id: `fc_${event.index}`,
2621
+ output_index: event.index,
2622
+ delta: event.delta
2623
+ }))];
2624
+ case "usage": return [];
2625
+ case "error": return [eventFrame("error", JSON.stringify({
2626
+ type: "error",
2627
+ code: "server_error",
2628
+ message: event.message
2629
+ }))];
2630
+ case "finish": return [];
2631
+ }
2632
+ },
2633
+ startFrames() {
2634
+ return [eventFrame("response.created", JSON.stringify({
2635
+ type: "response.created",
2636
+ response: {
2637
+ id: `resp_${Date.now().toString(36)}`,
2638
+ status: "in_progress",
2639
+ output: []
2640
+ }
2641
+ }))];
2642
+ },
2643
+ /**
2644
+ * 流结束帧:`response.completed` 带一个完整的 response 对象。
2645
+ *
2646
+ * 用量与收尾原因都从 `ctx` 来(`StreamState.finishStream` 填),因为 Codex
2647
+ * 读的是 `response.completed` 里的 `status`/`usage`,而不是一个 finish_reason
2648
+ * 字段 —— 少了这一帧客户端会一直等流结束。
2649
+ */
2650
+ endFrames(ctx) {
2651
+ return [completedFrame(ctx.model, ctx.stream ? ctx.usage : void 0, ctx.finish)];
2652
+ },
2653
+ /**
2654
+ * 非流式响应体 → 规范事件。
2655
+ *
2656
+ * 非流式 Responses body 形状:`{output:[{type:'message',content:[{type:'output_text',text}]}
2657
+ * |{type:'function_call',name,arguments}], status, usage:{input_tokens,output_tokens}}`。
2658
+ * 与流式的 `response.output_item.added` / `response.completed` 共用同一组
2659
+ * item→事件 映射,见 `outputItemEvents`。
2660
+ */
2661
+ decodeJson(body) {
2662
+ const out = [];
2663
+ let index = 0;
2664
+ let text = "";
2665
+ for (const item of arr(body.output) ?? []) {
2666
+ const it = obj(item);
2667
+ if (it === void 0) continue;
2668
+ if (it.type === "message") {
2669
+ for (const p of arr(it.content) ?? []) {
2670
+ const t = str(obj(p)?.text);
2671
+ if (t !== void 0) text += t;
2672
+ }
2673
+ continue;
2674
+ }
2675
+ if (it.type === "function_call") {
2676
+ out.push({
2677
+ type: "tool-start",
2678
+ index,
2679
+ name: str(it.name) ?? ""
2680
+ });
2681
+ out.push({
2682
+ type: "tool-args",
2683
+ index,
2684
+ delta: str(it.arguments) ?? ""
2685
+ });
2686
+ index += 1;
2687
+ continue;
2688
+ }
2689
+ if (it.type === "reasoning") for (const s of arr(it.summary) ?? []) {
2690
+ const t = str(obj(s)?.text);
2691
+ if (t !== void 0) out.push({
2692
+ type: "reasoning",
2693
+ text: t
2694
+ });
2695
+ }
2696
+ }
2697
+ if (text !== "") out.unshift({
2698
+ type: "text",
2699
+ text
2700
+ });
2701
+ const usage = obj(body.usage);
2702
+ if (usage !== void 0) out.push({
2703
+ type: "usage",
2704
+ promptTokens: num(usage.input_tokens) ?? 0,
2705
+ completionTokens: num(usage.output_tokens) ?? 0,
2706
+ cachedTokens: num(obj(usage.input_tokens_details)?.cached_tokens) ?? 0
2707
+ });
2708
+ const status = str(body.status);
2709
+ if (status === "incomplete") out.push({
2710
+ type: "finish",
2711
+ reason: "length"
2712
+ });
2713
+ else if (status === "failed") out.push({
2714
+ type: "finish",
2715
+ reason: "error"
2716
+ });
2717
+ else out.push({
2718
+ type: "finish",
2719
+ reason: "stop"
2720
+ });
2721
+ return out;
2722
+ },
2723
+ /** 规范事件 → 非流式 body。 */
2724
+ encodeJson(events, ctx) {
2725
+ const output = [];
2726
+ let status = "completed";
2727
+ let usage = {
2728
+ input: 0,
2729
+ output: 0
2730
+ };
2731
+ for (const ev of events) switch (ev.type) {
2732
+ case "text":
2733
+ if (output.length > 0 && output[output.length - 1]?.type === "message") {
2734
+ const c = output[output.length - 1].content;
2735
+ if (c.length > 0 && c[c.length - 1]?.type === "output_text") {
2736
+ c[c.length - 1].text += ev.text;
2737
+ break;
2738
+ }
2739
+ c.push({
2740
+ type: "output_text",
2741
+ text: ev.text
2742
+ });
2743
+ break;
2744
+ }
2745
+ output.push({
2746
+ type: "message",
2747
+ role: "assistant",
2748
+ content: [{
2749
+ type: "output_text",
2750
+ text: ev.text
2751
+ }]
2752
+ });
2753
+ break;
2754
+ case "reasoning": {
2755
+ const last = output[output.length - 1];
2756
+ if (last?.type === "reasoning") last.summary[0].text += ev.text;
2757
+ else output.push({
2758
+ type: "reasoning",
2759
+ summary: [{
2760
+ type: "summary_text",
2761
+ text: ev.text
2762
+ }]
2763
+ });
2764
+ break;
2765
+ }
2766
+ case "tool-start":
2767
+ output.push({
2768
+ type: "function_call",
2769
+ id: ev.id ?? `fc_${ev.index}`,
2770
+ call_id: ev.id ?? `call_${ev.index}`,
2771
+ name: ev.name ?? "",
2772
+ arguments: ""
2773
+ });
2774
+ break;
2775
+ case "tool-args": {
2776
+ const last = output[output.length - 1];
2777
+ if (last?.type === "function_call") last.arguments += ev.delta;
2778
+ break;
2779
+ }
2780
+ case "finish":
2781
+ if (ev.reason === "length") status = "incomplete";
2782
+ else if (ev.reason === "error") status = "failed";
2783
+ break;
2784
+ case "usage": usage = {
2785
+ input: ev.promptTokens,
2786
+ output: ev.completionTokens
2787
+ };
2788
+ }
2789
+ return {
2790
+ id: `resp_${Date.now().toString(36)}`,
2791
+ object: "response",
2792
+ status,
2793
+ model: ctx.model,
2794
+ output,
2795
+ usage: {
2796
+ input_tokens: usage.input,
2797
+ output_tokens: usage.output,
2798
+ total_tokens: usage.input + usage.output
2799
+ }
2800
+ };
2801
+ }
2802
+ };
2803
+ /** Responses 的 input 项 → Chat message。 */
2804
+ function inputItem(item) {
2805
+ const it = obj(item);
2806
+ if (it === void 0) return void 0;
2807
+ const type = str(it.type);
2808
+ if (type === "message" || type === void 0) {
2809
+ const role = str(it.role) ?? "user";
2810
+ const content = it.content;
2811
+ if (typeof content === "string") return {
2812
+ role,
2813
+ content
2814
+ };
2815
+ const text = [];
2816
+ for (const p of arr(content) ?? []) {
2817
+ const part = obj(p);
2818
+ const t = part === void 0 ? void 0 : str(part.text) ?? str(part.output_text);
2819
+ if (t !== void 0) text.push(t);
2820
+ }
2821
+ return {
2822
+ role,
2823
+ content: text.join("\n\n")
2824
+ };
2825
+ }
2826
+ if (type === "function_call") return {
2827
+ role: "assistant",
2828
+ content: "",
2829
+ tool_calls: [{
2830
+ id: str(it.call_id) ?? str(it.id) ?? "",
2831
+ type: "function",
2832
+ function: {
2833
+ name: str(it.name) ?? "",
2834
+ arguments: str(it.arguments) ?? ""
2835
+ }
2836
+ }]
2837
+ };
2838
+ if (type === "function_call_output") return {
2839
+ role: "tool",
2840
+ tool_call_id: str(it.call_id) ?? "",
2841
+ content: str(it.output) ?? ""
2842
+ };
2843
+ }
2844
+ /** Chat 的 content(字符串或 parts)→ 纯文本。 */
2845
+ function contentText$1(content) {
2846
+ if (typeof content === "string") return content;
2847
+ const parts = [];
2848
+ for (const p of arr(content) ?? []) {
2849
+ const t = str(obj(p)?.text);
2850
+ if (t !== void 0) parts.push(t);
2851
+ }
2852
+ return parts.join("\n\n");
2853
+ }
2854
+ /** Chat tools → Responses tools(扁平形态)。 */
2855
+ function convertTools$1(tools) {
2856
+ const out = [];
2857
+ for (const t of arr(tools) ?? []) {
2858
+ const tool = obj(t);
2859
+ const fn = tool === void 0 ? void 0 : obj(tool.function) ?? tool;
2860
+ if (fn === void 0) continue;
2861
+ const name = str(fn.name);
2862
+ if (name === void 0) continue;
2863
+ out.push({
2864
+ type: "function",
2865
+ function: {
2866
+ name,
2867
+ description: str(fn.description) ?? "",
2868
+ parameters: obj(fn.parameters) ?? {
2869
+ type: "object",
2870
+ properties: {}
2871
+ }
2872
+ }
2873
+ });
2874
+ }
2875
+ return out;
2876
+ }
2877
+ /**
2878
+ * Chat tool → Responses tool(`fromChat` 方向)。Responses 的 tools 是**扁平**的
2879
+ * `{ type:'function', name, description, parameters }`,而 Chat 的具名工具多包了
2880
+ * 一层 `function`。不解包的话 `tool.name` 恒为 undefined,工具被静默全部丢掉。
2881
+ */
2882
+ function chatToolToResponses(t) {
2883
+ const tool = obj(t);
2884
+ if (tool === void 0) return void 0;
2885
+ const fn = obj(tool.function) ?? tool;
2886
+ const name = str(fn.name);
2887
+ if (name === void 0) return void 0;
2888
+ return {
2889
+ type: "function",
2890
+ name,
2891
+ description: str(fn.description) ?? "",
2892
+ parameters: obj(fn.parameters) ?? {
2893
+ type: "object",
2894
+ properties: {}
2895
+ }
2896
+ };
2897
+ }
2898
+ /** `response.output_item.added`:函数调用项给出名字。 */
2899
+ function outputItemEvents(payload) {
2900
+ const item = obj(payload.item);
2901
+ if (item === void 0 || item.type !== "function_call") return [];
2902
+ return [{
2903
+ type: "tool-start",
2904
+ index: num(payload.output_index) ?? 0,
2905
+ ...str(item.id) === void 0 ? {} : { id: str(item.id) },
2906
+ name: str(item.name) ?? ""
2907
+ }];
2908
+ }
2909
+ /** `response.completed`:收尾 + 用量都在这帧。 */
2910
+ function completedEvents(payload) {
2911
+ const response = obj(payload.response);
2912
+ const out = [];
2913
+ const usage = obj(response?.usage);
2914
+ if (usage !== void 0) out.push({
2915
+ type: "usage",
2916
+ promptTokens: num(usage.input_tokens) ?? 0,
2917
+ completionTokens: num(usage.output_tokens) ?? 0,
2918
+ cachedTokens: num(obj(usage.input_tokens_details)?.cached_tokens) ?? 0
2919
+ });
2920
+ const status = str(response?.status);
2921
+ if (status === "failed") out.push({
2922
+ type: "finish",
2923
+ reason: "error"
2924
+ });
2925
+ else if (status === "incomplete") out.push({
2926
+ type: "finish",
2927
+ reason: finishOf(str(obj(response?.incomplete_details)?.reason))
2928
+ });
2929
+ else out.push({
2930
+ type: "finish",
2931
+ reason: outputHasToolCall(response?.output) ? "tool-calls" : "stop"
2932
+ });
2933
+ return out;
2934
+ }
2935
+ /** `response.output` 里有没有 `function_call` 项。 */
2936
+ function outputHasToolCall(output) {
2937
+ for (const item of Array.isArray(output) ? output : []) if (obj(item)?.type === "function_call") return true;
2938
+ return false;
2939
+ }
2940
+ /**
2941
+ * 拼 `response.completed` 帧(收尾时由调用方带用量与收尾原因调)。
2942
+ *
2943
+ * `output` 里要写一条 `function_call` 占位项(当 finish 是 tool-calls 时)——
2944
+ * Responses 没有 finish_reason 字段,客户端(以及我们自己的 `completedEvents`)
2945
+ * 只能靠 `output` 里有没有 function_call 判断「这轮是调工具还是说完」。
2946
+ * 少了这条,工具轮在往返后会被认成普通结束。
2947
+ */
2948
+ function completedFrame(model, usage, finish) {
2949
+ const response = {
2950
+ id: `resp_${Date.now().toString(36)}`,
2951
+ object: "response",
2952
+ status: finish === "length" ? "incomplete" : "completed",
2953
+ model,
2954
+ output: finish === "tool-calls" ? [{
2955
+ type: "function_call",
2956
+ id: `fc_${Date.now().toString(36)}`,
2957
+ call_id: `call_${Date.now().toString(36)}`,
2958
+ name: "",
2959
+ arguments: ""
2960
+ }] : []
2961
+ };
2962
+ if (finish === "length") response.incomplete_details = { reason: "max_output_tokens" };
2963
+ if (usage !== void 0) response.usage = {
2964
+ input_tokens: usage.promptTokens,
2965
+ output_tokens: usage.completionTokens,
2966
+ total_tokens: usage.promptTokens + usage.completionTokens,
2967
+ ...usage.cachedTokens !== void 0 && usage.cachedTokens > 0 ? { input_tokens_details: { cached_tokens: usage.cachedTokens } } : {}
2968
+ };
2969
+ return eventFrame("response.completed", JSON.stringify({
2970
+ type: "response.completed",
2971
+ response
2972
+ }));
2973
+ }
2974
+ //#endregion
2975
+ //#region src/protocol/gemini.ts
2976
+ /**
2977
+ * Google Gemini 协议(`generateContent` / `streamGenerateContent`)。
2978
+ *
2979
+ * ════════════════════════════════════════════════════════════════════
2980
+ * 与其他三种的差异
2981
+ * ════════════════════════════════════════════════════════════════════
2982
+ * 1. **模型名在 URL 路径里**:`/v1beta/models/<model>:generateContent`,
2983
+ * 请求体里没有 `model` 字段。
2984
+ * 2. **角色名不同**:`user` / `model`(不是 assistant)。
2985
+ * 3. **内容是 parts**:`{parts:[{text}]}`,不是字符串。
2986
+ * 4. **工具调用是整块**:`functionCall` part 一次给全 `{name,args}`,
2987
+ * 没有增量形态 —— 故 decode 时拆成 tool-start + tool-args 一段。
2988
+ * 5. **鉴权走 query 参数 `?key=`** 或 `x-goog-api-key` 头(不是 Bearer)。
2989
+ * 6. **流式响应是 JSON 数组分片**(`[{...},{...}]`),不是 SSE ——
2990
+ * `streamGenerateContent?alt=sse` 才给 SSE。我们只支持 `alt=sse`,
2991
+ * 非 SSE 形态明确不支持(见文件尾 `geminiStreamSse` 的说明)。
2992
+ *
2993
+ * 丢弃:`safetySettings` / `tools[].googleSearch` / `cachedContent` /
2994
+ * `systemInstruction` 之外的 `generationConfig` 大部分字段(只留
2995
+ * temperature / maxOutputTokens / stopSequences)。
2996
+ */
2997
+ const geminiCodec = {
2998
+ id: "gemini",
2999
+ /** Gemini 请求 → Chat Completions 请求。 */
3000
+ toChat(body, ctx) {
3001
+ const out = {};
3002
+ const messages = geminiMessages(body);
3003
+ const sys = systemText(body.systemInstruction);
3004
+ if (sys !== "") messages.unshift({
3005
+ role: "system",
3006
+ content: sys
3007
+ });
3008
+ out.messages = messages;
3009
+ const gen = obj(body.generationConfig);
3010
+ if (gen !== void 0) {
3011
+ if (num(gen.temperature) !== void 0) out.temperature = gen.temperature;
3012
+ if (num(gen.maxOutputTokens) !== void 0) out.max_tokens = gen.maxOutputTokens;
3013
+ if (arr(gen.stopSequences) !== void 0) out.stop = gen.stopSequences;
3014
+ }
3015
+ const tools = convertTools(body.tools);
3016
+ if (tools.length > 0) out.tools = tools;
3017
+ if (ctx.stream) out.stream = true;
3018
+ out.model = ctx.model;
3019
+ return out;
3020
+ },
3021
+ /** Chat Completions 请求 → Gemini 请求。 */
3022
+ fromChat(body, ctx) {
3023
+ const out = {};
3024
+ const systemParts = [];
3025
+ const contents = [];
3026
+ for (const m of arr(body.messages) ?? []) {
3027
+ const msg = obj(m);
3028
+ if (msg === void 0) continue;
3029
+ const role = str(msg.role);
3030
+ if (role === "system" || role === "developer") {
3031
+ const t = contentText(msg.content);
3032
+ if (t !== "") systemParts.push(t);
3033
+ continue;
3034
+ }
3035
+ if (role === "assistant") {
3036
+ const parts = [];
3037
+ const text = contentText(msg.content);
3038
+ if (text !== "") parts.push({ text });
3039
+ for (const tc of arr(msg.tool_calls) ?? []) {
3040
+ const fn = obj(obj(tc)?.function);
3041
+ parts.push({ functionCall: {
3042
+ name: str(fn?.name) ?? "",
3043
+ args: parseArgs(str(fn?.arguments))
3044
+ } });
3045
+ }
3046
+ contents.push({
3047
+ role: "model",
3048
+ parts
3049
+ });
3050
+ continue;
3051
+ }
3052
+ if (role === "tool") {
3053
+ contents.push({
3054
+ role: "user",
3055
+ parts: [{ functionResponse: {
3056
+ name: str(msg.name) ?? "",
3057
+ response: { result: contentText(msg.content) }
3058
+ } }]
3059
+ });
3060
+ continue;
3061
+ }
3062
+ contents.push({
3063
+ role: "user",
3064
+ parts: [{ text: contentText(msg.content) }]
3065
+ });
3066
+ }
3067
+ out.contents = contents;
3068
+ if (systemParts.length > 0) out.systemInstruction = { parts: [{ text: systemParts.join("\n\n") }] };
3069
+ if (num(body.temperature) !== void 0 || num(body.max_tokens) !== void 0) out.generationConfig = {
3070
+ ...num(body.temperature) === void 0 ? {} : { temperature: body.temperature },
3071
+ ...num(body.max_tokens) === void 0 ? {} : { maxOutputTokens: body.max_tokens }
3072
+ };
3073
+ if (arr(body.stop) !== void 0) out.generationConfig.stopSequences = body.stop;
3074
+ const tools = body.tools?.map(chatToolToGemini).filter((t) => t !== void 0);
3075
+ if (tools !== void 0 && tools.length > 0) out.tools = [{ functionDeclarations: tools }];
3076
+ return out;
3077
+ },
3078
+ createSession() {
3079
+ return { state: newGeminiToolMap() };
3080
+ },
3081
+ decodeFrame(frame, _event, session) {
3082
+ let payload;
3083
+ try {
3084
+ payload = JSON.parse(frame);
3085
+ } catch {
3086
+ return [];
3087
+ }
3088
+ return geminiBodyEvents(payload, mapOf(session));
3089
+ },
3090
+ encodeFrame(event, session) {
3091
+ const toolMap = mapOf(session);
3092
+ const chunk = (parts, finish) => [dataFrame(JSON.stringify({ candidates: [{
3093
+ content: {
3094
+ role: "model",
3095
+ parts
3096
+ },
3097
+ ...finish === void 0 ? {} : { finishReason: finish }
3098
+ }] }))];
3099
+ switch (event.type) {
3100
+ case "text": return chunk([{ text: event.text }]);
3101
+ case "reasoning": return chunk([{
3102
+ text: event.text,
3103
+ thought: true
3104
+ }]);
3105
+ case "tool-start": {
3106
+ const name = event.name ?? "";
3107
+ if (name !== "") toolMap.remember(event.index, name);
3108
+ return [];
3109
+ }
3110
+ case "tool-args":
3111
+ toolMap.appendArgs(event.index, event.delta);
3112
+ return [];
3113
+ case "usage": return [dataFrame(JSON.stringify({ usageMetadata: {
3114
+ promptTokenCount: event.promptTokens,
3115
+ candidatesTokenCount: event.completionTokens,
3116
+ totalTokenCount: event.promptTokens + event.completionTokens,
3117
+ ...event.cachedTokens > 0 ? { cachedContentTokenCount: event.cachedTokens } : {}
3118
+ } }))];
3119
+ case "error": return [dataFrame(JSON.stringify({ error: {
3120
+ code: 500,
3121
+ message: event.message,
3122
+ status: "INTERNAL"
3123
+ } }))];
3124
+ case "finish": return [];
3125
+ }
3126
+ },
3127
+ startFrames: () => [],
3128
+ /**
3129
+ * 流末一次性发出**完整的** functionCall。
3130
+ *
3131
+ * 为什么集中在这里:Gemini 没有参数增量形态,一个调用必须「名字+完整参数」
3132
+ * 用一个 part 给出。上游(OpenAI 系)逐帧发参数增量,`tool-args` 只能攒;
3133
+ * 攒到流末参数才完整,此时才发得出合法的 `functionCall`。
3134
+ *
3135
+ * 半截 JSON(上游断流)**不发**:发出去客户端会拿坏参数执行工具,
3136
+ * 比少一次调用危险得多。
3137
+ */
3138
+ endFrames(ctx, session) {
3139
+ const toolMap = mapOf(session);
3140
+ const out = [];
3141
+ for (const index of toolMap.pendingIndexes()) {
3142
+ const name = toolMap.nameOf(index);
3143
+ if (name === "") continue;
3144
+ const args = toolMap.completeArgs(index);
3145
+ if (args === void 0) continue;
3146
+ out.push(dataFrame(JSON.stringify({ candidates: [{ content: {
3147
+ role: "model",
3148
+ parts: [{ functionCall: {
3149
+ name,
3150
+ args
3151
+ } }]
3152
+ } }] })));
3153
+ }
3154
+ out.push(...chunkOf([], finishToGemini(ctx.finish ?? "stop")));
3155
+ return out;
3156
+ },
3157
+ /**
3158
+ * 非流式响应体 → 规范事件。
3159
+ *
3160
+ * Gemini 非流式 body 与流式一帧同形(`{candidates:[{content:{parts},finishReason}],
3161
+ * usageMetadata}`),所以直接复用 `decodeFrame` —— 这是四种协议里唯一
3162
+ * 「流与非流形状一致」的,没有第二份映射可写。
3163
+ */
3164
+ decodeJson(body) {
3165
+ return geminiBodyEvents(body, newGeminiToolMap());
3166
+ },
3167
+ /** 规范事件 → 非流式 body。 */
3168
+ encodeJson(events, ctx) {
3169
+ const toolMap = newGeminiToolMap();
3170
+ const parts = [];
3171
+ let finish = "STOP";
3172
+ let usage = {
3173
+ prompt: 0,
3174
+ output: 0,
3175
+ cached: 0
3176
+ };
3177
+ for (const ev of events) switch (ev.type) {
3178
+ case "text":
3179
+ parts.push({ text: ev.text });
3180
+ break;
3181
+ case "reasoning":
3182
+ parts.push({
3183
+ text: ev.text,
3184
+ thought: true
3185
+ });
3186
+ break;
3187
+ case "tool-start": {
3188
+ const name = ev.name ?? "";
3189
+ if (name !== "") toolMap.remember(ev.index, name);
3190
+ break;
3191
+ }
3192
+ case "tool-args": {
3193
+ const name = toolMap.nameOf(ev.index);
3194
+ if (name !== "") parts.push({ functionCall: {
3195
+ name,
3196
+ args: parseArgs(ev.delta)
3197
+ } });
3198
+ break;
3199
+ }
3200
+ case "finish":
3201
+ finish = finishToGemini(ev.reason);
3202
+ break;
3203
+ case "usage": usage = {
3204
+ prompt: ev.promptTokens,
3205
+ output: ev.completionTokens,
3206
+ cached: ev.cachedTokens
3207
+ };
3208
+ }
3209
+ return {
3210
+ candidates: [{
3211
+ content: {
3212
+ role: "model",
3213
+ parts
3214
+ },
3215
+ finishReason: finish,
3216
+ index: 0
3217
+ }],
3218
+ usageMetadata: {
3219
+ promptTokenCount: usage.prompt,
3220
+ candidatesTokenCount: usage.output,
3221
+ totalTokenCount: usage.prompt + usage.output,
3222
+ ...usage.cached > 0 ? { cachedContentTokenCount: usage.cached } : {}
3223
+ }
3224
+ };
3225
+ }
3226
+ };
3227
+ /** 一帧 `candidates`。与 `encodeFrame` 里的闭包同形,供 `endFrames` 用。 */
3228
+ function chunkOf(parts, finish) {
3229
+ return [dataFrame(JSON.stringify({ candidates: [{
3230
+ content: {
3231
+ role: "model",
3232
+ parts
3233
+ },
3234
+ ...finish === void 0 ? {} : { finishReason: finish }
3235
+ }] }))];
3236
+ }
3237
+ /** 新建一份空的工具名映射。 */
3238
+ function newGeminiToolMap() {
3239
+ const byName = /* @__PURE__ */ new Map();
3240
+ const byIndex = /* @__PURE__ */ new Map();
3241
+ /** index → 已累积的参数片段(跨帧拼接后才是完整 JSON)。 */
3242
+ const argsByIndex = /* @__PURE__ */ new Map();
3243
+ let seq = 0;
3244
+ return {
3245
+ indexOf(name) {
3246
+ const existing = byName.get(name);
3247
+ if (existing !== void 0) return existing;
3248
+ const index = seq++;
3249
+ byName.set(name, index);
3250
+ byIndex.set(index, name);
3251
+ return index;
3252
+ },
3253
+ nameOf(index) {
3254
+ return byIndex.get(index) ?? "";
3255
+ },
3256
+ remember(index, name) {
3257
+ byIndex.set(index, name);
3258
+ if (!byName.has(name)) byName.set(name, index);
3259
+ },
3260
+ appendArgs(index, delta) {
3261
+ const next = (argsByIndex.get(index) ?? "") + delta;
3262
+ argsByIndex.set(index, next);
3263
+ return next;
3264
+ },
3265
+ completeArgs(index) {
3266
+ const raw = argsByIndex.get(index);
3267
+ if (raw === void 0 || raw === "") return {};
3268
+ try {
3269
+ return obj(JSON.parse(raw)) ?? void 0;
3270
+ } catch {
3271
+ return;
3272
+ }
3273
+ },
3274
+ pendingIndexes() {
3275
+ return [...argsByIndex.keys()].sort((a, b) => a - b);
3276
+ }
3277
+ };
3278
+ }
3279
+ /**
3280
+ * 取本次调用的工具名映射:有会话用会话的,没有就临时建一份。
3281
+ *
3282
+ * 「没有会话」= 单帧独立转换(测试、非流式一次性)。结果与无状态等价 ——
3283
+ * 名字在自己的调用里总能拿到,只是不跨帧复用。
3284
+ */
3285
+ function mapOf(session) {
3286
+ const st = session?.state;
3287
+ return st instanceof Object && "indexOf" in st ? st : newGeminiToolMap();
3288
+ }
3289
+ /**
3290
+ * 一个 Gemini 响应体(流式一帧或非流式 body,两者同形)→ 规范事件。
3291
+ *
3292
+ * Gemini 是四种协议里唯一「流与非流形状一致」的,所以只有这一份映射。
3293
+ */
3294
+ function geminiBodyEvents(payload, tools) {
3295
+ const out = [];
3296
+ const candidate = obj(arr(payload.candidates)?.[0]);
3297
+ if (candidate !== void 0) {
3298
+ const finish = candidate.finishReason;
3299
+ if (finish !== void 0 && finish !== null) out.push({
3300
+ type: "finish",
3301
+ reason: finishOf(finish)
3302
+ });
3303
+ for (const p of arr(obj(candidate.content)?.parts) ?? []) {
3304
+ const part = obj(p);
3305
+ if (part === void 0) continue;
3306
+ const text = str(part.text);
3307
+ if (text !== void 0) {
3308
+ if (part.thought === true) out.push({
3309
+ type: "reasoning",
3310
+ text
3311
+ });
3312
+ else out.push({
3313
+ type: "text",
3314
+ text
3315
+ });
3316
+ continue;
3317
+ }
3318
+ const fc = obj(part.functionCall);
3319
+ if (fc !== void 0) {
3320
+ const name = str(fc.name) ?? "";
3321
+ if (name === "") continue;
3322
+ const index = tools.indexOf(name);
3323
+ out.push({
3324
+ type: "tool-start",
3325
+ index,
3326
+ name
3327
+ });
3328
+ out.push({
3329
+ type: "tool-args",
3330
+ index,
3331
+ delta: JSON.stringify(fc.args ?? {})
3332
+ });
3333
+ }
3334
+ }
3335
+ }
3336
+ const usage = obj(payload.usageMetadata);
3337
+ if (usage !== void 0) out.push({
3338
+ type: "usage",
3339
+ promptTokens: num(usage.promptTokenCount) ?? 0,
3340
+ completionTokens: num(usage.candidatesTokenCount) ?? 0,
3341
+ cachedTokens: num(usage.cachedContentTokenCount) ?? 0
3342
+ });
3343
+ return out;
3344
+ }
3345
+ /** Gemini 的 systemInstruction → 纯文本。 */
3346
+ function systemText(sys) {
3347
+ const parts = [];
3348
+ for (const p of arr(obj(sys)?.parts) ?? []) {
3349
+ const t = str(obj(p)?.text);
3350
+ if (t !== void 0) parts.push(t);
3351
+ }
3352
+ return parts.join("\n\n");
3353
+ }
3354
+ /** Gemini contents → Chat messages。 */
3355
+ function geminiMessages(body) {
3356
+ const out = [];
3357
+ for (const c of arr(body.contents) ?? []) {
3358
+ const item = obj(c);
3359
+ if (item === void 0) continue;
3360
+ const role = str(item.role) === "model" ? "assistant" : "user";
3361
+ const text = [];
3362
+ const toolCalls = [];
3363
+ for (const p of arr(item.parts) ?? []) {
3364
+ const part = obj(p);
3365
+ if (part === void 0) continue;
3366
+ const t = str(part.text);
3367
+ if (t !== void 0) {
3368
+ text.push(t);
3369
+ continue;
3370
+ }
3371
+ const fc = obj(part.functionCall);
3372
+ if (fc !== void 0) {
3373
+ toolCalls.push({
3374
+ id: `call_${str(fc.name) ?? ""}`,
3375
+ type: "function",
3376
+ function: {
3377
+ name: str(fc.name) ?? "",
3378
+ arguments: JSON.stringify(fc.args ?? {})
3379
+ }
3380
+ });
3381
+ continue;
3382
+ }
3383
+ const fr = obj(part.functionResponse);
3384
+ if (fr !== void 0) out.push({
3385
+ role: "tool",
3386
+ tool_call_id: `call_${str(fr.name) ?? ""}`,
3387
+ content: JSON.stringify(obj(fr.response)?.result ?? {})
3388
+ });
3389
+ }
3390
+ if (role === "assistant") {
3391
+ const msg = {
3392
+ role: "assistant",
3393
+ content: text.join("\n\n")
3394
+ };
3395
+ if (toolCalls.length > 0) msg.tool_calls = toolCalls;
3396
+ out.push(msg);
3397
+ } else if (text.length > 0) out.push({
3398
+ role: "user",
3399
+ content: text.join("\n\n")
3400
+ });
3401
+ }
3402
+ return out;
3403
+ }
3404
+ /** Chat 的 content(字符串或 parts)→ 纯文本。 */
3405
+ function contentText(content) {
3406
+ if (typeof content === "string") return content;
3407
+ const parts = [];
3408
+ for (const p of arr(content) ?? []) {
3409
+ const t = str(obj(p)?.text);
3410
+ if (t !== void 0) parts.push(t);
3411
+ }
3412
+ return parts.join("\n\n");
3413
+ }
3414
+ function parseArgs(s) {
3415
+ if (s === void 0 || s === "") return {};
3416
+ try {
3417
+ return obj(JSON.parse(s)) ?? {};
3418
+ } catch {
3419
+ return {};
3420
+ }
3421
+ }
3422
+ /** Chat tools → Gemini functionDeclarations。 */
3423
+ function chatToolToGemini(t) {
3424
+ const fn = obj(obj(t)?.function);
3425
+ const name = fn === void 0 ? void 0 : str(fn.name);
3426
+ if (fn === void 0 || name === void 0) return void 0;
3427
+ return {
3428
+ name,
3429
+ description: str(fn.description) ?? "",
3430
+ parameters: obj(fn.parameters) ?? {
3431
+ type: "object",
3432
+ properties: {}
3433
+ }
3434
+ };
3435
+ }
3436
+ /** Gemini tools → Chat tools。 */
3437
+ function convertTools(tools) {
3438
+ const out = [];
3439
+ for (const t of arr(tools) ?? []) for (const d of arr(obj(t)?.functionDeclarations) ?? []) {
3440
+ const decl = obj(d);
3441
+ if (decl === void 0) continue;
3442
+ const name = str(decl.name);
3443
+ if (name === void 0) continue;
3444
+ out.push({
3445
+ type: "function",
3446
+ function: {
3447
+ name,
3448
+ description: str(decl.description) ?? "",
3449
+ parameters: obj(decl.parameters) ?? {
3450
+ type: "object",
3451
+ properties: {}
3452
+ }
3453
+ }
3454
+ });
3455
+ }
3456
+ return out;
3457
+ }
3458
+ function finishToGemini(reason) {
3459
+ switch (reason) {
3460
+ case "length": return "MAX_TOKENS";
3461
+ case "tool-calls": return "STOP";
3462
+ case "content-filter": return "SAFETY";
3463
+ case "error": return "OTHER";
3464
+ default: return "STOP";
3465
+ }
3466
+ }
3467
+ //#endregion
3468
+ //#region src/protocol/index.ts
3469
+ const CODECS = {
3470
+ chat: chatCodec,
3471
+ anthropic: anthropicCodec,
3472
+ responses: responsesCodec,
3473
+ gemini: geminiCodec
3474
+ };
3475
+ /** 取编解码器。id 已由 `isProtocolId` 校验过,这里不再兜底。 */
3476
+ function codecOf(id) {
3477
+ return CODECS[id];
3478
+ }
3479
+ //#endregion
3480
+ //#region src/protocol/stream.ts
3481
+ /** 一次流式转码的有状态累加。 */
3482
+ var StreamState = class {
3483
+ /** 上游协议(解码用)。 */
3484
+ from;
3485
+ /** 客户端协议(编码用)。 */
3486
+ to;
3487
+ /** 上游 usage(跨帧合并,字段级 max —— 见 mergeUsage 的注释)。 */
3488
+ usage = {
3489
+ promptTokens: 0,
3490
+ completionTokens: 0,
3491
+ cachedTokens: 0
3492
+ };
3493
+ /** 已经吐过起始帧没有(起始帧只能在流最前发一次)。 */
3494
+ started = false;
3495
+ /** 收尾原因(用于补 endFrames;缺省 stop)。 */
3496
+ finish;
3497
+ /** 模型名(回显进 startFrames/endFrames,如 Anthropic 的 message_start)。 */
3498
+ model = "";
3499
+ /**
3500
+ * 解码侧会话与编码侧会话,各一份。
3501
+ *
3502
+ * **必须每帧复用同一个**:Gemini 的工具名↔index 映射就存在这里;每帧新建
3503
+ * 会让 `tool-args` 找不到 `tool-start` 给的名字(参数被丢),而放模块级会
3504
+ * 跨请求串名。会话在 `StreamState` 构造时建、随流结束丢弃 —— 生命周期与
3505
+ * 「一条流」严格对齐。
3506
+ */
3507
+ decodeSession;
3508
+ encodeSession;
3509
+ constructor(from, to, model = "") {
3510
+ this.from = from;
3511
+ this.to = to;
3512
+ this.model = model;
3513
+ this.decodeSession = codecOf(from).createSession?.();
3514
+ this.encodeSession = codecOf(to).createSession?.();
3515
+ }
3516
+ /** 当前累计的用量。 */
3517
+ currentUsage() {
3518
+ return { ...this.usage };
3519
+ }
3520
+ /** 把上游一帧原文(`data:` 载荷)翻成出站帧原文列表。 */
3521
+ translateFrame(frame, event) {
3522
+ const events = codecOf(this.from).decodeFrame(frame, event, this.decodeSession);
3523
+ const out = [];
3524
+ if (!this.started) {
3525
+ this.started = true;
3526
+ out.push(...codecOf(this.to).startFrames({
3527
+ model: this.model,
3528
+ stream: true
3529
+ }));
3530
+ }
3531
+ for (const ev of events) {
3532
+ if (ev.type === "usage") this.mergeUsage(ev);
3533
+ if (ev.type === "finish") this.finish = ev;
3534
+ if (ev.type === "finish" || ev.type === "usage") continue;
3535
+ out.push(...codecOf(this.to).encodeFrame(ev, this.encodeSession));
3536
+ }
3537
+ return out;
3538
+ }
3539
+ /**
3540
+ * 上游流结束:补发收尾帧。
3541
+ *
3542
+ * **顺序固定**:finish → usage → endFrames。各协议的收尾帧都要求这个次序
3543
+ * (Anthropic 的 message_delta 必须在 message_stop 前,Responses 的
3544
+ * `response.completed` 是最后一件且自带 usage)。
3545
+ *
3546
+ * `endFrames` 拿得到 finish 与 usage —— 有的协议(Responses)只有收尾帧
3547
+ * 里才有地方放 usage 与「是否被截断」。
3548
+ */
3549
+ finishStream() {
3550
+ const enc = codecOf(this.to);
3551
+ const ctx = {
3552
+ model: this.model,
3553
+ stream: true,
3554
+ finish: this.finish?.reason ?? "stop",
3555
+ usage: { ...this.usage }
3556
+ };
3557
+ if (!this.started) {
3558
+ this.started = true;
3559
+ return [...enc.startFrames(ctx), ...enc.endFrames(ctx, this.encodeSession)];
3560
+ }
3561
+ const finishSession = this.finishSessionWithUsage();
3562
+ const out = [];
3563
+ out.push(...enc.encodeFrame(this.finish ?? {
3564
+ type: "finish",
3565
+ reason: "stop"
3566
+ }, finishSession));
3567
+ out.push(...enc.encodeFrame({
3568
+ type: "usage",
3569
+ ...this.usage
3570
+ }, finishSession));
3571
+ out.push(...enc.endFrames(ctx, finishSession));
3572
+ return out;
3573
+ }
3574
+ /**
3575
+ * 带当前用量派生的编码会话。
3576
+ *
3577
+ * 不能只在 `encodeSession` 存在时才派生:`createSession` 是**可选**的
3578
+ * (只有 Gemini 这类有跨帧映射的协议实现它),Anthropic 没有 —— 按
3579
+ * `encodeSession === undefined` 就返回 undefined 会让 usage 永远送不进去
3580
+ * (2026-10-21 实测:output_tokens 恒为 0)。所以无论如何都造一个,
3581
+ * 有 state 就带上。
3582
+ */
3583
+ finishSessionWithUsage() {
3584
+ return {
3585
+ ...this.encodeSession,
3586
+ usage: { ...this.usage }
3587
+ };
3588
+ }
3589
+ /**
3590
+ * usage 合并:字段级 max。
3591
+ *
3592
+ * 与 `router/usage-tokens.ts` 的 `mergeUsage` 同规则(那里照 9router):
3593
+ * Claude 把输入用量放 `message_start`、输出放 `message_delta`,两帧都给
3594
+ * `usage` 字段,**覆盖**会让后一帧把前面的输入清零。
3595
+ */
3596
+ mergeUsage(ev) {
3597
+ this.usage.promptTokens = Math.max(this.usage.promptTokens, ev.promptTokens);
3598
+ this.usage.completionTokens = Math.max(this.usage.completionTokens, ev.completionTokens);
3599
+ this.usage.cachedTokens = Math.max(this.usage.cachedTokens, ev.cachedTokens);
3600
+ }
3601
+ };
3602
+ /**
3603
+ * 把上游 SSE 字节流转成客户端协议的 SSE 字节流。
1449
3604
  *
1450
- * `requestChars` = 请求体字符数(估输入),`outputChars` = 输出字符数(估输出)。
1451
- * 上游给了的字段**不覆盖**——估算只填空,不抢真数据。
3605
+ * 生命周期:上游正常读完 / 上游 error / 消费方取消,三条路径都要**释放上游
3606
+ * reader** 并把取消传下去 —— 否则上游 fetch body 永不 cancel,长跑会漏连接
3607
+ * (`router/index.ts` 的 `normalizeSSEStream` 里同一个坑,判据在
3608
+ * `router/disconnect-leak.test.ts`)。
1452
3609
  */
1453
- function withEstimates(usage, requestChars, outputChars) {
1454
- const prompt = usage?.promptTokens ?? 0;
1455
- const completion = usage?.completionTokens ?? 0;
1456
- const inputEstimated = prompt === 0;
1457
- const outputEstimated = completion === 0;
1458
- return {
1459
- promptTokens: inputEstimated ? estimateTokens(requestChars) : prompt,
1460
- completionTokens: outputEstimated ? estimateTokens(outputChars) : completion,
1461
- cachedTokens: usage?.cachedTokens ?? 0,
1462
- inputEstimated,
1463
- outputEstimated
3610
+ function translateStream(upstream, state) {
3611
+ const splitter = new SseSplitter();
3612
+ let reader;
3613
+ let released = false;
3614
+ const release = () => {
3615
+ if (released) return;
3616
+ released = true;
3617
+ try {
3618
+ reader?.releaseLock();
3619
+ } catch {}
1464
3620
  };
3621
+ return new ReadableStream({
3622
+ async start(ctrl) {
3623
+ reader = upstream.getReader();
3624
+ try {
3625
+ for (;;) {
3626
+ const { done, value } = await reader.read();
3627
+ if (done) break;
3628
+ const frames = splitter.push(value);
3629
+ for (const f of frames) {
3630
+ if (f.data === "[DONE]" || f.data === "") continue;
3631
+ const out = state.translateFrame(f.data, f.event);
3632
+ if (out.length > 0) ctrl.enqueue(framesToBytes(out));
3633
+ }
3634
+ }
3635
+ for (const f of splitter.end()) {
3636
+ if (f.data === "[DONE]" || f.data === "") continue;
3637
+ const out = state.translateFrame(f.data, f.event);
3638
+ if (out.length > 0) ctrl.enqueue(framesToBytes(out));
3639
+ }
3640
+ const tail = state.finishStream();
3641
+ if (tail.length > 0) ctrl.enqueue(framesToBytes(tail));
3642
+ } catch {} finally {
3643
+ release();
3644
+ }
3645
+ try {
3646
+ ctrl.close();
3647
+ } catch {}
3648
+ },
3649
+ cancel: (reason) => reader?.cancel(reason)
3650
+ });
1465
3651
  }
1466
3652
  //#endregion
1467
3653
  //#region src/router/index.ts
@@ -1625,28 +3811,35 @@ function reqMessages(rawBody) {
1625
3811
  return;
1626
3812
  }
1627
3813
  }
1628
- /** 丢弃响应的假 ServerResponse(测试模型用:记录状态+内容,用于判定成败)。 */
1629
- function sinkRes() {
1630
- let status = 200;
1631
- let text = "";
3814
+ /** 把 ResponseSink 包成一个 Router 能写的 ServerResponse。 */
3815
+ function sinkRes(sink) {
1632
3816
  const self = {
1633
3817
  headersSent: false,
1634
3818
  writableEnded: false,
1635
- writeHead: (code) => {
1636
- status = code;
3819
+ destroyed: false,
3820
+ writeHead: (code, headers) => {
3821
+ sink.head?.(code, headers ?? {});
1637
3822
  return self;
1638
3823
  },
1639
3824
  write: (chunk) => {
1640
- if (chunk !== void 0) text += String(chunk);
3825
+ if (chunk !== void 0) sink.write(typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk);
1641
3826
  return true;
1642
3827
  },
1643
3828
  end: (chunk) => {
1644
- if (chunk !== void 0) text += String(chunk);
3829
+ if (chunk !== void 0) self.write(chunk);
3830
+ sink.end();
1645
3831
  return self;
1646
3832
  },
1647
3833
  flushHeaders: () => {},
1648
- status: () => status,
1649
- body: () => text
3834
+ once: () => self,
3835
+ on: () => self,
3836
+ removeListener: () => self,
3837
+ destroy: () => {
3838
+ self.destroyed = true;
3839
+ },
3840
+ setHeader: () => self,
3841
+ removeHeader: () => self,
3842
+ writeableEnded: false
1650
3843
  };
1651
3844
  return self;
1652
3845
  }
@@ -2000,6 +4193,51 @@ async function writeChatResult(res, r, wantsStream, probe, startedAt = 0, firstB
2000
4193
  }
2001
4194
  /** SSE 终止帧(断流兜底补发用)。 */
2002
4195
  const DONE_FRAME = new TextEncoder().encode("data: [DONE]\n\n");
4196
+ /**
4197
+ * 取该供应商的 `chatOnce`,按它声明的 wire 包一层协议翻译。
4198
+ *
4199
+ * ════════════════════════════════════════════════════════════════════
4200
+ * 为什么翻译放在核心、而不是让插件自己做
4201
+ * ════════════════════════════════════════════════════════════════════
4202
+ * 插件自己翻译意味着「每个只会说 Anthropic 的供应商各写一份 Anthropic→OpenAI
4203
+ * 转换」,与账号池/冷却表/积分落盘是同一个错误:N 份互相不一致的实现,核心
4204
+ * 也就无从判断该不该换号。放在核心,插件契约不变(仍只吐 OpenAI SSE),
4205
+ * 换号/冷却/用量全部照旧生效。
4206
+ *
4207
+ * 翻译**只碰请求体与响应流**,不碰 `chatOnce` 的返回值语义:失败仍然返回
4208
+ * `{ok:false, state, message}`,核心的冷却/回退逻辑一行不改。
4209
+ *
4210
+ * 非流式同理:上游给 JSON 时,先把它翻成规范 JSON 再交给核心(`writeChatResult`
4211
+ * 的 `!('stream' in r)` 分支),客户端要流式而上游只给 JSON 时由现有的
4212
+ * `aggregateSSE` 兜底(那条路径本来就要把 JSON 折成流)。
4213
+ */
4214
+ function wireChatOnce(s) {
4215
+ const wire = s.wire ?? "chat";
4216
+ if (wire === "chat") return (uid, lv, req) => s.chatOnce(uid, lv, req);
4217
+ const codec = codecOf(wire);
4218
+ return async (uid, lv, req) => {
4219
+ let outReq = req;
4220
+ let body;
4221
+ try {
4222
+ body = JSON.parse(req.rawBody);
4223
+ } catch {
4224
+ body = void 0;
4225
+ }
4226
+ if (body !== void 0) outReq = {
4227
+ ...req,
4228
+ rawBody: JSON.stringify(codec.fromChat(body, {
4229
+ model: req.model,
4230
+ stream: req.stream
4231
+ }))
4232
+ };
4233
+ const r = await s.chatOnce(uid, lv, outReq);
4234
+ if (!r.ok || !("stream" in r)) return r;
4235
+ return {
4236
+ ok: true,
4237
+ stream: translateStream(r.stream, new StreamState(wire, "chat"))
4238
+ };
4239
+ };
4240
+ }
2003
4241
  /** 从上游响应体提取错误信息(OpenAI {error:{message}} 或 {code,msg}/{message})。 */
2004
4242
  function extractUpstreamError(body) {
2005
4243
  try {
@@ -2186,6 +4424,15 @@ var Router = class {
2186
4424
  } catch {}
2187
4425
  return out;
2188
4426
  }
4427
+ /**
4428
+ * 该供应商的 `chatOnce`,已按它声明的 wire 包好协议翻译。
4429
+ *
4430
+ * 每次调用都现取(而不是构造时缓存):供应商会被开关/热重载替换,
4431
+ * 缓存会把旧实例的翻译绑在新实例上。
4432
+ */
4433
+ wireOf(s) {
4434
+ return wireChatOnce(s);
4435
+ }
2189
4436
  /** 组合列表(面板用):用户自定义组合。 */
2190
4437
  async combos() {
2191
4438
  return [...this.customCombos];
@@ -2424,7 +4671,17 @@ var Router = class {
2424
4671
  * - model 命中组合名 → 在组合模型里按策略选一个,交给供应商;失败按回退顺序尝试剩余模型。
2425
4672
  * - 否则 → 依次尝试供应商,直到某个返回 true(已写响应)或全部返回 false。
2426
4673
  */
2427
- async chatCompletions(req, res) {
4674
+ /**
4675
+ * 规范请求入口。
4676
+ *
4677
+ * @param res 真实的客户端响应。协议非 chat 时**不直接用它**,而是经
4678
+ * `sink` 先收集再翻译(见 `protocol/entry.ts`)—— 因为 Router 只知道
4679
+ * 怎么写 OpenAI 形态,而客户端说的可能是 Anthropic/Gemini。
4680
+ * @param sink 可选的响应收集器:给了就把「写给 res 的东西」改成写给 sink,
4681
+ * 由调用方翻译后自己写真正的 res。只给非 chat 协议用。
4682
+ */
4683
+ async chatCompletions(req, res, sink) {
4684
+ const out = sink === void 0 ? res : sinkRes(sink);
2428
4685
  const probe = {
2429
4686
  requested: req.model,
2430
4687
  supplier: "",
@@ -2461,34 +4718,34 @@ var Router = class {
2461
4718
  const model = combo.models[i];
2462
4719
  if (model === void 0) continue;
2463
4720
  const trace = { attempts: 0 };
2464
- if (await this.chatWithModel(req, res, model, probe, trace, Date.now() + this.firstByteBudgetMs)) {
4721
+ if (await this.chatWithModel(req, out, model, probe, trace, Date.now() + this.firstByteBudgetMs)) {
2465
4722
  settle(true);
2466
4723
  return;
2467
4724
  }
2468
4725
  if (trace.lastState !== void 0 && isTransient(trace.lastState) && i < combo.models.length - 1) await new Promise((r) => setTimeout(r, TRANSIENT_SETTLE_MS));
2469
4726
  }
2470
4727
  settle(false, "combo: all models unavailable");
2471
- writeJson$2(res, 503, openAIError("no_healthy_supplier", `combo ${JSON.stringify(req.model)}: all models unavailable`));
4728
+ writeJson$2(out, 503, openAIError("no_healthy_supplier", `combo ${JSON.stringify(req.model)}: all models unavailable`));
2472
4729
  return;
2473
4730
  }
2474
4731
  const slash = req.model.indexOf("/");
2475
4732
  const alias = slash > 0 ? req.model.slice(0, slash) : "";
2476
4733
  const target = alias === "" ? void 0 : this.supplierByAlias(alias);
2477
4734
  if (target !== void 0) {
2478
- if (await this.chatWithTarget(target, req, res, probe)) {
4735
+ if (await this.chatWithTarget(target, req, out, probe)) {
2479
4736
  settle(true);
2480
4737
  return;
2481
4738
  }
2482
4739
  settle(false, `supplier ${target.id}: all accounts unavailable`);
2483
- writeJson$2(res, 503, openAIError("no_healthy_supplier", `supplier ${JSON.stringify(target.id)}: all accounts unavailable`));
4740
+ writeJson$2(out, 503, openAIError("no_healthy_supplier", `supplier ${JSON.stringify(target.id)}: all accounts unavailable`));
2484
4741
  return;
2485
4742
  }
2486
- if (await this.chatWithModel(req, res, req.model, probe)) {
4743
+ if (await this.chatWithModel(req, out, req.model, probe)) {
2487
4744
  settle(true);
2488
4745
  return;
2489
4746
  }
2490
4747
  settle(false, "all suppliers unavailable");
2491
- writeJson$2(res, 503, openAIError("no_healthy_supplier", "all suppliers unavailable"));
4748
+ writeJson$2(out, 503, openAIError("no_healthy_supplier", "all suppliers unavailable"));
2492
4749
  }
2493
4750
  /** 调指定供应商。
2494
4751
  * 模型名**原样**传给插件(不在这剥 alias/)——插件自己认得自己的别名
@@ -2572,8 +4829,9 @@ var Router = class {
2572
4829
  async chatWithSupplier(s, req, res, trace, probe, firstByteDeadlineAt = Date.now() + this.firstByteBudgetMs, onlyUid) {
2573
4830
  const pool = s.pool;
2574
4831
  const cfg = this.store.get(s.id);
4832
+ const chatOnce = this.wireOf(s);
2575
4833
  if (s.accounts !== void 0 && s.accounts().length === 0) {
2576
- const r = await s.chatOnce("", req.lv ?? "auto", req);
4834
+ const r = await chatOnce("", req.lv ?? "auto", req);
2577
4835
  if (!r.ok) {
2578
4836
  if (r.state === "no_such_model" || r.state === "bad_request") return false;
2579
4837
  if (trace !== void 0) trace.lastState = r.state;
@@ -2609,7 +4867,7 @@ var Router = class {
2609
4867
  }
2610
4868
  pickWhy = pool.lastWhy;
2611
4869
  tried.add(uid);
2612
- const r = await s.chatOnce(uid, req.lv ?? "auto", req);
4870
+ const r = await chatOnce(uid, req.lv ?? "auto", req);
2613
4871
  if (!r.ok) {
2614
4872
  if (r.state === "no_such_model" || r.state === "bad_request") {
2615
4873
  if (trace !== void 0) {
@@ -2664,7 +4922,19 @@ var Router = class {
2664
4922
  ok: false,
2665
4923
  error: `unknown supplier ${JSON.stringify(supplierId)}`
2666
4924
  };
2667
- const sink = sinkRes();
4925
+ let sinkStatus = 200;
4926
+ let sinkBody = "";
4927
+ const sink = sinkRes({
4928
+ head: (code) => {
4929
+ sinkStatus = code;
4930
+ },
4931
+ write: (bytes) => {
4932
+ sinkBody += new TextDecoder().decode(bytes);
4933
+ },
4934
+ end: () => {}
4935
+ });
4936
+ const statusOf = () => sinkStatus;
4937
+ const bodyOf = () => sinkBody;
2668
4938
  const req = {
2669
4939
  model,
2670
4940
  stream: false,
@@ -2688,8 +4958,8 @@ var Router = class {
2688
4958
  error: err.message
2689
4959
  };
2690
4960
  }
2691
- if (served && sink.status() < 400) return { ok: true };
2692
- const fromSink = served && sink.status() >= 400 ? extractUpstreamError(sink.body()) : "";
4961
+ if (served && statusOf() < 400) return { ok: true };
4962
+ const fromSink = served && statusOf() >= 400 ? extractUpstreamError(bodyOf()) : "";
2693
4963
  const detail = fromSink !== "" ? fromSink : trace.lastError;
2694
4964
  return {
2695
4965
  ok: false,
@@ -4120,7 +6390,7 @@ function guard(handler) {
4120
6390
  }
4121
6391
  };
4122
6392
  }
4123
- async function readBody$1(req, limit = 65536) {
6393
+ async function readBody$2(req, limit = 65536) {
4124
6394
  const chunks = [];
4125
6395
  let size = 0;
4126
6396
  return await new Promise((resolve, reject) => {
@@ -4193,7 +6463,7 @@ function supplierRoutes(base, loaded, store, router) {
4193
6463
  handler: async (req, res) => {
4194
6464
  let body;
4195
6465
  try {
4196
- body = JSON.parse(await readBody$1(req));
6466
+ body = JSON.parse(await readBody$2(req));
4197
6467
  } catch {
4198
6468
  writeJson$1(res, 400, {
4199
6469
  ok: false,
@@ -4230,7 +6500,7 @@ function supplierRoutes(base, loaded, store, router) {
4230
6500
  }
4231
6501
  try {
4232
6502
  let pick = {};
4233
- const raw = (await readBody$1(req)).trim();
6503
+ const raw = (await readBody$2(req)).trim();
4234
6504
  if (raw !== "") {
4235
6505
  const body = JSON.parse(raw);
4236
6506
  pick = {
@@ -4291,7 +6561,7 @@ function supplierRoutes(base, loaded, store, router) {
4291
6561
  kind: "exact",
4292
6562
  path: `${p}/alias`,
4293
6563
  handler: async (req, res) => {
4294
- const clean = (JSON.parse(await readBody$1(req)).alias ?? "").trim();
6564
+ const clean = (JSON.parse(await readBody$2(req)).alias ?? "").trim();
4295
6565
  if (clean !== "" && !/^[A-Za-z0-9_-]+$/.test(clean)) {
4296
6566
  writeJson$1(res, 400, {
4297
6567
  ok: false,
@@ -4314,7 +6584,7 @@ function supplierRoutes(base, loaded, store, router) {
4314
6584
  kind: "exact",
4315
6585
  path: `${p}/models/toggle`,
4316
6586
  handler: guard(async (req, res) => {
4317
- const body = JSON.parse(await readBody$1(req));
6587
+ const body = JSON.parse(await readBody$2(req));
4318
6588
  store.get(s.id);
4319
6589
  const models = await router.modelsOf(s.id);
4320
6590
  if (!body.id || !models.some((mm) => mm.id === body.id)) {
@@ -4333,7 +6603,7 @@ function supplierRoutes(base, loaded, store, router) {
4333
6603
  kind: "exact",
4334
6604
  path: `${p}/models/add`,
4335
6605
  handler: guard(async (req, res) => {
4336
- const id = (JSON.parse(await readBody$1(req)).id ?? "").trim();
6606
+ const id = (JSON.parse(await readBody$2(req)).id ?? "").trim();
4337
6607
  if (id === "") {
4338
6608
  writeJson$1(res, 400, {
4339
6609
  ok: false,
@@ -4357,7 +6627,7 @@ function supplierRoutes(base, loaded, store, router) {
4357
6627
  kind: "exact",
4358
6628
  path: `${p}/models/remove`,
4359
6629
  handler: async (req, res) => {
4360
- const body = JSON.parse(await readBody$1(req));
6630
+ const body = JSON.parse(await readBody$2(req));
4361
6631
  const cfg = store.get(s.id);
4362
6632
  if (!body.id || !cfg.custom.includes(body.id)) {
4363
6633
  writeJson$1(res, 400, {
@@ -4375,7 +6645,7 @@ function supplierRoutes(base, loaded, store, router) {
4375
6645
  kind: "exact",
4376
6646
  path: `${p}/models/bulk`,
4377
6647
  handler: guard(async (req, res) => {
4378
- const body = JSON.parse(await readBody$1(req));
6648
+ const body = JSON.parse(await readBody$2(req));
4379
6649
  const models = await router.modelsOf(s.id);
4380
6650
  store.setAllModelsEnabled(s.id, !!body.enabled, models.map((mm) => mm.id));
4381
6651
  router.invalidateModels(s.id);
@@ -4393,7 +6663,7 @@ function supplierRoutes(base, loaded, store, router) {
4393
6663
  });
4394
6664
  return;
4395
6665
  }
4396
- const body = JSON.parse(await readBody$1(req));
6666
+ const body = JSON.parse(await readBody$2(req));
4397
6667
  if (!Array.isArray(body.uids) || body.uids.some((u) => typeof u !== "string")) {
4398
6668
  writeJson$1(res, 400, {
4399
6669
  ok: false,
@@ -4419,7 +6689,7 @@ function supplierRoutes(base, loaded, store, router) {
4419
6689
  kind: "exact",
4420
6690
  path: `${p}/models/test`,
4421
6691
  handler: async (req, res) => {
4422
- const body = JSON.parse(await readBody$1(req));
6692
+ const body = JSON.parse(await readBody$2(req));
4423
6693
  const onlyUid = typeof body.uid === "string" && body.uid !== "" ? body.uid : void 0;
4424
6694
  const result = await router.testModel(s.id, body.id ?? "", onlyUid);
4425
6695
  writeJson$1(res, result.ok ? 200 : 400, result);
@@ -4455,7 +6725,7 @@ function supplierRoutes(base, loaded, store, router) {
4455
6725
  });
4456
6726
  return;
4457
6727
  }
4458
- const body = JSON.parse(await readBody$1(req));
6728
+ const body = JSON.parse(await readBody$2(req));
4459
6729
  try {
4460
6730
  writeJson$1(res, 200, {
4461
6731
  ok: true,
@@ -4480,7 +6750,7 @@ function supplierRoutes(base, loaded, store, router) {
4480
6750
  });
4481
6751
  return;
4482
6752
  }
4483
- const body = JSON.parse(await readBody$1(req));
6753
+ const body = JSON.parse(await readBody$2(req));
4484
6754
  try {
4485
6755
  const r = await m.addApiKey.call(loaded.supplier, {
4486
6756
  name: (body.name ?? "").trim(),
@@ -4506,7 +6776,7 @@ function supplierRoutes(base, loaded, store, router) {
4506
6776
  });
4507
6777
  return;
4508
6778
  }
4509
- const uid = JSON.parse(await readBody$1(req)).uid ?? "";
6779
+ const uid = JSON.parse(await readBody$2(req)).uid ?? "";
4510
6780
  const ok = await s.removeLink(uid);
4511
6781
  if (ok) store.clearCredits(s.id, uid);
4512
6782
  writeJson$1(res, ok ? 200 : 404, ok ? { ok: true } : {
@@ -4911,6 +7181,332 @@ var ExtStore = class {
4911
7181
  }
4912
7182
  };
4913
7183
  //#endregion
7184
+ //#region src/protocol/entry.ts
7185
+ /**
7186
+ * 读请求体 + 按协议翻成规范请求。
7187
+ *
7188
+ * 模型名从哪来因协议而异:Chat/Anthropic/Responses 在 body 的 `model`,
7189
+ * Gemini 在 URL 路径(`:generateContent` 之前那段)—— 由 `modelFromPath`
7190
+ * 调用方先解析好传进来。
7191
+ */
7192
+ async function parseInbound(req, protocol, opts) {
7193
+ const raw = await readBody$1(req, opts.limitBytes);
7194
+ if (raw === void 0) return {
7195
+ error: "request body too large",
7196
+ status: 413
7197
+ };
7198
+ let body;
7199
+ try {
7200
+ body = JSON.parse(raw);
7201
+ } catch {
7202
+ return {
7203
+ error: "invalid JSON body",
7204
+ status: 400
7205
+ };
7206
+ }
7207
+ const codec = codecOf(protocol);
7208
+ const model = opts.modelFromPath ?? str(body.model) ?? "";
7209
+ const stream = opts.streamFromPath ?? body.stream === true;
7210
+ const canonical = codec.toChat(body, {
7211
+ model,
7212
+ stream
7213
+ });
7214
+ canonical.stream = stream;
7215
+ canonical.model = model;
7216
+ return {
7217
+ body: canonical,
7218
+ stream,
7219
+ model,
7220
+ lv: opts.lvFromBody?.(body) ?? "auto",
7221
+ protocol
7222
+ };
7223
+ }
7224
+ /**
7225
+ * 一段上游字节像不像 SSE。
7226
+ *
7227
+ * 只在 content-type **没有**任何提示时才用(流式路径的 `head` 可能一次都不来)。
7228
+ * 判据是「第一段非空白内容是否 SSE 行」,不是「包含 data:」—— 后者会让一个
7229
+ * 错误 JSON(`{"error":"data: bad"}`)被误判成流。
7230
+ */
7231
+ function looksLikeSse(text) {
7232
+ const head = text.replace(/^\s+/, "");
7233
+ return head.startsWith("data:") || head.startsWith("event:") || head.startsWith(":");
7234
+ }
7235
+ /** 上游字节 → 文本(逐字节流式解码,跨 chunk 的多字节字符不会被打断)。 */
7236
+ const decoder = new TextDecoder();
7237
+ /** 文本 → 字节。 */
7238
+ const encoder = new TextEncoder();
7239
+ /**
7240
+ * 给 Router 的**非 chat 出口**:收集 OpenAI 形态的响应,翻译成客户端协议。
7241
+ *
7242
+ * ════════════════════════════════════════════════════════════════════
7243
+ * 两条路径分开处理,因为时序完全不同
7244
+ * ════════════════════════════════════════════════════════════════════
7245
+ * 非流式:Router 一次 `head(200) + end(json)`。整份 JSON 已在手里,
7246
+ * `decodeJson` → `encodeJson` 一个来回就完。
7247
+ * 流式: Router 分很多次 `write(bytes)`,且**第一个字节前可能失败换号**。
7248
+ * 不能等收齐再翻(那就没有流式了);每收到一段就翻一段。
7249
+ *
7250
+ * 流式那条为什么插一层 sink 而不是让 Router 直接写翻译后的字节:Router 的
7251
+ * `writeChatResult` 里有一整套「客户端断开就取消上游流」「断流补 `[DONE]`」
7252
+ * 的逻辑,那些必须继续对真实的 `res` 生效。sink 只是**转发的中继**:
7253
+ * 它翻译字节、写给真正的 res;断开时调用方取消本层(见 index.ts 挂载处),
7254
+ * Router 那侧的取消链路原样传到上游。
7255
+ */
7256
+ function outboundSink(protocol, model, res) {
7257
+ const codec = codecOf(protocol);
7258
+ let headSent = false;
7259
+ /** 流式:转码器(收到第一段 SSE 时初始化,跨 write 保持分帧状态)。 */
7260
+ let translator;
7261
+ /** 非流式:攒起来的 JSON(`undefined` = 还没收到过 body)。 */
7262
+ let jsonBuf;
7263
+ let status = 200;
7264
+ /**
7265
+ * 上游给的 content-type —— **判断流还是 JSON 只看它**,不看内容。
7266
+ *
7267
+ * 早先靠 `text.startsWith('data:')` 猜:那是错的。非流式 JSON body 里如果
7268
+ * 有以 `data:` 开头的字符串值(错误信息里带 "data: ...")、或者上游先发一段
7269
+ * 空白,就会被误判成 SSE,然后 JSON 被当帧喂给翻译器 —— 客户端拿到一个
7270
+ * 空流而不是错误信息。content-type 是 Router/上游**明确给出的**事实。
7271
+ */
7272
+ let contentType = "";
7273
+ const sendHead = (code, contentType) => {
7274
+ if (headSent) return;
7275
+ headSent = true;
7276
+ res.writeHead(code, {
7277
+ "Content-Type": contentType,
7278
+ "Cache-Control": "no-store"
7279
+ });
7280
+ if (typeof res.flushHeaders === "function") res.flushHeaders();
7281
+ };
7282
+ return {
7283
+ head(code, headers) {
7284
+ status = code;
7285
+ const ct = headers["content-type"] ?? headers["Content-Type"] ?? "";
7286
+ if (ct !== "") contentType = ct.toLowerCase();
7287
+ if (code !== 200) sendHead(code, ct !== "" ? ct : "application/json; charset=utf-8");
7288
+ },
7289
+ write(bytes) {
7290
+ const text = decoder.decode(bytes, { stream: true });
7291
+ if (text === "") return;
7292
+ if (translator === void 0 && (contentType.includes("text/event-stream") || contentType === "" && looksLikeSse(text))) translator = new ChunkTranslator(new StreamState("chat", protocol, model));
7293
+ if (translator !== void 0) {
7294
+ const frames = translator.push(text);
7295
+ if (frames.length > 0) {
7296
+ sendHead(200, "text/event-stream");
7297
+ res.write(encoder.encode(frames.join("")));
7298
+ } else if (!headSent) sendHead(200, "text/event-stream");
7299
+ return;
7300
+ }
7301
+ jsonBuf = (jsonBuf ?? "") + text;
7302
+ },
7303
+ end() {
7304
+ const tailChunk = decoder.decode();
7305
+ if (tailChunk !== "") {
7306
+ if (translator !== void 0) {
7307
+ const frames = translator.push(tailChunk);
7308
+ if (frames.length > 0) res.write(encoder.encode(frames.join("")));
7309
+ } else jsonBuf = (jsonBuf ?? "") + tailChunk;
7310
+ }
7311
+ if (jsonBuf !== void 0 && jsonBuf !== "") {
7312
+ finishJson(codec, protocol, model, res, sendHead, status, jsonBuf);
7313
+ return;
7314
+ }
7315
+ if (translator !== void 0) {
7316
+ const tail = translator.end();
7317
+ if (tail.length > 0) {
7318
+ sendHead(200, "text/event-stream");
7319
+ res.write(encoder.encode(tail.join("")));
7320
+ }
7321
+ if (headSent) res.end();
7322
+ else finishEmptyStream(codec, protocol, model, res, sendHead);
7323
+ return;
7324
+ }
7325
+ finishEmptyStream(codec, protocol, model, res, sendHead);
7326
+ }
7327
+ };
7328
+ }
7329
+ /**
7330
+ * 非流式:整份 JSON 翻译后写出。
7331
+ *
7332
+ * ⚠️ 方向在这里:Router 吐的是 **OpenAI Chat** 形态(`choices[0].message`),
7333
+ * 而客户端要的是 `protocol` 说的形态。所以**解码用 chat codec、编码用客户端
7334
+ * codec** —— 曾经两处都用客户端的 codec,于是 Chat JSON 被当成 Anthropic
7335
+ * body 解(`choices` 字段根本不存在),客户端拿到一个 content 为空的回复。
7336
+ */
7337
+ function finishJson(clientCodec, protocol, model, res, sendHead, status, jsonBuf) {
7338
+ let events;
7339
+ try {
7340
+ events = codecOf("chat").decodeJson(JSON.parse(jsonBuf));
7341
+ } catch {
7342
+ sendHead(status, "application/json; charset=utf-8");
7343
+ res.end(jsonBuf);
7344
+ return;
7345
+ }
7346
+ const err = events.find((e) => e.type === "error");
7347
+ if (err !== void 0 && err.type === "error") {
7348
+ sendHead(502, "application/json; charset=utf-8");
7349
+ res.end(JSON.stringify(errBody(protocol, err.message)));
7350
+ return;
7351
+ }
7352
+ const body = clientCodec.encodeJson(events, {
7353
+ model,
7354
+ stream: false
7355
+ });
7356
+ sendHead(status, "application/json; charset=utf-8");
7357
+ res.end(JSON.stringify(body));
7358
+ }
7359
+ /**
7360
+ * 流式但一个内容帧都没翻译出来(上游刚连上就断 / 首字节超时前就失败)。
7361
+ *
7362
+ * 给客户端一个**协议合法的空流**,别让 HTTP 连接裸断:客户端会把「没有响应」
7363
+ * 当成网络故障重试,而「上游没答」才是事实——用协议的 error 收尾说出来。
7364
+ */
7365
+ function finishEmptyStream(codec, protocol, model, res, sendHead) {
7366
+ const ctx = {
7367
+ model,
7368
+ stream: true,
7369
+ finish: "error"
7370
+ };
7371
+ const frames = [
7372
+ ...codec.startFrames(ctx),
7373
+ ...codec.encodeFrame({
7374
+ type: "error",
7375
+ message: "upstream closed before any content"
7376
+ }),
7377
+ ...codec.encodeFrame({
7378
+ type: "finish",
7379
+ reason: "error"
7380
+ }),
7381
+ ...codec.endFrames(ctx)
7382
+ ];
7383
+ sendHead(200, "text/event-stream");
7384
+ res.end(new TextEncoder().encode(frames.join("")));
7385
+ }
7386
+ /** 每种协议的 error body 形状。 */
7387
+ function errBody(protocol, message) {
7388
+ if (protocol === "anthropic") return {
7389
+ type: "error",
7390
+ error: {
7391
+ type: "api_error",
7392
+ message
7393
+ }
7394
+ };
7395
+ if (protocol === "gemini") return { error: {
7396
+ code: 502,
7397
+ message,
7398
+ status: "UNAVAILABLE"
7399
+ } };
7400
+ return { error: {
7401
+ message,
7402
+ type: "api_error",
7403
+ code: "upstream_error"
7404
+ } };
7405
+ }
7406
+ /**
7407
+ * 一段上游 OpenAI SSE 文本的转码器 —— **有状态**。
7408
+ *
7409
+ * 上游一帧被拆在两个 TCP 段里是常态(Router 按拿到的字节原样转交),所以
7410
+ * 分帧缓冲必须跨 `write()` 调用存活。这里用 `sse.ts` 的 `SseSplitter`(它内部
7411
+ * 留着半个帧)而不是每次现切 —— 早先每次现切会把不完整帧当完整帧解析,
7412
+ * 表现是「文本丢一半、工具参数截断」,而 HTTP 200,客户端看不到任何错误。
7413
+ *
7414
+ * 生命周期与 sink 一致:一个请求一个实例。
7415
+ */
7416
+ var ChunkTranslator = class {
7417
+ splitter = new SseSplitter();
7418
+ state;
7419
+ constructor(state) {
7420
+ this.state = state;
7421
+ }
7422
+ /** 喂一段文本,返回可写给客户端的帧。 */
7423
+ push(text) {
7424
+ return this.emit(this.splitter.push(new TextEncoder().encode(text)));
7425
+ }
7426
+ /** 流结束:冲刷残留帧(上游不发尾空行是有的),返回收尾帧。 */
7427
+ end() {
7428
+ return [...this.emit(this.splitter.end()), ...this.state.finishStream()];
7429
+ }
7430
+ emit(frames) {
7431
+ const out = [];
7432
+ for (const f of frames) {
7433
+ if (f.data === "" || f.data === "[DONE]") continue;
7434
+ out.push(...this.state.translateFrame(f.data, f.event));
7435
+ }
7436
+ return out;
7437
+ }
7438
+ };
7439
+ /** 读请求体(带大小上限)。超限返回 undefined。 */
7440
+ function readBody$1(req, limit) {
7441
+ return new Promise((resolve) => {
7442
+ const chunks = [];
7443
+ let size = 0;
7444
+ req.on("data", (chunk) => {
7445
+ size += chunk.length;
7446
+ if (size > limit) {
7447
+ req.destroy();
7448
+ resolve(void 0);
7449
+ return;
7450
+ }
7451
+ chunks.push(chunk);
7452
+ });
7453
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
7454
+ req.on("error", () => resolve(void 0));
7455
+ });
7456
+ }
7457
+ /**
7458
+ * 从 Gemini 的 URL 路径里取模型名。
7459
+ *
7460
+ * 形如 `/v1beta/models/<model>:generateContent`(或 `:streamGenerateContent`),
7461
+ * `<model>` 本身可以含斜杠,所以取 `models/` 之后到最后一个 `:` 之前那段。
7462
+ *
7463
+ * **必须先切掉 query**:Gemini CLI 默认带 `?alt=sse`,不切的话 `$` 锚点匹配
7464
+ * 不上,模型名变空 —— 表现为「404 model not found」而不是参数错误,很难查。
7465
+ * 顺便支持 `/v1beta/models/<model>` 这种带版本号但无方法的形态(走 GET 列表时
7466
+ * 会撞上,这里取不到 `/` 就是取不到,返回 undefined 让上层报 400)。
7467
+ */
7468
+ function geminiModelFromPath(pathname) {
7469
+ const path = pathname.split("?")[0] ?? "";
7470
+ return /\/models\/(.+?):(?:streamGenerateContent|generateContent|countTokens)$/.exec(path)?.[1];
7471
+ }
7472
+ /**
7473
+ * Gemini 的流式意图在**路径尾部**:`:streamGenerateContent` 是流式、
7474
+ * `:generateContent` 是非流式。返回 `undefined` 表示路径没表态(不是 Gemini
7475
+ * 形态,或路径不可识别),交给 body 的 `stream` 决定。
7476
+ *
7477
+ * 为什么不能只看 body:Gemini 的请求体里**没有** `stream` 字段 —— 它的官方
7478
+ * 契约就是靠 URL 区分两种方法。不认这个,流式请求会被当成非流式,客户端按
7479
+ * SSE 解析一整个 JSON ⇒ 空响应(2026-10-21 在 3080 实测到)。
7480
+ */
7481
+ function geminiStreamFromPath(pathname) {
7482
+ const path = pathname.split("?")[0] ?? "";
7483
+ if (/:streamGenerateContent$/.test(path)) return true;
7484
+ if (/:generateContent$/.test(path)) return false;
7485
+ }
7486
+ /**
7487
+ * 从请求体取推理等级(各协议字段名不同)。
7488
+ *
7489
+ * 只认「明确说了要多少思考」的形态,其余一律 'auto':把「有这个字段」当成
7490
+ * 「要高档推理」会让所有带 thinking 块的请求都按 high 走,费用与延迟都变。
7491
+ */
7492
+ function lvOf(body, protocol) {
7493
+ const direct = str(body.reasoning_effort);
7494
+ if (direct !== void 0) return direct;
7495
+ const effort = str(body.reasoning) ?? str(body.effort);
7496
+ if (effort !== void 0) return effort;
7497
+ const budget = body.generationConfig;
7498
+ if (budget !== void 0) {
7499
+ const tb = budget.thinkingConfig?.thinkingBudget;
7500
+ if (typeof tb === "number" && tb > 0) return "high";
7501
+ }
7502
+ const thinking = body.thinking;
7503
+ if (thinking !== void 0) {
7504
+ const tb = thinking.budget_tokens;
7505
+ if (typeof tb === "number" && tb > 0) return "high";
7506
+ }
7507
+ return "auto";
7508
+ }
7509
+ //#endregion
4914
7510
  //#region src/index.ts
4915
7511
  /**
4916
7512
  * Plugin identity for cordis.yml rows — 必须与 package.json 的 name 一致。
@@ -4949,6 +7545,33 @@ function writeJson(res, status, body) {
4949
7545
  });
4950
7546
  res.end(payload);
4951
7547
  }
7548
+ /**
7549
+ * 每种协议的 error body 形状。
7550
+ *
7551
+ * 为什么必须有:非 chat 入口的 400/413/501 若照 OpenAI 的
7552
+ * `{error:{message,type,code}}` 写,Claude Code 解析不到 `error.message`
7553
+ * 会显示「未知错误」——用户看到的是一个空错误,而真因(如 body 太大)
7554
+ * 就在手边却送不出去。
7555
+ */
7556
+ function errBodyFor(protocol, message) {
7557
+ if (protocol === "anthropic") return {
7558
+ type: "error",
7559
+ error: {
7560
+ type: "invalid_request_error",
7561
+ message
7562
+ }
7563
+ };
7564
+ if (protocol === "gemini") return { error: {
7565
+ code: 400,
7566
+ message,
7567
+ status: "INVALID_ARGUMENT"
7568
+ } };
7569
+ return { error: {
7570
+ message,
7571
+ type: "api_error",
7572
+ code: "invalid_request"
7573
+ } };
7574
+ }
4952
7575
  function readBody(req, limit) {
4953
7576
  return new Promise((resolve, reject) => {
4954
7577
  const chunks = [];
@@ -5138,9 +7761,61 @@ function apply(rawContext) {
5138
7761
  stream: !!peek.stream,
5139
7762
  model: typeof peek.model === "string" ? peek.model : "",
5140
7763
  lv,
5141
- ...session === void 0 ? {} : { session }
7764
+ ...session === void 0 ? {} : { session },
7765
+ protocol: "chat"
5142
7766
  }, res);
5143
7767
  }));
7768
+ /**
7769
+ * Anthropic / Responses / Gemini 三个非 chat 入口的**公共前半段**:
7770
+ * 鉴权 → 读 body → 翻成规范请求 → 交给 Router → 把响应翻回客户端协议。
7771
+ *
7772
+ * 抽出来是因为三者的差别只有「协议 id 是什么、模型名从哪取」,而鉴权/限流/
7773
+ * 归因/错误形状必须完全一致 —— 各写一份就会出现「Anthropic 入口认了
7774
+ * x-dsh-router-session、Gemini 入口不认」这种只能靠实测发现的漂移。
7775
+ */
7776
+ const enterProtocol = (protocol) => withV1Auth(async (req, res) => {
7777
+ const parsed = await parseInbound(req, protocol, {
7778
+ limitBytes: 8 << 20,
7779
+ modelFromPath: protocol === "gemini" ? geminiModelFromPath(req.url ?? "") : void 0,
7780
+ streamFromPath: protocol === "gemini" ? geminiStreamFromPath(req.url ?? "") : void 0,
7781
+ lvFromBody: (body) => lvOf(body, protocol)
7782
+ });
7783
+ if ("error" in parsed) {
7784
+ writeJson(res, parsed.status, errBodyFor(protocol, parsed.error));
7785
+ return;
7786
+ }
7787
+ if (parsed.model === "") {
7788
+ writeJson(res, 400, errBodyFor(protocol, "missing model"));
7789
+ return;
7790
+ }
7791
+ const sessionRaw = req.headers["x-dsh-router-session"];
7792
+ const session = (typeof sessionRaw === "string" && sessionRaw !== "" ? sessionRaw : void 0) ?? clientIdentityOf(parsed.body, req.headers["user-agent"]);
7793
+ const sink = outboundSink(protocol, parsed.model, res);
7794
+ await router.chatCompletions({
7795
+ rawBody: JSON.stringify(parsed.body),
7796
+ stream: parsed.stream,
7797
+ model: parsed.model,
7798
+ lv: parsed.lv,
7799
+ ...session === void 0 ? {} : { session },
7800
+ protocol
7801
+ }, res, sink);
7802
+ });
7803
+ route("/v1/messages", enterProtocol("anthropic"));
7804
+ route("/v1/messages/count_tokens", withV1Auth(async (_req, res) => {
7805
+ writeJson(res, 501, {
7806
+ type: "error",
7807
+ error: {
7808
+ type: "not_supported",
7809
+ message: "count_tokens is not supported; send the message instead"
7810
+ }
7811
+ });
7812
+ }));
7813
+ route("/v1/responses", enterProtocol("responses"));
7814
+ disposers.push(ctx.webServer.register({
7815
+ kind: "prefix",
7816
+ path: "/v1beta/models",
7817
+ handler: enterProtocol("gemini")
7818
+ }));
5144
7819
  route(`${ROUTER_API_BASE}/health`, (_req, res) => {
5145
7820
  const { suppliers } = router.status();
5146
7821
  writeJson(res, 200, {
@@ -5386,6 +8061,8 @@ function apply(rawContext) {
5386
8061
  ...c.detail !== void 0 ? { detail: c.detail } : {},
5387
8062
  ...typeof c.body === "string" ? { body: c.body } : {},
5388
8063
  ...c.editable === true ? { editable: true } : {},
8064
+ ...c.group !== void 0 ? { group: c.group } : {},
8065
+ ...c.sortable !== void 0 ? { sortable: c.sortable } : {},
5389
8066
  custom: c.custom === true,
5390
8067
  overridden: c.overridden === true
5391
8068
  })) } : {},