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,277 +1,286 @@
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 };
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
+ const nameCount = new Map();
18
+
19
+ // system_instruction system 顶级字段
20
+ const sysText = body.systemInstruction?.parts?.map(p => p.text || '').join('') || '';
21
+ const system = sysText || undefined;
22
+
23
+ // tools: functionDeclarations → Anthropic tools
24
+ let tools = undefined;
25
+ if (body.tools && Array.isArray(body.tools)) {
26
+ const allDeclarations = [];
27
+ for (const tool of body.tools) {
28
+ if (tool.functionDeclarations) {
29
+ allDeclarations.push(...tool.functionDeclarations);
30
+ }
31
+ }
32
+ if (allDeclarations.length > 0) {
33
+ tools = allDeclarations.map(fd => ({
34
+ name: fd.name,
35
+ description: fd.description || '',
36
+ input_schema: fd.parameters || { type: 'object', properties: {} },
37
+ }));
38
+ }
39
+ }
40
+
41
+ // contents messages
42
+ for (const msg of (body.contents || [])) {
43
+ const role = msg.role === 'model' ? 'assistant' : 'user';
44
+ const parts = msg.parts || [];
45
+
46
+ // assistant + functionCall
47
+ const functionCalls = parts.filter(p => p.functionCall);
48
+ if (role === 'assistant' && functionCalls.length > 0) {
49
+ const content = [];
50
+ const text = parts.filter(p => p.text).map(p => p.text).join('');
51
+ if (text) content.push({ type: 'text', text });
52
+ for (const fc of functionCalls) {
53
+ const toolId = generateToolUseId();
54
+ const fnName = fc.functionCall.name || 'unknown';
55
+ const count = nameCount.get(fnName) || 0;
56
+ nameCount.set(fnName, count + 1);
57
+ nameToId.set(fnName + '#' + count, toolId);
58
+ content.push({
59
+ type: 'tool_use',
60
+ id: toolId,
61
+ name: fnName,
62
+ input: fc.functionCall.args || {},
63
+ });
64
+ }
65
+ messages.push({ role: 'assistant', content });
66
+ continue;
67
+ }
68
+
69
+ // user + functionResponse → tool_result
70
+ const functionResponses = parts.filter(p => p.functionResponse);
71
+ if (role === 'user' && functionResponses.length > 0) {
72
+ const content = [];
73
+ const text = parts.filter(p => p.text).map(p => p.text).join('');
74
+ if (text) content.push({ type: 'text', text });
75
+ const respCount = new Map();
76
+ for (const fr of functionResponses) {
77
+ const fnName = fr.functionResponse.name || 'unknown';
78
+ const count = respCount.get(fnName) || 0;
79
+ respCount.set(fnName, count + 1);
80
+ const toolId = nameToId.get(fnName + '#' + count) || fnName;
81
+ content.push({
82
+ type: 'tool_result',
83
+ tool_use_id: toolId,
84
+ content: typeof fr.functionResponse.response === 'string'
85
+ ? fr.functionResponse.response
86
+ : JSON.stringify(fr.functionResponse.response || {}),
87
+ });
88
+ }
89
+ messages.push({ role: 'user', content });
90
+ continue;
91
+ }
92
+
93
+ // 纯文本
94
+ const text = parts.filter(p => p.text).map(p => p.text).join('');
95
+ if (text) {
96
+ messages.push({ role, content: text });
97
+ }
98
+ }
99
+
100
+ const result = {
101
+ model: targetModel,
102
+ max_tokens: body.generationConfig?.maxOutputTokens || 4096,
103
+ messages,
104
+ };
105
+
106
+ if (system) result.system = system;
107
+ if (tools) result.tools = tools;
108
+ if (body.generationConfig?.temperature !== undefined) result.temperature = body.generationConfig.temperature;
109
+ if (body.generationConfig?.topP !== undefined) result.top_p = body.generationConfig.topP;
110
+ if (body.generationConfig?.stopSequences) result.stop_sequences = body.generationConfig.stopSequences;
111
+
112
+ return { ...result, nameToId };
113
+ }
114
+
115
+ // ==================== 响应转换 ====================
116
+
117
+ function convertResponse(geminiBody) {
118
+ const candidate = geminiBody.candidates?.[0];
119
+ const content = [];
120
+
121
+ if (candidate?.content?.parts) {
122
+ for (const part of candidate.content.parts) {
123
+ if (part.text) {
124
+ content.push({ type: 'text', text: part.text });
125
+ }
126
+ if (part.functionCall) {
127
+ content.push({
128
+ type: 'tool_use',
129
+ id: generateToolUseId(),
130
+ name: part.functionCall.name,
131
+ input: part.functionCall.args || {},
132
+ });
133
+ }
134
+ }
135
+ }
136
+
137
+ const stopReason = candidate?.content?.parts?.some(p => p.functionCall)
138
+ ? 'tool_use' : mapFinishReason(candidate?.finishReason);
139
+
140
+ return {
141
+ id: '',
142
+ type: 'message',
143
+ role: 'assistant',
144
+ content,
145
+ stop_reason: stopReason,
146
+ usage: {
147
+ input_tokens: geminiBody.usageMetadata?.promptTokenCount || 0,
148
+ output_tokens: geminiBody.usageMetadata?.candidatesTokenCount || 0,
149
+ },
150
+ };
151
+ }
152
+
153
+ function mapFinishReason(reason) {
154
+ if (!reason) return null;
155
+ if (reason === 'STOP') return 'end_turn';
156
+ if (reason === 'MAX_TOKENS') return 'max_tokens';
157
+ if (reason === 'SAFETY') return 'end_turn';
158
+ return 'end_turn';
159
+ }
160
+
161
+ // ==================== SSE 流式转换 ====================
162
+
163
+ function createSSEConverter(nameToId = new Map()) {
164
+ const state = { started: false, textBlockStarted: false, textBlockClosed: false, blockIndex: 0, sentFunctionCall: new Map(), nameToId, nameCount: new Map(), buffer: '' };
165
+
166
+ return {
167
+ convertChunk(chunkText) {
168
+ let output = '';
169
+ state.buffer += chunkText;
170
+ const lines = state.buffer.split('\n');
171
+ state.buffer = lines.pop() || '';
172
+
173
+ for (const line of lines) {
174
+ const trimmed = line.trim();
175
+ if (!trimmed.startsWith('data: ')) continue;
176
+ const dataStr = trimmed.slice(6);
177
+ if (!dataStr) continue;
178
+
179
+ let chunk;
180
+ try { chunk = JSON.parse(dataStr); } catch { continue; }
181
+
182
+ const candidate = chunk.candidates?.[0];
183
+ if (!candidate) continue;
184
+
185
+ // message_start
186
+ if (!state.started) {
187
+ state.started = true;
188
+ output += encodeAnthropicEvent('message_start', {
189
+ type: 'message_start',
190
+ message: {
191
+ id: '',
192
+ type: 'message',
193
+ role: 'assistant',
194
+ content: [],
195
+ stop_reason: null,
196
+ usage: { input_tokens: 0, output_tokens: 0 },
197
+ },
198
+ });
199
+ }
200
+
201
+ const parts = candidate.content?.parts || [];
202
+
203
+ // 文本增量
204
+ const text = parts.filter(p => p.text).map(p => p.text).join('') || '';
205
+ if (text) {
206
+ if (!state.textBlockStarted) {
207
+ state.textBlockStarted = true;
208
+ state.textBlockIndex = state.blockIndex++;
209
+ output += encodeAnthropicEvent('content_block_start', {
210
+ type: 'content_block_start',
211
+ index: state.textBlockIndex,
212
+ content_block: { type: 'text', text: '' },
213
+ });
214
+ }
215
+ output += encodeAnthropicEvent('content_block_delta', {
216
+ type: 'content_block_delta',
217
+ index: state.textBlockIndex,
218
+ delta: { type: 'text_delta', text },
219
+ });
220
+ }
221
+
222
+ // functionCall 增量(去重,首次生成 ID 后缓存)
223
+ for (const part of parts) {
224
+ if (!part.functionCall) continue;
225
+ if (state.textBlockStarted && !state.textBlockClosed) {
226
+ state.textBlockClosed = true;
227
+ output += encodeAnthropicEvent('content_block_stop', {
228
+ type: 'content_block_stop',
229
+ index: state.textBlockIndex,
230
+ });
231
+ }
232
+ const key = part.functionCall.name + (typeof part.functionCall.args === 'string' ? part.functionCall.args : JSON.stringify(part.functionCall.args || {}));
233
+ if (state.sentFunctionCall.has(key)) continue;
234
+ const toolId = generateToolUseId();
235
+ state.sentFunctionCall.set(key, toolId);
236
+ const fnName = part.functionCall.name;
237
+ const count = state.nameCount.get(fnName) || 0;
238
+ state.nameCount.set(fnName, count + 1);
239
+ state.nameToId.set(fnName + '#' + count, toolId);
240
+
241
+ const idx = state.blockIndex++;
242
+ output += encodeAnthropicEvent('content_block_start', {
243
+ type: 'content_block_start',
244
+ index: idx,
245
+ content_block: {
246
+ type: 'tool_use',
247
+ id: toolId,
248
+ name: part.functionCall.name,
249
+ input: {},
250
+ },
251
+ });
252
+ output += encodeAnthropicEvent('content_block_delta', {
253
+ type: 'content_block_delta',
254
+ index: idx,
255
+ delta: { type: 'input_json_delta', partial_json: JSON.stringify(part.functionCall.args || {}) },
256
+ });
257
+ output += encodeAnthropicEvent('content_block_stop', {
258
+ type: 'content_block_stop',
259
+ index: idx,
260
+ });
261
+ }
262
+
263
+ // finish
264
+ if (candidate.finishReason) {
265
+ if (state.textBlockStarted && !state.textBlockClosed) {
266
+ output += encodeAnthropicEvent('content_block_stop', {
267
+ type: 'content_block_stop',
268
+ index: state.textBlockIndex,
269
+ });
270
+ }
271
+ output += encodeAnthropicEvent('message_delta', {
272
+ type: 'message_delta',
273
+ delta: { stop_reason: mapFinishReason(candidate.finishReason) },
274
+ usage: { output_tokens: 0 },
275
+ });
276
+ output += encodeAnthropicEvent('message_stop', { type: 'message_stop' });
277
+ }
278
+ }
279
+
280
+ return output || null;
281
+ },
282
+ flush() { return ''; },
283
+ };
284
+ }
285
+
286
+ module.exports = { convertRequest, convertResponse, createSSEConverter };