11import { BaseTextAdapter } from '@tanstack/ai/adapters'
2- import { convertToolsToProviderFormat } from '../tools/tool-converter '
2+ import { ANTHROPIC_STRUCTURED_OUTPUT_MODELS } from '../model-meta '
33import { validateTextProviderOptions } from '../text/text-provider-options'
4+ import { convertToolsToProviderFormat } from '../tools/tool-converter'
45import {
56 createAnthropicClient ,
67 generateId ,
78 getAnthropicApiKeyFromEnv ,
89} from '../utils'
9- import type {
10- ANTHROPIC_MODELS ,
11- AnthropicChatModelProviderOptionsByName ,
12- AnthropicModelInputModalitiesByName ,
13- } from '../model-meta'
14- import type {
15- StructuredOutputOptions ,
16- StructuredOutputResult ,
17- } from '@tanstack/ai/adapters'
10+ import type Anthropic_SDK from '@anthropic-ai/sdk'
1811import type {
1912 Base64ImageSource ,
2013 Base64PDFSource ,
2114 DocumentBlockParam ,
2215 ImageBlockParam ,
2316 MessageParam ,
17+ RawMessageStreamEvent ,
2418 TextBlockParam ,
2519 URLImageSource ,
2620 URLPDFSource ,
2721} from '@anthropic-ai/sdk/resources/messages'
28- import type Anthropic_SDK from '@anthropic-ai/sdk'
2922import type {
3023 ContentPart ,
3124 Modality ,
@@ -34,15 +27,24 @@ import type {
3427 TextOptions ,
3528} from '@tanstack/ai'
3629import type {
37- ExternalTextProviderOptions ,
38- InternalTextProviderOptions ,
39- } from '../text/text-provider-options '
30+ StructuredOutputOptions ,
31+ StructuredOutputResult ,
32+ } from '@tanstack/ai/adapters '
4033import type {
4134 AnthropicDocumentMetadata ,
4235 AnthropicImageMetadata ,
4336 AnthropicMessageMetadataByModality ,
4437 AnthropicTextMetadata ,
4538} from '../message-types'
39+ import type {
40+ ANTHROPIC_MODELS ,
41+ AnthropicChatModelProviderOptionsByName ,
42+ AnthropicModelInputModalitiesByName ,
43+ } from '../model-meta'
44+ import type {
45+ ExternalTextProviderOptions ,
46+ InternalTextProviderOptions ,
47+ } from '../text/text-provider-options'
4648import type { AnthropicClientConfig } from '../utils'
4749
4850/**
@@ -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,34 +152,110 @@ 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 (
167+ requestParams ,
168+ chatOptions ,
169+ outputSchema ,
170+ )
171+ }
172+
173+ return this . toolBasedStructuredOutput (
174+ requestParams ,
175+ chatOptions ,
176+ outputSchema ,
177+ )
178+ }
179+
180+ /**
181+ * Native structured output using `output_config.format` with `json_schema`.
182+ * Supported by Claude 4+ models.
183+ */
184+ private async nativeStructuredOutput (
185+ requestParams : InternalTextProviderOptions ,
186+ chatOptions : StructuredOutputOptions < AnthropicTextProviderOptions > [ 'chatOptions' ] ,
187+ outputSchema : StructuredOutputOptions < AnthropicTextProviderOptions > [ 'outputSchema' ] ,
188+ ) : Promise < StructuredOutputResult < unknown > > {
189+ const createParams = {
190+ ...requestParams ,
191+ stream : false as const ,
192+ output_config : {
193+ format : {
194+ type : 'json_schema' as const ,
195+ name : 'structured_output' ,
196+ schema : outputSchema ,
197+ } ,
198+ } ,
199+ }
200+
201+ let response : Awaited < ReturnType < typeof this . client . messages . create > >
202+ try {
203+ response = await this . client . messages . create ( createParams , {
204+ signal : chatOptions . request ?. signal ,
205+ headers : chatOptions . request ?. headers ,
206+ } )
207+ } catch ( error : unknown ) {
208+ const err = error as Error
209+ throw new Error (
210+ `Structured output generation failed: ${ err . message || 'Unknown error occurred' } ` ,
211+ )
212+ }
213+
214+ const rawText = response . content
215+ . map ( ( b ) => {
216+ if ( b . type === 'text' ) {
217+ return b . text
218+ }
219+ return ''
220+ } )
221+ . join ( '' )
222+
223+ let parsed : unknown
224+ try {
225+ parsed = JSON . parse ( rawText )
226+ } catch {
227+ throw new Error (
228+ `Failed to parse structured output JSON. Content: ${ rawText . slice ( 0 , 200 ) } ${ rawText . length > 200 ? '...' : '' } ` ,
229+ )
230+ }
231+
232+ return { data : parsed , rawText }
233+ }
234+
235+ /**
236+ * Tool-based structured output fallback for older models (Claude 3.x).
237+ * Creates a tool with the output schema and forces the model to call it.
238+ */
239+ private async toolBasedStructuredOutput (
240+ requestParams : InternalTextProviderOptions ,
241+ chatOptions : StructuredOutputOptions < AnthropicTextProviderOptions > [ 'chatOptions' ] ,
242+ outputSchema : StructuredOutputOptions < AnthropicTextProviderOptions > [ 'outputSchema' ] ,
243+ ) : Promise < StructuredOutputResult < unknown > > {
164244 const structuredOutputTool = {
165245 name : 'structured_output' ,
166246 description :
167247 'Use this tool to provide your response in the required structured format.' ,
168248 input_schema : {
249+ ...outputSchema ,
169250 type : 'object' as const ,
170251 properties : outputSchema . properties ?? { } ,
171252 required : outputSchema . required ?? [ ] ,
172253 } ,
173254 }
174255
256+ let response : Awaited < ReturnType < typeof this . client . messages . create > >
175257 try {
176- // Make non-streaming request with tool_choice forced to our structured output tool
177- const response = await this . client . messages . create (
258+ response = await this . client . messages . create (
178259 {
179260 ...requestParams ,
180261 stream : false ,
@@ -186,50 +267,48 @@ export class AnthropicTextAdapter<
186267 headers : chatOptions . request ?. headers ,
187268 } ,
188269 )
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- }
225270 } catch ( error : unknown ) {
226271 const err = error as Error
227272 throw new Error (
228273 `Structured output generation failed: ${ err . message || 'Unknown error occurred' } ` ,
229274 )
230275 }
276+
277+ let parsed : unknown = null
278+ let rawText = ''
279+
280+ for ( const block of response . content ) {
281+ if ( block . type === 'tool_use' && block . name === 'structured_output' ) {
282+ parsed = block . input
283+ rawText = JSON . stringify ( block . input )
284+ break
285+ }
286+ }
287+
288+ if ( parsed === null ) {
289+ rawText = response . content
290+ . map ( ( b ) => {
291+ if ( b . type === 'text' ) {
292+ return b . text
293+ }
294+ return ''
295+ } )
296+ . join ( '' )
297+ try {
298+ parsed = JSON . parse ( rawText )
299+ } catch {
300+ throw new Error (
301+ `Failed to extract structured output from response. Content: ${ rawText . slice ( 0 , 200 ) } ${ rawText . length > 200 ? '...' : '' } ` ,
302+ )
303+ }
304+ }
305+
306+ return { data : parsed , rawText }
231307 }
232308
309+ /**
310+ * Map framework-agnostic text options to the Anthropic request format.
311+ */
233312 private mapCommonOptionsToAnthropic (
234313 options : TextOptions < AnthropicTextProviderOptions > ,
235314 ) {
@@ -293,6 +372,9 @@ export class AnthropicTextAdapter<
293372 return requestParams
294373 }
295374
375+ /**
376+ * Convert a framework-agnostic content part to an Anthropic content block.
377+ */
296378 private convertContentPartToAnthropic (
297379 part : ContentPart ,
298380 ) : TextBlockParam | ImageBlockParam | DocumentBlockParam {
@@ -362,6 +444,9 @@ export class AnthropicTextAdapter<
362444 }
363445 }
364446
447+ /**
448+ * Convert framework-agnostic messages to Anthropic's message format.
449+ */
365450 private formatMessages (
366451 messages : Array < ModelMessage > ,
367452 ) : InternalTextProviderOptions [ 'messages' ] {
@@ -453,8 +538,11 @@ export class AnthropicTextAdapter<
453538 return formattedMessages
454539 }
455540
541+ /**
542+ * Process a raw Anthropic SSE stream into AG-UI lifecycle chunks.
543+ */
456544 private async * processAnthropicStream (
457- stream : AsyncIterable < Anthropic_SDK . Beta . BetaRawMessageStreamEvent > ,
545+ stream : AsyncIterable < RawMessageStreamEvent > ,
458546 model : string ,
459547 genId : ( ) => string ,
460548 ) : AsyncIterable < StreamChunk > {
0 commit comments