protocol-proxy 2.3.4 → 2.5.0

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.
@@ -1,329 +1,368 @@
1
- /**
2
- * OpenAI → Anthropic 协议转换
3
- */
4
-
5
- const { encodeAnthropicEvent } = require('./sse-helpers');
6
-
7
- // ==================== 请求转换 ====================
8
-
9
- function convertRequest(body, targetModel) {
10
- const result = {
11
- model: targetModel,
12
- max_tokens: body.max_tokens || 4096,
13
- stream: body.stream || false,
14
- };
15
-
16
- if (body.temperature !== undefined) result.temperature = body.temperature;
17
- if (body.top_p !== undefined) result.top_p = body.top_p;
18
- if (body.stop !== undefined) result.stop_sequences = Array.isArray(body.stop) ? body.stop : [body.stop];
19
-
20
- // 处理 messages 和 system
21
- const systemMessages = [];
22
- const otherMessages = [];
23
-
24
- for (const msg of (body.messages || [])) {
25
- if (msg.role === 'system') {
26
- systemMessages.push(typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content));
27
- } else {
28
- otherMessages.push(msg);
29
- }
30
- }
31
-
32
- if (systemMessages.length > 0) {
33
- result.system = systemMessages.join('\n\n');
34
- }
35
-
36
- // 转换消息角色和内容
37
- result.messages = otherMessages.map(msg => convertMessage(msg));
38
-
39
- // 转换 tools / functions
40
- if (body.tools && Array.isArray(body.tools)) {
41
- result.tools = body.tools.map(t => convertTool(t));
42
- } else if (body.functions && Array.isArray(body.functions)) {
43
- // 旧版 functions 转 tools
44
- result.tools = body.functions.map(f => ({
45
- name: f.name,
46
- description: f.description,
47
- input_schema: f.parameters || { type: 'object', properties: {} },
48
- }));
49
- }
50
-
51
- // 转换 tool_choice
52
- if (body.tool_choice) {
53
- result.tool_choice = convertToolChoice(body.tool_choice);
54
- }
55
-
56
- return result;
57
- }
58
-
59
- function convertMessage(msg) {
60
- if (msg.role === 'tool') {
61
- // OpenAI tool result Anthropic tool_result content block
62
- return {
63
- role: 'user',
64
- content: [{
65
- type: 'tool_result',
66
- tool_use_id: msg.tool_call_id,
67
- content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
68
- }],
69
- };
70
- }
71
-
72
- if (msg.role === 'assistant' && msg.tool_calls) {
73
- // assistant message with tool_calls → assistant with tool_use blocks
74
- const content = [];
75
- if (msg.content) {
76
- content.push({ type: 'text', text: msg.content });
77
- }
78
- for (const tc of msg.tool_calls) {
79
- let input = {};
80
- try {
81
- input = tc.function?.arguments ? JSON.parse(tc.function.arguments) : {};
82
- } catch {
83
- input = {};
84
- }
85
- content.push({
86
- type: 'tool_use',
87
- id: tc.id,
88
- name: tc.function?.name,
89
- input,
90
- });
91
- }
92
- return { role: 'assistant', content };
93
- }
94
-
95
- // 普通消息
96
- return {
97
- role: msg.role,
98
- content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
99
- };
100
- }
101
-
102
- function convertTool(tool) {
103
- return {
104
- name: tool.function?.name || tool.name,
105
- description: tool.function?.description || tool.description,
106
- input_schema: tool.function?.parameters || tool.input_schema || { type: 'object', properties: {} },
107
- };
108
- }
109
-
110
- function convertToolChoice(tc) {
111
- if (tc === 'auto') return { type: 'auto' };
112
- if (tc === 'none') return { type: 'none' };
113
- if (tc === 'required') return { type: 'any' };
114
- if (typeof tc === 'object' && tc.type === 'function') {
115
- return { type: 'tool', name: tc.function?.name };
116
- }
117
- return { type: 'auto' };
118
- }
119
-
120
- // ==================== 响应转换 ====================
121
-
122
- function convertResponse(anthropicBody) {
123
- const choice = {
124
- index: 0,
125
- message: {
126
- role: 'assistant',
127
- content: '',
128
- },
129
- finish_reason: null,
130
- };
131
-
132
- // 提取文本内容和 tool_calls
133
- const toolCalls = [];
134
- const textParts = [];
135
-
136
- for (const block of (anthropicBody.content || [])) {
137
- if (block.type === 'text') {
138
- textParts.push(block.text);
139
- } else if (block.type === 'tool_use') {
140
- toolCalls.push({
141
- id: block.id,
142
- type: 'function',
143
- function: {
144
- name: block.name,
145
- arguments: JSON.stringify(block.input || {}),
146
- },
147
- });
148
- }
149
- }
150
-
151
- choice.message.content = textParts.join('');
152
- if (toolCalls.length > 0) {
153
- choice.message.tool_calls = toolCalls;
154
- }
155
-
156
- // 映射 stop_reason
157
- choice.finish_reason = mapStopReason(anthropicBody.stop_reason);
158
-
159
- return {
160
- id: anthropicBody.id,
161
- object: 'chat.completion',
162
- created: Math.floor(Date.now() / 1000),
163
- model: anthropicBody.model,
164
- choices: [choice],
165
- usage: anthropicBody.usage ? {
166
- prompt_tokens: anthropicBody.usage.input_tokens,
167
- completion_tokens: anthropicBody.usage.output_tokens,
168
- total_tokens: (anthropicBody.usage.input_tokens || 0) + (anthropicBody.usage.output_tokens || 0),
169
- } : undefined,
170
- };
171
- }
172
-
173
- function mapStopReason(reason) {
174
- if (!reason) return null;
175
- if (reason === 'end_turn') return 'stop';
176
- if (reason === 'max_tokens') return 'length';
177
- if (reason === 'tool_use') return 'tool_calls';
178
- if (reason === 'stop_sequence') return 'stop';
179
- return reason;
180
- }
181
-
182
- // ==================== SSE 流式转换 ====================
183
-
184
- function createSSEConverter(targetModel) {
185
- const state = {
186
- messageId: null,
187
- blockType: null,
188
- blockIndex: 0,
189
- toolUseId: null,
190
- toolName: null,
191
- toolCallIndex: -1,
192
- sentRole: false,
193
- sentToolInit: false,
194
- buffer: '',
195
- };
196
-
197
- return {
198
- convertChunk(chunkText) {
199
- let output = '';
200
- state.buffer += chunkText;
201
- const lines = state.buffer.split('\n');
202
- state.buffer = lines.pop() || ''; // 保留不完整的最后一行
203
-
204
- for (const line of lines) {
205
- const converted = processLine(line.trim(), state, targetModel);
206
- if (converted) output += converted;
207
- }
208
-
209
- return output;
210
- },
211
- flush() {
212
- // 结束时不发送额外内容,[DONE] 在 finish_reason 时已经发送
213
- return '';
214
- },
215
- };
216
- }
217
-
218
- function processLine(line, state, targetModel) {
219
- if (!line.startsWith('data:')) return '';
220
- const dataStr = line.slice(5).trim();
221
- if (dataStr === '[DONE]') return '';
222
-
223
- let event;
224
- try {
225
- event = JSON.parse(dataStr);
226
- } catch {
227
- return '';
228
- }
229
-
230
- if (!event || !event.type) return '';
231
-
232
- switch (event.type) {
233
- case 'message_start': {
234
- state.messageId = event.message?.id;
235
- state.sentRole = false;
236
- return '';
237
- }
238
-
239
- case 'content_block_start': {
240
- state.blockType = event.content_block?.type;
241
- state.blockIndex = event.index;
242
- state.sentToolInit = false;
243
-
244
- if (state.blockType === 'tool_use') {
245
- state.toolUseId = event.content_block.id;
246
- state.toolName = event.content_block.name;
247
- state.toolCallIndex = (state.toolCallIndex || 0) + 1;
248
- }
249
- return '';
250
- }
251
-
252
- case 'content_block_delta': {
253
- const delta = event.delta;
254
- if (!delta) return '';
255
-
256
- // 发送 role(如果是第一个内容块)
257
- let prefix = '';
258
- if (!state.sentRole) {
259
- prefix = encodeOpenAIChunk(state.messageId, targetModel, { role: 'assistant' });
260
- state.sentRole = true;
261
- }
262
-
263
- if (delta.type === 'text_delta' && delta.text) {
264
- return prefix + encodeOpenAIChunk(state.messageId, targetModel, { content: delta.text });
265
- }
266
-
267
- if (delta.type === 'input_json_delta' && delta.partial_json !== undefined) {
268
- if (!state.sentToolInit) {
269
- state.sentToolInit = true;
270
- const toolCallChunk = {
271
- tool_calls: [{
272
- index: state.toolCallIndex,
273
- id: state.toolUseId,
274
- type: 'function',
275
- function: { name: state.toolName, arguments: delta.partial_json },
276
- }],
277
- };
278
- return prefix + encodeOpenAIChunk(state.messageId, targetModel, toolCallChunk);
279
- }
280
- return prefix + encodeOpenAIChunk(state.messageId, targetModel, {
281
- tool_calls: [{ index: state.toolCallIndex, function: { arguments: delta.partial_json } }],
282
- });
283
- }
284
-
285
- return prefix;
286
- }
287
-
288
- case 'content_block_stop': {
289
- state.blockType = null;
290
- return '';
291
- }
292
-
293
- case 'message_delta': {
294
- const stopReason = event.delta?.stop_reason;
295
- if (stopReason) {
296
- return encodeOpenAIChunk(state.messageId, targetModel, {}, mapStopReason(stopReason));
297
- }
298
- return '';
299
- }
300
-
301
- case 'message_stop': {
302
- return 'data: [DONE]\n\n';
303
- }
304
-
305
- default:
306
- return '';
307
- }
308
- }
309
-
310
- function encodeOpenAIChunk(id, model, delta, finishReason = null) {
311
- const chunk = {
312
- id: id || 'chatcmpl-proxy',
313
- object: 'chat.completion.chunk',
314
- created: Math.floor(Date.now() / 1000),
315
- model: model || 'proxy-model',
316
- choices: [{
317
- index: 0,
318
- delta,
319
- finish_reason: finishReason,
320
- }],
321
- };
322
- return `data: ${JSON.stringify(chunk)}\n\n`;
323
- }
324
-
325
- module.exports = {
326
- convertRequest,
327
- convertResponse,
328
- createSSEConverter,
329
- };
1
+ /**
2
+ * OpenAI → Anthropic 协议转换
3
+ */
4
+
5
+ const { encodeAnthropicEvent } = require('./sse-helpers');
6
+
7
+ // ==================== 请求转换 ====================
8
+
9
+ function convertRequest(body, targetModel) {
10
+ const result = {
11
+ model: targetModel,
12
+ max_tokens: body.max_tokens || 4096,
13
+ stream: body.stream || false,
14
+ };
15
+
16
+ if (body.temperature !== undefined) result.temperature = body.temperature;
17
+ if (body.top_p !== undefined) result.top_p = body.top_p;
18
+ if (body.stop !== undefined) result.stop_sequences = Array.isArray(body.stop) ? body.stop : [body.stop];
19
+
20
+ // 处理 messages 和 system
21
+ const systemMessages = [];
22
+ const otherMessages = [];
23
+
24
+ for (const msg of (body.messages || [])) {
25
+ if (msg.role === 'system') {
26
+ systemMessages.push(typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content));
27
+ } else {
28
+ otherMessages.push(msg);
29
+ }
30
+ }
31
+
32
+ if (systemMessages.length > 0) {
33
+ result.system = systemMessages.join('\n\n');
34
+ }
35
+
36
+ // 转换消息角色和内容
37
+ result.messages = otherMessages.map(msg => convertMessage(msg));
38
+
39
+ // 转换 tools / functions
40
+ if (body.tools && Array.isArray(body.tools)) {
41
+ result.tools = body.tools.map(t => convertTool(t));
42
+ } else if (body.functions && Array.isArray(body.functions)) {
43
+ // 旧版 functions 转 tools
44
+ result.tools = body.functions.map(f => ({
45
+ name: f.name,
46
+ description: f.description,
47
+ input_schema: f.parameters || { type: 'object', properties: {} },
48
+ }));
49
+ }
50
+
51
+ // 转换 tool_choice
52
+ if (body.tool_choice) {
53
+ result.tool_choice = convertToolChoice(body.tool_choice);
54
+ }
55
+
56
+ return result;
57
+ }
58
+
59
+ function convertContentBlock(block) {
60
+ if (block.type === 'text') {
61
+ return { type: 'text', text: block.text || '' };
62
+ }
63
+ if (block.type === 'image_url' && block.image_url?.url) {
64
+ const url = block.image_url.url;
65
+ if (url.startsWith('data:')) {
66
+ const match = url.match(/^data:([^;]+);base64,(.+)$/);
67
+ if (match) {
68
+ return { type: 'image', source: { type: 'base64', media_type: match[1], data: match[2] } };
69
+ }
70
+ }
71
+ return { type: 'image', source: { type: 'url', url } };
72
+ }
73
+ return null;
74
+ }
75
+
76
+ function convertMessage(msg) {
77
+ if (msg.role === 'tool') {
78
+ // OpenAI tool result Anthropic tool_result content block
79
+ return {
80
+ role: 'user',
81
+ content: [{
82
+ type: 'tool_result',
83
+ tool_use_id: msg.tool_call_id,
84
+ content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
85
+ }],
86
+ };
87
+ }
88
+
89
+ if (msg.role === 'assistant' && msg.tool_calls) {
90
+ // assistant message with tool_calls → assistant with tool_use blocks
91
+ const content = [];
92
+ if (msg.reasoning_content) {
93
+ content.push({ type: 'thinking', thinking: msg.reasoning_content });
94
+ }
95
+ if (msg.content) {
96
+ content.push({ type: 'text', text: msg.content });
97
+ }
98
+ for (const tc of msg.tool_calls) {
99
+ let input = {};
100
+ try {
101
+ input = tc.function?.arguments ? JSON.parse(tc.function.arguments) : {};
102
+ } catch {
103
+ input = {};
104
+ }
105
+ content.push({
106
+ type: 'tool_use',
107
+ id: tc.id,
108
+ name: tc.function?.name,
109
+ input,
110
+ });
111
+ }
112
+ return { role: 'assistant', content };
113
+ }
114
+
115
+ // 普通消息 content 为数组时逐块转换
116
+ if (Array.isArray(msg.content)) {
117
+ const content = msg.content.map(convertContentBlock).filter(Boolean);
118
+ if (msg.role === 'assistant' && msg.reasoning_content) {
119
+ content.unshift({ type: 'thinking', thinking: msg.reasoning_content });
120
+ }
121
+ return { role: msg.role, content };
122
+ }
123
+
124
+ // assistant 消息带 reasoning_content
125
+ if (msg.role === 'assistant' && msg.reasoning_content) {
126
+ return {
127
+ role: 'assistant',
128
+ content: [
129
+ { type: 'thinking', thinking: msg.reasoning_content },
130
+ { type: 'text', text: typeof msg.content === 'string' ? msg.content : '' },
131
+ ],
132
+ };
133
+ }
134
+
135
+ return {
136
+ role: msg.role,
137
+ content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
138
+ };
139
+ }
140
+
141
+ function convertTool(tool) {
142
+ return {
143
+ name: tool.function?.name || tool.name,
144
+ description: tool.function?.description || tool.description,
145
+ input_schema: tool.function?.parameters || tool.input_schema || { type: 'object', properties: {} },
146
+ };
147
+ }
148
+
149
+ function convertToolChoice(tc) {
150
+ if (tc === 'auto') return { type: 'auto' };
151
+ if (tc === 'none') return { type: 'none' };
152
+ if (tc === 'required') return { type: 'any' };
153
+ if (typeof tc === 'object' && tc.type === 'function') {
154
+ return { type: 'tool', name: tc.function?.name };
155
+ }
156
+ return { type: 'auto' };
157
+ }
158
+
159
+ // ==================== 响应转换 ====================
160
+
161
+ function convertResponse(anthropicBody) {
162
+ const choice = {
163
+ index: 0,
164
+ message: {
165
+ role: 'assistant',
166
+ content: '',
167
+ },
168
+ finish_reason: null,
169
+ };
170
+
171
+ // 提取文本内容和 tool_calls
172
+ const toolCalls = [];
173
+ const textParts = [];
174
+
175
+ for (const block of (anthropicBody.content || [])) {
176
+ if (block.type === 'text') {
177
+ textParts.push(block.text);
178
+ } else if (block.type === 'tool_use') {
179
+ toolCalls.push({
180
+ id: block.id,
181
+ type: 'function',
182
+ function: {
183
+ name: block.name,
184
+ arguments: JSON.stringify(block.input || {}),
185
+ },
186
+ });
187
+ }
188
+ }
189
+
190
+ choice.message.content = textParts.join('');
191
+ if (toolCalls.length > 0) {
192
+ choice.message.tool_calls = toolCalls;
193
+ }
194
+
195
+ // 映射 stop_reason
196
+ choice.finish_reason = mapStopReason(anthropicBody.stop_reason);
197
+
198
+ return {
199
+ id: anthropicBody.id,
200
+ object: 'chat.completion',
201
+ created: Math.floor(Date.now() / 1000),
202
+ model: anthropicBody.model,
203
+ choices: [choice],
204
+ usage: anthropicBody.usage ? {
205
+ prompt_tokens: anthropicBody.usage.input_tokens,
206
+ completion_tokens: anthropicBody.usage.output_tokens,
207
+ total_tokens: (anthropicBody.usage.input_tokens || 0) + (anthropicBody.usage.output_tokens || 0),
208
+ } : undefined,
209
+ };
210
+ }
211
+
212
+ function mapStopReason(reason) {
213
+ if (!reason) return null;
214
+ if (reason === 'end_turn') return 'stop';
215
+ if (reason === 'max_tokens') return 'length';
216
+ if (reason === 'tool_use') return 'tool_calls';
217
+ if (reason === 'stop_sequence') return 'stop';
218
+ return reason;
219
+ }
220
+
221
+ // ==================== SSE 流式转换 ====================
222
+
223
+ function createSSEConverter(targetModel) {
224
+ const state = {
225
+ messageId: null,
226
+ blockType: null,
227
+ blockIndex: 0,
228
+ toolUseId: null,
229
+ toolName: null,
230
+ toolCallIndex: -1,
231
+ sentRole: false,
232
+ sentToolInit: false,
233
+ buffer: '',
234
+ };
235
+
236
+ return {
237
+ convertChunk(chunkText) {
238
+ let output = '';
239
+ state.buffer += chunkText;
240
+ const lines = state.buffer.split('\n');
241
+ state.buffer = lines.pop() || ''; // 保留不完整的最后一行
242
+
243
+ for (const line of lines) {
244
+ const converted = processLine(line.trim(), state, targetModel);
245
+ if (converted) output += converted;
246
+ }
247
+
248
+ return output;
249
+ },
250
+ flush() {
251
+ // 结束时不发送额外内容,[DONE] 在 finish_reason 时已经发送
252
+ return '';
253
+ },
254
+ };
255
+ }
256
+
257
+ function processLine(line, state, targetModel) {
258
+ if (!line.startsWith('data:')) return '';
259
+ const dataStr = line.slice(5).trim();
260
+ if (dataStr === '[DONE]') return '';
261
+
262
+ let event;
263
+ try {
264
+ event = JSON.parse(dataStr);
265
+ } catch {
266
+ return '';
267
+ }
268
+
269
+ if (!event || !event.type) return '';
270
+
271
+ switch (event.type) {
272
+ case 'message_start': {
273
+ state.messageId = event.message?.id;
274
+ state.sentRole = false;
275
+ return '';
276
+ }
277
+
278
+ case 'content_block_start': {
279
+ state.blockType = event.content_block?.type;
280
+ state.blockIndex = event.index;
281
+ state.sentToolInit = false;
282
+
283
+ if (state.blockType === 'tool_use') {
284
+ state.toolUseId = event.content_block.id;
285
+ state.toolName = event.content_block.name;
286
+ state.toolCallIndex = (state.toolCallIndex || 0) + 1;
287
+ }
288
+ return '';
289
+ }
290
+
291
+ case 'content_block_delta': {
292
+ const delta = event.delta;
293
+ if (!delta) return '';
294
+
295
+ // 发送 role(如果是第一个内容块)
296
+ let prefix = '';
297
+ if (!state.sentRole) {
298
+ prefix = encodeOpenAIChunk(state.messageId, targetModel, { role: 'assistant' });
299
+ state.sentRole = true;
300
+ }
301
+
302
+ if (delta.type === 'text_delta' && delta.text) {
303
+ return prefix + encodeOpenAIChunk(state.messageId, targetModel, { content: delta.text });
304
+ }
305
+
306
+ if (delta.type === 'input_json_delta' && delta.partial_json !== undefined) {
307
+ if (!state.sentToolInit) {
308
+ state.sentToolInit = true;
309
+ const toolCallChunk = {
310
+ tool_calls: [{
311
+ index: state.toolCallIndex,
312
+ id: state.toolUseId,
313
+ type: 'function',
314
+ function: { name: state.toolName, arguments: delta.partial_json },
315
+ }],
316
+ };
317
+ return prefix + encodeOpenAIChunk(state.messageId, targetModel, toolCallChunk);
318
+ }
319
+ return prefix + encodeOpenAIChunk(state.messageId, targetModel, {
320
+ tool_calls: [{ index: state.toolCallIndex, function: { arguments: delta.partial_json } }],
321
+ });
322
+ }
323
+
324
+ return prefix;
325
+ }
326
+
327
+ case 'content_block_stop': {
328
+ state.blockType = null;
329
+ return '';
330
+ }
331
+
332
+ case 'message_delta': {
333
+ const stopReason = event.delta?.stop_reason;
334
+ if (stopReason) {
335
+ return encodeOpenAIChunk(state.messageId, targetModel, {}, mapStopReason(stopReason));
336
+ }
337
+ return '';
338
+ }
339
+
340
+ case 'message_stop': {
341
+ return 'data: [DONE]\n\n';
342
+ }
343
+
344
+ default:
345
+ return '';
346
+ }
347
+ }
348
+
349
+ function encodeOpenAIChunk(id, model, delta, finishReason = null) {
350
+ const chunk = {
351
+ id: id || 'chatcmpl-proxy',
352
+ object: 'chat.completion.chunk',
353
+ created: Math.floor(Date.now() / 1000),
354
+ model: model || 'proxy-model',
355
+ choices: [{
356
+ index: 0,
357
+ delta,
358
+ finish_reason: finishReason,
359
+ }],
360
+ };
361
+ return `data: ${JSON.stringify(chunk)}\n\n`;
362
+ }
363
+
364
+ module.exports = {
365
+ convertRequest,
366
+ convertResponse,
367
+ createSSEConverter,
368
+ };