-
Notifications
You must be signed in to change notification settings - Fork 2.5k
fix: preserve extra_content for Gemini 3 thought_signature support #5267
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "kilo-code": patch | ||
| --- | ||
|
|
||
| fix: preserve extra_content for Gemini 3 thought_signature support |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -502,15 +502,29 @@ export function convertToOpenAiMessages( | |
| } | ||
|
|
||
| // Process tool use messages | ||
| let tool_calls: OpenAI.Chat.ChatCompletionMessageToolCall[] = toolMessages.map((toolMessage) => ({ | ||
| id: normalizeId(toolMessage.id), | ||
| type: "function", | ||
| function: { | ||
| name: toolMessage.name, | ||
| // json string | ||
| arguments: JSON.stringify(toolMessage.input), | ||
| }, | ||
| })) | ||
| // kilocode_change start: Use type assertion to access extra_content which may be added for Gemini 3 support | ||
| let tool_calls: (OpenAI.Chat.ChatCompletionMessageToolCall & { | ||
| extra_content?: Record<string, unknown> | ||
| })[] = toolMessages.map((toolMessage) => { | ||
| const toolCall: OpenAI.Chat.ChatCompletionMessageToolCall & { | ||
| extra_content?: Record<string, unknown> | ||
| } = { | ||
| id: normalizeId(toolMessage.id), | ||
| type: "function", | ||
| function: { | ||
| name: toolMessage.name, | ||
| // json string | ||
| arguments: JSON.stringify(toolMessage.input), | ||
| }, | ||
| } | ||
| // Preserve extra_content for Gemini 3 thought_signature support | ||
| const toolMessageWithExtra = toolMessage as any | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [WARNING]: Unvalidated Same pattern as in interface ToolUseBlockWithExtra extends Anthropic.ToolUseBlockParam {
extra_content?: Record<string, unknown>
}
const toolMessageWithExtra = toolMessage as ToolUseBlockWithExtraThis would provide compile-time safety while still allowing the extra field. |
||
| if (toolMessageWithExtra.extra_content) { | ||
| toolCall.extra_content = toolMessageWithExtra.extra_content | ||
| } | ||
| return toolCall | ||
| }) | ||
| // kilocode_change end | ||
|
|
||
| // Check if the message has reasoning_details (used by Gemini 3, xAI, etc.) | ||
| const messageWithDetails = anthropicMessage as any | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3086,6 +3086,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike { | |
| id: chunk.id, | ||
| name: chunk.name, | ||
| arguments: chunk.arguments, | ||
| extra_content: chunk.extra_content, | ||
| }) | ||
|
|
||
| for (const event of events) { | ||
|
|
@@ -3103,7 +3104,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike { | |
| } | ||
|
|
||
| // Initialize streaming in NativeToolCallParser | ||
| NativeToolCallParser.startStreamingToolCall(event.id, event.name as ToolName) | ||
| NativeToolCallParser.startStreamingToolCall( | ||
| event.id, | ||
| event.name as ToolName, | ||
| event.extra_content, | ||
| ) | ||
|
|
||
| // Before adding a new tool, finalize any preceding text block | ||
| // This prevents the text block from blocking tool presentation | ||
|
|
@@ -3128,6 +3133,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike { | |
| // Store the ID for native protocol | ||
| ;(partialToolUse as any).id = event.id | ||
|
|
||
| // Preserve extra_content for Gemini 3 thought_signature support | ||
| if (event.extra_content) { | ||
| partialToolUse.extra_content = event.extra_content | ||
| } | ||
|
|
||
| // Add to content and present | ||
| this.assistantMessageContent.push(partialToolUse) | ||
| this.userMessageContentReady = false | ||
|
|
@@ -3146,6 +3156,14 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike { | |
| // Store the ID for native protocol | ||
| ;(partialToolUse as any).id = event.id | ||
|
|
||
| // Preserve extra_content from the original tool use (Gemini 3 thought_signature) | ||
| const existingToolUse = this.assistantMessageContent[ | ||
| toolUseIndex | ||
| ] as any | ||
| if (existingToolUse?.extra_content && !partialToolUse.extra_content) { | ||
| partialToolUse.extra_content = existingToolUse.extra_content | ||
| } | ||
|
|
||
| // Update the existing tool use with new partial data | ||
| this.assistantMessageContent[toolUseIndex] = partialToolUse | ||
|
|
||
|
|
@@ -3164,6 +3182,16 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike { | |
| // Store the tool call ID | ||
| ;(finalToolUse as any).id = event.id | ||
|
|
||
| // Preserve extra_content from the original tool use (Gemini 3 thought_signature) | ||
| if (toolUseIndex !== undefined) { | ||
| const existingToolUse = this.assistantMessageContent[ | ||
| toolUseIndex | ||
| ] as any | ||
| if (existingToolUse?.extra_content && !finalToolUse.extra_content) { | ||
| finalToolUse.extra_content = existingToolUse.extra_content | ||
| } | ||
| } | ||
|
|
||
| // Get the index and replace partial with final | ||
| if (toolUseIndex !== undefined) { | ||
| this.assistantMessageContent[toolUseIndex] = finalToolUse | ||
|
|
@@ -3762,12 +3790,17 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike { | |
| continue | ||
| } | ||
| seenToolUseIds.add(sanitizedId) | ||
| assistantContent.push({ | ||
| const toolUseBlock: any = { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [WARNING]: Type safety regression — The previous code pushed a typed object literal directly into Consider using an intersection type instead to preserve type safety while allowing the extra property: const toolUseBlock: Anthropic.ToolUseBlockParam & { extra_content?: Record<string, unknown> } = {
type: "tool_use" as const,
id: sanitizedId,
name: mcpBlock.name,
input: mcpBlock.arguments,
}The same applies to the second |
||
| type: "tool_use" as const, | ||
| id: sanitizedId, | ||
| name: mcpBlock.name, // Original dynamic name | ||
| input: mcpBlock.arguments, // Direct tool arguments | ||
| }) | ||
| } | ||
| // Preserve extra_content for Gemini 3 thought_signature support | ||
| if (mcpBlock.extra_content) { | ||
| toolUseBlock.extra_content = mcpBlock.extra_content | ||
| } | ||
| assistantContent.push(toolUseBlock) | ||
| } | ||
| } else { | ||
| // Regular ToolUse | ||
|
|
@@ -3792,12 +3825,17 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike { | |
| // was told the tool was named, preventing confusion in multi-turn conversations. | ||
| const toolNameForHistory = toolUse.originalName ?? toolUse.name | ||
|
|
||
| assistantContent.push({ | ||
| const toolUseBlock: any = { | ||
| type: "tool_use" as const, | ||
| id: sanitizedId, | ||
| name: toolNameForHistory, | ||
| input, | ||
| }) | ||
| } | ||
| // Preserve extra_content for Gemini 3 thought_signature support | ||
| if (toolUse.extra_content) { | ||
| toolUseBlock.extra_content = toolUse.extra_content | ||
| } | ||
| assistantContent.push(toolUseBlock) | ||
| } | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[WARNING]: Unvalidated
as anycast on untrusted API response data(toolCall as any).extra_contentextracts an arbitrary property from the API stream response without any validation. This value flows through the entire pipeline and is eventually sent back to the API in subsequent requests.Consider adding minimal validation (e.g., checking it's a plain object) to prevent unexpected data types from propagating: