Repository navigation
Conversation
Gemini returns a thoughtSignature as a sibling of each functionCall part, and Gemini 3 function calling rejects the follow-up request with HTTP 400 when the signature is not sent back with the same tool call. The client dropped it end to end: - _parse_response_data only copied keys from inside functionCall into _gemini_extra, so the part-level thoughtSignature was lost. - stream() collapsed every part's signature into one response-level value that is not tied to any tool call. - _build_payload never emitted a part-level thoughtSignature. - The context sanitizer and the durable runtime rebuilt tool calls without the signature, so it also vanished across steps. Keep the signature on the normalized tool call as _gemini_thought_signature, carry it through the sanitizer, store it in the runtime keyed by Call Instance ID (provider_thought_signatures, mirroring provider_call_ids), restore it when the prompt is rebuilt, and emit it as a part-level sibling of functionCall in the Gemini payload. Tool calls without a signature serialize exactly as before. Fixes dataelement#1028
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #1028. With a Gemini 3 model on the native Gemini provider, the first tool call succeeds and the follow-up request fails with HTTP 400: Google returns a
thoughtSignatureas a sibling of eachfunctionCallpart and requires it back, unchanged, in the same part on the next request — but Clawith dropped it at every step:GeminiClient._parse_response_datacopied only keys from insidefunctionCallinto_gemini_extra; the part-levelthoughtSignaturenever reached the normalized tool call.GeminiClient.stream()collapsed all parts' signatures into one response-levelreasoning_signaturethat is not tied to any tool call._sanitize_tool_calls_for_context(caller.py) and the durable runtime (model_step_service.py) rebuild tool calls from id/name/arguments only, so the signature also vanished between steps.GeminiClient._build_payloadnever emitted a part-levelthoughtSignature(it merged_gemini_extrainto thefunctionCallobject instead).Changes
The signature now travels on the normalized tool call as
_gemini_thought_signature, next to the existing_gemini_extra:backend/app/services/llm/client.py_parse_response_datastores the part-levelthoughtSignatureon the tool call.stream()binds the signature of the part that carries afunctionCallto that tool call (the response-levelreasoning_signaturebehaviour is unchanged)._build_payloademits{"functionCall": {...}, "thoughtSignature": ...}as a part-level sibling when the tool call carries a signature.backend/app/services/llm/caller.py—_sanitize_tool_calls_for_contextcarries_gemini_thought_signatureexactly like_gemini_extra.backend/app/services/agent_runtime/model_step_service.py—_with_call_instancesstores the signatures keyed by Call Instance ID inadditional_kwargs["provider_thought_signatures"](mirroringprovider_call_ids, and only when at least one signature exists, so stored messages for other providers keep their exact previous shape);_prompt_messagesre-attaches the signature to the rebuilt tool call by Call Instance ID.Tool calls without a signature serialize byte-identically to before.
Scope note: this covers the native Gemini provider from the issue's reproduction steps. The OpenAI-compatible Gemini endpoint (
.../v1beta/openai/), where the signature travels asextra_content.google.thought_signatureon OpenAI-shaped tool calls, loses it in a different place (stream aggregation inOpenAICompatibleClient) and is not changed here.Tests
New
backend/tests/test_llm_gemini_thought_signature.py(9 tests): non-streaming parse keeps the signature on the tool call and the next payload sends it back as a part-level sibling; streaming (httpxMockTransportSSE) binds the signature to its own tool call; the context sanitizer keeps it; the runtime stores it under the Call Instance ID and restores it onto the rebuilt tool call, ending in the final Gemini payload. With the three source files reverted, the 6 behaviour tests fail (the signature is lost end to end, matching the issue) and the 3 guards still pass (a response without a signature gains no key, a payload without a signature is byte-unchanged, runtime storage without signatures keeps its exact previous shape); with the fix, all 9 pass.Full backend suite (root pytest config): 9 failed / 2564 passed with the fix vs 9 failed / 2555 passed on unmodified
mainin the same sandbox — the failure sets are identical (pre-existingtest_deploy_tools.pyenvironment failures) and the +9 are the new tests.ruff checkon the changed files reports the same 38 pre-existing findings before and after; this change adds none.