Skip to content

v1.20.13 — Gemini 3.x list-content fix

Choose a tag to compare

@jgouviergmail jgouviergmail released this 21 May 22:41

🔧 Fixed — Responses no longer fail or degrade with Gemini 3.x models

Gemini 3.x (gemini-3.5-flash, gemini-3-pro-preview) returns AIMessage.content as a list of content blocks ([{"type":"text","text":"…"}]) instead of a plain string like earlier Gemini and every other provider. Several code paths assumed str and broke:

  • SSE token streaming aborted with a ChatStreamChunk ValidationError — surfacing to the user as "Un problème est survenu lors de la génération de la réponse" — in both Pipeline and ReAct modes (they share the response-node token stream).
  • ReAct mode also crashed in format_conversation_history ('list' object has no attribute 'strip') whenever the conversation history held Gemini list-content messages — independent of the response model (mixed-provider setups were equally affected).
  • Silent degradations: skipped psyche / registry-filtering / HTML-widget post-processing, garbled voice (TTS) output, distorted token counts, dropped tokens after a HITL resumption, email-draft generation, and the structured_output JSON-mode fallback.

♻️ Changed — Canonical content-normalization primitive (DRY)

New coerce_content_to_text() (infrastructure/llm/message_text.py) normalizes any .content shape (str, Gemini 3.x list[dict] blocks, None) to plain text, mirroring LangChain BaseMessage.text. Applied at the shared chokepoints (SSE token formatter, format_conversation_history, structured_output fallback) so all callers are protected; four pre-existing bespoke list → text implementations consolidated onto it. Non-destructive — messages stored in graph state are never mutated.

Tests: new helper + regression suites reproducing both crashes; Ruff / Black / MyPy clean; 8316 unit tests green.
Docs: docs/technical/LLM_PROVIDER_CONSTRAINTS.md.
No DB migration, no new env var, no endpoint change.

🤖 Generated with Claude Code