Skip to content

fix(google): add fallback thought signature sentinel#6138

Open
nightcityblade wants to merge 2 commits into
livekit:mainfrom
nightcityblade:fix/issue-6135
Open

fix(google): add fallback thought signature sentinel#6138
nightcityblade wants to merge 2 commits into
livekit:mainfrom
nightcityblade:fix/issue-6135

Conversation

@nightcityblade

Copy link
Copy Markdown
Contributor

Fixes #6135

Summary

  • inject Google's documented skip_thought_signature_validator sentinel when a Gemini fallback sees function calls without stored thought signatures
  • keep existing real thought signatures unchanged and omit signatures for models that do not require them
  • add regression coverage for existing, missing, and disabled thought signature formatting

Tests

  • uv run --with-editable livekit-agents --with-editable livekit-plugins/livekit-plugins-google pytest tests/test_google_thought_signatures.py -q
  • uv run ruff check livekit-agents/livekit/agents/llm/_provider_format/google.py tests/test_google_thought_signatures.py

@nightcityblade nightcityblade requested a review from a team as a code owner June 17, 2026 15:16
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


nightcityblade seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@devin-ai-integration devin-ai-integration Bot 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.

Devin Review found 1 potential issue.

Open in Devin Review

Comment on lines +70 to +73
if thought_signatures is not None:
fc_part["thought_signature"] = thought_signatures.get(
msg.call_id, b"skip_thought_signature_validator"
)

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.

🚩 Sentinel value relies on undocumented Google API behavior

The sentinel b"skip_thought_signature_validator" at livekit-agents/livekit/agents/llm/_provider_format/google.py:72 is described in the comment as "Google's documented validator-bypass sentinel." This is a critical dependency on an external API behavior β€” if Google removes or changes this sentinel in a future API version, all FallbackAdapter multi-turn function calls to Gemini 2.5+ would start failing. It would be worth confirming this sentinel is part of a stable, documented API contract rather than an internal implementation detail that could change without notice.

Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It's pretty stable IMO (6+ months and counting) and I tested locally.

Comment on lines +35 to +36
turns, _ = ctx.to_provider_format(format="google", thought_signatures={})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Consider removing the parameter, to resemble what happens in livekit-agents

Suggested change
turns, _ = ctx.to_provider_format(format="google", thought_signatures={})
turns, _ = ctx.to_provider_format(format="google")

@igui

igui commented Jun 17, 2026

Copy link
Copy Markdown

Thanks @nightcityblade , take into account the code assumes the _thought_signatures is present, but in our case in the original issue, the _thought_signatures dictionary is None as in a brand new google.LLM instance. Also, take into account older versions of gemini LLMs might not accept the thought-signatures parameter!
I recall this being the case but it's being a while since I tested in the older pre 2.5 models.

@nightcityblade

Copy link
Copy Markdown
Contributor Author

Thanks for the feedback β€” addressed in 3ef3206.

Changes:

  • Treat a missing/None _thought_signatures cache as an empty mapping for Gemini 2.5+ so the formatter can still add the fallback sentinel.
  • Initialize the cache before storing a returned thought signature if it was None.
  • Updated the formatting test to call ctx.to_provider_format(format="google") where no thought-signature argument is needed, and added coverage for storing into a None cache.

Checks run:

  • uv run ruff check livekit-plugins/livekit-plugins-google/livekit/plugins/google/llm.py tests/test_google_thought_signatures.py tests/test_plugin_google_llm.py
  • uv run ruff format --check livekit-plugins/livekit-plugins-google/livekit/plugins/google/llm.py tests/test_google_thought_signatures.py tests/test_plugin_google_llm.py
  • uv run pytest tests/test_google_thought_signatures.py tests/test_plugin_google_llm.py

@devin-ai-integration devin-ai-integration Bot 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.

Devin Review found 2 new potential issues.

Open in Devin Review

):
if getattr(self._llm, "_thought_signatures", None) is None:
self._llm._thought_signatures = {}
self._llm._thought_signatures[tool_call.call_id] = part.thought_signature

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.

🚩 Unbounded growth of _thought_signatures dict

The _thought_signatures dict on the LLM instance (livekit-plugins/livekit-plugins-google/livekit/plugins/google/llm.py:232) accumulates an entry for every function call made across the LLM's lifetime and is never pruned. For long-lived agents that make many tool calls, this dict will grow indefinitely. This is a pre-existing issue (not introduced by this PR), but worth noting since this PR increases the reliance on this dict by always consulting it for Gemini 2.5+ models. A possible improvement would be to evict entries once their corresponding chat turns are no longer in the active context.

Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Comment on lines +70 to +73
if thought_signatures is not None:
fc_part["thought_signature"] = thought_signatures.get(
msg.call_id, b"skip_thought_signature_validator"
)

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.

🚩 Behavioral change: function calls without signatures now get sentinel instead of no signature

The old formatter code at livekit-agents/livekit/agents/llm/_provider_format/google.py:66-68 (base) used if thought_signatures and (sig := thought_signatures.get(msg.call_id)): β€” this meant empty dicts were falsy (no injection), and missing call_ids got no signature. The new code at lines 70-73 uses if thought_signatures is not None: with a sentinel default. This is a significant behavioral change: every function_call part in a Gemini 2.5+ request will now carry either a real signature or b"skip_thought_signature_validator". This is the intended fix, but it changes the wire format for all multi-turn requests, not just the FallbackAdapter case. Confirm that Google's API accepts the sentinel for function calls that previously had valid signatures in prior turns (i.e., ensure mixing real signatures and sentinels in the same request is valid).

Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

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.

google.LLM in FallbackAdapter fails with HTTP 400 when ChatContext contains function calls from a non-Google model

3 participants