protocol-proxy 2.2.0 → 2.3.2
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 +253 -0
- package/lib/converters/gemini-to-anthropic.js +275 -0
- package/lib/converters/gemini-to-openai.js +238 -0
- package/lib/converters/openai-to-gemini.js +284 -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,253 @@
|
|
|
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() };
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
convertChunk(chunkText) {
|
|
140
|
+
let output = '';
|
|
141
|
+
const lines = chunkText.split('\n');
|
|
142
|
+
|
|
143
|
+
for (const line of lines) {
|
|
144
|
+
const trimmed = line.trim();
|
|
145
|
+
if (!trimmed.startsWith('data: ')) continue;
|
|
146
|
+
const dataStr = trimmed.slice(6);
|
|
147
|
+
if (!dataStr) continue;
|
|
148
|
+
|
|
149
|
+
let chunk;
|
|
150
|
+
try { chunk = JSON.parse(dataStr); } catch { continue; }
|
|
151
|
+
|
|
152
|
+
const candidate = chunk.candidates?.[0];
|
|
153
|
+
if (!candidate) continue;
|
|
154
|
+
|
|
155
|
+
// message_start(只发一次)
|
|
156
|
+
if (!state.started) {
|
|
157
|
+
state.started = true;
|
|
158
|
+
output += encodeAnthropicEvent('message_start', {
|
|
159
|
+
type: 'message_start',
|
|
160
|
+
message: {
|
|
161
|
+
id: '',
|
|
162
|
+
type: 'message',
|
|
163
|
+
role: 'assistant',
|
|
164
|
+
content: [],
|
|
165
|
+
stop_reason: null,
|
|
166
|
+
usage: { input_tokens: 0, output_tokens: 0 },
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const parts = candidate.content?.parts || [];
|
|
172
|
+
|
|
173
|
+
// 文本增量
|
|
174
|
+
const text = parts.filter(p => p.text).map(p => p.text).join('') || '';
|
|
175
|
+
if (text) {
|
|
176
|
+
if (!state.textBlockStarted) {
|
|
177
|
+
state.textBlockStarted = true;
|
|
178
|
+
state.textBlockIndex = state.blockIndex++;
|
|
179
|
+
output += encodeAnthropicEvent('content_block_start', {
|
|
180
|
+
type: 'content_block_start',
|
|
181
|
+
index: state.textBlockIndex,
|
|
182
|
+
content_block: { type: 'text', text: '' },
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
output += encodeAnthropicEvent('content_block_delta', {
|
|
186
|
+
type: 'content_block_delta',
|
|
187
|
+
index: state.textBlockIndex,
|
|
188
|
+
delta: { type: 'text_delta', text },
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// functionCall 增量(去重,首次生成 ID 后缓存)
|
|
193
|
+
for (const part of parts) {
|
|
194
|
+
if (!part.functionCall) continue;
|
|
195
|
+
// 在首个 functionCall 前关闭 text block
|
|
196
|
+
if (state.textBlockStarted && !state.textBlockClosed) {
|
|
197
|
+
state.textBlockClosed = true;
|
|
198
|
+
output += encodeAnthropicEvent('content_block_stop', {
|
|
199
|
+
type: 'content_block_stop',
|
|
200
|
+
index: state.textBlockIndex,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
const key = part.functionCall.name + (typeof part.functionCall.args === 'string' ? part.functionCall.args : JSON.stringify(part.functionCall.args || {}));
|
|
204
|
+
if (state.sentFunctionCall.has(key)) continue;
|
|
205
|
+
const toolId = generateToolUseId();
|
|
206
|
+
state.sentFunctionCall.set(key, toolId);
|
|
207
|
+
|
|
208
|
+
const idx = state.blockIndex++;
|
|
209
|
+
output += encodeAnthropicEvent('content_block_start', {
|
|
210
|
+
type: 'content_block_start',
|
|
211
|
+
index: idx,
|
|
212
|
+
content_block: {
|
|
213
|
+
type: 'tool_use',
|
|
214
|
+
id: toolId,
|
|
215
|
+
name: part.functionCall.name,
|
|
216
|
+
input: {},
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
output += encodeAnthropicEvent('content_block_delta', {
|
|
220
|
+
type: 'content_block_delta',
|
|
221
|
+
index: idx,
|
|
222
|
+
delta: { type: 'input_json_delta', partial_json: JSON.stringify(part.functionCall.args || {}) },
|
|
223
|
+
});
|
|
224
|
+
output += encodeAnthropicEvent('content_block_stop', {
|
|
225
|
+
type: 'content_block_stop',
|
|
226
|
+
index: idx,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// finish
|
|
231
|
+
if (candidate.finishReason) {
|
|
232
|
+
if (state.textBlockStarted && !state.textBlockClosed) {
|
|
233
|
+
output += encodeAnthropicEvent('content_block_stop', {
|
|
234
|
+
type: 'content_block_stop',
|
|
235
|
+
index: state.textBlockIndex,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
output += encodeAnthropicEvent('message_delta', {
|
|
239
|
+
type: 'message_delta',
|
|
240
|
+
delta: { stop_reason: mapFinishReason(candidate.finishReason) },
|
|
241
|
+
usage: { output_tokens: 0 },
|
|
242
|
+
});
|
|
243
|
+
output += encodeAnthropicEvent('message_stop', { type: 'message_stop' });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return output || null;
|
|
248
|
+
},
|
|
249
|
+
flush() { return ''; },
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module.exports = { convertRequest, convertResponse, createSSEConverter };
|
|
@@ -0,0 +1,275 @@
|
|
|
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 };
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
convertChunk(chunkText) {
|
|
162
|
+
let output = '';
|
|
163
|
+
const lines = chunkText.split('\n');
|
|
164
|
+
|
|
165
|
+
for (const line of lines) {
|
|
166
|
+
const trimmed = line.trim();
|
|
167
|
+
if (!trimmed.startsWith('data: ')) continue;
|
|
168
|
+
const dataStr = trimmed.slice(6);
|
|
169
|
+
if (!dataStr) continue;
|
|
170
|
+
|
|
171
|
+
let chunk;
|
|
172
|
+
try { chunk = JSON.parse(dataStr); } catch { continue; }
|
|
173
|
+
|
|
174
|
+
const candidate = chunk.candidates?.[0];
|
|
175
|
+
if (!candidate) continue;
|
|
176
|
+
|
|
177
|
+
// message_start
|
|
178
|
+
if (!state.started) {
|
|
179
|
+
state.started = true;
|
|
180
|
+
output += encodeAnthropicEvent('message_start', {
|
|
181
|
+
type: 'message_start',
|
|
182
|
+
message: {
|
|
183
|
+
id: '',
|
|
184
|
+
type: 'message',
|
|
185
|
+
role: 'assistant',
|
|
186
|
+
content: [],
|
|
187
|
+
stop_reason: null,
|
|
188
|
+
usage: { input_tokens: 0, output_tokens: 0 },
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const parts = candidate.content?.parts || [];
|
|
194
|
+
|
|
195
|
+
// 文本增量
|
|
196
|
+
const text = parts.filter(p => p.text).map(p => p.text).join('') || '';
|
|
197
|
+
if (text) {
|
|
198
|
+
if (!state.textBlockStarted) {
|
|
199
|
+
state.textBlockStarted = true;
|
|
200
|
+
state.textBlockIndex = state.blockIndex++;
|
|
201
|
+
output += encodeAnthropicEvent('content_block_start', {
|
|
202
|
+
type: 'content_block_start',
|
|
203
|
+
index: state.textBlockIndex,
|
|
204
|
+
content_block: { type: 'text', text: '' },
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
output += encodeAnthropicEvent('content_block_delta', {
|
|
208
|
+
type: 'content_block_delta',
|
|
209
|
+
index: state.textBlockIndex,
|
|
210
|
+
delta: { type: 'text_delta', text },
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// functionCall 增量(去重,首次生成 ID 后缓存)
|
|
215
|
+
for (const part of parts) {
|
|
216
|
+
if (!part.functionCall) continue;
|
|
217
|
+
if (state.textBlockStarted && !state.textBlockClosed) {
|
|
218
|
+
state.textBlockClosed = true;
|
|
219
|
+
output += encodeAnthropicEvent('content_block_stop', {
|
|
220
|
+
type: 'content_block_stop',
|
|
221
|
+
index: state.textBlockIndex,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
const key = part.functionCall.name + (typeof part.functionCall.args === 'string' ? part.functionCall.args : JSON.stringify(part.functionCall.args || {}));
|
|
225
|
+
if (state.sentFunctionCall.has(key)) continue;
|
|
226
|
+
const toolId = generateToolUseId();
|
|
227
|
+
state.sentFunctionCall.set(key, toolId);
|
|
228
|
+
state.nameToId.set(part.functionCall.name, toolId);
|
|
229
|
+
|
|
230
|
+
const idx = state.blockIndex++;
|
|
231
|
+
output += encodeAnthropicEvent('content_block_start', {
|
|
232
|
+
type: 'content_block_start',
|
|
233
|
+
index: idx,
|
|
234
|
+
content_block: {
|
|
235
|
+
type: 'tool_use',
|
|
236
|
+
id: toolId,
|
|
237
|
+
name: part.functionCall.name,
|
|
238
|
+
input: {},
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
output += encodeAnthropicEvent('content_block_delta', {
|
|
242
|
+
type: 'content_block_delta',
|
|
243
|
+
index: idx,
|
|
244
|
+
delta: { type: 'input_json_delta', partial_json: JSON.stringify(part.functionCall.args || {}) },
|
|
245
|
+
});
|
|
246
|
+
output += encodeAnthropicEvent('content_block_stop', {
|
|
247
|
+
type: 'content_block_stop',
|
|
248
|
+
index: idx,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// finish
|
|
253
|
+
if (candidate.finishReason) {
|
|
254
|
+
if (state.textBlockStarted && !state.textBlockClosed) {
|
|
255
|
+
output += encodeAnthropicEvent('content_block_stop', {
|
|
256
|
+
type: 'content_block_stop',
|
|
257
|
+
index: state.textBlockIndex,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
output += encodeAnthropicEvent('message_delta', {
|
|
261
|
+
type: 'message_delta',
|
|
262
|
+
delta: { stop_reason: mapFinishReason(candidate.finishReason) },
|
|
263
|
+
usage: { output_tokens: 0 },
|
|
264
|
+
});
|
|
265
|
+
output += encodeAnthropicEvent('message_stop', { type: 'message_stop' });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return output || null;
|
|
270
|
+
},
|
|
271
|
+
flush() { return ''; },
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
module.exports = { convertRequest, convertResponse, createSSEConverter };
|