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
5 changes: 5 additions & 0 deletions .changeset/salty-regions-relate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@livekit/agents': patch
---

preserve thought_signature across parallel tool calls for Gemini 3+ for inference gateway
13 changes: 11 additions & 2 deletions agents/src/inference/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,10 @@ export class LLMStream extends llm.LLMStream {
if (this.toolCallId && tool.id && tool.index !== this.toolIndex) {
callChunk = this.createRunningToolCallChunk(id, delta);
this.toolCallId = this.fncName = this.fncRawArguments = undefined;
this.toolExtra = undefined;
// Note: We intentionally do NOT reset toolExtra here.
// For Gemini 3+, the thought_signature is only provided on the first tool call
// in a parallel batch, but must be applied to ALL tool calls in the batch.
// We preserve toolExtra so subsequent tool calls inherit the thought_signature.
}

// Start or continue building the current tool call
Expand All @@ -443,9 +446,14 @@ export class LLMStream extends llm.LLMStream {
this.fncName = tool.function.name;
this.fncRawArguments = tool.function.arguments || '';
// Extract extra from tool call (e.g., Google thought signatures)
this.toolExtra =
// Only update toolExtra if this tool call has extra_content.
// Otherwise, inherit from previous tool call (for parallel Gemini tool calls).
const newToolExtra =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
((tool as any).extra_content as Record<string, unknown> | undefined) ?? undefined;
if (newToolExtra) {
this.toolExtra = newToolExtra;
}
} else if (tool.function.arguments) {
this.fncRawArguments = (this.fncRawArguments || '') + tool.function.arguments;
}
Expand All @@ -464,6 +472,7 @@ export class LLMStream extends llm.LLMStream {
) {
const callChunk = this.createRunningToolCallChunk(id, delta);
this.toolCallId = this.fncName = this.fncRawArguments = undefined;
// Reset toolExtra at the end of the response (not between parallel tool calls)
this.toolExtra = undefined;
return callChunk;
}
Expand Down
4 changes: 2 additions & 2 deletions agents/src/voice/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,16 +325,16 @@ export class Agent<UserData = any> {
);
}

// TODO(brian): make parallelToolCalls configurable
const { toolChoice } = modelSettings;
const connOptions = activity.agentSession.connOptions.llmConnOptions;

// parallelToolCalls is not passed here - it will use the value from LLM's modelOptions
// This allows users to configure it via: new inference.LLM({ modelOptions: { parallel_tool_calls: false } })
const stream = activity.llm.chat({
chatCtx,
toolCtx,
toolChoice,
connOptions,
parallelToolCalls: true,

@toubatbrian toubatbrian Jan 27, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Note: although this was hardcoded to true, the actual value was still correctly been overriden by user config inside LLM in previous version.

});

let cleaned = false;
Expand Down