Skip to content

refactor: add stream_content() abstract method to Provider#276

Merged
cpsievert merged 9 commits intomainfrom
feat/stream-content-refactor
Apr 2, 2026
Merged

refactor: add stream_content() abstract method to Provider#276
cpsievert merged 9 commits intomainfrom
feat/stream-content-refactor

Conversation

@cpsievert
Copy link
Copy Markdown
Collaborator

@cpsievert cpsievert commented Apr 2, 2026

Summary

This PR refactors the provider streaming interface to return structured Content objects instead of raw strings, mirroring ellmer's stream_text()stream_content() refactor. This is the foundation for upcoming partial turn preservation and cooperative stream cancellation (#279).

Motivation

Currently, each provider's stream_text() method returns Optional[str], which loses type information about what kind of content was streamed (plain text vs. thinking/reasoning). By switching to stream_content() that returns Optional[Content] (specifically ContentText or ContentThinking), we can:

  1. Distinguish text from thinking content during streaming — this enables content="all" to yield ContentThinking objects rather than plain strings
  2. Lay the groundwork for partial turn preservation — PR 2 will append these Content objects to a partial AssistantTurn during streaming, so if the stream is interrupted, the accumulated content is preserved
  3. Stay aligned with ellmer — the R package made this same refactor as a prerequisite to its stream cancellation feature (PR #951)

Changes

  • chatlas/_provider.py: stream_text() is no longer abstract. New abstract stream_content() returns Optional[Content]. The concrete stream_text() delegates to stream_content() and extracts the text string (preserving backwards compatibility for any code that calls stream_text() directly)

  • All providers (_provider_anthropic.py, _provider_openai.py, _provider_google.py, _provider_openai_completions.py, _provider_snowflake.py): Renamed stream_text()stream_content(), now return ContentText.model_construct() or ContentThinking as appropriate. Uses model_construct() to bypass Pydantic's ContentText validator that corrupts whitespace-only streaming chunks (e.g., "\n""[empty string]")

  • chatlas/_chat.py: _submit_turns and _submit_turns_async now call stream_content() instead of stream_text(). When content="all", ContentThinking objects are yielded directly (not as strings), allowing callers to distinguish thinking from text content

  • Google provider: Replaced the chunk.text shortcut with explicit parts[0] access to correctly detect thought=True on parts and return ContentThinking for thinking content

Test plan

  • All existing VCR tests pass (452 tests) — streaming behavior is unchanged for content="text" (the default)
  • Type checker passes on all modified files
  • Ruff format passes

🤖 Generated with Claude Code

cpsievert and others added 6 commits April 2, 2026 12:05
…() concrete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace stream_text() with stream_content() returning Content objects
(ContentText/ContentThinking) instead of raw strings on all five
provider implementations: Anthropic, OpenAI, Google, OpenAI Completions,
and Snowflake.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…g for content='all'

- Add content_text() helper to extract displayable text from Content objects
- Thread content_mode parameter from _chat_impl to _submit_turns
- Use stream_content() instead of stream_text() in streaming loops
- Yield ContentThinking objects when content="all" mode is active
- Use model_construct() for streaming ContentText to avoid the
  whitespace-to-"[empty string]" validator corrupting raw chunks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Handle thinking parts in non-streaming Google responses by checking
part.get("thought") and emitting ContentThinking. Also add CHANGELOG
entry for the stream_content() refactor.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

This comment was marked as resolved.

cpsievert and others added 3 commits April 2, 2026 12:25
- Add ContentThinking to all content="all" overload return types so
  callers know they can receive thinking content during streaming
- Restore reasoning_summary_text.done separator in OpenAI provider
  that was accidentally dropped during the stream_content() refactor
- Revert unrelated formatting changes to _content_expand.py and
  _parallel.py to keep the PR focused

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add overloads to _submit_turns/_submit_turns_async so content_mode="text"
  narrows return type to Generator[str], fixing ChatResponse type mismatch
- Fix Google provider stream_content() optional subscript error by
  guarding against None candidates before indexing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@cpsievert cpsievert merged commit a837e85 into main Apr 2, 2026
8 checks passed
@cpsievert cpsievert deleted the feat/stream-content-refactor branch April 2, 2026 17:41
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