Skip to content

A tool call's extra_content rides back to the endpoint within the turn - #1183

Merged
WaylandYang merged 2 commits into
deeplethe:devfrom
gedaiu:fix/echo-gemini-thought-signature
Oct 10, 2026
Merged

WaylandYang merged 2 commits into
deeplethe:devfrom
gedaiu:fix/echo-gemini-thought-signature

Conversation

@gedaiu

@gedaiu gedaiu commented Oct 10, 2026

Copy link
Copy Markdown
Contributor

What happened

Against Gemini's OpenAI-compatible endpoint (gemini-3.8-flash), every chat question that used a tool ended with answer_failed on its second turn, while the model connection test passed. Reproduced with curl: Gemini 3 attaches a signature to each function call,

"tool_calls":[{"extra_content":{"google":{"thought_signature":"Eq0C…"}},"function":{…},"id":"call_170866","type":"function"}]

and the turn that returns the tool result without it is refused:

400 INVALID_ARGUMENT: Function call is missing a thought_signature in functionCall parts.
This is required for tools to work correctly … https://ai.google.dev/gemini-api/docs/thought-signatures

The same turn with the signature echoed answers normally. Function calls from earlier, completed turns are accepted without one (also verified), so this is about the in-turn replay only.

What changed

  • utopia_llm::ToolCall gains extra_content: Option<Value>, the endpoint's object kept opaque, read in both the unary and the streaming parser, written back by to_message. Not an object (absent, null, a string) reads as none and adds no key.
  • rig_model: a streamed call carries it in RawStreamingToolCall::additional_params, a unary one in ToolCall::additional_params (rig preserves both into history), and push_message writes it back as extra_content.

Tests: both transports keep the Gemini shape and echo it; absent or malformed extra_content adds no key; the turn → rig → wire round trip carries it; a call without one stays as before.

cargo fmt --check, cargo clippy --all-targets -- -D warnings, cargo test -p utopia-llm -p utopia-server pass (49 + 603).

Related: #1135 (embedding index), the other half of running rc8 against Gemini.

Gemini 3 attaches extra_content.google.thought_signature to every
function call and refuses the turn that returns the tool result without
it: HTTP 400 "Function call is missing a thought_signature". Utopia kept
id, name and arguments of a call and dropped the rest, so every question
that used a tool failed on its second turn against Gemini while the
connection test, a single tool-free message, passed.

ToolCall keeps the endpoint's extra_content as an opaque object, in both
transports; rig carries it in the slot it already has for provider
metadata, additional_params; the wire writes it back as extra_content.
A call without one adds no key. Calls from earlier, completed turns need
no signature, so persisted history is left alone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Bogdan Szabo <bogdan@re-cinq.com>

@WaylandYang WaylandYang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thank you for this, and for the curl reproduction: it made the cause plain. The change reads correctly. extra_content is kept opaque, read in both the unary and the streaming parser, and written back only when the endpoint sent an object, so other endpoints see the same requests as before. Checked here: the utopia-llm tests and the adapter tests pass, and the branch merges cleanly with dev. Merging now. Thanks again.

@WaylandYang
WaylandYang merged commit d862b81 into deeplethe:dev Oct 10, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants