diff --git a/packages/core/src/code_assist/converter.test.ts b/packages/core/src/code_assist/converter.test.ts index ab040fe6..0b71e33f 100644 --- a/packages/core/src/code_assist/converter.test.ts +++ b/packages/core/src/code_assist/converter.test.ts @@ -17,6 +17,7 @@ import { GenerateContentResponse, FinishReason, BlockedReason, + Part, } from '@google/genai'; describe('converter', () => { @@ -349,5 +350,94 @@ describe('converter', () => { { role: 'user', parts: [{ text: 'string 2' }] }, ]); }); + + it('should convert thought parts to text parts for API compatibility', () => { + const contentWithThought: ContentListUnion = { + role: 'model', + parts: [ + { text: 'regular text' }, + { thought: 'thinking about the problem' } as Part & { + thought: string; + }, + { text: 'more text' }, + ], + }; + expect(toContents(contentWithThought)).toEqual([ + { + role: 'model', + parts: [ + { text: 'regular text' }, + { text: '[Thought: thinking about the problem]' }, + { text: 'more text' }, + ], + }, + ]); + }); + + it('should combine text and thought for text parts with thoughts', () => { + const contentWithTextAndThought: ContentListUnion = { + role: 'model', + parts: [ + { + text: 'Here is my response', + thought: 'I need to be careful here', + } as Part & { thought: string }, + ], + }; + expect(toContents(contentWithTextAndThought)).toEqual([ + { + role: 'model', + parts: [ + { + text: 'Here is my response\n[Thought: I need to be careful here]', + }, + ], + }, + ]); + }); + + it('should preserve non-thought properties while removing thought', () => { + const contentWithComplexPart: ContentListUnion = { + role: 'model', + parts: [ + { + functionCall: { name: 'calculate', args: { x: 5, y: 10 } }, + thought: 'Performing calculation', + } as Part & { thought: string }, + ], + }; + expect(toContents(contentWithComplexPart)).toEqual([ + { + role: 'model', + parts: [ + { + functionCall: { name: 'calculate', args: { x: 5, y: 10 } }, + }, + ], + }, + ]); + }); + + it('should convert invalid text content to valid text part with thought', () => { + const contentWithInvalidText: ContentListUnion = { + role: 'model', + parts: [ + { + text: 123, // Invalid - should be string + thought: 'Processing number', + } as Part & { thought: string; text: number }, + ], + }; + expect(toContents(contentWithInvalidText)).toEqual([ + { + role: 'model', + parts: [ + { + text: '123\n[Thought: Processing number]', + }, + ], + }, + ]); + }); }); }); diff --git a/packages/core/src/code_assist/converter.ts b/packages/core/src/code_assist/converter.ts index 7ca17386..89f5b525 100644 --- a/packages/core/src/code_assist/converter.ts +++ b/packages/core/src/code_assist/converter.ts @@ -189,13 +189,18 @@ function toContent(content: ContentUnion): Content { }; } if ('parts' in content) { - // it's a Content - return content; + // it's a Content - process parts to handle thought filtering + return { + ...content, + parts: content.parts + ? toParts(content.parts.filter((p) => p != null)) + : [], + }; } // it's a Part return { role: 'user', - parts: [content as Part], + parts: [toPart(content as Part)], }; } @@ -208,6 +213,41 @@ function toPart(part: PartUnion): Part { // it's a string return { text: part }; } + + // Handle thought parts for CountToken API compatibility + // The CountToken API expects parts to have certain required "oneof" fields initialized, + // but thought parts don't conform to this schema and cause API failures + if ('thought' in part && part.thought) { + const thoughtText = `[Thought: ${part.thought}]`; + + const newPart = { ...part }; + delete (newPart as Record)['thought']; + + const hasApiContent = + 'functionCall' in newPart || + 'functionResponse' in newPart || + 'inlineData' in newPart || + 'fileData' in newPart; + + if (hasApiContent) { + // It's a functionCall or other non-text part. Just strip the thought. + return newPart; + } + + // If no other valid API content, this must be a text part. + // Combine existing text (if any) with the thought, preserving other properties. + const text = (newPart as { text?: unknown }).text; + const existingText = text ? String(text) : ''; + const combinedText = existingText + ? `${existingText}\n${thoughtText}` + : thoughtText; + + return { + ...newPart, + text: combinedText, + }; + } + return part; }