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.
- package/lib/converters/anthropic-to-gemini.js +255 -0
- package/lib/converters/anthropic-to-openai.js +23 -10
- package/lib/converters/gemini-to-anthropic.js +277 -0
- package/lib/converters/gemini-to-openai.js +240 -0
- package/lib/converters/openai-to-anthropic.js +4 -2
- package/lib/converters/openai-to-gemini.js +286 -0
- package/lib/detector.js +4 -0
- package/lib/proxy-server.js +44 -5
- package/lib/stats-store.js +11 -13
- package/package.json +2 -3
- package/public/app.js +24 -4
- package/public/index.html +9 -4
- package/public/style.css +44 -2
- package/server.js +4 -0
|
@@ -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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
360
|
+
index: state.textBlockIndex,
|
|
348
361
|
});
|
|
349
362
|
}
|
|
350
|
-
for (
|
|
363
|
+
for (const [, tool] of state.toolCalls) {
|
|
351
364
|
output += encodeAnthropicEvent('content_block_stop', {
|
|
352
365
|
type: 'content_block_stop',
|
|
353
|
-
index:
|
|
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 };
|