display_stream_events() helper handles writing text to stdout and
reasoning to stderr with proper newlines at each reasoning-to-text
transition. Used by sync prompt and chat streaming loops. Async prompt
loop has the same logic inline.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Streaming loops in prompt and chat commands now use stream_events()
instead of __iter__. Reasoning events are displayed on stderr in
dim text. Text events go to stdout as before.
New flags:
-R / --no-reasoning Suppress reasoning output on stderr
-S / --no-stream (shortcut for existing --no-stream)
-L (shortcut for existing -n/--no-log)
ChainResponse.stream_events() and AsyncChainResponse.astream_events()
added so tool-calling flows also surface reasoning events.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New m022_parts_table migration creates a parts table with direction
(input/output), role, part_type, content, content_json, tool_call_id,
and server_executed columns.
log_to_db() writes both input parts (from prompt.input_parts) and
output parts (from response.parts) to the table. from_row() loads
output parts and makes them available via the parts property.
Tested live: parts table created, input/output parts written and
loaded correctly with gpt-5.4-mini via CLI.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
model.prompt() and conversation.prompt() now accept a parts= parameter
for passing explicit Part objects. Prompt.input_parts synthesizes a
unified list of input Parts from prompt=, system=, attachments=, and
parts= parameters.
prompt= remains sugar for a TextPart(role="user"). system= becomes
a TextPart(role="system"). attachments= become AttachmentParts.
All parameters combine (parts first, then system, then prompt, then
attachments).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Chat.execute() and AsyncChat.execute() now yield StreamEvent instead
of bare strings. Text chunks, tool call names/args are all emitted
as typed events. Reasoning token counts from usage data are stored
on the response and _build_parts() prepends a redacted ReasoningPart.
StreamEvent gains server_executed and tool_name fields for use by
plugins with server-side tool execution.
Tested live against gpt-5.4-mini: text streaming, tool calls, and
reasoning tokens (with reasoning_effort='high') all work correctly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Response.__iter__ now handles str | StreamEvent from execute().
Plain str yields are backward compatible. StreamEvent yields are
processed by the assembler: text events yield as str to consumers,
reasoning/tool_call/tool_result events are filtered from __iter__
but available via stream_events(). Parts are assembled from events
after completion. Same changes for AsyncResponse.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Response.stream_events() yields StreamEvents wrapping text chunks.
AsyncResponse.astream_events() is the async equivalent.
response.parts returns a list of Part objects after completion.
Currently only handles plain str chunks (Phase 1 baseline).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 1 of the parts project: define the Part dataclass hierarchy
(TextPart, ReasoningPart, ToolCallPart, ToolResultPart, AttachmentPart)
and StreamEvent in a new llm/parts.py module. All Part types have
to_dict()/from_dict() for JSON roundtripping. 14 tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Recorded instance of streaming tool response variant "a".
This is the typical response, where "arguments":"" arrives
first in the stream, followed by "arguments":"{}"
The response data is a real capture from the OpenRouter API,
however some request and header data may be from other test fixtures.
* Recorded instance of streaming tool response variant "b".
This is a streaming response where the first arguments
you get is a fully formed "arguments":"{}"
The response data is a real capture from the OpenRouter API,
however some request and header data may be from other test fixtures.
* Test cases for streaming tool responses.
Note that the replays are marked as "read-only", as they are variants
seen in the wild where the streaming tool call argument fragments
arrive in a specific order.
* Fix streaming tool response variant "b", where "arguments":"{}" is what arrives first.
The previous code erroneously caused the first "arguments" to be duplicated,
by using "+=" even when being initially set.
This went unnoticed as many models stream "arguments":"" first.
When a more fully formed "arguments" fragment arrived first, it was causing
"Error: Extra data: line 1 column 3 (char 2)"
* Recorded instance of streaming tool response variant "c".
This was failing with "Error: unsupported operand type(s) for +=: 'NoneType' and 'str'"
The response data is a real capture from the OpenRouter API,
however some request and header data may be from other test fixtures.
* Test case for streaming tool response variant "c".
* Fix streaming tool response variant "c".
However, I'm not sure why arguments was initially not present or seen as None.
Closes#1014
- llm.ToolOutput(output='...', attachments=[...]) for tools to return attachments
- New table: `tool_results_attachments`
- Table is populated when tools return attachments
- llm --tools-debug shows attachments returned by tools
- llm logs shows attachments returned by tools