Skip to content

Commit 8932b8f

Browse files
refactor: Gemini流处理与OpenAI格式统一
Co-authored-by: aider (vertex_ai/gemini-2.5-pro) <aider@aider.chat>
1 parent 419e39f commit 8932b8f

File tree

3 files changed

+56
-26
lines changed

3 files changed

+56
-26
lines changed

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

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import { randomUUID } from 'node:crypto';
2-
import {
3-
GeminiEventType,
4-
ServerGeminiStreamEvent,
5-
} from '@google/gemini-cli-core';
2+
import { type StreamChunk } from '../types.js';
63

74
// --- OpenAI 响应结构接口 ---
85
interface OpenAIDelta {
@@ -34,7 +31,7 @@ interface OpenAIChunk {
3431
// --- 新的、有状态的转换器 ---
3532
export function createOpenAIStreamTransformer(
3633
model: string,
37-
): TransformStream<ServerGeminiStreamEvent, Uint8Array> {
34+
): TransformStream<StreamChunk, Uint8Array> {
3835
const chatID = `chatcmpl-${randomUUID()}`;
3936
const creationTime = Math.floor(Date.now() / 1000);
4037
const encoder = new TextEncoder();
@@ -67,10 +64,10 @@ export function createOpenAIStreamTransformer(
6764
};
6865

6966
return new TransformStream({
70-
transform(event: ServerGeminiStreamEvent, controller) {
67+
transform(chunk: StreamChunk, controller) {
7168
console.log(
72-
`[Stream Transformer] Received event: ${event.type}`,
73-
'value' in event && event.value ? JSON.stringify(event.value) : '',
69+
`[Stream Transformer] Received chunk: ${chunk.type}`,
70+
chunk.data ? JSON.stringify(chunk.data) : '',
7471
);
7572
let delta: OpenAIDelta = {};
7673

@@ -79,16 +76,16 @@ export function createOpenAIStreamTransformer(
7976
isFirstChunk = false;
8077
}
8178

82-
switch (event.type) {
83-
case GeminiEventType.Content:
84-
if (event.value) {
85-
delta.content = event.value;
79+
switch (chunk.type) {
80+
case 'text':
81+
if (chunk.data) {
82+
delta.content = chunk.data;
8683
enqueueChunk(controller, createChunk(delta));
8784
}
8885
break;
8986

90-
case GeminiEventType.ToolCallRequest: {
91-
const { name, args } = event.value;
87+
case 'tool_code': {
88+
const { name, args } = chunk.data;
9289
// **重要**: 在 ID 中嵌入函数名,以便在收到工具响应时可以解析它
9390
const toolCallId = `call_${name}_${randomUUID()}`;
9491

@@ -124,16 +121,9 @@ export function createOpenAIStreamTransformer(
124121
break;
125122
}
126123

127-
case GeminiEventType.ChatCompressed:
128-
case GeminiEventType.Thought:
124+
case 'reasoning':
129125
// 这些事件目前在 OpenAI 格式中没有直接对应项,可以选择忽略或以某种方式记录
130-
console.log(`[Stream Transformer] Ignoring event: ${event.type}`);
131-
break;
132-
133-
// 错误和取消事件应在更高层处理,但为完整性起见
134-
case GeminiEventType.Error:
135-
case GeminiEventType.UserCancelled:
136-
// 可以在这里发送一个带有错误信息的 data chunk,如果需要的话
126+
console.log(`[Stream Transformer] Ignoring chunk: ${chunk.type}`);
137127
break;
138128
}
139129
},

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

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import { type Config, GeminiChat } from '@google/gemini-cli-core';
7+
import {
8+
type Config,
9+
GeminiChat,
10+
GeminiEventType,
11+
} from '@google/gemini-cli-core';
812
import {
913
type Content,
1014
type Part,
@@ -17,6 +21,8 @@ import {
1721
type OpenAIMessage,
1822
type MessageContentPart,
1923
type OpenAIChatCompletionRequest,
24+
type StreamChunk,
25+
type ReasoningData,
2026
} from './types.js';
2127

2228
export class GeminiApiClient {
@@ -134,7 +140,7 @@ export class GeminiApiClient {
134140
messages: OpenAIMessage[];
135141
tools?: OpenAIChatCompletionRequest['tools'];
136142
tool_choice?: any;
137-
}) {
143+
}): Promise<AsyncGenerator<StreamChunk>> {
138144
const history = messages.map(msg => this.openAIMessageToGemini(msg));
139145
const lastMessage = history.pop();
140146
console.log(
@@ -183,6 +189,31 @@ export class GeminiApiClient {
183189
});
184190

185191
console.log('[GeminiApiClient] Got stream from Gemini.');
186-
return geminiStream;
192+
// Transform the event stream to a simpler StreamChunk stream
193+
return (async function* (): AsyncGenerator<StreamChunk> {
194+
for await (const event of geminiStream) {
195+
switch (event.type) {
196+
case GeminiEventType.Content:
197+
if (event.value) {
198+
yield { type: 'text', data: event.value };
199+
}
200+
break;
201+
case GeminiEventType.Thought:
202+
const reasoningData: ReasoningData = {
203+
reasoning: `${event.value.subject}: ${event.value.description}`,
204+
};
205+
yield { type: 'reasoning', data: reasoningData };
206+
break;
207+
case GeminiEventType.ToolCallRequest:
208+
yield { type: 'tool_code', data: event.value };
209+
break;
210+
// Ignore other event types for now
211+
case GeminiEventType.ChatCompressed:
212+
case GeminiEventType.Error:
213+
case GeminiEventType.UserCancelled:
214+
break;
215+
}
216+
}
217+
})();
187218
}
188219
}

packages/mcp-server/src/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,12 @@ export interface OpenAIChatCompletionRequest {
3030
tools?: any[]; // 对应 Gemini 的 Tool[]
3131
tool_choice?: any; // 对应 Gemini 的 ToolConfig
3232
}
33+
34+
export interface ReasoningData {
35+
reasoning: string;
36+
}
37+
38+
export type StreamChunk =
39+
| { type: 'text'; data: string }
40+
| { type: 'reasoning'; data: ReasoningData }
41+
| { type: 'tool_code'; data: { name: string; args: Record<string, unknown> } };

0 commit comments

Comments
 (0)