Skip to content

Commit 5baad04

Browse files
refactor: 更新 Gemini 客户端和流转换器以支持工具调用和流式响应
Co-authored-by: aider (vertex_ai/gemini-2.5-pro) <aider@aider.chat>
1 parent d0e9454 commit 5baad04

File tree

2 files changed

+82
-61
lines changed

2 files changed

+82
-61
lines changed

packages/mcp-server/src/bridge/stream-transformer.ts

Lines changed: 53 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import { GenerateContentResponse, FinishReason } from '@google/genai';
21
import { randomUUID } from 'node:crypto';
2+
import {
3+
GeminiEventType,
4+
ServerGeminiStreamEvent,
5+
} from '@google/gemini-cli-core';
36

4-
// --- 更新的 OpenAI 响应结构接口 ---
7+
// --- OpenAI 响应结构接口 ---
58
interface OpenAIDelta {
69
role?: 'assistant';
710
content?: string | null;
@@ -28,21 +31,15 @@ interface OpenAIChunk {
2831
}[];
2932
}
3033

31-
type ToolCallState = {
32-
id: string;
33-
name: string;
34-
arguments: string;
35-
};
36-
37-
// --- 更新的、有状态的转换器 ---
34+
// --- 新的、有状态的转换器 ---
3835
export function createOpenAIStreamTransformer(
3936
model: string,
40-
): TransformStream<GenerateContentResponse, Uint8Array> {
37+
): TransformStream<ServerGeminiStreamEvent, Uint8Array> {
4138
const chatID = `chatcmpl-${randomUUID()}`;
4239
const creationTime = Math.floor(Date.now() / 1000);
4340
const encoder = new TextEncoder();
4441
let isFirstChunk = true;
45-
const toolCallStates: ToolCallState[] = [];
42+
let toolCallIndex = 0;
4643

4744
const createChunk = (
4845
delta: OpenAIDelta,
@@ -70,76 +67,78 @@ export function createOpenAIStreamTransformer(
7067
};
7168

7269
return new TransformStream({
73-
transform(geminiChunk, controller) {
74-
const parts = geminiChunk.candidates?.[0]?.content?.parts || [];
75-
const finishReason = geminiChunk.candidates?.[0]?.finishReason;
76-
77-
for (const part of parts) {
78-
let delta: OpenAIDelta = {};
70+
transform(event: ServerGeminiStreamEvent, controller) {
71+
let delta: OpenAIDelta = {};
7972

80-
if (isFirstChunk) {
81-
delta.role = 'assistant';
82-
isFirstChunk = false;
83-
}
73+
if (isFirstChunk) {
74+
delta.role = 'assistant';
75+
isFirstChunk = false;
76+
}
8477

85-
if (part.text) {
86-
delta.content = part.text;
87-
enqueueChunk(controller, createChunk(delta));
88-
}
78+
switch (event.type) {
79+
case GeminiEventType.Content:
80+
if (event.value) {
81+
delta.content = event.value;
82+
enqueueChunk(controller, createChunk(delta));
83+
}
84+
break;
8985

90-
if (part.functionCall && part.functionCall.name) {
91-
const callId = `call_${randomUUID()}`;
86+
case GeminiEventType.ToolCallRequest: {
87+
const { name, args } = event.value;
88+
// **重要**: 在 ID 中嵌入函数名,以便在收到工具响应时可以解析它
89+
const toolCallId = `call_${name}_${randomUUID()}`;
9290

93-
// 模拟分块发送 tool_calls
94-
// 1. 发送带有 name 的块
91+
// OpenAI 流式工具调用需要分块发送
92+
// 1. 发送包含函数名的块
9593
const nameDelta: OpenAIDelta = {
94+
...delta, // 包含 role (如果是第一个块)
9695
tool_calls: [
9796
{
98-
index: toolCallStates.length,
99-
id: callId,
97+
index: toolCallIndex,
98+
id: toolCallId,
10099
type: 'function',
101-
function: { name: part.functionCall.name, arguments: '' },
100+
function: { name: name, arguments: '' },
102101
},
103102
],
104103
};
105-
if (isFirstChunk) {
106-
nameDelta.role = 'assistant';
107-
isFirstChunk = false;
108-
}
109104
enqueueChunk(controller, createChunk(nameDelta));
110105

111-
// 2. 发送带有 arguments 的块
106+
// 2. 发送包含参数的块
112107
const argsDelta: OpenAIDelta = {
113108
tool_calls: [
114109
{
115-
index: toolCallStates.length,
116-
id: callId,
110+
index: toolCallIndex,
111+
id: toolCallId,
117112
type: 'function',
118-
function: { arguments: JSON.stringify(part.functionCall.args) },
113+
function: { arguments: JSON.stringify(args) },
119114
},
120115
],
121116
};
122117
enqueueChunk(controller, createChunk(argsDelta));
123118

124-
toolCallStates.push({
125-
id: callId,
126-
name: part.functionCall.name,
127-
arguments: JSON.stringify(part.functionCall.args),
128-
});
119+
toolCallIndex++;
120+
break;
129121
}
130-
}
131122

132-
if (finishReason && finishReason !== 'FINISH_REASON_UNSPECIFIED') {
133-
const reason =
134-
finishReason === FinishReason.STOP
135-
? toolCallStates.length > 0
136-
? 'tool_calls'
137-
: 'stop'
138-
: finishReason.toLowerCase();
139-
enqueueChunk(controller, createChunk({}, reason));
123+
case GeminiEventType.ChatCompressed:
124+
case GeminiEventType.Thought:
125+
// 这些事件目前在 OpenAI 格式中没有直接对应项,可以选择忽略或以某种方式记录
126+
console.log(`[Stream Transformer] Ignoring event: ${event.type}`);
127+
break;
128+
129+
// 错误和取消事件应在更高层处理,但为完整性起见
130+
case GeminiEventType.Error:
131+
case GeminiEventType.UserCancelled:
132+
// 可以在这里发送一个带有错误信息的 data chunk,如果需要的话
133+
break;
140134
}
141135
},
136+
142137
flush(controller) {
138+
// 在流结束时,发送一个带有 `tool_calls` 或 `stop` 的 finish_reason
139+
const finish_reason = toolCallIndex > 0 ? 'tool_calls' : 'stop';
140+
enqueueChunk(controller, createChunk({}, finish_reason));
141+
143142
const doneString = `data: [DONE]\n\n`;
144143
controller.enqueue(encoder.encode(doneString));
145144
},

packages/mcp-server/src/gemini-client.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ export class GeminiApiClient {
3939
}
4040

4141
const functionDeclarations: FunctionDeclaration[] = openAITools
42-
.filter((tool) => tool.type === 'function' && tool.function)
43-
.map((tool) => ({
42+
.filter(tool => tool.type === 'function' && tool.function)
43+
.map(tool => ({
4444
name: tool.function.name,
4545
description: tool.function.description,
4646
parameters: tool.function.parameters,
@@ -53,20 +53,40 @@ export class GeminiApiClient {
5353
return [{ functionDeclarations }];
5454
}
5555

56+
/**
57+
* 从 tool_call_id 中解析出原始的函数名。
58+
* ID 格式为 "call_{functionName}_{uuid}"
59+
*/
60+
private parseFunctionNameFromId(toolCallId: string): string {
61+
const parts = toolCallId.split('_');
62+
if (parts.length > 2 && parts[0] === 'call') {
63+
// 重新组合可能包含下划线的函数名
64+
return parts.slice(1, parts.length - 1).join('_');
65+
}
66+
// 回退机制,虽然不理想,但比发送错误名称要好
67+
return 'unknown_tool_from_id';
68+
}
69+
5670
/**
5771
* 将 OpenAI 格式的消息转换为 Gemini 格式的 Content 对象。
5872
*/
5973
private openAIMessageToGemini(msg: OpenAIMessage): Content {
6074
const role = msg.role === 'assistant' ? 'model' : 'user';
6175

6276
if (msg.role === 'tool') {
77+
const functionName = this.parseFunctionNameFromId(msg.tool_call_id || '');
6378
return {
6479
role: 'user', // Gemini 使用 'user' role 来承载 functionResponse
6580
parts: [
6681
{
6782
functionResponse: {
68-
name: msg.tool_call_id || 'unknown_tool', // 需要一个工具名
69-
response: { content: msg.content },
83+
name: functionName,
84+
response: {
85+
// Gemini 期望 response 是一个对象,我们把工具的输出放在这里
86+
// 假设工具输出是一个 JSON 字符串,我们解析它
87+
// 如果不是,就直接作为字符串
88+
output: msg.content,
89+
},
7090
},
7191
},
7292
],
@@ -90,11 +110,12 @@ export class GeminiApiClient {
90110
const mimeType = mimePart.split(':')[1].split(';')[0];
91111
return { inlineData: { mimeType, data: dataPart } };
92112
}
113+
// Gemini API 更喜欢 inlineData,但 fileData 也可以作为备选
93114
return { fileData: { mimeType: 'image/jpeg', fileUri: imageUrl } };
94115
}
95-
return { text: '' };
116+
return null;
96117
})
97-
.filter((p) => p.text !== '' || p.inlineData || p.fileData);
118+
.filter((p): p is Part => p !== null);
98119

99120
return { role, parts };
100121
}
@@ -116,12 +137,13 @@ export class GeminiApiClient {
116137
tools?: OpenAIChatCompletionRequest['tools'];
117138
tool_choice?: any;
118139
}) {
119-
const history = messages.map(this.openAIMessageToGemini);
140+
const history = messages.map(msg => this.openAIMessageToGemini(msg));
120141
const lastMessage = history.pop();
121142
if (!lastMessage) {
122143
throw new Error('No message to send.');
123144
}
124145

146+
// 为每个请求创建一个新的、独立的聊天会话
125147
const oneShotChat = new GeminiChat(
126148
this.config,
127149
this.contentGenerator,

0 commit comments

Comments
 (0)