Skip to content

Commit 76cd6c7

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

File tree

9 files changed

+96
-103
lines changed

9 files changed

+96
-103
lines changed
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 the public API** — this is an internal implementation change. Users who pass `providerOptions` with tool choice types should note the types are now imported from `@anthropic-ai/sdk/resources/messages` instead of the beta namespace.

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: 33 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type {
2121
DocumentBlockParam,
2222
ImageBlockParam,
2323
MessageParam,
24+
RawMessageStreamEvent,
2425
TextBlockParam,
2526
URLImageSource,
2627
URLPDFSource,
@@ -121,7 +122,7 @@ export class AnthropicTextAdapter<
121122
try {
122123
const requestParams = this.mapCommonOptionsToAnthropic(options)
123124

124-
const stream = await this.client.beta.messages.create(
125+
const stream = await this.client.messages.create(
125126
{ ...requestParams, stream: true },
126127
{
127128
signal: options.request?.signal,
@@ -147,10 +148,9 @@ export class AnthropicTextAdapter<
147148
}
148149

149150
/**
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).
151+
* Generate structured output using Anthropic's native structured output API.
152+
* Uses `output_config.format` with `json_schema` type to constrain
153+
* the model's response to match the provided schema.
154154
*/
155155
async structuredOutput(
156156
options: StructuredOutputOptions<AnthropicTextProviderOptions>,
@@ -159,63 +159,39 @@ export class AnthropicTextAdapter<
159159

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
164-
const structuredOutputTool = {
165-
name: 'structured_output',
166-
description:
167-
'Use this tool to provide your response in the required structured format.',
168-
input_schema: {
169-
type: 'object' as const,
170-
properties: outputSchema.properties ?? {},
171-
required: outputSchema.required ?? [],
162+
const createParams = {
163+
...requestParams,
164+
stream: false as const,
165+
output_config: {
166+
format: {
167+
type: 'json_schema' as const,
168+
schema: outputSchema,
169+
},
172170
},
173171
}
174172

175173
try {
176-
// Make non-streaming request with tool_choice forced to our structured output tool
177-
const response = await this.client.messages.create(
178-
{
179-
...requestParams,
180-
stream: false,
181-
tools: [structuredOutputTool],
182-
tool_choice: { type: 'tool', name: 'structured_output' },
183-
},
184-
{
185-
signal: chatOptions.request?.signal,
186-
headers: chatOptions.request?.headers,
187-
},
188-
)
189-
190-
// Extract the tool use content from the response
191-
let parsed: unknown = null
192-
let rawText = ''
174+
const response = await this.client.messages.create(createParams, {
175+
signal: chatOptions.request?.signal,
176+
headers: chatOptions.request?.headers,
177+
})
193178

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-
}
179+
const rawText = response.content
180+
.map((b) => {
181+
if (b.type === 'text') {
182+
return b.text
183+
}
184+
return ''
185+
})
186+
.join('')
201187

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-
}
188+
let parsed: unknown
189+
try {
190+
parsed = JSON.parse(rawText)
191+
} catch {
192+
throw new Error(
193+
`Failed to parse structured output JSON. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? '...' : ''}`,
194+
)
219195
}
220196

221197
return {
@@ -454,7 +430,7 @@ export class AnthropicTextAdapter<
454430
}
455431

456432
private async *processAnthropicStream(
457-
stream: AsyncIterable<Anthropic_SDK.Beta.BetaRawMessageStreamEvent>,
433+
stream: AsyncIterable<RawMessageStreamEvent>,
458434
model: string,
459435
genId: () => string,
460436
): AsyncIterable<StreamChunk> {

packages/typescript/ai-anthropic/src/text/text-provider-options.ts

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
import type {
2-
BetaContextManagementConfig,
3-
BetaToolChoiceAny,
4-
BetaToolChoiceAuto,
5-
BetaToolChoiceTool,
6-
} from '@anthropic-ai/sdk/resources/beta/messages/messages'
7-
import type { AnthropicTool } from '../tools'
1+
import type { BetaContextManagementConfig } from '@anthropic-ai/sdk/resources/beta/messages/messages'
82
import type {
93
MessageParam,
104
TextBlockParam,
5+
ToolChoiceAny,
6+
ToolChoiceAuto,
7+
ToolChoiceTool,
8+
ToolUnion,
119
} from '@anthropic-ai/sdk/resources/messages'
1210

1311
export interface AnthropicContainerOptions {
@@ -38,8 +36,8 @@ export interface AnthropicContainerOptions {
3836
export interface AnthropicContextManagementOptions {
3937
/**
4038
* Context management configuration.
41-
42-
This allows you to control how Claude manages context across multiple requests, such as whether to clear function results or not.
39+
*
40+
* This allows you to control how Claude manages context across multiple requests, such as whether to clear function results or not.
4341
*/
4442
context_management?: BetaContextManagementConfig | null
4543
}
@@ -62,26 +60,26 @@ export interface AnthropicServiceTierOptions {
6260
export interface AnthropicStopSequencesOptions {
6361
/**
6462
* Custom text sequences that will cause the model to stop generating.
65-
66-
Anthropic models will normally stop when they have naturally completed their turn, which will result in a response stop_reason of "end_turn".
67-
68-
If you want the model to stop generating when it encounters custom strings of text, you can use the stop_sequences parameter. If the model encounters one of the custom sequences, the response stop_reason value will be "stop_sequence" and the response stop_sequence value will contain the matched stop sequence.
63+
*
64+
* Anthropic models will normally stop when they have naturally completed their turn, which will result in a response stop_reason of "end_turn".
65+
*
66+
* If you want the model to stop generating when it encounters custom strings of text, you can use the stop_sequences parameter. If the model encounters one of the custom sequences, the response stop_reason value will be "stop_sequence" and the response stop_sequence value will contain the matched stop sequence.
6967
*/
7068
stop_sequences?: Array<string>
7169
}
7270

7371
export interface AnthropicThinkingOptions {
7472
/**
7573
* Configuration for enabling Claude's extended thinking.
76-
77-
When enabled, responses include thinking content blocks showing Claude's thinking process before the final answer. Requires a minimum budget of 1,024 tokens and counts towards your max_tokens limit.
74+
*
75+
* When enabled, responses include thinking content blocks showing Claude's thinking process before the final answer. Requires a minimum budget of 1,024 tokens and counts towards your max_tokens limit.
7876
*/
7977
thinking?:
8078
| {
8179
/**
8280
* Determines how many tokens Claude can use for its internal reasoning process. Larger budgets can enable more thorough analysis for complex problems, improving response quality.
83-
84-
Must be ≥1024 and less than max_tokens
81+
*
82+
* Must be ≥1024 and less than max_tokens
8583
*/
8684
budget_tokens: number
8785

@@ -93,17 +91,17 @@ Must be ≥1024 and less than max_tokens
9391
}
9492

9593
export interface AnthropicToolChoiceOptions {
96-
tool_choice?: BetaToolChoiceAny | BetaToolChoiceTool | BetaToolChoiceAuto
94+
tool_choice?: ToolChoiceAny | ToolChoiceTool | ToolChoiceAuto
9795
}
9896

9997
export interface AnthropicSamplingOptions {
10098
/**
10199
* Only sample from the top K options for each subsequent token.
102-
103-
Used to remove "long tail" low probability responses.
104-
Recommended for advanced use cases only. You usually only need to use temperature.
105-
106-
Required range: x >= 0
100+
*
101+
* Used to remove "long tail" low probability responses.
102+
* Recommended for advanced use cases only. You usually only need to use temperature.
103+
*
104+
* Required range: x >= 0
107105
*/
108106
top_k?: number
109107
}
@@ -132,8 +130,8 @@ export interface InternalTextProviderOptions extends ExternalTextProviderOptions
132130
*/
133131
stream?: boolean
134132
/**
135-
* stem prompt.
136-
133+
* System prompt.
134+
137135
A system prompt is a way of providing context and instructions to Claude, such as specifying a particular goal or role.
138136
*/
139137
system?: string | Array<TextBlockParam>
@@ -145,12 +143,12 @@ export interface InternalTextProviderOptions extends ExternalTextProviderOptions
145143
*/
146144
temperature?: number
147145

148-
tools?: Array<AnthropicTool>
146+
tools?: Array<ToolUnion>
149147

150148
/**
151149
* Use nucleus sampling.
152-
153-
In nucleus sampling, we compute the cumulative distribution over all the options for each subsequent token in decreasing probability order and cut it off once it reaches a particular probability specified by top_p. You should either alter temperature or top_p, but not both.
150+
*
151+
* In nucleus sampling, we compute the cumulative distribution over all the options for each subsequent token in decreasing probability order and cut it off once it reaches a particular probability specified by top_p. You should either alter temperature or top_p, but not both.
154152
*/
155153
top_p?: number
156154
}

packages/typescript/ai-anthropic/src/tools/bash-tool.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
import type {
2-
BetaToolBash20241022,
3-
BetaToolBash20250124,
4-
} from '@anthropic-ai/sdk/resources/beta'
1+
import type { ToolBash20250124 } from '@anthropic-ai/sdk/resources/messages'
52
import type { Tool } from '@tanstack/ai'
63

7-
export type BashTool = BetaToolBash20241022 | BetaToolBash20250124
4+
export type BashTool = ToolBash20250124
85

96
export function convertBashToolToAdapterFormat(tool: Tool): BashTool {
107
const metadata = tool.metadata as BashTool

packages/typescript/ai-anthropic/src/tools/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { ToolUnion } from '@anthropic-ai/sdk/resources/messages'
12
import type { BashTool } from './bash-tool'
23
import type { CodeExecutionTool } from './code-execution-tool'
34
import type { ComputerUseTool } from './computer-use-tool'
@@ -7,7 +8,13 @@ import type { TextEditorTool } from './text-editor-tool'
78
import type { WebFetchTool } from './web-fetch-tool'
89
import type { WebSearchTool } from './web-search-tool'
910

11+
/**
12+
* Union of all Anthropic tool types supported by this adapter.
13+
* Includes GA tools (via ToolUnion) and beta-only tools that
14+
* have no GA equivalent yet.
15+
*/
1016
export type AnthropicTool =
17+
| ToolUnion
1118
| BashTool
1219
| CodeExecutionTool
1320
| ComputerUseTool

packages/typescript/ai-anthropic/src/tools/tool-converter.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import { convertMemoryToolToAdapterFormat } from './memory-tool'
66
import { convertTextEditorToolToAdapterFormat } from './text-editor-tool'
77
import { convertWebFetchToolToAdapterFormat } from './web-fetch-tool'
88
import { convertWebSearchToolToAdapterFormat } from './web-search-tool'
9-
import type { AnthropicTool } from '.'
9+
import type { ToolUnion } from '@anthropic-ai/sdk/resources/messages'
1010
import type { Tool } from '@tanstack/ai'
1111

1212
/**
13-
* Converts standard Tool format to Anthropic-specific tool format
13+
* Converts standard Tool format to Anthropic-specific tool format.
1414
*
1515
* @param tools - Array of standard Tool objects
1616
* @returns Array of Anthropic-specific tool definitions
@@ -32,10 +32,14 @@ import type { Tool } from '@tanstack/ai'
3232
*
3333
* const anthropicTools = convertToolsToProviderFormat(tools);
3434
* ```
35+
*
36+
* Returns Array<ToolUnion> for compatibility with the stable messages API.
37+
* Beta-only tools (ComputerUse, CodeExecution, Memory, WebFetch) are
38+
* structurally compatible at runtime but not part of the GA ToolUnion type.
3539
*/
3640
export function convertToolsToProviderFormat<TTool extends Tool>(
3741
tools: Array<TTool>,
38-
): Array<AnthropicTool> {
42+
): Array<ToolUnion> {
3943
return tools.map((tool) => {
4044
const name = tool.name
4145

@@ -57,5 +61,5 @@ export function convertToolsToProviderFormat<TTool extends Tool>(
5761
default:
5862
return convertCustomToolToAdapterFormat(tool)
5963
}
60-
})
64+
}) as Array<ToolUnion>
6165
}

packages/typescript/ai-anthropic/tests/anthropic-adapter.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ describe('Anthropic adapter option mapping', () => {
7777
}
7878
})()
7979

80-
mocks.betaMessagesCreate.mockResolvedValueOnce(mockStream)
80+
mocks.messagesCreate.mockResolvedValueOnce(mockStream)
8181

8282
const providerOptions = {
8383
container: {
@@ -132,8 +132,8 @@ describe('Anthropic adapter option mapping', () => {
132132
chunks.push(chunk)
133133
}
134134

135-
expect(mocks.betaMessagesCreate).toHaveBeenCalledTimes(1)
136-
const [payload] = mocks.betaMessagesCreate.mock.calls[0]
135+
expect(mocks.messagesCreate).toHaveBeenCalledTimes(1)
136+
const [payload] = mocks.messagesCreate.mock.calls[0]
137137

138138
expect(payload).toMatchObject({
139139
model: 'claude-3-7-sonnet-20250219',

pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)