Skip to content

feat(messages): add stream, thinking and extra_body - #10

Merged
theQuert merged 2 commits into
mainfrom
claude/messages-stream-thinking
Oct 7, 2026
Merged

theQuert merged 2 commits into
mainfrom
claude/messages-stream-thinking

Conversation

@theQuert

@theQuert theQuert commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Closes #3.

messages.create() (sync and async) gains stream, thinking and extra_body. The spec is in openspec/changes/add-messages-stream-and-thinking/.

Behavior

  • stream=True
    • Sends "stream": true and returns a Stream (or AsyncStream) of MessageStreamEvent, in arrival order.
    • ping events are skipped.
    • An error event raises the APIError subclass that matches its error type, with the event as body, and closes the response.
      • rate_limit_error raises RateLimitError (429).
      • overloaded_error raises InternalServerError (529).
      • The stream itself is HTTP 200, so the class is chosen from the error type.
    • The stream ends at end of body. There is no [DONE] sentinel.
  • thinking
    • Sent unchanged.
    • New ThinkingBlock and RedactedThinkingBlock content blocks, in both responses and content_block_start events.
    • MessageStreamDelta carries thinking, signature, stop_reason and stop_sequence.
    • Its type is now optional. Before this, every real stream would fail at the message_delta event, because that delta has no type.
    • Events expose usage (MessageDeltaUsage).
    • stop_reason accepts pause_turn, refusal and model_context_window_exceeded.
  • extra_body
    • Merged into the top level of the request body, as the OpenAI and Anthropic SDKs do.
    • Chat completions still nest it under "extra_body". That behavior is unchanged, because Gemini's OpenAI-compatible endpoint expects the nested form.
  • Stream classes
    • Stream and AsyncStream are generic and take a parse function, so chat and messages share one SSE reader.
    • Chat behavior is unchanged.
    • Chat return annotations are now Stream[ChatCompletionChunk].

Bug fix found while testing. A streamed request (chat or messages) that got an HTTP error status raised httpx.ResponseNotRead. The cause was that _raise_for_status read the body of an unread streaming response. The body is now read first, so the matching APIError subclass is raised.

Proof

  • New tests:
    • tests/test_messages.py: sync thinking+text stream, with event order, ping skipped, and thinking/signature/text deltas.
    • tests/test_messages.py: message_delta with stop_reason and usage.
    • tests/test_messages.py: async text stream.
    • tests/test_messages.py: an error event raises, maps to the right class (sync and async), and closes the response.
    • tests/test_messages.py: a non-stream response with thinking and redacted_thinking blocks, plus top-level extra_body.
    • tests/test_streaming.py: an HTTP 400 on a chat stream, and a 404 on an async messages stream, raise APIError / NotFoundError.
  • Before the change, the new tests failed. The messages module failed to import, and both stream-error tests got ResponseNotRead.
  • Full suite, rebased on current main: 111 passed on each of:
    • Python 3.13
    • Python 3.9
    • Python 3.9 with the minimum declared dependencies (pydantic 2.0.3, httpx 0.25.0, typing-extensions 4.7.1)
  • mypy reports the same 12 findings as main.
  • New ruff findings are UP006/UP007/UP045 style hints only. They match the existing Optional/Dict style of these files.
  • openspec validate add-messages-stream-and-thinking --strict passes.
  • Event and block shapes follow the Anthropic Messages streaming types in anthropic-python 1.11.0. They are respx mocks; no live calls were made.

Review: an independent read-only review found no blockers. It raised two P2s, both fixed in the second commit:

  • Error events always mapped to the base APIError.
  • The response was not closed when an error event arrived.

Not in scope: Anthropic server-tool content blocks (server_tool_use, web_search_tool_result, ...) are still not modelled. A response containing them would fail validation.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Uy5vXsRpMhug7v3XwTMZHv

theQuert and others added 2 commits October 7, 2026 08:26
messages.create() (sync and async) can now stream and request extended
thinking on the Anthropic-native /v1/messages endpoint.

- stream=True returns Stream / AsyncStream of MessageStreamEvent. Ping
  events are skipped and error events raise APIError.
- thinking is sent unchanged. Message.content and stream events carry
  thinking / redacted_thinking blocks and thinking / signature deltas.
- extra_body is merged into the top level of the request body.
- MessageStreamDelta no longer requires a type, so message_delta events
  (stop_reason, stop_sequence) validate. Events expose usage.
- Stream / AsyncStream take a parse function and are generic, so chat and
  messages share one SSE reader.
- A streamed request that fails with an HTTP error raised
  httpx.ResponseNotRead. The error body is now read first, so the matching
  APIError is raised, for chat as well.

Closes #3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uy5vXsRpMhug7v3XwTMZHv
An error event arrives after an HTTP 200, so it always became a bare
APIError and retry logic keyed on RateLimitError / InternalServerError never
matched. Map the Anthropic error type to its usual status instead (for
example rate_limit_error to 429, overloaded_error to 529). Close the
response when parsing an event raises, so an error event does not leave the
connection open. A non-object error field no longer raises AttributeError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uy5vXsRpMhug7v3XwTMZHv
@theQuert
theQuert merged commit 183b141 into main Oct 7, 2026
3 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.

messages.create has no stream or thinking parameters

1 participant