🐛 fix: Gemini 3 Pro does not display thought summaries (#10345)

* 💄 style: update filter logic to retain thoughtSignature metadata in Google stream processing

* add tests
This commit is contained in:
sxjeru
2025-11-22 00:02:23 +08:00
committed by GitHub
parent 9a799ec6a8
commit 89e296a1c3
4 changed files with 161 additions and 3 deletions

View File

@@ -1239,4 +1239,65 @@ describe('GoogleGenerativeAIStream', () => {
]);
});
});
describe('Thought filtering logic', () => {
it('should keep text and thoughtSignature when both exist in parts', async () => {
vi.spyOn(uuidModule, 'nanoid').mockReturnValueOnce('1');
const data = [
{
candidates: [
{
content: {
parts: [
{
text: 'Here is my answer',
thoughtSignature: 'sig123',
},
],
role: 'model',
},
finishReason: 'STOP',
index: 0,
},
],
usageMetadata: {
promptTokenCount: 10,
candidatesTokenCount: 5,
totalTokenCount: 15,
promptTokensDetails: [{ modality: 'TEXT', tokenCount: 10 }],
thoughtsTokenCount: 50,
},
},
];
const mockGoogleStream = new ReadableStream({
start(controller) {
data.forEach((item) => {
controller.enqueue(item);
});
controller.close();
},
});
const protocolStream = GoogleGenerativeAIStream(mockGoogleStream);
const chunks = await decodeStreamChunks(protocolStream);
expect(chunks).toEqual(
[
'id: chat_1',
'event: text',
'data: "Here is my answer"\n',
'id: chat_1',
'event: stop',
'data: "STOP"\n',
'id: chat_1',
'event: usage',
'data: {"inputTextTokens":10,"outputImageTokens":0,"outputReasoningTokens":50,"outputTextTokens":5,"totalInputTokens":10,"totalOutputTokens":55,"totalTokens":15}\n',
].map((i) => i + '\n'),
);
});
});
});

View File

@@ -106,10 +106,10 @@ const transformGoogleGenerativeAIStream = (
}
// Parse text from candidate.content.parts
// Filter out thought content (thought: true) and thoughtSignature
// Filter out thought content (thought: true) only, keep thoughtSignature as it's just metadata
const text =
candidate?.content?.parts
?.filter((part: any) => part.text && !part.thought && !part.thoughtSignature)
?.filter((part: any) => part.text && !part.thought)
.map((part: any) => part.text)
.join('') || '';

View File

@@ -621,3 +621,99 @@ describe('resolveModelThinkingBudget', () => {
expect(resolveModelThinkingBudget('unknown-model', 99_999)).toBe(24_576);
});
});
describe('thinkingConfig includeThoughts logic', () => {
it('should enable thinking when thinkingBudget is set', async () => {
const mockStreamData = (async function* (): AsyncGenerator<GenerateContentResponse> {})();
vi.spyOn(instance['client'].models, 'generateContentStream').mockResolvedValue(mockStreamData);
await instance.chat({
messages: [{ content: 'Hello', role: 'user' }],
model: 'gemini-2.5-pro',
thinkingBudget: 5000,
temperature: 0,
});
const callArgs = (instance['client'].models.generateContentStream as any).mock.calls[0];
const config = callArgs[0].config;
expect(config.thinkingConfig?.includeThoughts).toBe(true);
});
it('should enable thinking when thinkingLevel is set', async () => {
const mockStreamData = (async function* (): AsyncGenerator<GenerateContentResponse> {})();
vi.spyOn(instance['client'].models, 'generateContentStream').mockResolvedValue(mockStreamData);
await instance.chat({
messages: [{ content: 'Hello', role: 'user' }],
model: 'gemini-3-pro',
thinkingLevel: 'high',
temperature: 0,
});
const callArgs = (instance['client'].models.generateContentStream as any).mock.calls[0];
const config = callArgs[0].config;
expect(config.thinkingConfig?.includeThoughts).toBe(true);
});
it('should enable thinking for gemini-3-pro-image models', async () => {
const mockStreamData = (async function* (): AsyncGenerator<GenerateContentResponse> {})();
vi.spyOn(instance['client'].models, 'generateContentStream').mockResolvedValue(mockStreamData);
await instance.chat({
messages: [{ content: 'Hello', role: 'user' }],
model: 'gemini-3-pro-image-preview',
temperature: 0,
});
const callArgs = (instance['client'].models.generateContentStream as any).mock.calls[0];
const config = callArgs[0].config;
expect(config.thinkingConfig?.includeThoughts).toBe(true);
});
it('should enable thinking for thinking-enabled models', async () => {
const mockStreamData = (async function* (): AsyncGenerator<GenerateContentResponse> {})();
vi.spyOn(instance['client'].models, 'generateContentStream').mockResolvedValue(mockStreamData);
await instance.chat({
messages: [{ content: 'Hello', role: 'user' }],
model: 'gemini-2.0-flash-thinking-exp',
temperature: 0,
});
const callArgs = (instance['client'].models.generateContentStream as any).mock.calls[0];
const config = callArgs[0].config;
expect(config.thinkingConfig?.includeThoughts).toBe(true);
});
it('should disable thinking when resolvedThinkingBudget is 0', async () => {
const mockStreamData = (async function* (): AsyncGenerator<GenerateContentResponse> {})();
vi.spyOn(instance['client'].models, 'generateContentStream').mockResolvedValue(mockStreamData);
await instance.chat({
messages: [{ content: 'Hello', role: 'user' }],
model: 'gemini-2.5-flash-lite',
thinkingBudget: 0,
temperature: 0,
});
const callArgs = (instance['client'].models.generateContentStream as any).mock.calls[0];
const config = callArgs[0].config;
expect(config.thinkingConfig?.includeThoughts).toBeUndefined();
});
it('should add thinkingLevel to config for 3.x models when provided', async () => {
const mockStreamData = (async function* (): AsyncGenerator<GenerateContentResponse> {})();
vi.spyOn(instance['client'].models, 'generateContentStream').mockResolvedValue(mockStreamData);
await instance.chat({
messages: [{ content: 'Hello', role: 'user' }],
model: 'gemini-3-pro',
thinkingLevel: 'high',
temperature: 0,
});
const callArgs = (instance['client'].models.generateContentStream as any).mock.calls[0];
const config = callArgs[0].config as any;
expect(config.thinkingConfig?.thinkingLevel).toBe('high');
});
});

View File

@@ -202,7 +202,8 @@ export class LobeGoogleAI implements LobeRuntimeAI {
const thinkingConfig: ThinkingConfig = {
includeThoughts:
(!!thinkingBudget ||
(model && (model.includes('-2.5-') || model.includes('thinking')))) &&
!!thinkingLevel ||
(model && (model.includes('-3-pro-image') || model.includes('thinking')))) &&
resolvedThinkingBudget !== 0
? true
: undefined,