@jsonstudio/llms 0.4.4 → 0.4.6

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 (160) hide show
  1. package/dist/conversion/codec-registry.js +11 -1
  2. package/dist/conversion/codecs/anthropic-openai-codec.d.ts +13 -0
  3. package/dist/conversion/codecs/anthropic-openai-codec.js +18 -473
  4. package/dist/conversion/codecs/gemini-openai-codec.js +91 -48
  5. package/dist/conversion/codecs/responses-openai-codec.js +9 -2
  6. package/dist/conversion/hub/format-adapters/anthropic-format-adapter.js +3 -0
  7. package/dist/conversion/hub/format-adapters/chat-format-adapter.js +3 -0
  8. package/dist/conversion/hub/format-adapters/gemini-format-adapter.js +3 -0
  9. package/dist/conversion/hub/format-adapters/responses-format-adapter.d.ts +19 -0
  10. package/dist/conversion/hub/format-adapters/responses-format-adapter.js +9 -0
  11. package/dist/conversion/hub/node-support.js +3 -1
  12. package/dist/conversion/hub/pipeline/hub-pipeline.js +37 -32
  13. package/dist/conversion/hub/response/provider-response.js +1 -1
  14. package/dist/conversion/hub/response/response-mappers.js +1 -1
  15. package/dist/conversion/hub/response/response-runtime.js +109 -10
  16. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +70 -156
  17. package/dist/conversion/hub/semantic-mappers/chat-mapper.js +63 -52
  18. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +76 -143
  19. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +40 -160
  20. package/dist/conversion/hub/standardized-bridge.js +3 -0
  21. package/dist/conversion/hub/tool-governance/rules.js +2 -2
  22. package/dist/conversion/index.d.ts +5 -0
  23. package/dist/conversion/index.js +5 -0
  24. package/dist/conversion/pipeline/codecs/v2/anthropic-openai-pipeline.d.ts +12 -0
  25. package/dist/conversion/pipeline/codecs/v2/anthropic-openai-pipeline.js +100 -0
  26. package/dist/conversion/pipeline/codecs/v2/openai-openai-pipeline.d.ts +15 -0
  27. package/dist/conversion/pipeline/codecs/v2/openai-openai-pipeline.js +174 -0
  28. package/dist/conversion/pipeline/codecs/v2/responses-openai-pipeline.d.ts +14 -0
  29. package/dist/conversion/pipeline/codecs/v2/responses-openai-pipeline.js +166 -0
  30. package/dist/conversion/pipeline/codecs/v2/shared/openai-chat-helpers.d.ts +13 -0
  31. package/dist/conversion/pipeline/codecs/v2/shared/openai-chat-helpers.js +66 -0
  32. package/dist/conversion/pipeline/hooks/adapter-context.d.ts +7 -0
  33. package/dist/conversion/pipeline/hooks/adapter-context.js +18 -0
  34. package/dist/conversion/pipeline/hooks/protocol-hooks.d.ts +67 -0
  35. package/dist/conversion/pipeline/hooks/protocol-hooks.js +1 -0
  36. package/dist/conversion/pipeline/index.d.ts +35 -0
  37. package/dist/conversion/pipeline/index.js +103 -0
  38. package/dist/conversion/pipeline/meta/meta-bag.d.ts +20 -0
  39. package/dist/conversion/pipeline/meta/meta-bag.js +81 -0
  40. package/dist/conversion/pipeline/schema/canonical-chat.d.ts +18 -0
  41. package/dist/conversion/pipeline/schema/canonical-chat.js +1 -0
  42. package/dist/conversion/pipeline/schema/index.d.ts +1 -0
  43. package/dist/conversion/pipeline/schema/index.js +1 -0
  44. package/dist/conversion/responses/responses-openai-bridge.d.ts +48 -0
  45. package/dist/conversion/responses/responses-openai-bridge.js +157 -1146
  46. package/dist/conversion/shared/anthropic-message-utils.d.ts +12 -0
  47. package/dist/conversion/shared/anthropic-message-utils.js +587 -0
  48. package/dist/conversion/shared/bridge-actions.d.ts +39 -0
  49. package/dist/conversion/shared/bridge-actions.js +709 -0
  50. package/dist/conversion/shared/bridge-conversation-store.d.ts +41 -0
  51. package/dist/conversion/shared/bridge-conversation-store.js +279 -0
  52. package/dist/conversion/shared/bridge-id-utils.d.ts +7 -0
  53. package/dist/conversion/shared/bridge-id-utils.js +42 -0
  54. package/dist/conversion/shared/bridge-instructions.d.ts +1 -0
  55. package/dist/conversion/shared/bridge-instructions.js +113 -0
  56. package/dist/conversion/shared/bridge-message-types.d.ts +39 -0
  57. package/dist/conversion/shared/bridge-message-types.js +1 -0
  58. package/dist/conversion/shared/bridge-message-utils.d.ts +22 -0
  59. package/dist/conversion/shared/bridge-message-utils.js +473 -0
  60. package/dist/conversion/shared/bridge-metadata.d.ts +1 -0
  61. package/dist/conversion/shared/bridge-metadata.js +1 -0
  62. package/dist/conversion/shared/bridge-policies.d.ts +18 -0
  63. package/dist/conversion/shared/bridge-policies.js +276 -0
  64. package/dist/conversion/shared/bridge-request-adapter.d.ts +28 -0
  65. package/dist/conversion/shared/bridge-request-adapter.js +430 -0
  66. package/dist/conversion/shared/chat-output-normalizer.d.ts +4 -0
  67. package/dist/conversion/shared/chat-output-normalizer.js +56 -0
  68. package/dist/conversion/shared/chat-request-filters.js +24 -1
  69. package/dist/conversion/shared/gemini-tool-utils.d.ts +5 -0
  70. package/dist/conversion/shared/gemini-tool-utils.js +130 -0
  71. package/dist/conversion/shared/metadata-passthrough.d.ts +11 -0
  72. package/dist/conversion/shared/metadata-passthrough.js +57 -0
  73. package/dist/conversion/shared/output-content-normalizer.d.ts +12 -0
  74. package/dist/conversion/shared/output-content-normalizer.js +119 -0
  75. package/dist/conversion/shared/reasoning-normalizer.d.ts +21 -0
  76. package/dist/conversion/shared/reasoning-normalizer.js +368 -0
  77. package/dist/conversion/shared/reasoning-tool-normalizer.d.ts +12 -0
  78. package/dist/conversion/shared/reasoning-tool-normalizer.js +132 -0
  79. package/dist/conversion/shared/reasoning-tool-parser.d.ts +10 -0
  80. package/dist/conversion/shared/reasoning-tool-parser.js +95 -0
  81. package/dist/conversion/shared/reasoning-utils.d.ts +2 -0
  82. package/dist/conversion/shared/reasoning-utils.js +42 -0
  83. package/dist/conversion/shared/responses-conversation-store.js +5 -11
  84. package/dist/conversion/shared/responses-message-utils.d.ts +15 -0
  85. package/dist/conversion/shared/responses-message-utils.js +206 -0
  86. package/dist/conversion/shared/responses-output-builder.d.ts +15 -0
  87. package/dist/conversion/shared/responses-output-builder.js +179 -0
  88. package/dist/conversion/shared/responses-output-utils.d.ts +7 -0
  89. package/dist/conversion/shared/responses-output-utils.js +108 -0
  90. package/dist/conversion/shared/responses-request-adapter.d.ts +28 -0
  91. package/dist/conversion/shared/responses-request-adapter.js +9 -40
  92. package/dist/conversion/shared/responses-response-utils.d.ts +3 -0
  93. package/dist/conversion/shared/responses-response-utils.js +209 -0
  94. package/dist/conversion/shared/responses-tool-utils.d.ts +12 -0
  95. package/dist/conversion/shared/responses-tool-utils.js +90 -0
  96. package/dist/conversion/shared/responses-types.d.ts +33 -0
  97. package/dist/conversion/shared/responses-types.js +1 -0
  98. package/dist/conversion/shared/tool-call-utils.d.ts +11 -0
  99. package/dist/conversion/shared/tool-call-utils.js +56 -0
  100. package/dist/conversion/shared/tool-governor.js +5 -0
  101. package/dist/conversion/shared/tool-mapping.d.ts +19 -0
  102. package/dist/conversion/shared/tool-mapping.js +124 -0
  103. package/dist/conversion/shared/tool-normalizers.d.ts +4 -0
  104. package/dist/conversion/shared/tool-normalizers.js +84 -0
  105. package/dist/router/virtual-router/bootstrap.js +18 -3
  106. package/dist/router/virtual-router/provider-registry.js +4 -2
  107. package/dist/router/virtual-router/types.d.ts +212 -0
  108. package/dist/sse/index.d.ts +38 -2
  109. package/dist/sse/index.js +27 -0
  110. package/dist/sse/json-to-sse/anthropic-json-to-sse-converter.d.ts +14 -0
  111. package/dist/sse/json-to-sse/anthropic-json-to-sse-converter.js +106 -73
  112. package/dist/sse/json-to-sse/chat-json-to-sse-converter.js +6 -2
  113. package/dist/sse/json-to-sse/gemini-json-to-sse-converter.d.ts +14 -0
  114. package/dist/sse/json-to-sse/gemini-json-to-sse-converter.js +99 -0
  115. package/dist/sse/json-to-sse/index.d.ts +7 -0
  116. package/dist/sse/json-to-sse/index.js +2 -0
  117. package/dist/sse/json-to-sse/sequencers/anthropic-sequencer.d.ts +13 -0
  118. package/dist/sse/json-to-sse/sequencers/anthropic-sequencer.js +150 -0
  119. package/dist/sse/json-to-sse/sequencers/chat-sequencer.d.ts +39 -0
  120. package/dist/sse/json-to-sse/sequencers/chat-sequencer.js +49 -3
  121. package/dist/sse/json-to-sse/sequencers/gemini-sequencer.d.ts +10 -0
  122. package/dist/sse/json-to-sse/sequencers/gemini-sequencer.js +95 -0
  123. package/dist/sse/json-to-sse/sequencers/responses-sequencer.js +31 -5
  124. package/dist/sse/registry/sse-codec-registry.d.ts +32 -0
  125. package/dist/sse/registry/sse-codec-registry.js +30 -1
  126. package/dist/sse/shared/reasoning-dispatcher.d.ts +10 -0
  127. package/dist/sse/shared/reasoning-dispatcher.js +25 -0
  128. package/dist/sse/shared/responses-output-normalizer.d.ts +12 -0
  129. package/dist/sse/shared/responses-output-normalizer.js +45 -0
  130. package/dist/sse/shared/serializers/anthropic-event-serializer.d.ts +2 -0
  131. package/dist/sse/shared/serializers/anthropic-event-serializer.js +9 -0
  132. package/dist/sse/shared/serializers/gemini-event-serializer.d.ts +2 -0
  133. package/dist/sse/shared/serializers/gemini-event-serializer.js +5 -0
  134. package/dist/sse/shared/serializers/index.d.ts +41 -0
  135. package/dist/sse/shared/serializers/index.js +2 -0
  136. package/dist/sse/shared/writer.d.ts +127 -0
  137. package/dist/sse/shared/writer.js +37 -1
  138. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.d.ts +11 -0
  139. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.js +92 -127
  140. package/dist/sse/sse-to-json/builders/anthropic-response-builder.d.ts +16 -0
  141. package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +151 -0
  142. package/dist/sse/sse-to-json/builders/response-builder.d.ts +165 -0
  143. package/dist/sse/sse-to-json/builders/response-builder.js +27 -6
  144. package/dist/sse/sse-to-json/chat-sse-to-json-converter.d.ts +114 -0
  145. package/dist/sse/sse-to-json/chat-sse-to-json-converter.js +79 -3
  146. package/dist/sse/sse-to-json/gemini-sse-to-json-converter.d.ts +13 -0
  147. package/dist/sse/sse-to-json/gemini-sse-to-json-converter.js +160 -0
  148. package/dist/sse/sse-to-json/index.d.ts +7 -0
  149. package/dist/sse/sse-to-json/index.js +2 -0
  150. package/dist/sse/sse-to-json/parsers/sse-parser.js +53 -1
  151. package/dist/sse/types/anthropic-types.d.ts +170 -0
  152. package/dist/sse/types/anthropic-types.js +8 -5
  153. package/dist/sse/types/chat-types.d.ts +10 -0
  154. package/dist/sse/types/chat-types.js +2 -1
  155. package/dist/sse/types/core-interfaces.d.ts +1 -1
  156. package/dist/sse/types/gemini-types.d.ts +116 -0
  157. package/dist/sse/types/gemini-types.js +5 -0
  158. package/dist/sse/types/index.d.ts +5 -2
  159. package/dist/sse/types/index.js +2 -0
  160. package/package.json +1 -1
