llm-simple-router 0.7.1 → 0.8.2

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 (153) hide show
  1. package/dist/admin/proxy-enhancement.js +3 -1
  2. package/dist/admin/routes.d.ts +1 -0
  3. package/dist/admin/routes.js +3 -1
  4. package/dist/admin/settings-import-export.d.ts +1 -0
  5. package/dist/admin/settings-import-export.js +7 -0
  6. package/dist/admin/transform-rules.d.ts +8 -0
  7. package/dist/admin/transform-rules.js +38 -0
  8. package/dist/admin/usage.js +1 -1
  9. package/dist/core/container.d.ts +1 -0
  10. package/dist/core/container.js +1 -0
  11. package/dist/db/migrations/034_create_provider_transform_rules.sql +11 -0
  12. package/dist/db/transform-rules.d.ts +16 -0
  13. package/dist/db/transform-rules.js +51 -0
  14. package/dist/index.js +30 -1
  15. package/dist/metrics/sse-parser.d.ts +2 -0
  16. package/dist/metrics/sse-parser.js +4 -0
  17. package/dist/monitor/request-tracker.d.ts +2 -0
  18. package/dist/monitor/request-tracker.js +22 -1
  19. package/dist/monitor/types.d.ts +1 -1
  20. package/dist/proxy/enhancement/response-cleaner.js +14 -6
  21. package/dist/proxy/handler/openai.js +13 -4
  22. package/dist/proxy/handler/proxy-handler-utils.js +2 -7
  23. package/dist/proxy/handler/proxy-handler.js +85 -18
  24. package/dist/proxy/patch/deepseek/index.d.ts +15 -3
  25. package/dist/proxy/patch/deepseek/index.js +29 -6
  26. package/dist/proxy/patch/deepseek/patch-cache-control.d.ts +6 -0
  27. package/dist/proxy/patch/deepseek/patch-cache-control.js +30 -0
  28. package/dist/proxy/patch/deepseek/patch-non-deepseek-tools.d.ts +16 -0
  29. package/dist/proxy/patch/deepseek/patch-non-deepseek-tools.js +74 -0
  30. package/dist/proxy/patch/deepseek/patch-orphan-tool-results.d.ts +10 -1
  31. package/dist/proxy/patch/deepseek/patch-orphan-tool-results.js +58 -15
  32. package/dist/proxy/patch/deepseek/patch-thinking-blocks.d.ts +5 -1
  33. package/dist/proxy/patch/deepseek/patch-thinking-blocks.js +37 -4
  34. package/dist/proxy/patch/deepseek/patch-thinking-param.d.ts +6 -0
  35. package/dist/proxy/patch/deepseek/patch-thinking-param.js +32 -0
  36. package/dist/proxy/patch/deepseek/utils.d.ts +8 -0
  37. package/dist/proxy/patch/deepseek/utils.js +38 -0
  38. package/dist/proxy/patch/index.d.ts +2 -2
  39. package/dist/proxy/patch/index.js +50 -4
  40. package/dist/proxy/patch/router-cleanup.js +1 -24
  41. package/dist/proxy/patch/safe-sse-parser.d.ts +9 -0
  42. package/dist/proxy/patch/safe-sse-parser.js +16 -0
  43. package/dist/proxy/patch/tool-round-limiter.d.ts +38 -0
  44. package/dist/proxy/patch/tool-round-limiter.js +115 -0
  45. package/dist/proxy/pipeline-snapshot.d.ts +4 -0
  46. package/dist/proxy/proxy-core.js +1 -0
  47. package/dist/proxy/proxy-logging.d.ts +1 -1
  48. package/dist/proxy/proxy-logging.js +3 -3
  49. package/dist/proxy/routing/enhancement-config.d.ts +1 -0
  50. package/dist/proxy/routing/enhancement-config.js +2 -0
  51. package/dist/proxy/transform/id-utils.d.ts +3 -0
  52. package/dist/proxy/transform/id-utils.js +9 -0
  53. package/dist/proxy/transform/message-mapper.d.ts +15 -0
  54. package/dist/proxy/transform/message-mapper.js +173 -0
  55. package/dist/proxy/transform/plugin-registry.d.ts +23 -0
  56. package/dist/proxy/transform/plugin-registry.js +130 -0
  57. package/dist/proxy/transform/plugin-types.d.ts +46 -0
  58. package/dist/proxy/transform/plugin-types.js +15 -0
  59. package/dist/proxy/transform/provider-meta.d.ts +29 -0
  60. package/dist/proxy/transform/provider-meta.js +72 -0
  61. package/dist/proxy/transform/request-transform.d.ts +4 -0
  62. package/dist/proxy/transform/request-transform.js +151 -0
  63. package/dist/proxy/transform/response-transform.d.ts +4 -0
  64. package/dist/proxy/transform/response-transform.js +99 -0
  65. package/dist/proxy/transform/sanitize.d.ts +3 -0
  66. package/dist/proxy/transform/sanitize.js +24 -0
  67. package/dist/proxy/transform/stream-ant2oa.d.ts +20 -0
  68. package/dist/proxy/transform/stream-ant2oa.js +200 -0
  69. package/dist/proxy/transform/stream-oa2ant.d.ts +25 -0
  70. package/dist/proxy/transform/stream-oa2ant.js +201 -0
  71. package/dist/proxy/transform/stream-transform-base.d.ts +19 -0
  72. package/dist/proxy/transform/stream-transform-base.js +61 -0
  73. package/dist/proxy/transform/thinking-mapper.d.ts +4 -0
  74. package/dist/proxy/transform/thinking-mapper.js +15 -0
  75. package/dist/proxy/transform/tool-mapper.d.ts +8 -0
  76. package/dist/proxy/transform/tool-mapper.js +67 -0
  77. package/dist/proxy/transform/transform-coordinator.d.ts +11 -0
  78. package/dist/proxy/transform/transform-coordinator.js +32 -0
  79. package/dist/proxy/transform/types.d.ts +43 -0
  80. package/dist/proxy/transform/types.js +1 -0
  81. package/dist/proxy/transform/usage-mapper.d.ts +8 -0
  82. package/dist/proxy/transform/usage-mapper.js +46 -0
  83. package/dist/proxy/transport/stream.d.ts +1 -1
  84. package/dist/proxy/transport/stream.js +19 -10
  85. package/dist/proxy/transport/transport-fn.d.ts +3 -0
  86. package/dist/proxy/transport/transport-fn.js +11 -4
  87. package/dist/storage/log-file-compressor.js +5 -6
  88. package/dist/storage/log-file-writer.js +11 -13
  89. package/dist/storage/types.d.ts +2 -0
  90. package/dist/storage/types.js +7 -0
  91. package/frontend-dist/assets/{CardContent-CxOF1feY.js → CardContent-BVMQ2_pg.js} +1 -1
  92. package/frontend-dist/assets/{CardTitle-BSEFcEOM.js → CardTitle-GLv7QyIY.js} +1 -1
  93. package/frontend-dist/assets/{CascadingModelSelect-DTwksDPZ.js → CascadingModelSelect-CBhqKFDX.js} +1 -1
  94. package/frontend-dist/assets/{Checkbox-RfsERG07.js → Checkbox-HPVDmEdV.js} +1 -1
  95. package/frontend-dist/assets/{CollapsibleTrigger-Dsjo7QlC.js → CollapsibleTrigger-DhxD9tpM.js} +1 -1
  96. package/frontend-dist/assets/{Collection-rQ4eIYfa.js → Collection-BRt7YxN8.js} +1 -1
  97. package/frontend-dist/assets/{Dashboard-YejfAPiB.js → Dashboard-D1Ys8Zog.js} +1 -1
  98. package/frontend-dist/assets/{DialogTitle-DeFTnmgC.js → DialogTitle-23q73lwF.js} +1 -1
  99. package/frontend-dist/assets/{Input-CENz_g9t.js → Input-CAnKUBBK.js} +1 -1
  100. package/frontend-dist/assets/{Label-BAciBrrd.js → Label-DWdYtVMI.js} +1 -1
  101. package/frontend-dist/assets/{Login-DQkYFq7R.js → Login-w5WFOinP.js} +1 -1
  102. package/frontend-dist/assets/{Logs-Dol8AX7z.js → Logs-C1F1ZmWF.js} +1 -1
  103. package/frontend-dist/assets/{ModelMappings-VEYW1TrW.js → ModelMappings-BzmecWEH.js} +1 -1
  104. package/frontend-dist/assets/{Monitor-C0r9WefB.js → Monitor-DrAZFTKR.js} +1 -1
  105. package/frontend-dist/assets/{PopoverTrigger-Cyqik5SE.js → PopoverTrigger-Bj65uUbv.js} +1 -1
  106. package/frontend-dist/assets/{PopperContent-B7IuAHeq.js → PopperContent-gzzf1XHe.js} +1 -1
  107. package/frontend-dist/assets/Providers-DSgf4mb6.js +1 -0
  108. package/frontend-dist/assets/ProxyEnhancement-Bb1cCP6d.js +5 -0
  109. package/frontend-dist/assets/{RetryRules-F0295m4_.js → RetryRules-BwPfEZtm.js} +1 -1
  110. package/frontend-dist/assets/{RouterKeys-CFbPtUE_.js → RouterKeys-CzTSq1Mx.js} +1 -1
  111. package/frontend-dist/assets/{RovingFocusItem-D291Vjh8.js → RovingFocusItem-CXM_Yfkm.js} +1 -1
  112. package/frontend-dist/assets/{Schedules-DWhF3uod.js → Schedules-DVilCXrC.js} +1 -1
  113. package/frontend-dist/assets/{SelectValue-BWlgUZa3.js → SelectValue-C0-LzGQY.js} +1 -1
  114. package/frontend-dist/assets/{Settings-BnIzEF_k.js → Settings-Bpk53zVX.js} +1 -1
  115. package/frontend-dist/assets/{Setup-BglKyQKq.js → Setup-Dn7EgC49.js} +1 -1
  116. package/frontend-dist/assets/{Switch-DyCR-CPu.js → Switch-BO8Ooae6.js} +1 -1
  117. package/frontend-dist/assets/{TableHeader-DVUlBL35.js → TableHeader-Bded9VTC.js} +1 -1
  118. package/frontend-dist/assets/{TabsTrigger-BU1DY-C8.js → TabsTrigger-BzKMi9AF.js} +1 -1
  119. package/frontend-dist/assets/{Teleport-BQgusr9g.js → Teleport-DizRK5O3.js} +1 -1
  120. package/frontend-dist/assets/{TooltipTrigger-Bv_QoBns.js → TooltipTrigger-EiIy2zn8.js} +1 -1
  121. package/frontend-dist/assets/{UnifiedRequestDialog-f_evI835.js → UnifiedRequestDialog-BABsTaGb.js} +1 -1
  122. package/frontend-dist/assets/{VisuallyHidden-Con10z4F.js → VisuallyHidden-5AozJQza.js} +1 -1
  123. package/frontend-dist/assets/{VisuallyHiddenInput-yrDtxucb.js → VisuallyHiddenInput-DdiZrV2i.js} +1 -1
  124. package/frontend-dist/assets/{alert-dialog-2Db6Z7JQ.js → alert-dialog-DlKUuTPe.js} +1 -1
  125. package/frontend-dist/assets/arrow-down-CxWKmZ2I.js +1 -0
  126. package/frontend-dist/assets/{badge-DEhZfeI0.js → badge-9KJEMa53.js} +1 -1
  127. package/frontend-dist/assets/button-Ul8WlrM5.js +12 -0
  128. package/frontend-dist/assets/check-7ahK--N4.js +1 -0
  129. package/frontend-dist/assets/{copy-CwqZSuIG.js → copy-DzU2pAMG.js} +1 -1
  130. package/frontend-dist/assets/{dialog-CVMKSdPr.js → dialog-B9j-FMrd.js} +1 -1
  131. package/frontend-dist/assets/{file-text-D0K8Hovo.js → file-text-Bj3ZIo-E.js} +1 -1
  132. package/frontend-dist/assets/index-Bz_ZaXNn.css +1 -0
  133. package/frontend-dist/assets/{index-Ct718O93.js → index-MedWZMHB.js} +1 -1
  134. package/frontend-dist/assets/{lib-H3YI7EK4.js → lib-Hhs3NqfD.js} +1 -1
  135. package/frontend-dist/assets/loader-circle-5TJUukEe.js +1 -0
  136. package/frontend-dist/assets/{useClipboard-Cd7k-5Yq.js → useClipboard-BmmsNSGV.js} +1 -1
  137. package/frontend-dist/assets/{useFocusGuards-luoLXnwV.js → useFocusGuards-A-9V2Y-b.js} +1 -1
  138. package/frontend-dist/assets/useFormControl-DEO19lRe.js +1 -0
  139. package/frontend-dist/assets/{useLogRetention-DB4Iu6o_.js → useLogRetention-BfnBFZ5K.js} +1 -1
  140. package/frontend-dist/assets/useNonce-BfwUJ1Ci.js +1 -0
  141. package/frontend-dist/assets/x-Cfopt3QL.js +1 -0
  142. package/frontend-dist/index.html +20 -20
  143. package/package.json +1 -1
  144. package/frontend-dist/assets/Providers-D8Z97edN.js +0 -1
  145. package/frontend-dist/assets/ProxyEnhancement-Kn8r2SN6.js +0 -5
  146. package/frontend-dist/assets/arrow-down-WyouvE7T.js +0 -1
  147. package/frontend-dist/assets/button-Cnkbp_6J.js +0 -12
  148. package/frontend-dist/assets/check-BuqB5Nyb.js +0 -1
  149. package/frontend-dist/assets/index-xjdbFKXJ.css +0 -1
  150. package/frontend-dist/assets/loader-circle-Be82FnVY.js +0 -1
  151. package/frontend-dist/assets/useFormControl-Da4ViGZF.js +0 -1
  152. package/frontend-dist/assets/useNonce-DvAdQ48J.js +0 -1
  153. package/frontend-dist/assets/x-DB22csQl.js +0 -1
