Skip to content

fix(llm): tolerate list-shaped streaming delta content in strip_thinking_tokens - #6625

Merged
chenghao-mou merged 2 commits into
livekit:mainfrom
biztex:fix/strip-thinking-tokens-list-content
Jul 31, 2026
Merged

fix(llm): tolerate list-shaped streaming delta content in strip_thinking_tokens#6625
chenghao-mou merged 2 commits into
livekit:mainfrom
biztex:fix/strip-thinking-tokens-list-content

Conversation

@biztex

@biztex biztex commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Fixes #6323

Problem

strip_thinking_tokens assumes delta.content is str | None. The OpenAI-compatible ecosystem allows it to be a list of typed content parts ([{"type": "text", "text": ...}]) — Mistral emits this shape on some streamed chunks — and the openai SDK constructs stream models without validation (construct()), so when openai.LLM points at such a provider via base_url, the raw list reaches the filter and crashes the session. Wrapped as a retryable APIConnectionError mid-stream, a FallbackAdapter then silently fails the whole session over to its fallback leg — a hard-to-diagnose degradation.

Re: the open question on the issue about the source of the claim ("the OpenAI streaming spec only documents string"): correct for OpenAI's own service — the authority here is Mistral's chat-completions API, whose assistant/delta message content is typed string | ContentChunk[] in their reference and SDK. Since the plugin explicitly supports OpenAI-compatible providers through base_url, and the SDK's unvalidated stream models pass whatever the provider sends, the list shape is reachable in practice (that's how the original reporter hit it in production). Answered in more detail on the issue thread.

Change

Flatten list-shaped content to its concatenated text parts before the tag scan — dict parts ({"text": ...}), plain strings, and typed objects with a .text attribute. Unknown content types are dropped with a warning instead of crashing. String/None behavior is byte-identical.

Tests

Five new cases in tests/test_llm_utils.py: the issue's exact repro ([{"type": "text", "text": "Hallo"}]), mixed part lists, thinking-tag stripping across list-shaped chunks, non-text parts ignored, and typed objects with .text.

Relationship to #6324

This supersedes the stalled #6324 (same diagnosis, by @KSerProject — credit where due): that PR has had merge conflicts and an unsigned CLA for four weeks. Happy to close this one if the original author returns.

…ing_tokens

The OpenAI-compatible ecosystem allows delta.content to be a list of typed
content parts instead of a string - Mistral emits this shape on some streamed
chunks - and the openai SDK constructs stream models without validation, so
the list reaches strip_thinking_tokens as-is when the plugin points at such a
provider. The filter assumed str and crashed the session; wrapped as a
retryable APIConnectionError mid-stream, a FallbackAdapter then silently
moved the whole session to its fallback leg.

Flatten list-shaped content to its concatenated text parts (dict parts,
plain strings, and typed objects with a text attribute) before the tag scan.
Unknown content types are dropped with a warning instead of crashing.

Supersedes the stalled livekit#6324; credit to KSerProject for the original report
and fix direction.

Fixes livekit#6323

@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: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@chenghao-mou chenghao-mou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm, thanks for the PR. One small nit.

Comment thread livekit-agents/livekit/agents/llm/utils.py Outdated
a list of content parts with nothing textual in it (an image part, or an
empty list) was flattened to an empty string, claiming the provider sent
empty text. It now reports no content, like the None and unexpected-type
paths already do. A part that carries an empty string still yields "", the
same as a plain "" delta.
@chenghao-mou
chenghao-mou merged commit f971a26 into livekit:main Jul 31, 2026
15 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.

strip_thinking_tokens crashes on list-shaped streaming delta content (AttributeError: 'list' object has no attribute 'find')

2 participants