Skip to content

fix: yield thinking tag boundaries in content='all' mode#297

Merged
cpsievert merged 2 commits intomainfrom
fix/thinking-tags-all-mode
May 7, 2026
Merged

fix: yield thinking tag boundaries in content='all' mode#297
cpsievert merged 2 commits intomainfrom
fix/thinking-tags-all-mode

Conversation

@cpsievert
Copy link
Copy Markdown
Collaborator

Summary

Extends the <thinking> tag boundary emission (from #294) to content="all" mode. Previously, tag boundary strings were only yielded to consumers in content="text" mode. Now they're yielded in all modes.

What changed

Removed the if content_mode == "text": guards around the tag boundary yield statements in both sync and async _submit_turns. The emit() calls (internal echo) were already unconditional; this makes the consumer-facing yields match.

In content="all" mode, the stream now yields:

  1. "<thinking>\n" (string)
  2. N × ContentThinking objects (unchanged)
  3. "\n</thinking>\n\n" (string)
  4. Response text strings

Why

This is needed for posit-dev/shinychat#210, which removes server-side thinking detection and relies entirely on the client's tag parser seeing <thinking> tags in the stream. Without this fix, tool-use apps (which require content="all") with thinking-capable models would render thinking content as regular assistant text — the client never sees tag boundaries.

Related PRs

PR Relationship
#294 Introduced tag boundaries for content="text" mode. This PR extends that to content="all".
tidyverse/ellmer#975 R companion — same fix likely needed for stream="content" mode.
posit-dev/shinychat#210 Depends on this fix for correctness in content="all" scenarios.

Test plan

  • All 11 streaming thinking tests pass
  • TestStreamThinkingAll::test_tag_boundaries_yielded — verifies boundaries present
  • TestStreamThinkingAll::test_order_of_chunks — verifies exact sequence
  • TestStreamThinkingAsync::test_content_all_async — async equivalent

Previously `<thinking>\n` and `\n</thinking>\n\n` boundary strings were
only yielded to consumers when `content="text"`. Remove that guard so
they are emitted in all modes, including `content="all"`.

This is required by shinychat PR posit-dev/shinychat#210, which removes
server-side thinking detection and relies on the client tag parser seeing
`<thinking>` markers in the stream. Without this fix, tool-use apps
(which require `content="all"`) with thinking-capable models render
thinking content as regular assistant text.

Both the sync and async `_submit_turns` paths are updated. Tests in
`TestStreamThinkingAll` and the async `test_content_all_async` are
updated to assert that tag boundary strings ARE present in the output.

This comment was marked as resolved.

cpsievert added a commit to cpsievert/ellmer that referenced this pull request May 7, 2026
Remove the `if (!yield_as_content)` guards so that `<thinking>` and
`</thinking>` boundary strings are yielded to consumers in all stream
modes, not just stream='text'.

Needed for posit-dev/shinychat#210, which removes server-side thinking
detection and relies on the client's tag parser seeing tag boundaries
in the stream. Without this, tool-use apps (stream='content') with
thinking-capable models would render thinking as regular text.

Companion to posit-dev/chatlas#297.
@cpsievert cpsievert merged commit fe8c3fd into main May 7, 2026
8 checks passed
@cpsievert cpsievert deleted the fix/thinking-tags-all-mode branch May 7, 2026 00:19
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