66from typing import Any , override
77
88import anthropic
9+ import pydantic
910
1011from .. import core
1112
@@ -119,6 +120,7 @@ async def stream_events(
119120 self ,
120121 messages : list [core .messages .Message ],
121122 tools : Sequence [core .tools .ToolLike ] | None = None ,
123+ output_type : type [pydantic .BaseModel ] | None = None ,
122124 ) -> AsyncGenerator [core .llm .StreamEvent ]:
123125 """Yield raw stream events from Anthropic API."""
124126 system_prompt , anthropic_messages = _messages_to_anthropic (messages )
@@ -140,12 +142,30 @@ async def stream_events(
140142 "budget_tokens" : self ._budget_tokens ,
141143 }
142144
145+ # Structured output: use beta API with output_format
146+ use_beta = False
147+ if output_type is not None :
148+ from anthropic .lib ._parse ._transform import transform_schema
149+
150+ kwargs ["output_format" ] = {
151+ "type" : "json_schema" ,
152+ "schema" : transform_schema (output_type ),
153+ }
154+ kwargs ["betas" ] = ["structured-outputs-2025-11-13" ]
155+ use_beta = True
156+
143157 # Track block types by index to know what End event to emit
144158 block_types : dict [int , str ] = {} # index -> "text" | "thinking" | "tool_use"
145159 tool_ids : dict [int , str ] = {} # index -> tool_call_id
146160 signature_buffer : dict [int , str ] = {} # index -> accumulated signature
147161
148- async with self ._client .messages .stream (** kwargs ) as stream :
162+ stream_cm : Any # BetaAsyncMessageStreamManager | AsyncMessageStreamManager
163+ if use_beta :
164+ stream_cm = self ._client .beta .messages .stream (** kwargs )
165+ else :
166+ stream_cm = self ._client .messages .stream (** kwargs )
167+
168+ async with stream_cm as stream :
149169 async for event in stream :
150170 if event .type == "content_block_start" :
151171 block = event .content_block
@@ -208,8 +228,17 @@ async def stream(
208228 self ,
209229 messages : list [core .messages .Message ],
210230 tools : Sequence [core .tools .ToolLike ] | None = None ,
231+ output_type : type [pydantic .BaseModel ] | None = None ,
211232 ) -> AsyncGenerator [core .messages .Message ]:
212- """Stream Messages (uses StreamProcessor internally)."""
233+ """Stream Messages (uses StreamHandler internally)."""
213234 handler = core .llm .StreamHandler ()
214- async for event in self .stream_events (messages , tools ):
215- yield handler .handle_event (event )
235+ msg : core .messages .Message | None = None
236+ async for event in self .stream_events (messages , tools , output_type = output_type ):
237+ msg = handler .handle_event (event )
238+ yield msg
239+
240+ # After stream completes, validate and yield final message with output
241+ if output_type is not None and msg is not None and msg .text :
242+ msg = msg .model_copy ()
243+ msg .output = output_type .model_validate_json (msg .text )
244+ yield msg
0 commit comments