Skip to content

Commit 23214a0

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

10 files changed

Lines changed: 305 additions & 75 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: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
generateId,
77
getAnthropicApiKeyFromEnv,
88
} from '../utils'
9+
import { ANTHROPIC_STRUCTURED_OUTPUT_MODELS } from '../model-meta'
910
import 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,
@@ -121,7 +123,7 @@ export class AnthropicTextAdapter<
121123
try {
122124
const requestParams = this.mapCommonOptionsToAnthropic(options)
123125

124-
const stream = await this.client.beta.messages.create(
126+
const stream = await this.client.messages.create(
125127
{ ...requestParams, stream: true },
126128
{
127129
signal: options.request?.signal,
@@ -147,20 +149,85 @@ export class AnthropicTextAdapter<
147149
}
148150

149151
/**
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).
152+
* Generate structured output.
153+
* Uses Anthropic's native `output_config` with `json_schema` for Claude 4+ models.
154+
* Falls back to a tool-use workaround for older models that lack native support.
154155
*/
155156
async structuredOutput(
156157
options: StructuredOutputOptions<AnthropicTextProviderOptions>,
157158
): Promise<StructuredOutputResult<unknown>> {
158159
const { chatOptions, outputSchema } = options
159-
160160
const requestParams = this.mapCommonOptionsToAnthropic(chatOptions)
161161

162-
// Create a tool that will capture the structured output
163-
// Anthropic's SDK requires input_schema with type: 'object' literal
162+
if (ANTHROPIC_STRUCTURED_OUTPUT_MODELS.has(chatOptions.model)) {
163+
return this.nativeStructuredOutput(requestParams, chatOptions, outputSchema)
164+
}
165+
166+
return this.toolBasedStructuredOutput(requestParams, chatOptions, outputSchema)
167+
}
168+
169+
/**
170+
* Native structured output using `output_config.format` with `json_schema`.
171+
* Supported by Claude 4+ models.
172+
*/
173+
private async nativeStructuredOutput(
174+
requestParams: InternalTextProviderOptions,
175+
chatOptions: StructuredOutputOptions<AnthropicTextProviderOptions>['chatOptions'],
176+
outputSchema: StructuredOutputOptions<AnthropicTextProviderOptions>['outputSchema'],
177+
): Promise<StructuredOutputResult<unknown>> {
178+
const createParams = {
179+
...requestParams,
180+
stream: false as const,
181+
output_config: {
182+
format: {
183+
type: 'json_schema' as const,
184+
schema: outputSchema,
185+
},
186+
},
187+
}
188+
189+
try {
190+
const response = await this.client.messages.create(createParams, {
191+
signal: chatOptions.request?.signal,
192+
headers: chatOptions.request?.headers,
193+
})
194+
195+
const rawText = response.content
196+
.map((b) => {
197+
if (b.type === 'text') {
198+
return b.text
199+
}
200+
return ''
201+
})
202+
.join('')
203+
204+
let parsed: unknown
205+
try {
206+
parsed = JSON.parse(rawText)
207+
} catch {
208+
throw new Error(
209+
`Failed to parse structured output JSON. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? '...' : ''}`,
210+
)
211+
}
212+
213+
return { data: parsed, rawText }
214+
} catch (error: unknown) {
215+
const err = error as Error
216+
throw new Error(
217+
`Structured output generation failed: ${err.message || 'Unknown error occurred'}`,
218+
)
219+
}
220+
}
221+
222+
/**
223+
* Tool-based structured output fallback for older models (Claude 3.x).
224+
* Creates a tool with the output schema and forces the model to call it.
225+
*/
226+
private async toolBasedStructuredOutput(
227+
requestParams: InternalTextProviderOptions,
228+
chatOptions: StructuredOutputOptions<AnthropicTextProviderOptions>['chatOptions'],
229+
outputSchema: StructuredOutputOptions<AnthropicTextProviderOptions>['outputSchema'],
230+
): Promise<StructuredOutputResult<unknown>> {
164231
const structuredOutputTool = {
165232
name: 'structured_output',
166233
description:
@@ -173,7 +240,6 @@ export class AnthropicTextAdapter<
173240
}
174241

175242
try {
176-
// Make non-streaming request with tool_choice forced to our structured output tool
177243
const response = await this.client.messages.create(
178244
{
179245
...requestParams,
@@ -187,7 +253,6 @@ export class AnthropicTextAdapter<
187253
},
188254
)
189255

190-
// Extract the tool use content from the response
191256
let parsed: unknown = null
192257
let rawText = ''
193258

@@ -200,7 +265,6 @@ export class AnthropicTextAdapter<
200265
}
201266

202267
if (parsed === null) {
203-
// Fallback: try to extract text content and parse as JSON
204268
rawText = response.content
205269
.map((b) => {
206270
if (b.type === 'text') {
@@ -218,10 +282,7 @@ export class AnthropicTextAdapter<
218282
}
219283
}
220284

221-
return {
222-
data: parsed,
223-
rawText,
224-
}
285+
return { data: parsed, rawText }
225286
} catch (error: unknown) {
226287
const err = error as Error
227288
throw new Error(
@@ -454,7 +515,7 @@ export class AnthropicTextAdapter<
454515
}
455516

456517
private async *processAnthropicStream(
457-
stream: AsyncIterable<Anthropic_SDK.Beta.BetaRawMessageStreamEvent>,
518+
stream: AsyncIterable<RawMessageStreamEvent>,
458519
model: string,
459520
genId: () => string,
460521
): AsyncIterable<StreamChunk> {

packages/typescript/ai-anthropic/src/model-meta.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ interface ModelMeta<
2020
input: Array<'text' | 'image' | 'audio' | 'video' | 'document'>
2121
extended_thinking?: boolean
2222
priority_tier?: boolean
23+
structured_output?: boolean
2324
}
2425
context_window?: number
2526
max_output_tokens?: number
@@ -65,6 +66,7 @@ const CLAUDE_OPUS_4_6 = {
6566
input: ['text', 'image', 'document'],
6667
extended_thinking: true,
6768
priority_tier: true,
69+
structured_output: true,
6870
},
6971
} as const satisfies ModelMeta<
7072
AnthropicContainerOptions &
@@ -95,6 +97,7 @@ const CLAUDE_OPUS_4_5 = {
9597
input: ['text', 'image', 'document'],
9698
extended_thinking: true,
9799
priority_tier: true,
100+
structured_output: true,
98101
},
99102
} as const satisfies ModelMeta<
100103
AnthropicContainerOptions &
@@ -125,6 +128,7 @@ const CLAUDE_SONNET_4_5 = {
125128
input: ['text', 'image', 'document'],
126129
extended_thinking: true,
127130
priority_tier: true,
131+
structured_output: true,
128132
},
129133
} as const satisfies ModelMeta<
130134
AnthropicContainerOptions &
@@ -155,6 +159,7 @@ const CLAUDE_HAIKU_4_5 = {
155159
input: ['text', 'image', 'document'],
156160
extended_thinking: true,
157161
priority_tier: true,
162+
structured_output: true,
158163
},
159164
} as const satisfies ModelMeta<
160165
AnthropicContainerOptions &
@@ -185,6 +190,7 @@ const CLAUDE_OPUS_4_1 = {
185190
input: ['text', 'image', 'document'],
186191
extended_thinking: true,
187192
priority_tier: true,
193+
structured_output: true,
188194
},
189195
} as const satisfies ModelMeta<
190196
AnthropicContainerOptions &
@@ -215,6 +221,7 @@ const CLAUDE_SONNET_4 = {
215221
input: ['text', 'image', 'document'],
216222
extended_thinking: true,
217223
priority_tier: true,
224+
structured_output: true,
218225
},
219226
} as const satisfies ModelMeta<
220227
AnthropicContainerOptions &
@@ -244,6 +251,7 @@ const CLAUDE_SONNET_3_7 = {
244251
input: ['text', 'image', 'document'],
245252
extended_thinking: true,
246253
priority_tier: true,
254+
structured_output: false,
247255
},
248256
} as const satisfies ModelMeta<
249257
AnthropicContainerOptions &
@@ -274,6 +282,7 @@ const CLAUDE_OPUS_4 = {
274282
input: ['text', 'image', 'document'],
275283
extended_thinking: true,
276284
priority_tier: true,
285+
structured_output: true,
277286
},
278287
} as const satisfies ModelMeta<
279288
AnthropicContainerOptions &
@@ -304,6 +313,7 @@ const CLAUDE_HAIKU_3_5 = {
304313
input: ['text', 'image', 'document'],
305314
extended_thinking: false,
306315
priority_tier: true,
316+
structured_output: false,
307317
},
308318
} as const satisfies ModelMeta<
309319
AnthropicContainerOptions &
@@ -334,6 +344,7 @@ const CLAUDE_HAIKU_3 = {
334344
input: ['text', 'image', 'document'],
335345
extended_thinking: false,
336346
priority_tier: false,
347+
structured_output: false,
337348
},
338349
} as const satisfies ModelMeta<
339350
AnthropicContainerOptions &
@@ -404,6 +415,20 @@ export const ANTHROPIC_MODELS = [
404415
CLAUDE_HAIKU_3.id,
405416
] as const
406417

418+
/**
419+
* Models that support Anthropic's native structured output API (output_config with json_schema).
420+
* Only Claude 4+ models support this feature.
421+
*/
422+
export const ANTHROPIC_STRUCTURED_OUTPUT_MODELS: ReadonlySet<string> = new Set([
423+
CLAUDE_OPUS_4_6.id,
424+
CLAUDE_OPUS_4_5.id,
425+
CLAUDE_SONNET_4_5.id,
426+
CLAUDE_HAIKU_4_5.id,
427+
CLAUDE_OPUS_4_1.id,
428+
CLAUDE_SONNET_4.id,
429+
CLAUDE_OPUS_4.id,
430+
])
431+
407432
// const ANTHROPIC_IMAGE_MODELS = [] as const
408433
// const ANTHROPIC_EMBEDDING_MODELS = [] as const
409434
// const ANTHROPIC_AUDIO_MODELS = [] as const

0 commit comments

Comments
 (0)