66 generateId ,
77 getAnthropicApiKeyFromEnv ,
88} from '../utils'
9+ import { ANTHROPIC_STRUCTURED_OUTPUT_MODELS } from '../model-meta'
910import type {
1011 ANTHROPIC_MODELS ,
1112 AnthropicChatModelProviderOptionsByName ,
@@ -21,6 +22,7 @@ import type {
2122 DocumentBlockParam ,
2223 ImageBlockParam ,
2324 MessageParam ,
25+ RawMessageStreamEvent ,
2426 TextBlockParam ,
2527 URLImageSource ,
2628 URLPDFSource ,
@@ -115,13 +117,16 @@ export class AnthropicTextAdapter<
115117 this . client = createAnthropicClient ( config )
116118 }
117119
120+ /**
121+ * Stream chat completions from Anthropic, yielding AG-UI lifecycle chunks.
122+ */
118123 async * chatStream (
119124 options : TextOptions < AnthropicTextProviderOptions > ,
120125 ) : AsyncIterable < StreamChunk > {
121126 try {
122127 const requestParams = this . mapCommonOptionsToAnthropic ( options )
123128
124- const stream = await this . client . beta . messages . create (
129+ const stream = await this . client . messages . create (
125130 { ...requestParams , stream : true } ,
126131 {
127132 signal : options . request ?. signal ,
@@ -147,20 +152,87 @@ export class AnthropicTextAdapter<
147152 }
148153
149154 /**
150- * Generate structured output using Anthropic's tool-based approach.
151- * Anthropic doesn't have native structured output, so we use a tool with the schema
152- * and force the model to call it.
153- * The outputSchema is already JSON Schema (converted in the ai layer).
155+ * Generate structured output.
156+ * Uses Anthropic's native `output_config` with `json_schema` for Claude 4+ models.
157+ * Falls back to a tool-use workaround for older models that lack native support.
154158 */
155159 async structuredOutput (
156160 options : StructuredOutputOptions < AnthropicTextProviderOptions > ,
157161 ) : Promise < StructuredOutputResult < unknown > > {
158162 const { chatOptions, outputSchema } = options
159-
160163 const requestParams = this . mapCommonOptionsToAnthropic ( chatOptions )
161164
162- // Create a tool that will capture the structured output
163- // Anthropic's SDK requires input_schema with type: 'object' literal
165+ if ( ANTHROPIC_STRUCTURED_OUTPUT_MODELS . has ( chatOptions . model ) ) {
166+ return this . nativeStructuredOutput ( requestParams , chatOptions , outputSchema )
167+ }
168+
169+ return this . toolBasedStructuredOutput ( requestParams , chatOptions , outputSchema )
170+ }
171+
172+ /**
173+ * Native structured output using `output_config.format` with `json_schema`.
174+ * Supported by Claude 4+ models.
175+ */
176+ private async nativeStructuredOutput (
177+ requestParams : InternalTextProviderOptions ,
178+ chatOptions : StructuredOutputOptions < AnthropicTextProviderOptions > [ 'chatOptions' ] ,
179+ outputSchema : StructuredOutputOptions < AnthropicTextProviderOptions > [ 'outputSchema' ] ,
180+ ) : Promise < StructuredOutputResult < unknown > > {
181+ const createParams = {
182+ ...requestParams ,
183+ stream : false as const ,
184+ output_config : {
185+ format : {
186+ type : 'json_schema' as const ,
187+ name : 'structured_output' ,
188+ schema : outputSchema ,
189+ } ,
190+ } ,
191+ }
192+
193+ let response : Awaited < ReturnType < typeof this . client . messages . create > >
194+ try {
195+ response = await this . client . messages . create ( createParams , {
196+ signal : chatOptions . request ?. signal ,
197+ headers : chatOptions . request ?. headers ,
198+ } )
199+ } catch ( error : unknown ) {
200+ const err = error as Error
201+ throw new Error (
202+ `Structured output generation failed: ${ err . message || 'Unknown error occurred' } ` ,
203+ )
204+ }
205+
206+ const rawText = response . content
207+ . map ( ( b ) => {
208+ if ( b . type === 'text' ) {
209+ return b . text
210+ }
211+ return ''
212+ } )
213+ . join ( '' )
214+
215+ let parsed : unknown
216+ try {
217+ parsed = JSON . parse ( rawText )
218+ } catch {
219+ throw new Error (
220+ `Failed to parse structured output JSON. Content: ${ rawText . slice ( 0 , 200 ) } ${ rawText . length > 200 ? '...' : '' } ` ,
221+ )
222+ }
223+
224+ return { data : parsed , rawText }
225+ }
226+
227+ /**
228+ * Tool-based structured output fallback for older models (Claude 3.x).
229+ * Creates a tool with the output schema and forces the model to call it.
230+ */
231+ private async toolBasedStructuredOutput (
232+ requestParams : InternalTextProviderOptions ,
233+ chatOptions : StructuredOutputOptions < AnthropicTextProviderOptions > [ 'chatOptions' ] ,
234+ outputSchema : StructuredOutputOptions < AnthropicTextProviderOptions > [ 'outputSchema' ] ,
235+ ) : Promise < StructuredOutputResult < unknown > > {
164236 const structuredOutputTool = {
165237 name : 'structured_output' ,
166238 description :
@@ -172,9 +244,9 @@ export class AnthropicTextAdapter<
172244 } ,
173245 }
174246
247+ let response : Awaited < ReturnType < typeof this . client . messages . create > >
175248 try {
176- // Make non-streaming request with tool_choice forced to our structured output tool
177- const response = await this . client . messages . create (
249+ response = await this . client . messages . create (
178250 {
179251 ...requestParams ,
180252 stream : false ,
@@ -186,50 +258,48 @@ export class AnthropicTextAdapter<
186258 headers : chatOptions . request ?. headers ,
187259 } ,
188260 )
189-
190- // Extract the tool use content from the response
191- let parsed : unknown = null
192- let rawText = ''
193-
194- for ( const block of response . content ) {
195- if ( block . type === 'tool_use' && block . name === 'structured_output' ) {
196- parsed = block . input
197- rawText = JSON . stringify ( block . input )
198- break
199- }
200- }
201-
202- if ( parsed === null ) {
203- // Fallback: try to extract text content and parse as JSON
204- rawText = response . content
205- . map ( ( b ) => {
206- if ( b . type === 'text' ) {
207- return b . text
208- }
209- return ''
210- } )
211- . join ( '' )
212- try {
213- parsed = JSON . parse ( rawText )
214- } catch {
215- throw new Error (
216- `Failed to extract structured output from response. Content: ${ rawText . slice ( 0 , 200 ) } ${ rawText . length > 200 ? '...' : '' } ` ,
217- )
218- }
219- }
220-
221- return {
222- data : parsed ,
223- rawText,
224- }
225261 } catch ( error : unknown ) {
226262 const err = error as Error
227263 throw new Error (
228264 `Structured output generation failed: ${ err . message || 'Unknown error occurred' } ` ,
229265 )
230266 }
267+
268+ let parsed : unknown = null
269+ let rawText = ''
270+
271+ for ( const block of response . content ) {
272+ if ( block . type === 'tool_use' && block . name === 'structured_output' ) {
273+ parsed = block . input
274+ rawText = JSON . stringify ( block . input )
275+ break
276+ }
277+ }
278+
279+ if ( parsed === null ) {
280+ rawText = response . content
281+ . map ( ( b ) => {
282+ if ( b . type === 'text' ) {
283+ return b . text
284+ }
285+ return ''
286+ } )
287+ . join ( '' )
288+ try {
289+ parsed = JSON . parse ( rawText )
290+ } catch {
291+ throw new Error (
292+ `Failed to extract structured output from response. Content: ${ rawText . slice ( 0 , 200 ) } ${ rawText . length > 200 ? '...' : '' } ` ,
293+ )
294+ }
295+ }
296+
297+ return { data : parsed , rawText }
231298 }
232299
300+ /**
301+ * Map framework-agnostic text options to the Anthropic request format.
302+ */
233303 private mapCommonOptionsToAnthropic (
234304 options : TextOptions < AnthropicTextProviderOptions > ,
235305 ) {
@@ -293,6 +363,9 @@ export class AnthropicTextAdapter<
293363 return requestParams
294364 }
295365
366+ /**
367+ * Convert a framework-agnostic content part to an Anthropic content block.
368+ */
296369 private convertContentPartToAnthropic (
297370 part : ContentPart ,
298371 ) : TextBlockParam | ImageBlockParam | DocumentBlockParam {
@@ -362,6 +435,9 @@ export class AnthropicTextAdapter<
362435 }
363436 }
364437
438+ /**
439+ * Convert framework-agnostic messages to Anthropic's message format.
440+ */
365441 private formatMessages (
366442 messages : Array < ModelMessage > ,
367443 ) : InternalTextProviderOptions [ 'messages' ] {
@@ -453,8 +529,11 @@ export class AnthropicTextAdapter<
453529 return formattedMessages
454530 }
455531
532+ /**
533+ * Process a raw Anthropic SSE stream into AG-UI lifecycle chunks.
534+ */
456535 private async * processAnthropicStream (
457- stream : AsyncIterable < Anthropic_SDK . Beta . BetaRawMessageStreamEvent > ,
536+ stream : AsyncIterable < RawMessageStreamEvent > ,
458537 model : string ,
459538 genId : ( ) => string ,
460539 ) : AsyncIterable < StreamChunk > {
0 commit comments