Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/core/src/core/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,8 @@ export class GeminiClient {
const text =
message.parts
?.filter(
(part): part is { text: string } => typeof part.text === 'string',
(part): part is { text: string } =>
typeof part.text === 'string' && !part.thought,
)
.map((part) => part.text)
.join('') ?? '';
Expand Down
21 changes: 21 additions & 0 deletions packages/core/src/core/geminiChat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4136,6 +4136,27 @@ describe('GeminiChat', async () => {
expect(chat.getLastModelMessageText()).toBe('new answer');
expect(structuredCloneSpy).not.toHaveBeenCalled();
});

it('filters out thought parts from the last model message', () => {
chat.addHistory({
role: 'model',
parts: [
{ text: 'internal reasoning...', thought: true },
{ text: 'visible response' },
],
});

expect(chat.getLastModelMessageText()).toBe('visible response');
});

it('returns undefined when all text parts are thoughts', () => {
chat.addHistory({
role: 'model',
parts: [{ text: 'only thinking', thought: true }],
});

expect(chat.getLastModelMessageText()).toBeUndefined();
});
});

describe('sendMessageStream with retries', () => {
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/core/geminiChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2776,7 +2776,8 @@ export class GeminiChat {
const text =
message.parts
?.filter(
(part): part is { text: string } => typeof part.text === 'string',
(part): part is { text: string } =>
typeof part.text === 'string' && !part.thought,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The inline filter here is a subset of the existing isPlainTextPart type guard (line 613) which delegates to isValidNonThoughtTextPart (line 859). The shared predicate already checks !part.thought && !part.thoughtSignature plus defensive guards for functionCall/functionResponse/inlineData/fileData — matching the codebase's stated principle: "Technically, the model should never generate parts that have text and any of these but we don't trust them so check anyways."

Reusing the file-local getPlainTextFromParts() helper (line 625) would cover the thoughtSignature gap, eliminate the duplication with client.ts, and centralize future filter changes:

Suggested change
typeof part.text === 'string' && !part.thought,
const text = getPlainTextFromParts(message.parts);

For the client.ts fallback, export getPlainTextFromParts (or isValidNonThoughtTextPart) and import it there. Also consider adding a test case for thoughtSignature-only parts to protect the broader predicate.

— qwen3.7-max via Qwen Code /review

)
.map((part) => part.text)
.join('') ?? '';
Expand Down
Loading