mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-31 14:09:42 +07:00
🐛 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:
@@ -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'),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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('') || '';
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user