mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
Filter thought parts before passing them to CountToken (#6859)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Arya Gummadi <aryagummadi@google.com>
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
|||||||
GenerateContentResponse,
|
GenerateContentResponse,
|
||||||
FinishReason,
|
FinishReason,
|
||||||
BlockedReason,
|
BlockedReason,
|
||||||
|
Part,
|
||||||
} from '@google/genai';
|
} from '@google/genai';
|
||||||
|
|
||||||
describe('converter', () => {
|
describe('converter', () => {
|
||||||
@@ -349,5 +350,94 @@ describe('converter', () => {
|
|||||||
{ role: 'user', parts: [{ text: 'string 2' }] },
|
{ 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]',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -189,13 +189,18 @@ function toContent(content: ContentUnion): Content {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
if ('parts' in content) {
|
if ('parts' in content) {
|
||||||
// it's a Content
|
// it's a Content - process parts to handle thought filtering
|
||||||
return content;
|
return {
|
||||||
|
...content,
|
||||||
|
parts: content.parts
|
||||||
|
? toParts(content.parts.filter((p) => p != null))
|
||||||
|
: [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
// it's a Part
|
// it's a Part
|
||||||
return {
|
return {
|
||||||
role: 'user',
|
role: 'user',
|
||||||
parts: [content as Part],
|
parts: [toPart(content as Part)],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,6 +213,41 @@ function toPart(part: PartUnion): Part {
|
|||||||
// it's a string
|
// it's a string
|
||||||
return { text: part };
|
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<string, unknown>)['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;
|
return part;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user