@@ -1,3 +1,6 @@
1
+ import { OpenAIOpenAIPipelineCodec } from './pipeline/codecs/v2/openai-openai-pipeline.js';
2
+ import { AnthropicOpenAIPipelineCodec } from './pipeline/codecs/v2/anthropic-openai-pipeline.js';
3
+ import { ResponsesOpenAIPipelineCodec } from './pipeline/codecs/v2/responses-openai-pipeline.js';
1
4
  export class CodecRegistry {
2
5
  factories = new Map();
3
6
  instances = new Map();
@@ -23,5 +26,12 @@ export class CodecRegistry {
23
26
  // reimplement codecs here; the router will not be switched yet. This file is
24
27
  // prepared so later phases can register actual codecs.
25
28
  export function getDefaultCodecFactories() {
26
- return {};
29
+ return {
30
+ 'openai-openai': () => new OpenAIOpenAIPipelineCodec(),
31
+ 'openai-openai-v2': () => new OpenAIOpenAIPipelineCodec(),
32
+ 'anthropic-openai': () => new AnthropicOpenAIPipelineCodec(),
33
+ 'anthropic-openai-v2': () => new AnthropicOpenAIPipelineCodec(),
34
+ 'responses-openai': () => new ResponsesOpenAIPipelineCodec(),
35
+ 'responses-openai-v2': () => new ResponsesOpenAIPipelineCodec()
36
+ };
27
37
  }
@@ -0,0 +1,13 @@
1
+ import type { ConversionCodec, ConversionContext, ConversionProfile } from '../types.js';
2
+ import { buildAnthropicFromOpenAIChat, buildOpenAIChatFromAnthropic } from '../shared/anthropic-message-utils.js';
3
+ export { buildAnthropicFromOpenAIChat, buildOpenAIChatFromAnthropic };
4
+ export { buildAnthropicRequestFromOpenAIChat } from '../shared/anthropic-message-utils.js';
5
+ export declare class AnthropicOpenAIConversionCodec implements ConversionCodec {
6
+ private readonly _dependencies;
7
+ readonly id = "anthropic-openai";
8
+ private initialized;
9
+ constructor(_dependencies: any);
10
+ initialize(): Promise<void>;
11
+ convertRequest(payload: any, _profile: ConversionProfile, context: ConversionContext): Promise<any>;
12
+ convertResponse(payload: any, _profile: ConversionProfile, context: ConversionContext): Promise<any>;
13
+ }
@@ -1,470 +1,8 @@
1
1
  import { runStandardChatRequestFilters } from '../index.js';
2
2
  import { FilterEngine, ResponseToolTextCanonicalizeFilter, ResponseToolArgumentsStringifyFilter, ResponseFinishInvariantsFilter } from '../../filters/index.js';
3
- function isObject(v) { return !!v && typeof v === 'object' && !Array.isArray(v); }
4
- export function mapAnthropicToolsToOpenAI(tools) {
5
- const out = [];
6
- for (const t of (Array.isArray(tools) ? tools : [])) {
7
- try {
8
- const name = typeof t?.name === 'string' ? String(t.name) : undefined;
9
- const description = typeof t?.description === 'string' ? String(t.description) : undefined;
10
- const params = t?.input_schema && isObject(t.input_schema) ? t.input_schema : { type: 'object', properties: {}, additionalProperties: true };
11
- if (name) {
12
- out.push({ type: 'function', function: { name, ...(description ? { description } : {}), parameters: params } });
13
- }
14
- }
15
- catch { /* ignore invalid tool */ }
16
- }
17
- return out;
18
- }
19
- function safeJson(v) { try {
20
- return JSON.stringify(v ?? {});
21
- }
22
- catch {
23
- return '{}';
24
- } }
25
- function flattenAnthropicText(content) {
26
- if (content == null)
27
- return '';
28
- if (typeof content === 'string')
29
- return content;
30
- if (Array.isArray(content))
31
- return content.map(flattenAnthropicText).join('');
32
- if (typeof content === 'object') {
33
- const t = String(content.type || '').toLowerCase();
34
- if (t === 'text' && typeof content.text === 'string')
35
- return String(content.text);
36
- if (Array.isArray(content.content))
37
- return content.content.map(flattenAnthropicText).join('');
38
- if (typeof content.content === 'string')
39
- return String(content.content);
40
- }
41
- return '';
42
- }
43
- export function buildOpenAIChatFromAnthropic(payload) {
44
- const newMessages = [];
45
- // 支持 string 或 Anthropic content blocks 数组两种 system 形状
46
- const systemField = payload?.system;
47
- const systemBlocks = Array.isArray(systemField)
48
- ? systemField
49
- : (systemField !== undefined && systemField !== null ? [systemField] : []);
50
- for (const block of systemBlocks) {
51
- const text = flattenAnthropicText(block).trim();
52
- if (text) {
53
- newMessages.push({ role: 'system', content: text });
54
- }
55
- }
56
- const msgs = Array.isArray(payload?.messages) ? payload.messages : [];
57
- for (const m of msgs) {
58
- if (!m || typeof m !== 'object')
59
- continue;
60
- const role = typeof m.role === 'string' ? String(m.role) : 'user';
61
- const content = m.content;
62
- if (!Array.isArray(content)) {
63
- const text = flattenAnthropicText(content);
64
- if (text)
65
- newMessages.push({ role, content: text });
66
- continue;
67
- }
68
- const textParts = [];
69
- const toolCalls = [];
70
- const toolResults = [];
71
- for (const block of content) {
72
- if (!block || typeof block !== 'object')
73
- continue;
74
- const t = String(block.type || '').toLowerCase();
75
- if (t === 'text' && typeof block.text === 'string') {
76
- const s = block.text.trim();
77
- if (s)
78
- textParts.push(s);
79
- }
80
- else if (t === 'tool_use') {
81
- const name = typeof block.name === 'string' ? String(block.name) : undefined;
82
- const id = typeof block.id === 'string' ? String(block.id) : undefined;
83
- const input = block.input ?? {};
84
- if (name) {
85
- const args = safeJson(input);
86
- toolCalls.push({ id, type: 'function', function: { name, arguments: args } });
87
- }
88
- }
89
- else if (t === 'tool_result') {
90
- const callId = block.tool_call_id
91
- || block.call_id
92
- || block.tool_use_id
93
- || block.id
94
- || undefined;
95
- let contentStr = '';
96
- const c = block.content;
97
- if (typeof c === 'string')
98
- contentStr = c;
99
- else if (c != null) {
100
- try {
101
- contentStr = JSON.stringify(c);
102
- }
103
- catch {
104
- contentStr = String(c);
105
- }
106
- }
107
- toolResults.push({ role: 'tool', tool_call_id: callId, content: contentStr });
108
- }
109
- }
110
- if (textParts.length > 0 || toolCalls.length > 0) {
111
- const msg = { role, content: textParts.join('\n') };
112
- if (toolCalls.length)
113
- msg.tool_calls = toolCalls;
114
- newMessages.push(msg);
115
- }
116
- for (const tr of toolResults)
117
- newMessages.push(tr);
118
- }
119
- const request = { messages: newMessages };
120
- if (payload && typeof payload === 'object') {
121
- if (typeof payload.model === 'string')
122
- request.model = payload.model;
123
- if (typeof payload.max_tokens === 'number')
124
- request.max_tokens = payload.max_tokens;
125
- if (typeof payload.temperature === 'number')
126
- request.temperature = payload.temperature;
127
- if (typeof payload.top_p === 'number')
128
- request.top_p = payload.top_p;
129
- if (typeof payload.stream === 'boolean')
130
- request.stream = payload.stream;
131
- if (payload.tool_choice !== undefined)
132
- request.tool_choice = payload.tool_choice;
133
- if (Array.isArray(payload.tools)) {
134
- request.tools = mapAnthropicToolsToOpenAI(payload.tools);
135
- }
136
- }
137
- return request;
138
- }
139
- function buildAnthropicFromOpenAIChat(oa) {
140
- // 将 OpenAI finish_reason 映射为 Anthropic stop_reason
141
- const mapFinishReason = (reason) => {
142
- const r = (reason || '').toString();
143
- const mapping = {
144
- stop: 'end_turn',
145
- length: 'max_tokens',
146
- tool_calls: 'tool_use',
147
- content_filter: 'stop_sequence',
148
- function_call: 'tool_use'
149
- };
150
- return mapping[r] || 'end_turn';
151
- };
152
- const choices = Array.isArray(oa?.choices) ? oa.choices : [];
153
- const primary = (choices[0] && typeof choices[0] === 'object') ? choices[0] : {};
154
- const msg = primary?.message || {};
155
- const role = String(msg?.role || 'assistant');
156
- const blocks = [];
157
- const content = msg?.content;
158
- // 保持与请求侧对称:即便 content 为空字符串也显式生成 text block,
159
- // 这样 AnthropicMessage → Chat → AnthropicMessage 闭环时,空文本块不会被静默丢弃
160
- // (例如 provider 返回的 [{type:'text',text:''},{type:'tool_use',...}] 应在闭环后保持同样结构)。
161
- if (typeof content === 'string') {
162
- blocks.push({ type: 'text', text: content });
163
- }
164
- if (Array.isArray(content)) {
165
- const text = content.map((p) => (typeof p?.text === 'string' ? p.text : (typeof p === 'string' ? p : ''))).filter(Boolean).join('');
166
- if (text)
167
- blocks.push({ type: 'text', text: text });
168
- }
169
- const tool_calls = Array.isArray(msg?.tool_calls) ? msg.tool_calls : [];
170
- for (const tc of tool_calls) {
171
- try {
172
- const id = String(tc?.id || `call_${Math.random().toString(36).slice(2, 10)}`);
173
- const fn = tc?.function || {};
174
- const name = String(fn?.name || 'tool');
175
- const argsRaw = fn?.arguments;
176
- let input;
177
- if (typeof argsRaw === 'string') {
178
- try {
179
- input = JSON.parse(argsRaw);
180
- }
181
- catch {
182
- input = { _raw: argsRaw };
183
- }
184
- }
185
- else {
186
- input = (argsRaw ?? {});
187
- }
188
- blocks.push({ type: 'tool_use', id, name, input });
189
- }
190
- catch { /* ignore */ }
191
- }
192
- const u = (oa?.usage ?? {});
193
- const input_tokens = Number(u.prompt_tokens ?? u.input_tokens ?? 0);
194
- const output_tokens = Number(u.completion_tokens ?? u.output_tokens ?? 0);
195
- let stop_reason = mapFinishReason(oa?.choices?.[0]?.finish_reason);
196
- // 对于“仅选择工具”的响应,即便 Chat finish_reason 被归一到 'stop',
197
- // 只要存在 tool_calls,就应在 Anthropic 侧显式标记为 tool_use,
198
- // 与 Responses 路径保持一致(Responses 会在 required_action 中保留 tool 调用)。
199
- if (tool_calls.length > 0 && stop_reason === 'end_turn') {
200
- stop_reason = 'tool_use';
201
- }
202
- return {
203
- id: `resp_${Date.now()}`,
204
- type: 'message',
205
- role,
206
- model: String(oa?.model || 'unknown'),
207
- created: Math.floor(Date.now() / 1000),
208
- content: blocks,
209
- usage: (input_tokens || output_tokens) ? { input_tokens, output_tokens } : undefined,
210
- stop_reason
211
- };
212
- }
213
- /**
214
- * ChatRequest → AnthropicRequest 编码器(请求侧)
215
- *
216
- * 用于在 v3 管线中,将 canonical Chat 请求编码为 Anthropic /v1/messages wire 形状:
217
- * - messages: Chat messages → Anthropic content blocks(仅 text,工具调用由 tools 列表承载)
218
- * - tools: Chat tools[].function → Anthropic { name, description, input_schema }
219
- * - 参数:max_tokens/temperature/top_p/stop → Anthropic 对应字段
220
- */
221
- export function buildAnthropicRequestFromOpenAIChat(chatReq) {
222
- const model = String(chatReq?.model || 'unknown');
223
- const messages = [];
224
- const collectText = (val) => {
225
- if (!val)
226
- return '';
227
- if (typeof val === 'string')
228
- return val;
229
- if (Array.isArray(val))
230
- return val.map(collectText).join('');
231
- if (typeof val === 'object') {
232
- if (typeof val.text === 'string')
233
- return String(val.text);
234
- if (Array.isArray(val.content))
235
- return collectText(val.content);
236
- }
237
- return '';
238
- };
239
- const msgs = Array.isArray(chatReq?.messages) ? chatReq.messages : [];
240
- // 预扫描 Chat 历史中的 tool_calls,只有存在配对的 tool_use/id 时,
241
- // 才允许在后续请求里生成对应的 tool_result 块,避免“凭空”制造
242
- // 未配对的 tool_result(上一轮未执行工具却发送 tool_result)。
243
- const knownToolCallIds = new Set();
244
- for (const m of msgs) {
245
- if (!m || typeof m !== 'object')
246
- continue;
247
- const role = String(m.role || 'user');
248
- if (role !== 'assistant')
249
- continue;
250
- const toolCalls = m.tool_calls;
251
- if (!Array.isArray(toolCalls))
252
- continue;
253
- for (const tc of toolCalls) {
254
- if (!tc || typeof tc !== 'object')
255
- continue;
256
- const id = tc.id;
257
- if (typeof id === 'string' && id) {
258
- knownToolCallIds.add(id);
259
- }
260
- }
261
- }
262
- // 兼容已有 Anthropic 形状:如果顶层已经存在 system[],优先保留其文本,
263
- // 后续再追加来自 Chat system 消息的文本,避免信息丢失。
264
- const systemBlocks = [];
265
- const pushSystemBlock = (text) => {
266
- const trimmed = text.trim();
267
- if (trimmed)
268
- systemBlocks.push({ type: 'text', text: trimmed });
269
- };
270
- try {
271
- const sys = chatReq?.system;
272
- const ingestSystem = (val) => {
273
- if (!val)
274
- return;
275
- if (typeof val === 'string') {
276
- pushSystemBlock(val);
277
- return;
278
- }
279
- if (Array.isArray(val)) {
280
- for (const entry of val)
281
- ingestSystem(entry);
282
- return;
283
- }
284
- if (typeof val === 'object') {
285
- if (typeof val.text === 'string') {
286
- pushSystemBlock(String(val.text));
287
- return;
288
- }
289
- if (Array.isArray(val.content)) {
290
- ingestSystem(val.content);
291
- return;
292
- }
293
- }
294
- };
295
- ingestSystem(sys);
296
- }
297
- catch {
298
- // ignore system pre-scan errors
299
- }
300
- for (const m of msgs) {
301
- if (!m || typeof m !== 'object')
302
- continue;
303
- const role = String(m.role || 'user');
304
- const text = collectText(m.content).trim();
305
- // Chat 中的 system 消息应折叠到顶层 system[],而不是作为 messages[0].role='system' 发送,
306
- // 以满足 GLM Anthropic 兼容端“messages[*].role 只能是 user/assistant”的约束。
307
- if (role === 'system') {
308
- if (text)
309
- pushSystemBlock(text);
310
- continue;
311
- }
312
- // Chat 中的 role:'tool' 消息在 Anthropic 中表示为 user 侧的 tool_result block。
313
- // 为避免出现 role:'tool' 的顶层消息(会触发 422),在这里专门映射:
314
- if (role === 'tool') {
315
- const rawToolCallId = m.tool_call_id ||
316
- m.call_id ||
317
- m.tool_use_id ||
318
- m.id ||
319
- undefined;
320
- const toolCallId = rawToolCallId ? String(rawToolCallId) : undefined;
321
- const hasPair = toolCallId ? knownToolCallIds.has(toolCallId) : false;
322
- // 如果当前请求历史中不存在匹配的 tool_use/id,则视为未配对的
323
- // tool_result —— 不再编码为 Anthropic 的 tool_result,而是退化为
324
- // 普通 user 文本,避免向上游发送“凭空”的工具结果。
325
- if (!hasPair) {
326
- if (text) {
327
- messages.push({
328
- role: 'user',
329
- content: [{ type: 'text', text }]
330
- });
331
- }
332
- continue;
333
- }
334
- const block = {
335
- type: 'tool_result',
336
- content: text
337
- };
338
- if (toolCallId) {
339
- block.tool_use_id = toolCallId;
340
- }
341
- messages.push({
342
- role: 'user',
343
- content: [block]
344
- });
345
- continue;
346
- }
347
- const blocks = [];
348
- if (text) {
349
- blocks.push({ type: 'text', text });
350
- }
351
- // 将 Chat 段中的 tool_calls 映射回 Anthropic 的 tool_use block,保持
352
- // Anthropic→Chat→Anthropic 闭环时 tool_use 的数量和分组不变:
353
- // - role 保持为 assistant;
354
- // - 每个 tool_call 生成一个 {type:'tool_use', id,name,input} block。
355
- const toolCalls = Array.isArray(m.tool_calls) ? m.tool_calls : [];
356
- for (const tc of toolCalls) {
357
- if (!tc || typeof tc !== 'object')
358
- continue;
359
- const id = String(tc.id || `call_${Math.random().toString(36).slice(2, 10)}`);
360
- const fn = tc.function || {};
361
- const name = typeof fn.name === 'string' ? String(fn.name) : 'tool';
362
- const argsRaw = fn.arguments;
363
- let input;
364
- if (typeof argsRaw === 'string') {
365
- try {
366
- input = JSON.parse(argsRaw);
367
- }
368
- catch {
369
- input = { _raw: argsRaw };
370
- }
371
- }
372
- else {
373
- input = (argsRaw ?? {});
374
- }
375
- blocks.push({ type: 'tool_use', id, name, input });
376
- }
377
- if (blocks.length > 0) {
378
- messages.push({ role, content: blocks });
379
- }
380
- }
381
- const toolsArr = [];
382
- const tools = Array.isArray(chatReq?.tools) ? chatReq.tools : [];
383
- for (const t of tools) {
384
- if (!t || typeof t !== 'object')
385
- continue;
386
- const fn = t.function || {};
387
- const chatName = typeof fn.name === 'string' ? fn.name : undefined;
388
- const anthName = !chatName && typeof t.name === 'string' ? String(t.name) : undefined;
389
- // Chat 形状:tools[].function.parameters
390
- if (chatName) {
391
- const description = typeof fn.description === 'string' ? fn.description : undefined;
392
- const params = fn.parameters || {};
393
- const input_schema = {
394
- type: 'object',
395
- properties: params.properties || {}
396
- };
397
- if (Array.isArray(params.required)) {
398
- input_schema.required = params.required;
399
- }
400
- if (typeof params.additionalProperties === 'boolean') {
401
- input_schema.additionalProperties = params.additionalProperties;
402
- }
403
- const toolDef = { name: chatName, input_schema };
404
- if (description)
405
- toolDef.description = description;
406
- toolsArr.push(toolDef);
407
- continue;
408
- }
409
- // 已是 Anthropic 形状:{ name, input_schema }
410
- if (anthName) {
411
- const description = typeof t.description === 'string' ? String(t.description) : undefined;
412
- const schemaSrc = t.input_schema;
413
- let input_schema;
414
- if (schemaSrc && typeof schemaSrc === 'object') {
415
- try {
416
- input_schema = JSON.parse(JSON.stringify(schemaSrc));
417
- }
418
- catch {
419
- input_schema = schemaSrc;
420
- }
421
- }
422
- else {
423
- input_schema = { type: 'object', properties: {}, additionalProperties: true };
424
- }
425
- const toolDef = { name: anthName, input_schema };
426
- if (description)
427
- toolDef.description = description;
428
- toolsArr.push(toolDef);
429
- }
430
- }
431
- const out = { model };
432
- // 从 ChatRequest 中恢复 Anthropic system[](某些 Anthropic 兼容端要求 messages[*].role 只能是 user/assistant)
433
- if (systemBlocks.length) {
434
- out.system = systemBlocks;
435
- }
436
- out.messages = messages;
437
- if (toolsArr.length > 0) {
438
- out.tools = toolsArr;
439
- }
440
- // 透传 metadata(如果存在),避免上游诊断/路由信息丢失
441
- try {
442
- if (chatReq && typeof chatReq === 'object' && chatReq.metadata && typeof chatReq.metadata === 'object') {
443
- out.metadata = JSON.parse(JSON.stringify(chatReq.metadata));
444
- }
445
- }
446
- catch {
447
- // best-effort
448
- }
449
- // 参数映射:尽量保持与 AnthropicInputNode.extractParameters 对称
450
- const mt = Number(chatReq?.max_tokens ?? chatReq?.maxTokens ?? NaN);
451
- if (Number.isFinite(mt) && mt > 0)
452
- out.max_tokens = mt;
453
- if (typeof chatReq?.temperature === 'number')
454
- out.temperature = chatReq.temperature;
455
- if (typeof chatReq?.top_p === 'number')
456
- out.top_p = chatReq.top_p;
457
- if (typeof chatReq?.stream === 'boolean')
458
- out.stream = chatReq.stream;
459
- const stop = chatReq?.stop;
460
- if (typeof stop === 'string' && stop.trim()) {
461
- out.stop_sequences = [stop.trim()];
462
- }
463
- else if (Array.isArray(stop) && stop.length > 0) {
464
- out.stop_sequences = stop.map((s) => String(s)).filter(Boolean);
465
- }
466
- return out;
467
- }
3
+ import { buildAnthropicFromOpenAIChat, buildOpenAIChatFromAnthropic, mapAnthropicToolsToChat } from '../shared/anthropic-message-utils.js';
4
+ export { buildAnthropicFromOpenAIChat, buildOpenAIChatFromAnthropic };
5
+ export { buildAnthropicRequestFromOpenAIChat } from '../shared/anthropic-message-utils.js';
468
6
  export class AnthropicOpenAIConversionCodec {
469
7
  _dependencies;
470
8
  id = 'anthropic-openai';
@@ -475,26 +13,32 @@ export class AnthropicOpenAIConversionCodec {
475
13
  }
476
14
  async initialize() { this.initialized = true; }
477
15
  async convertRequest(payload, _profile, context) {
478
- if (!this.initialized)
16
+ if (!this.initialized) {
479
17
  await this.initialize();
18
+ }
480
19
  const model = String(payload?.model || 'unknown');
481
20
  const { messages } = buildOpenAIChatFromAnthropic(payload);
482
21
  const out = { model, messages };
483
22
  // 最小必要的形状转换:Anthropic tools → OpenAI Chat function 工具
484
23
  try {
485
- if (Array.isArray(payload?.tools)) {
486
- out.tools = mapAnthropicToolsToOpenAI(payload.tools);
24
+ const normalizedTools = mapAnthropicToolsToChat(payload?.tools);
25
+ if (normalizedTools) {
26
+ out.tools = normalizedTools;
487
27
  }
488
28
  }
489
29
  catch { /* keep tools undefined on failure */ }
490
- if (Array.isArray(payload?.stop_sequences))
30
+ if (Array.isArray(payload?.stop_sequences)) {
491
31
  out.stop = payload.stop_sequences;
492
- if (typeof payload?.temperature === 'number')
32
+ }
33
+ if (typeof payload?.temperature === 'number') {
493
34
  out.temperature = payload.temperature;
494
- if (typeof payload?.top_p === 'number')
35
+ }
36
+ if (typeof payload?.top_p === 'number') {
495
37
  out.top_p = payload.top_p;
496
- if (typeof payload?.max_tokens === 'number')
38
+ }
39
+ if (typeof payload?.max_tokens === 'number') {
497
40
  out.max_tokens = payload.max_tokens;
41
+ }
498
42
  // 不设置默认 tool_choice;统一由 Chat 后半段治理
499
43
  // 统一流控:非流式由核心过滤器处理;此处不设置 stream
500
44
  // 统一走 Chat 请求过滤链路,使 /v1/messages 与 /v1/chat 在工具治理上保持一致
@@ -507,8 +51,9 @@ export class AnthropicOpenAIConversionCodec {
507
51
  return runStandardChatRequestFilters(out, _profile, ctxForFilters);
508
52
  }
509
53
  async convertResponse(payload, _profile, context) {
510
- if (!this.initialized)
54
+ if (!this.initialized) {
511
55
  await this.initialize();
56
+ }
512
57
  // 如果是 /v1/messages 且请求端声明了流式(或需要伪流式),合成 Anthropic SSE 流并透传给上层
513
58
  // 对非流式 JSON 响应,先在 OpenAI Chat canonical 上运行统一的响应工具治理,
514
59
  // 再映射回 Anthropic Messages 形状,保证与 /v1/chat 路径的行为一致。