Skip to content

Commit 44fed8d

Browse files
committed
feat: update @anthropic-ai/sdk to version 0.74.0 and use real structured output
1 parent c10cd97 commit 44fed8d

10 files changed

Lines changed: 378 additions & 120 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@tanstack/ai-anthropic': minor
3+
---
4+
5+
Use Anthropic's native structured output API instead of the tool-use workaround
6+
7+
Upgrades `@anthropic-ai/sdk` from ^0.71.2 to ^0.74.0 and migrates structured output to use the GA `output_config.format` with `json_schema` type. Previously, structured output was emulated by forcing a tool call and extracting the input — this now uses Anthropic's first-class structured output support for more reliable schema-constrained responses.
8+
9+
Also migrates streaming and tool types from `client.beta.messages` to the stable `client.messages` API, replacing beta type imports (`BetaToolChoiceAuto`, `BetaToolBash20241022`, `BetaRawMessageStreamEvent`, etc.) with their GA equivalents.
10+
11+
**No breaking changes to runtime behavior.** However, this is a **type-level breaking change** for TypeScript consumers who import tool choice or streaming types directly: the beta type exports (`BetaToolChoiceAuto`, `BetaToolChoiceTool`, `BetaRawMessageStreamEvent`, etc.) have been replaced with their GA equivalents (`ToolChoiceAuto`, `ToolChoiceTool`, `RawMessageStreamEvent`, etc.) from `@anthropic-ai/sdk/resources/messages`. Consumers referencing these types will need to update both the import paths and the type names accordingly.

packages/typescript/ai-anthropic/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"test:types": "tsc"
4141
},
4242
"dependencies": {
43-
"@anthropic-ai/sdk": "^0.71.2"
43+
"@anthropic-ai/sdk": "^0.74.0"
4444
},
4545
"peerDependencies": {
4646
"@tanstack/ai": "workspace:^",

packages/typescript/ai-anthropic/src/adapters/text.ts

Lines changed: 149 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,24 @@
11
import { BaseTextAdapter } from '@tanstack/ai/adapters'
2-
import { convertToolsToProviderFormat } from '../tools/tool-converter'
2+
import { ANTHROPIC_STRUCTURED_OUTPUT_MODELS } from '../model-meta'
33
import { validateTextProviderOptions } from '../text/text-provider-options'
4+
import { convertToolsToProviderFormat } from '../tools/tool-converter'
45
import {
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'
1811
import 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'
2922
import type {
3023
ContentPart,
3124
Modality,
@@ -34,15 +27,24 @@ import type {
3427
TextOptions,
3528
} from '@tanstack/ai'
3629
import type {
37-
ExternalTextProviderOptions,
38-
InternalTextProviderOptions,
39-
} from '../text/text-provider-options'
30+
StructuredOutputOptions,
31+
StructuredOutputResult,
32+
} from '@tanstack/ai/adapters'
4033
import 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'
4648
import 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

Comments
 (0)