protocol-proxy 2.2.0 → 2.3.4

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.
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Anthropic → Gemini 协议转换
3
+ */
4
+
5
+ const { encodeAnthropicEvent } = require('./sse-helpers');
6
+
7
+ function generateToolUseId() {
8
+ return 'toolu_' + Math.random().toString(36).slice(2, 14) + Math.random().toString(36).slice(2, 10);
9
+ }
10
+
11
+ // ==================== 请求转换 ====================
12
+
13
+ function convertRequest(body, targetModel) {
14
+ const contents = [];
15
+ let systemInstruction = null;
16
+
17
+ // system 顶级字段
18
+ if (body.system) {
19
+ const text = typeof body.system === 'string'
20
+ ? body.system
21
+ : (Array.isArray(body.system) ? body.system.map(s => s.text || s).join('\n') : '');
22
+ if (text) systemInstruction = { parts: [{ text }] };
23
+ }
24
+
25
+ // messages → contents
26
+ for (const msg of (body.messages || [])) {
27
+ if (msg.role === 'assistant') {
28
+ const parts = [];
29
+ if (typeof msg.content === 'string') {
30
+ parts.push({ text: msg.content });
31
+ } else if (Array.isArray(msg.content)) {
32
+ for (const block of msg.content) {
33
+ if (block.type === 'text' && block.text) parts.push({ text: block.text });
34
+ if (block.type === 'tool_use') {
35
+ parts.push({
36
+ functionCall: { name: block.name, args: block.input || {} },
37
+ });
38
+ }
39
+ }
40
+ }
41
+ if (parts.length > 0) contents.push({ role: 'model', parts });
42
+ } else if (msg.role === 'user') {
43
+ const parts = [];
44
+ if (typeof msg.content === 'string') {
45
+ parts.push({ text: msg.content });
46
+ } else if (Array.isArray(msg.content)) {
47
+ for (const block of msg.content) {
48
+ if (block.type === 'text' && block.text) parts.push({ text: block.text });
49
+ if (block.type === 'tool_result') {
50
+ parts.push({
51
+ functionResponse: {
52
+ name: block.tool_use_id || 'unknown',
53
+ response: typeof block.content === 'string' ? { result: block.content } : block.content || {},
54
+ },
55
+ });
56
+ }
57
+ }
58
+ }
59
+ if (parts.length > 0) contents.push({ role: 'user', parts });
60
+ }
61
+ }
62
+
63
+ const result = { contents };
64
+ if (systemInstruction) result.systemInstruction = systemInstruction;
65
+
66
+ // 转换 tools → functionDeclarations
67
+ if (body.tools && Array.isArray(body.tools)) {
68
+ const functionDeclarations = body.tools.map(t => ({
69
+ name: t.name,
70
+ description: t.description || '',
71
+ parameters: t.input_schema || { type: 'object', properties: {} },
72
+ }));
73
+ result.tools = [{ functionDeclarations }];
74
+ }
75
+
76
+ // generationConfig
77
+ const gc = {};
78
+ if (body.max_tokens !== undefined) gc.maxOutputTokens = body.max_tokens;
79
+ if (body.temperature !== undefined) gc.temperature = body.temperature;
80
+ if (body.top_p !== undefined) gc.topP = body.top_p;
81
+ if (body.stop_sequences) gc.stopSequences = body.stop_sequences;
82
+ if (Object.keys(gc).length > 0) result.generationConfig = gc;
83
+
84
+ return result;
85
+ }
86
+
87
+ // ==================== 响应转换 ====================
88
+
89
+ function convertResponse(geminiBody) {
90
+ const candidate = geminiBody.candidates?.[0];
91
+ const content = [];
92
+
93
+ if (candidate?.content?.parts) {
94
+ for (const part of candidate.content.parts) {
95
+ if (part.text) {
96
+ content.push({ type: 'text', text: part.text });
97
+ }
98
+ if (part.functionCall) {
99
+ content.push({
100
+ type: 'tool_use',
101
+ id: generateToolUseId(),
102
+ name: part.functionCall.name,
103
+ input: part.functionCall.args || {},
104
+ });
105
+ }
106
+ }
107
+ }
108
+
109
+ const stopReason = candidate?.content?.parts?.some(p => p.functionCall)
110
+ ? 'tool_use' : mapFinishReason(candidate?.finishReason);
111
+
112
+ return {
113
+ id: '',
114
+ type: 'message',
115
+ role: 'assistant',
116
+ content,
117
+ stop_reason: stopReason,
118
+ usage: {
119
+ input_tokens: geminiBody.usageMetadata?.promptTokenCount || 0,
120
+ output_tokens: geminiBody.usageMetadata?.candidatesTokenCount || 0,
121
+ },
122
+ };
123
+ }
124
+
125
+ function mapFinishReason(reason) {
126
+ if (!reason) return null;
127
+ if (reason === 'STOP') return 'end_turn';
128
+ if (reason === 'MAX_TOKENS') return 'max_tokens';
129
+ if (reason === 'SAFETY') return 'end_turn';
130
+ return 'end_turn';
131
+ }
132
+
133
+ // ==================== SSE 流式转换 ====================
134
+
135
+ function createSSEConverter() {
136
+ const state = { started: false, textBlockStarted: false, textBlockClosed: false, blockIndex: 0, sentFunctionCall: new Map(), buffer: '' };
137
+
138
+ return {
139
+ convertChunk(chunkText) {
140
+ let output = '';
141
+ state.buffer += chunkText;
142
+ const lines = state.buffer.split('\n');
143
+ state.buffer = lines.pop() || '';
144
+
145
+ for (const line of lines) {
146
+ const trimmed = line.trim();
147
+ if (!trimmed.startsWith('data: ')) continue;
148
+ const dataStr = trimmed.slice(6);
149
+ if (!dataStr) continue;
150
+
151
+ let chunk;
152
+ try { chunk = JSON.parse(dataStr); } catch { continue; }
153
+
154
+ const candidate = chunk.candidates?.[0];
155
+ if (!candidate) continue;
156
+
157
+ // message_start(只发一次)
158
+ if (!state.started) {
159
+ state.started = true;
160
+ output += encodeAnthropicEvent('message_start', {
161
+ type: 'message_start',
162
+ message: {
163
+ id: '',
164
+ type: 'message',
165
+ role: 'assistant',
166
+ content: [],
167
+ stop_reason: null,
168
+ usage: { input_tokens: 0, output_tokens: 0 },
169
+ },
170
+ });
171
+ }
172
+
173
+ const parts = candidate.content?.parts || [];
174
+
175
+ // 文本增量
176
+ const text = parts.filter(p => p.text).map(p => p.text).join('') || '';
177
+ if (text) {
178
+ if (!state.textBlockStarted) {
179
+ state.textBlockStarted = true;
180
+ state.textBlockIndex = state.blockIndex++;
181
+ output += encodeAnthropicEvent('content_block_start', {
182
+ type: 'content_block_start',
183
+ index: state.textBlockIndex,
184
+ content_block: { type: 'text', text: '' },
185
+ });
186
+ }
187
+ output += encodeAnthropicEvent('content_block_delta', {
188
+ type: 'content_block_delta',
189
+ index: state.textBlockIndex,
190
+ delta: { type: 'text_delta', text },
191
+ });
192
+ }
193
+
194
+ // functionCall 增量(去重,首次生成 ID 后缓存)
195
+ for (const part of parts) {
196
+ if (!part.functionCall) continue;
197
+ // 在首个 functionCall 前关闭 text block
198
+ if (state.textBlockStarted && !state.textBlockClosed) {
199
+ state.textBlockClosed = true;
200
+ output += encodeAnthropicEvent('content_block_stop', {
201
+ type: 'content_block_stop',
202
+ index: state.textBlockIndex,
203
+ });
204
+ }
205
+ const key = part.functionCall.name + (typeof part.functionCall.args === 'string' ? part.functionCall.args : JSON.stringify(part.functionCall.args || {}));
206
+ if (state.sentFunctionCall.has(key)) continue;
207
+ const toolId = generateToolUseId();
208
+ state.sentFunctionCall.set(key, toolId);
209
+
210
+ const idx = state.blockIndex++;
211
+ output += encodeAnthropicEvent('content_block_start', {
212
+ type: 'content_block_start',
213
+ index: idx,
214
+ content_block: {
215
+ type: 'tool_use',
216
+ id: toolId,
217
+ name: part.functionCall.name,
218
+ input: {},
219
+ },
220
+ });
221
+ output += encodeAnthropicEvent('content_block_delta', {
222
+ type: 'content_block_delta',
223
+ index: idx,
224
+ delta: { type: 'input_json_delta', partial_json: JSON.stringify(part.functionCall.args || {}) },
225
+ });
226
+ output += encodeAnthropicEvent('content_block_stop', {
227
+ type: 'content_block_stop',
228
+ index: idx,
229
+ });
230
+ }
231
+
232
+ // finish
233
+ if (candidate.finishReason) {
234
+ if (state.textBlockStarted && !state.textBlockClosed) {
235
+ output += encodeAnthropicEvent('content_block_stop', {
236
+ type: 'content_block_stop',
237
+ index: state.textBlockIndex,
238
+ });
239
+ }
240
+ output += encodeAnthropicEvent('message_delta', {
241
+ type: 'message_delta',
242
+ delta: { stop_reason: mapFinishReason(candidate.finishReason) },
243
+ usage: { output_tokens: 0 },
244
+ });
245
+ output += encodeAnthropicEvent('message_stop', { type: 'message_stop' });
246
+ }
247
+ }
248
+
249
+ return output || null;
250
+ },
251
+ flush() { return ''; },
252
+ };
253
+ }
254
+
255
+ module.exports = { convertRequest, convertResponse, createSSEConverter };
@@ -218,7 +218,10 @@ function createSSEConverter(targetModel) {
218
218
  messageId: null,
219
219
  started: false,
220
220
  textBlockStarted: false,
221
- toolCalls: new Map(), // index -> { id, name, args }
221
+ textBlockClosed: false,
222
+ textBlockIndex: 0,
223
+ blockIndex: 0,
224
+ toolCalls: new Map(), // index -> { id, name, args, blockIndex }
222
225
  buffer: '',
223
226
  };
