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.
@@ -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 };