@@ -0,0 +1,200 @@
1
+ import { BaseSSETransform } from "./stream-transform-base.js";
2
+ import { generateChatcmplId } from "./id-utils.js";
3
+ import { mapStopReasonToFinishReason } from "./usage-mapper.js";
4
+ export class AnthropicToOpenAITransform extends BaseSSETransform {
5
+ chatcmplId = generateChatcmplId();
6
+ firstContentBlock = true;
7
+ inputTokens = 0;
8
+ outputTokens = 0;
9
+ finishReasonEmitted = false;
10
+ currentToolCallIndex = 0;
11
+ blockToToolCallIndex = new Map();
12
+ // track content block types for PSF capture
13
+ contentBlockTypes = new Map();
14
+ contentBlockSignatures = new Map();
15
+ // PSF accumulation for streaming
16
+ thinkingSignatures = [];
17
+ cacheUsage;
18
+ processEvent(event) {
19
+ let data;
20
+ try {
21
+ data = JSON.parse(event.data);
22
+ }
23
+ catch (err) {
24
+ this.emit("warning", err);
25
+ return;
26
+ }
27
+ switch (data.type) {
28
+ case "message_start": {
29
+ const msg = data.message;
30
+ const usage = msg?.usage;
31
+ this.inputTokens = usage?.input_tokens ?? 0;
32
+ // capture cache usage from initial usage
33
+ if (usage?.cache_read_input_tokens != null || usage?.cache_creation_input_tokens != null) {
34
+ this.cacheUsage = {
35
+ cache_read_input_tokens: usage.cache_read_input_tokens,
36
+ cache_creation_input_tokens: usage.cache_creation_input_tokens,
37
+ };
38
+ }
39
+ break;
40
+ }
41
+ case "content_block_start": {
42
+ const block = data.content_block;
43
+ const blockType = block?.type;
44
+ const blockIdx = data.index ?? 0;
45
+ // track block type for PSF capture at content_block_stop
46
+ this.contentBlockTypes.set(blockIdx, blockType);
47
+ if (blockType === "thinking" && block.signature) {
48
+ this.contentBlockSignatures.set(blockIdx, block.signature);
49
+ }
50
+ if (this.firstContentBlock) {
51
+ this.pushOpenAISSE({
52
+ id: this.chatcmplId, object: "chat.completion.chunk",
53
+ choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null }],
54
+ });
55
+ this.firstContentBlock = false;
56
+ }
57
+ if (blockType === "tool_use") {
58
+ const tcIndex = this.currentToolCallIndex++;
59
+ this.blockToToolCallIndex.set(blockIdx, tcIndex);
60
+ this.pushOpenAISSE({
61
+ id: this.chatcmplId, object: "chat.completion.chunk",
62
+ choices: [{
63
+ index: 0,
64
+ delta: {
65
+ tool_calls: [{
66
+ index: tcIndex, id: block.id, type: "function",
67
+ function: { name: block.name, arguments: "" },
68
+ }],
69
+ },
70
+ finish_reason: null,
71
+ }],
72
+ });
73
+ }
74
+ break;
75
+ }
76
+ case "content_block_delta": {
77
+ const delta = data.delta;
78
+ const blockIdx = data.index ?? 0;
79
+ const deltaType = delta?.type;
80
+ if (deltaType === "text_delta") {
81
+ const text = delta.text;
82
+ if (!text)
83
+ break;
84
+ this.pushOpenAISSE({
85
+ id: this.chatcmplId, object: "chat.completion.chunk",
86
+ choices: [{ index: 0, delta: { content: text }, finish_reason: null }],
87
+ });
88
+ }
89
+ else if (deltaType === "thinking_delta") {
90
+ const thinking = delta.thinking;
91
+ if (!thinking)
92
+ break;
93
+ this.pushOpenAISSE({
94
+ id: this.chatcmplId, object: "chat.completion.chunk",
95
+ choices: [{ index: 0, delta: { reasoning_content: thinking }, finish_reason: null }],
96
+ });
97
+ }
98
+ else if (deltaType === "input_json_delta") {
99
+ const partialJson = delta.partial_json;
100
+ if (!partialJson)
101
+ break;
102
+ const tcIndex = this.blockToToolCallIndex.get(blockIdx) ?? 0;
103
+ this.pushOpenAISSE({
104
+ id: this.chatcmplId, object: "chat.completion.chunk",
105
+ choices: [{
106
+ index: 0,
107
+ delta: { tool_calls: [{ index: tcIndex, function: { arguments: partialJson } }] },
108
+ finish_reason: null,
109
+ }],
110
+ });
111
+ }
112
+ break;
113
+ }
114
+ case "content_block_stop": {
115
+ const blockIdx = data.index ?? 0;
116
+ // capture thinking signature from completed block
117
+ const sig = this.contentBlockSignatures.get(blockIdx);
118
+ if (sig && this.contentBlockTypes.get(blockIdx) === "thinking") {
119
+ this.thinkingSignatures.push({ index: blockIdx, signature: sig });
120
+ }
121
+ // Anthropic may also send signature in content_block_stop's content_block field
122
+ const stopBlock = data.content_block;
123
+ if (stopBlock?.type === "thinking" && stopBlock.signature && !sig) {
124
+ this.thinkingSignatures.push({ index: blockIdx, signature: stopBlock.signature });
125
+ }
126
+ break;
127
+ }
128
+ case "message_delta": {
129
+ const msgDelta = data.delta;
130
+ const usage = data.usage;
131
+ this.outputTokens = usage?.output_tokens ?? this.outputTokens;
132
+ const stopReason = msgDelta?.stop_reason;
133
+ if (stopReason && !this.finishReasonEmitted) {
134
+ this.finishReasonEmitted = true;
135
+ const fr = mapStopReasonToFinishReason(stopReason);
136
+ this.pushOpenAISSE({
137
+ id: this.chatcmplId, object: "chat.completion.chunk",
138
+ choices: [{ index: 0, delta: {}, finish_reason: fr }],
139
+ });
140
+ }
141
+ break;
142
+ }
143
+ case "message_stop": {
144
+ // emit PSF as custom message_meta event before final usage
145
+ if (this.thinkingSignatures.length > 0 || this.cacheUsage) {
146
+ const meta = {};
147
+ if (this.thinkingSignatures.length > 0)
148
+ meta.thinking_signatures = this.thinkingSignatures;
149
+ if (this.cacheUsage)
150
+ meta.cache_usage = this.cacheUsage;
151
+ this.push(`event: message_meta\ndata: ${JSON.stringify({ provider_meta: { anthropic: meta } })}\n\n`);
152
+ }
153
+ this.pushOpenAISSE({
154
+ id: this.chatcmplId, object: "chat.completion.chunk",
155
+ choices: [],
156
+ usage: {
157
+ prompt_tokens: this.inputTokens,
158
+ completion_tokens: this.outputTokens,
159
+ total_tokens: this.inputTokens + this.outputTokens,
160
+ },
161
+ });
162
+ this.pushDone();
163
+ break;
164
+ }
165
+ case "error": {
166
+ const error = data.error;
167
+ this.pushOpenAISSE({
168
+ error: {
169
+ message: error?.message ?? "Stream error",
170
+ type: error?.type ?? "api_error",
171
+ code: "upstream_error",
172
+ },
173
+ });
174
+ this.pushDone();
175
+ break;
176
+ }
177
+ case "ping": {
178
+ break;
179
+ }
180
+ default: {
181
+ this.emit("warning", { event: "unknown_event", type: data.type });
182
+ break;
183
+ }
184
+ }
185
+ }
186
+ flushPendingData() {
187
+ // Anthropic 流不产生交错数据
188
+ }
189
+ ensureTerminated() {
190
+ if (!this.done) {
191
+ if (!this.finishReasonEmitted) {
192
+ this.pushOpenAISSE({
193
+ id: this.chatcmplId, object: "chat.completion.chunk",
194
+ choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
195
+ });
196
+ }
197
+ this.pushDone();
198
+ }
199
+ }
200
+ }
@@ -0,0 +1,25 @@
1
+ import { BaseSSETransform } from "./stream-transform-base.js";
2
+ export declare class OpenAIToAnthropicTransform extends BaseSSETransform {
3
+ private state;
4
+ private blockIndex;
5
+ private msgId;
6
+ private inputTokens;
7
+ private outputTokens;
8
+ private pendingStopReason;
9
+ private hasSentMessageStop;
10
+ private hasSentMessageStart;
11
+ private activeToolCallIndex;
12
+ private toolCallBlocks;
13
+ private completedToolCallIndices;
14
+ private finishReasonReceived;
15
+ protected processEvent(event: {
16
+ event?: string;
17
+ data?: string;
18
+ }): void;
19
+ private ensureBlockState;
20
+ private handleToolCallDelta;
21
+ private closeCurrentBlock;
22
+ private emitStopSequence;
23
+ protected flushPendingData(): void;
24
+ protected ensureTerminated(): void;
25
+ }
@@ -0,0 +1,201 @@
1
+ import { BaseSSETransform } from "./stream-transform-base.js";
2
+ import { generateMsgId } from "./id-utils.js";
3
+ import { mapFinishReasonToStopReason } from "./usage-mapper.js";
4
+ export class OpenAIToAnthropicTransform extends BaseSSETransform {
5
+ state = "init";
6
+ blockIndex = 0;
7
+ msgId = generateMsgId();
8
+ inputTokens = 0;
9
+ outputTokens = 0;
10
+ pendingStopReason = null;
11
+ hasSentMessageStop = false;
12
+ hasSentMessageStart = false;
13
+ activeToolCallIndex = -1;
14
+ toolCallBlocks = new Map();
15
+ completedToolCallIndices = new Set();
16
+ finishReasonReceived = false;
17
+ processEvent(event) {
18
+ let chunk;
19
+ try {
20
+ chunk = JSON.parse(event.data);
21
+ }
22
+ catch (err) {
23
+ this.emit("warning", err);
24
+ return;
25
+ }
26
+ // P0 fix: always extract usage when present, even if choices are in the same chunk
27
+ if (chunk.usage) {
28
+ const usage = chunk.usage;
29
+ this.inputTokens = usage.prompt_tokens ?? this.inputTokens;
30
+ this.outputTokens = usage.completion_tokens ?? this.outputTokens;
31
+ }
32
+ // Usage-only chunk (no choices) triggers stop sequence
33
+ if (chunk.usage && !(Array.isArray(chunk.choices) && chunk.choices.length > 0)) {
34
+ if (this.pendingStopReason !== null) {
35
+ this.emitStopSequence();
36
+ }
37
+ return;
38
+ }
39
+ const choices = chunk.choices;
40
+ const choice = choices?.[0];
41
+ if (!choice)
42
+ return;
43
+ const delta = choice.delta;
44
+ if (!delta)
45
+ return;
46
+ if (!this.hasSentMessageStart) {
47
+ this.pushAnthropicSSE("message_start", {
48
+ type: "message_start",
49
+ message: {
50
+ id: this.msgId, type: "message", role: "assistant", content: [],
51
+ model: this.model, status: "in_progress",
52
+ usage: { input_tokens: this.inputTokens },
53
+ },
54
+ });
55
+ this.hasSentMessageStart = true;
56
+ }
57
+ if (delta.reasoning_content != null && delta.reasoning_content !== "") {
58
+ this.ensureBlockState("thinking");
59
+ this.pushAnthropicSSE("content_block_delta", {
60
+ type: "content_block_delta", index: this.blockIndex,
61
+ delta: { type: "thinking_delta", thinking: delta.reasoning_content },
62
+ });
63
+ }
64
+ if (delta.content != null && delta.content !== "") {
65
+ this.ensureBlockState("text");
66
+ this.pushAnthropicSSE("content_block_delta", {
67
+ type: "content_block_delta", index: this.blockIndex,
68
+ delta: { type: "text_delta", text: delta.content },
69
+ });
70
+ }
71
+ const toolCalls = delta.tool_calls;
72
+ if (toolCalls) {
73
+ for (const tc of toolCalls) {
74
+ this.handleToolCallDelta(tc);
75
+ }
76
+ }
77
+ const finishReason = choice.finish_reason;
78
+ if (finishReason && !this.finishReasonReceived) {
79
+ this.finishReasonReceived = true;
80
+ this.closeCurrentBlock();
81
+ this.pendingStopReason = mapFinishReasonToStopReason(finishReason);
82
+ }
83
+ }
84
+ ensureBlockState(target) {
85
+ if (this.state === target)
86
+ return;
87
+ if (this.state !== "init") {
88
+ this.pushAnthropicSSE("content_block_stop", {
89
+ type: "content_block_stop", index: this.blockIndex,
90
+ });
91
+ this.blockIndex++;
92
+ }
93
+ this.state = target;
94
+ const blockContent = target === "text" ? { type: "text", text: "" } : { type: "thinking", thinking: "" };
95
+ this.pushAnthropicSSE("content_block_start", {
96
+ type: "content_block_start", index: this.blockIndex, content_block: blockContent,
97
+ });
98
+ }
99
+ handleToolCallDelta(tc) {
100
+ const idx = tc.index ?? 0;
101
+ const fn = tc.function;
102
+ const tcId = tc.id;
103
+ const tcName = fn?.name;
104
+ if (tcId && tcName) {
105
+ if (this.state !== "init") {
106
+ this.pushAnthropicSSE("content_block_stop", {
107
+ type: "content_block_stop", index: this.blockIndex,
108
+ });
109
+ this.blockIndex++;
110
+ }
111
+ this.activeToolCallIndex = idx;
112
+ this.state = "tool_use";
113
+ this.pushAnthropicSSE("content_block_start", {
114
+ type: "content_block_start", index: this.blockIndex,
115
+ content_block: { type: "tool_use", id: tcId, name: tcName, input: {} },
116
+ });
117
+ this.completedToolCallIndices.add(idx);
118
+ const args = fn?.arguments;
119
+ if (args && args !== "") {
120
+ this.pushAnthropicSSE("content_block_delta", {
121
+ type: "content_block_delta", index: this.blockIndex,
122
+ delta: { type: "input_json_delta", partial_json: args },
123
+ });
124
+ }
125
+ return;
126
+ }
127
+ if (idx !== this.activeToolCallIndex && this.completedToolCallIndices.has(idx)) {
128
+ const args = fn?.arguments;
129
+ if (args) {
130
+ const existing = this.toolCallBlocks.get(idx);
131
+ if (existing) {
132
+ existing.args += args;
133
+ }
134
+ else {
135
+ this.toolCallBlocks.set(idx, { id: "", name: "", args });
136
+ }
137
+ }
138
+ return;
139
+ }
140
+ if (idx !== this.activeToolCallIndex && !this.completedToolCallIndices.has(idx)) {
141
+ if (this.state !== "init") {
142
+ this.pushAnthropicSSE("content_block_stop", {
143
+ type: "content_block_stop", index: this.blockIndex,
144
+ });
145
+ this.blockIndex++;
146
+ }
147
+ this.activeToolCallIndex = idx;
148
+ this.state = "tool_use";
149
+ // P1 fix: emit content_block_start for previously unseen tool call index
150
+ this.pushAnthropicSSE("content_block_start", {
151
+ type: "content_block_start", index: this.blockIndex,
152
+ content_block: { type: "tool_use", id: `tool_${idx}`, name: `tool_${idx}`, input: {} },
153
+ });
154
+ this.completedToolCallIndices.add(idx);
155
+ }
156
+ const args = fn?.arguments;
157
+ if (args && args !== "") {
158
+ this.pushAnthropicSSE("content_block_delta", {
159
+ type: "content_block_delta", index: this.blockIndex,
160
+ delta: { type: "input_json_delta", partial_json: args },
161
+ });
162
+ }
163
+ }
164
+ closeCurrentBlock() {
165
+ if (this.state !== "init" && this.state !== "closing") {
166
+ this.pushAnthropicSSE("content_block_stop", {
167
+ type: "content_block_stop", index: this.blockIndex,
168
+ });
169
+ this.state = "closing";
170
+ }
171
+ }
172
+ emitStopSequence() {
173
+ if (this.hasSentMessageStop)
174
+ return;
175
+ const stopReason = this.pendingStopReason ?? "end_turn";
176
+ this.pushAnthropicSSE("message_delta", {
177
+ type: "message_delta",
178
+ delta: { stop_reason: stopReason, stop_sequence: null },
179
+ usage: { output_tokens: this.outputTokens },
180
+ });
181
+ this.pushAnthropicSSE("message_stop", { type: "message_stop" });
182
+ this.hasSentMessageStop = true;
183
+ this.pendingStopReason = null;
184
+ }
185
+ flushPendingData() {
186
+ for (const [idx, data] of this.toolCallBlocks) {
187
+ if (data.args) {
188
+ this.emit("warning", { event: "buffered_tool_call", index: idx, argsLength: data.args.length });
189
+ }
190
+ }
191
+ this.toolCallBlocks.clear();
192
+ }
193
+ ensureTerminated() {
194
+ if (!this.hasSentMessageStop) {
195
+ this.closeCurrentBlock();
196
+ if (this.pendingStopReason === null)
197
+ this.pendingStopReason = "end_turn";
198
+ this.emitStopSequence();
199
+ }
200
+ }
201
+ }
@@ -0,0 +1,19 @@
1
+ import { Transform, TransformCallback } from "stream";
2
+ import { SafeSSEParser } from "../patch/safe-sse-parser.js";
3
+ export declare abstract class BaseSSETransform extends Transform {
4
+ protected parser: SafeSSEParser;
5
+ protected done: boolean;
6
+ protected model: string;
7
+ constructor(model: string);
8
+ _transform(chunk: Buffer, _: BufferEncoding, callback: TransformCallback): void;
9
+ _flush(callback: TransformCallback): void;
10
+ protected abstract processEvent(event: {
11
+ event?: string;
12
+ data?: string;
13
+ }): void;
14
+ protected abstract flushPendingData(): void;
15
+ protected abstract ensureTerminated(): void;
16
+ protected pushAnthropicSSE(eventType: string, data: unknown): void;
17
+ protected pushOpenAISSE(data: unknown): void;
18
+ protected pushDone(): void;
19
+ }
@@ -0,0 +1,61 @@
1
+ import { Transform } from "stream";
2
+ import { SafeSSEParser } from "../patch/safe-sse-parser.js";
3
+ export class BaseSSETransform extends Transform {
4
+ parser = new SafeSSEParser();
5
+ done = false;
6
+ model;
7
+ constructor(model) {
8
+ super();
9
+ this.model = model;
10
+ }
11
+ _transform(chunk, _, callback) {
12
+ if (this.done) {
13
+ callback();
14
+ return;
15
+ }
16
+ try {
17
+ const text = chunk.toString("utf-8");
18
+ const events = this.parser.feed(text);
19
+ for (const event of events) {
20
+ if (event.data == null)
21
+ continue;
22
+ try {
23
+ this.processEvent(event);
24
+ }
25
+ catch (err) {
26
+ this.emit("warning", { event: "process_error", error: String(err) });
27
+ }
28
+ }
29
+ }
30
+ catch (err) {
31
+ this.emit("warning", { event: "buffer_overflow", error: String(err) });
32
+ this.flushPendingData();
33
+ this.ensureTerminated();
34
+ }
35
+ callback();
36
+ }
37
+ _flush(callback) {
38
+ const events = this.parser.flush();
39
+ for (const event of events) {
40
+ try {
41
+ this.processEvent(event);
42
+ }
43
+ catch (err) {
44
+ this.emit("warning", err);
45
+ }
46
+ }
47
+ this.flushPendingData();
48
+ this.ensureTerminated();
49
+ callback();
50
+ }
51
+ pushAnthropicSSE(eventType, data) {
52
+ this.push(`event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n`);
53
+ }
54
+ pushOpenAISSE(data) {
55
+ this.push(`data: ${JSON.stringify(data)}\n\n`);
56
+ }
57
+ pushDone() {
58
+ this.push("data: [DONE]\n\n");
59
+ this.done = true;
60
+ }
61
+ }
@@ -0,0 +1,4 @@
1
+ /** OpenAI reasoning → Anthropic thinking */
2
+ export declare function mapReasoningToThinking(reasoning: Record<string, unknown>): Record<string, unknown>;
3
+ /** Anthropic thinking → OpenAI reasoning */
4
+ export declare function mapThinkingToReasoning(thinking: Record<string, unknown> | undefined): Record<string, unknown> | undefined;
@@ -0,0 +1,15 @@
1
+ const EFFORT_BUDGET = { low: 1024, medium: 8192, high: 32768 };
2
+ const DEFAULT_BUDGET = 8192;
3
+ /** OpenAI reasoning → Anthropic thinking */
4
+ export function mapReasoningToThinking(reasoning) {
5
+ const effort = reasoning.effort;
6
+ const maxTokens = reasoning.max_tokens;
7
+ const budget = maxTokens ?? EFFORT_BUDGET[effort ?? ""] ?? DEFAULT_BUDGET;
8
+ return { type: "enabled", budget_tokens: budget };
9
+ }
10
+ /** Anthropic thinking → OpenAI reasoning */
11
+ export function mapThinkingToReasoning(thinking) {
12
+ if (!thinking || thinking.type !== "enabled")
13
+ return undefined;
14
+ return { max_tokens: thinking.budget_tokens };
15
+ }
@@ -0,0 +1,8 @@
1
+ /** OpenAI tools[] → Anthropic tools[] */
2
+ export declare function convertToolsOA2Ant(tools: unknown[]): unknown[];
3
+ /** Anthropic tools[] → OpenAI tools[] */
4
+ export declare function convertToolsAnt2OA(tools: unknown[]): unknown[];
5
+ /** OpenAI tool_choice → Anthropic tool_choice */
6
+ export declare function mapToolChoiceOA2Ant(tc: unknown): unknown;
7
+ /** Anthropic tool_choice → OpenAI tool_choice */
8
+ export declare function mapToolChoiceAnt2OA(tc: unknown): unknown;
@@ -0,0 +1,67 @@
1
+ /** OpenAI tools[] → Anthropic tools[] */
2
+ export function convertToolsOA2Ant(tools) {
3
+ return tools.map((t) => {
4
+ const tool = t;
5
+ const fn = tool.function;
6
+ const result = { name: fn.name };
7
+ if (fn.description != null)
8
+ result.description = fn.description;
9
+ if (fn.parameters != null)
10
+ result.input_schema = fn.parameters;
11
+ return result;
12
+ });
13
+ }
14
+ /** Anthropic tools[] → OpenAI tools[] */
15
+ export function convertToolsAnt2OA(tools) {
16
+ return tools.map((t) => {
17
+ const tool = t;
18
+ return {
19
+ type: "function",
20
+ function: {
21
+ name: tool.name,
22
+ ...(tool.description != null ? { description: tool.description } : {}),
23
+ ...(tool.input_schema != null ? { parameters: tool.input_schema } : {}),
24
+ },
25
+ };
26
+ });
27
+ }
28
+ /** OpenAI tool_choice → Anthropic tool_choice */
29
+ export function mapToolChoiceOA2Ant(tc) {
30
+ if (tc === "none")
31
+ return undefined;
32
+ if (tc === "auto")
33
+ return { type: "auto" };
34
+ if (tc === "required")
35
+ return { type: "any" };
36
+ if (typeof tc === "object" && tc !== null) {
37
+ const obj = tc;
38
+ if (obj.type === "function" && obj.function) {
39
+ const fn = obj.function;
40
+ return { type: "tool", name: fn.name };
41
+ }
42
+ }
43
+ return { type: "auto" };
44
+ }
45
+ /** Anthropic tool_choice → OpenAI tool_choice */
46
+ export function mapToolChoiceAnt2OA(tc) {
47
+ if (typeof tc === "string") {
48
+ if (tc === "auto")
49
+ return "auto";
50
+ if (tc === "any")
51
+ return "required";
52
+ return "auto";
53
+ }
54
+ if (typeof tc === "object" && tc !== null) {
55
+ const obj = tc;
56
+ if (obj.type === "auto") {
57
+ if (obj.disable_parallel_tool_use)
58
+ return { type: "auto", parallel_tool_calls: false };
59
+ return "auto";
60
+ }
61
+ if (obj.type === "any")
62
+ return "required";
63
+ if (obj.type === "tool")
64
+ return { type: "function", function: { name: obj.name } };
65
+ }
66
+ return "auto";
67
+ }
@@ -0,0 +1,11 @@
1
+ import type { Transform } from "stream";
2
+ export declare class TransformCoordinator {
3
+ needsTransform(entryApiType: string, providerApiType: string): boolean;
4
+ transformRequest(body: Record<string, unknown>, entryApiType: string, providerApiType: string, model: string): {
5
+ body: Record<string, unknown>;
6
+ upstreamPath: string;
7
+ };
8
+ transformResponse(bodyStr: string, sourceApiType: string, targetApiType: string): string;
9
+ transformErrorResponse(bodyStr: string, sourceApiType: string, targetApiType: string): string;
10
+ createFormatTransform(entryApiType: string, providerApiType: string, model: string): Transform | undefined;
11
+ }
@@ -0,0 +1,32 @@
1
+ import { transformRequestBody } from "./request-transform.js";
2
+ import { transformResponseBody, transformErrorResponse } from "./response-transform.js";
3
+ import { OpenAIToAnthropicTransform } from "./stream-oa2ant.js";
4
+ import { AnthropicToOpenAITransform } from "./stream-ant2oa.js";
5
+ export class TransformCoordinator {
6
+ needsTransform(entryApiType, providerApiType) {
7
+ return entryApiType !== providerApiType;
8
+ }
9
+ transformRequest(body, entryApiType, providerApiType, model) {
10
+ const upstreamPath = providerApiType === "openai" ? "/v1/chat/completions" : "/v1/messages";
11
+ return { body: transformRequestBody(body, entryApiType, providerApiType, model), upstreamPath };
12
+ }
13
+ transformResponse(bodyStr, sourceApiType, targetApiType) {
14
+ return transformResponseBody(bodyStr, sourceApiType, targetApiType);
15
+ }
16
+ transformErrorResponse(bodyStr, sourceApiType, targetApiType) {
17
+ return transformErrorResponse(bodyStr, sourceApiType, targetApiType);
18
+ }
19
+ createFormatTransform(entryApiType, providerApiType, model) {
20
+ if (!this.needsTransform(entryApiType, providerApiType))
21
+ return undefined;
22
+ // 上游=provider格式, 客户端=entry格式
23
+ // OA provider + Ant client → OpenAIToAnthropicTransform
24
+ if (providerApiType === "openai" && entryApiType === "anthropic") {
25
+ return new OpenAIToAnthropicTransform(model);
26
+ }
27
+ if (providerApiType === "anthropic" && entryApiType === "openai") {
28
+ return new AnthropicToOpenAITransform(model);
29
+ }
30
+ return undefined;
31
+ }
32
+ }