224
227
 
@@ -292,32 +295,41 @@ function processLine(line, state, targetModel) {
292
295
  if (delta.content !== undefined && delta.content !== null) {
293
296
  if (!state.textBlockStarted) {
294
297
  state.textBlockStarted = true;
298
+ state.textBlockIndex = state.blockIndex++;
295
299
  output += encodeAnthropicEvent('content_block_start', {
296
300
  type: 'content_block_start',
297
- index: 0,
301
+ index: state.textBlockIndex,
298
302
  content_block: { type: 'text', text: '' },
299
303
  });
300
304
  }
301
305
  output += encodeAnthropicEvent('content_block_delta', {
302
306
  type: 'content_block_delta',
303
- index: 0,
307
+ index: state.textBlockIndex,
304
308
  delta: { type: 'text_delta', text: delta.content },
305
309
  });
306
310
  }
307
311
 
308
312
  // tool_calls
309
313
  if (delta.tool_calls && Array.isArray(delta.tool_calls)) {
314
+ // 在首个 tool_call 前关闭 text block
315
+ if (state.textBlockStarted && !state.textBlockClosed) {
316
+ state.textBlockClosed = true;
317
+ output += encodeAnthropicEvent('content_block_stop', {
318
+ type: 'content_block_stop',
319
+ index: state.textBlockIndex,
320
+ });
321
+ }
310
322
  for (const tc of delta.tool_calls) {
311
323
  const idx = tc.index || 0;
312
324
  let tool = state.toolCalls.get(idx);
313
325
 
314
326
  if (!tool) {
315
327
  // 新的 tool_call
316
- tool = { id: tc.id, name: tc.function?.name, args: '' };
328
+ tool = { id: tc.id, name: tc.function?.name, args: '', blockIndex: state.blockIndex++ };
317
329
  state.toolCalls.set(idx, tool);
318
330
  output += encodeAnthropicEvent('content_block_start', {
319
331
  type: 'content_block_start',
320
- index: idx + 1, // text block 占 index 0
332
+ index: tool.blockIndex,
321
333
  content_block: {
322
334
  type: 'tool_use',
323
335
  id: tc.id,
@@ -331,7 +343,7 @@ function processLine(line, state, targetModel) {
331
343
  tool.args += tc.function.arguments;
332
344
  output += encodeAnthropicEvent('content_block_delta', {
333
345
  type: 'content_block_delta',
334
- index: idx + 1,
346
+ index: tool.blockIndex,
335
347
  delta: { type: 'input_json_delta', partial_json: tc.function.arguments },
336
348
  });
337
349
  }
@@ -341,16 +353,17 @@ function processLine(line, state, targetModel) {
341
353
  // finish_reason
342
354
  if (choice.finish_reason) {
343
355
  const stopReason = mapFinishReason(choice.finish_reason);
344
- if (state.textBlockStarted) {
356
+ if (state.textBlockStarted && !state.textBlockClosed) {
357
+ state.textBlockClosed = true;
345
358
  output += encodeAnthropicEvent('content_block_stop', {
346
359
  type: 'content_block_stop',
347
- index: 0,
360
+ index: state.textBlockIndex,
348
361
  });
349
362
  }
350
- for (let i = 0; i < state.toolCalls.size; i++) {
363
+ for (const [, tool] of state.toolCalls) {
351
364
  output += encodeAnthropicEvent('content_block_stop', {
352
365
  type: 'content_block_stop',
353
- index: i + 1,
366
+ index: tool.blockIndex,
354
367
  });
355
368
  }
356
369
  output += encodeAnthropicEvent('message_delta', {
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Gemini → Anthropic 协议转换
3
+ */
4
+
5
+ const { encodeAnthropicEvent } = require('./sse-helpers');
6
+
7
+ function generateToolUseId() {
8
+ return 'toolu_' + Math.random().toString(36).slice(2, 14) + Math.random().toString(36).slice(2, 10);
9
+ }
10
+
11
+ // ==================== 请求转换 ====================
12
+
13
+ function convertRequest(body, targetModel) {
14
+ const messages = [];
15
+ // 追踪 Gemini 函数名 → 生成的 tool_use id,用于后续 tool_result 转换
16
+ const nameToId = new Map();
17
+
18
+ // system_instruction → system 顶级字段
19
+ const sysText = body.systemInstruction?.parts?.map(p => p.text || '').join('') || '';
20
+ const system = sysText || undefined;
21
+
22
+ // tools: functionDeclarations → Anthropic tools
23
+ let tools = undefined;
24
+ if (body.tools && Array.isArray(body.tools)) {
25
+ const allDeclarations = [];
26
+ for (const tool of body.tools) {
27
+ if (tool.functionDeclarations) {
28
+ allDeclarations.push(...tool.functionDeclarations);
29
+ }
30
+ }
31
+ if (allDeclarations.length > 0) {
32
+ tools = allDeclarations.map(fd => ({
33
+ name: fd.name,
34
+ description: fd.description || '',
35
+ input_schema: fd.parameters || { type: 'object', properties: {} },
36
+ }));
37
+ }
38
+ }
39
+
40
+ // contents → messages
41
+ for (const msg of (body.contents || [])) {
42
+ const role = msg.role === 'model' ? 'assistant' : 'user';
43
+ const parts = msg.parts || [];
44
+
45
+ // assistant + functionCall
46
+ const functionCalls = parts.filter(p => p.functionCall);
47
+ if (role === 'assistant' && functionCalls.length > 0) {
48
+ const content = [];
49
+ const text = parts.filter(p => p.text).map(p => p.text).join('');
50
+ if (text) content.push({ type: 'text', text });
51
+ for (const fc of functionCalls) {
52
+ const toolId = generateToolUseId();
53
+ const fnName = fc.functionCall.name || 'unknown';
54
+ nameToId.set(fnName, toolId);
55
+ content.push({
56
+ type: 'tool_use',
57
+ id: toolId,
58
+ name: fnName,
59
+ input: fc.functionCall.args || {},
60
+ });
61
+ }
62
+ messages.push({ role: 'assistant', content });
63
+ continue;
64
+ }
65
+
66
+ // user + functionResponse → tool_result
67
+ const functionResponses = parts.filter(p => p.functionResponse);
68
+ if (role === 'user' && functionResponses.length > 0) {
69
+ const content = [];
70
+ const text = parts.filter(p => p.text).map(p => p.text).join('');
71
+ if (text) content.push({ type: 'text', text });
72
+ for (const fr of functionResponses) {
73
+ // 用函数名查找之前生成的 tool_use id,找不到则用原始名
74
+ const toolId = nameToId.get(fr.functionResponse.name) || fr.functionResponse.name || 'unknown';
75
+ content.push({
76
+ type: 'tool_result',
77
+ tool_use_id: toolId,
78
+ content: typeof fr.functionResponse.response === 'string'
79
+ ? fr.functionResponse.response
80
+ : JSON.stringify(fr.functionResponse.response || {}),
81
+ });
82
+ }
83
+ messages.push({ role: 'user', content });
84
+ continue;
85
+ }
86
+
87
+ // 纯文本
88
+ const text = parts.filter(p => p.text).map(p => p.text).join('');
89
+ if (text) {
90
+ messages.push({ role, content: text });
91
+ }
92
+ }
93
+
94
+ const result = {
95
+ model: targetModel,
96
+ max_tokens: body.generationConfig?.maxOutputTokens || 4096,
97
+ messages,
98
+ };
99
+
100
+ if (system) result.system = system;
101
+ if (tools) result.tools = tools;
102
+ if (body.generationConfig?.temperature !== undefined) result.temperature = body.generationConfig.temperature;
103
+ if (body.generationConfig?.topP !== undefined) result.top_p = body.generationConfig.topP;
104
+ if (body.generationConfig?.stopSequences) result.stop_sequences = body.generationConfig.stopSequences;
105
+
106
+ return { ...result, nameToId };
107
+ }
108
+
109
+ // ==================== 响应转换 ====================
110
+
111
+ function convertResponse(geminiBody) {
112
+ const candidate = geminiBody.candidates?.[0];
113
+ const content = [];
114
+
115
+ if (candidate?.content?.parts) {
116
+ for (const part of candidate.content.parts) {
117
+ if (part.text) {
118
+ content.push({ type: 'text', text: part.text });
119
+ }
120
+ if (part.functionCall) {
121
+ content.push({
122
+ type: 'tool_use',
123
+ id: generateToolUseId(),
124
+ name: part.functionCall.name,
125
+ input: part.functionCall.args || {},
126
+ });
127
+ }
128
+ }
129
+ }
130
+
131
+ const stopReason = candidate?.content?.parts?.some(p => p.functionCall)
132
+ ? 'tool_use' : mapFinishReason(candidate?.finishReason);
133
+
134
+ return {
135
+ id: '',
136
+ type: 'message',
137
+ role: 'assistant',
138
+ content,
139
+ stop_reason: stopReason,
140
+ usage: {
141
+ input_tokens: geminiBody.usageMetadata?.promptTokenCount || 0,
142
+ output_tokens: geminiBody.usageMetadata?.candidatesTokenCount || 0,
143
+ },
144
+ };
145
+ }
146
+
147
+ function mapFinishReason(reason) {
148
+ if (!reason) return null;
149
+ if (reason === 'STOP') return 'end_turn';
150
+ if (reason === 'MAX_TOKENS') return 'max_tokens';
151
+ if (reason === 'SAFETY') return 'end_turn';
152
+ return 'end_turn';
153
+ }
154
+
155
+ // ==================== SSE 流式转换 ====================
156
+
157
+ function createSSEConverter(nameToId = new Map()) {
158
+ const state = { started: false, textBlockStarted: false, textBlockClosed: false, blockIndex: 0, sentFunctionCall: new Map(), nameToId, buffer: '' };
159
+
160
+ return {
161
+ convertChunk(chunkText) {
162
+ let output = '';
163
+ state.buffer += chunkText;
164
+ const lines = state.buffer.split('\n');
165
+ state.buffer = lines.pop() || '';
166
+
167
+ for (const line of lines) {
168
+ const trimmed = line.trim();
169
+ if (!trimmed.startsWith('data: ')) continue;
170
+ const dataStr = trimmed.slice(6);
171
+ if (!dataStr) continue;
172
+
173
+ let chunk;
174
+ try { chunk = JSON.parse(dataStr); } catch { continue; }
175
+
176
+ const candidate = chunk.candidates?.[0];
177
+ if (!candidate) continue;
178
+
179
+ // message_start
180
+ if (!state.started) {
181
+ state.started = true;
182
+ output += encodeAnthropicEvent('message_start', {
183
+ type: 'message_start',
184
+ message: {
185
+ id: '',
186
+ type: 'message',
187
+ role: 'assistant',
188
+ content: [],
189
+ stop_reason: null,
190
+ usage: { input_tokens: 0, output_tokens: 0 },
191
+ },
192
+ });
193
+ }
194
+
195
+ const parts = candidate.content?.parts || [];
196
+
197
+ // 文本增量
198
+ const text = parts.filter(p => p.text).map(p => p.text).join('') || '';
199
+ if (text) {
200
+ if (!state.textBlockStarted) {
201
+ state.textBlockStarted = true;
202
+ state.textBlockIndex = state.blockIndex++;
203
+ output += encodeAnthropicEvent('content_block_start', {
204
+ type: 'content_block_start',
205
+ index: state.textBlockIndex,
206
+ content_block: { type: 'text', text: '' },
207
+ });
208
+ }
209
+ output += encodeAnthropicEvent('content_block_delta', {
210
+ type: 'content_block_delta',
211
+ index: state.textBlockIndex,
212
+ delta: { type: 'text_delta', text },
213
+ });
214
+ }
215
+
216
+ // functionCall 增量(去重,首次生成 ID 后缓存)
217
+ for (const part of parts) {
218
+ if (!part.functionCall) continue;
219
+ if (state.textBlockStarted && !state.textBlockClosed) {
220
+ state.textBlockClosed = true;
221
+ output += encodeAnthropicEvent('content_block_stop', {
222
+ type: 'content_block_stop',
223
+ index: state.textBlockIndex,
224
+ });
225
+ }
226
+ const key = part.functionCall.name + (typeof part.functionCall.args === 'string' ? part.functionCall.args : JSON.stringify(part.functionCall.args || {}));
227
+ if (state.sentFunctionCall.has(key)) continue;
228
+ const toolId = generateToolUseId();
229
+ state.sentFunctionCall.set(key, toolId);
230
+ state.nameToId.set(part.functionCall.name, toolId);
231
+
232
+ const idx = state.blockIndex++;
233
+ output += encodeAnthropicEvent('content_block_start', {
234
+ type: 'content_block_start',
235
+ index: idx,
236
+ content_block: {
237
+ type: 'tool_use',
238
+ id: toolId,
239
+ name: part.functionCall.name,
240
+ input: {},
241
+ },
242
+ });
243
+ output += encodeAnthropicEvent('content_block_delta', {
244
+ type: 'content_block_delta',
245
+ index: idx,
246
+ delta: { type: 'input_json_delta', partial_json: JSON.stringify(part.functionCall.args || {}) },
247
+ });
248
+ output += encodeAnthropicEvent('content_block_stop', {
249
+ type: 'content_block_stop',
250
+ index: idx,
251
+ });
252
+ }
253
+
254
+ // finish
255
+ if (candidate.finishReason) {
256
+ if (state.textBlockStarted && !state.textBlockClosed) {
257
+ output += encodeAnthropicEvent('content_block_stop', {
258
+ type: 'content_block_stop',
259
+ index: state.textBlockIndex,
260
+ });
261
+ }
262
+ output += encodeAnthropicEvent('message_delta', {
263
+ type: 'message_delta',
264
+ delta: { stop_reason: mapFinishReason(candidate.finishReason) },
265
+ usage: { output_tokens: 0 },
266
+ });
267
+ output += encodeAnthropicEvent('message_stop', { type: 'message_stop' });
268
+ }
269
+ }
270
+
271
+ return output || null;
272
+ },
273
+ flush() { return ''; },
274
+ };
275
+ }
276
+
277
+ module.exports = { convertRequest, convertResponse, createSSEConverter };