Skip to content

Commit e6562d7

Browse files
feat(api): add support for structured outputs beta
https://docs.claude.com/en/docs/build-with-claude/structured-outputs
1 parent 4dc6b9a commit e6562d7

31 files changed

+1896
-64
lines changed

.stats.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
configured_endpoints: 34
2-
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/anthropic%2Fanthropic-5e665f72d2774cd751988ccc94f623f264d9358aa073289779de5815d36e89a3.yml
3-
openapi_spec_hash: c5f969a677c73796d192cf09dbb047f9
4-
config_hash: bbb07992b537724667f4589012714eb7
2+
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/anthropic%2Fanthropic-2f35b2ff9174d526a6d35796d2703490bfa5692312af67cbdfa4500283dabe31.yml
3+
openapi_spec_hash: dc52b25c487e97d355ef645644aa13e7
4+
config_hash: 239752fc0713a82e121ea45f7e2ebbf6

api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,8 +246,10 @@ Types:
246246
- <code><a href="./src/resources/beta/messages/messages.ts">BetaFileImageSource</a></code>
247247
- <code><a href="./src/resources/beta/messages/messages.ts">BetaImageBlockParam</a></code>
248248
- <code><a href="./src/resources/beta/messages/messages.ts">BetaInputJSONDelta</a></code>
249+
- <code><a href="./src/resources/beta/messages/messages.ts">BetaJSONOutputFormat</a></code>
249250
- <code><a href="./src/resources/beta/messages/messages.ts">BetaInputTokensClearAtLeast</a></code>
250251
- <code><a href="./src/resources/beta/messages/messages.ts">BetaInputTokensTrigger</a></code>
252+
- <code><a href="./src/resources/beta/messages/messages.ts">BetaJSONOutputFormat</a></code>
251253
- <code><a href="./src/resources/beta/messages/messages.ts">BetaMCPToolResultBlock</a></code>
252254
- <code><a href="./src/resources/beta/messages/messages.ts">BetaMCPToolUseBlock</a></code>
253255
- <code><a href="./src/resources/beta/messages/messages.ts">BetaMCPToolUseBlockParam</a></code>

examples/parsing-json-schema.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/env -S npm run tsn -T
2+
3+
import Anthropic from '@anthropic-ai/sdk';
4+
import { betaJSONSchemaOutputFormat } from '@anthropic-ai/sdk/helpers/beta/json-schema';
5+
6+
const NumbersResponse = {
7+
type: 'object',
8+
properties: {
9+
primes: {
10+
type: 'array',
11+
items: {
12+
type: 'number',
13+
},
14+
},
15+
},
16+
required: ['primes'],
17+
} as const;
18+
19+
async function main() {
20+
const client = new Anthropic();
21+
22+
const message = await client.beta.messages.parse({
23+
model: 'claude-sonnet-4-5-20250929-structured-outputs',
24+
max_tokens: 1024,
25+
messages: [{ role: 'user', content: 'What are the first 3 prime numbers?' }],
26+
output_format: betaJSONSchemaOutputFormat(NumbersResponse),
27+
});
28+
29+
console.log('=== Full Message ===');
30+
console.log(JSON.stringify(message, null, 2));
31+
console.log('=== Parsed Output ===');
32+
console.log('\nPrime numbers:', message.parsed_output?.primes);
33+
}
34+
35+
main().catch(console.error);

examples/parsing-raw.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/usr/bin/env -S npm run tsn -T
2+
3+
import Anthropic from '@anthropic-ai/sdk';
4+
5+
async function main() {
6+
const client = new Anthropic();
7+
8+
const message = await client.beta.messages.parse({
9+
model: 'claude-sonnet-4-5-20250929-structured-outputs',
10+
max_tokens: 100,
11+
output_format: {
12+
type: 'json_schema',
13+
schema: {
14+
type: 'object',
15+
properties: {
16+
answer: { type: 'string', description: 'The final answer' },
17+
},
18+
required: ['answer'],
19+
additionalProperties: false,
20+
},
21+
},
22+
messages: [
23+
{
24+
role: 'user',
25+
content: 'What is bigger: 738 * 5678 or 98123 - 2711?',
26+
},
27+
],
28+
});
29+
console.log(JSON.stringify(message.parsed_output, null, 2));
30+
}
31+
32+
main().catch(console.error);

examples/parsing-streaming.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/usr/bin/env -S npm run tsn -T
2+
3+
import { betaZodOutputFormat } from '@anthropic-ai/sdk/helpers/beta/zod';
4+
import Anthropic from '@anthropic-ai/sdk';
5+
import { z } from 'zod';
6+
7+
const WeatherResponse = z.object({
8+
city: z.string(),
9+
temperature: z.number(),
10+
conditions: z.array(z.string()),
11+
forecast: z.array(
12+
z.object({
13+
day: z.string(),
14+
high: z.number(),
15+
low: z.number(),
16+
condition: z.string(),
17+
}),
18+
),
19+
});
20+
21+
async function main() {
22+
const client = new Anthropic();
23+
24+
const stream = client.beta.messages.stream({
25+
model: 'claude-sonnet-4-5-20250929-structured-outputs',
26+
max_tokens: 1024,
27+
messages: [{ role: 'user', content: 'Provide a weather report for San Francisco.' }],
28+
output_format: betaZodOutputFormat(WeatherResponse),
29+
});
30+
31+
for await (const event of stream) {
32+
if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
33+
process.stdout.write(event.delta.text);
34+
}
35+
}
36+
37+
// Get the final parsed result
38+
const finalMessage = await stream.finalMessage();
39+
40+
console.log('\n\n=== Final Parsed Results ===');
41+
console.log('City:', finalMessage.parsed_output?.city);
42+
console.log('Temperature:', finalMessage.parsed_output?.temperature);
43+
console.log('Conditions:', finalMessage.parsed_output?.conditions);
44+
console.log('Forecast:', finalMessage.parsed_output?.forecast);
45+
}
46+
47+
main().catch(console.error);

examples/parsing-zod.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env -S npm run tsn -T
2+
3+
import { betaZodOutputFormat } from '@anthropic-ai/sdk/helpers/beta/zod';
4+
import Anthropic from '@anthropic-ai/sdk';
5+
import { z } from 'zod';
6+
7+
const NumbersResponse = z.object({
8+
primes: z.array(z.number()),
9+
});
10+
11+
async function main() {
12+
const client = new Anthropic();
13+
14+
const message = await client.beta.messages.parse({
15+
model: 'claude-sonnet-4-5-20250929-structured-outputs',
16+
max_tokens: 1024,
17+
messages: [{ role: 'user', content: 'What are the first 3 prime numbers?' }],
18+
output_format: betaZodOutputFormat(NumbersResponse),
19+
});
20+
21+
console.log('=== Full Message ===');
22+
console.log(JSON.stringify(message, null, 2));
23+
console.log('=== Parsed Output ===');
24+
console.log('\nPrime numbers:', message.parsed_output!.primes);
25+
}
26+
27+
main().catch(console.error);

jest.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const config: JestConfigWithTsJest = {
1818
'<rootDir>/packages/',
1919
],
2020
testPathIgnorePatterns: ['scripts'],
21+
prettierPath: null,
2122
};
2223

2324
export default config;

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"@types/node": "^20.17.6",
4646
"@typescript-eslint/eslint-plugin": "8.31.1",
4747
"@typescript-eslint/parser": "8.31.1",
48+
"deep-object-diff": "^1.1.9",
4849
"eslint": "^9.20.1",
4950
"eslint-plugin-prettier": "^5.4.1",
5051
"eslint-plugin-unused-imports": "^4.1.4",

packages/bedrock-sdk/yarn.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"@jridgewell/trace-mapping" "^0.3.9"
1717

1818
"@anthropic-ai/sdk@file:../../dist":
19-
version "0.60.0"
19+
version "0.63.0"
2020
dependencies:
2121
json-schema-to-ts "^3.1.1"
2222

src/helpers/beta/json-schema.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
22
import { Promisable, BetaRunnableTool } from '../../lib/tools/BetaRunnableTool';
33
import { BetaToolResultContentBlockParam } from '../../resources/beta';
4+
import { AutoParseableBetaOutputFormat } from '../../lib/beta-parser';
5+
import { AnthropicError } from '../..';
6+
import { transformJSONSchema } from '../../lib/transform-json-schema';
47

58
type NoInfer<T> = T extends infer R ? R : never;
69

@@ -30,3 +33,43 @@ export function betaTool<const Schema extends Exclude<JSONSchema, boolean> & { t
3033
parse: (content: unknown) => content as FromSchema<Schema>,
3134
} as any;
3235
}
36+
37+
/**
38+
* Creates a JSON schema output format object from the given JSON schema.
39+
* If this is passed to the `.parse()` method then the response message will contain a
40+
* `.parsed` property that is the result of parsing the content with the given JSON schema.
41+
*
42+
*/
43+
export function betaJSONSchemaOutputFormat<
44+
const Schema extends Exclude<JSONSchema, boolean> & { type: 'object' },
45+
>(
46+
jsonSchema: Schema,
47+
options?: {
48+
transform?: boolean;
49+
},
50+
): AutoParseableBetaOutputFormat<NoInfer<FromSchema<Schema>>> {
51+
if (jsonSchema.type !== 'object') {
52+
throw new Error(`JSON schema for tool must be an object, but got ${jsonSchema.type}`);
53+
}
54+
55+
const transform = options?.transform ?? true;
56+
if (transform) {
57+
// todo: doing this is arguably necessary, but it does change the schema the user passed in
58+
// so I'm not sure how we should handle that
59+
jsonSchema = transformJSONSchema(jsonSchema) as Schema;
60+
}
61+
62+
return {
63+
type: 'json_schema',
64+
schema: {
65+
...jsonSchema,
66+
},
67+
parse: (content) => {
68+
try {
69+
return JSON.parse(content);
70+
} catch (error) {
71+
throw new AnthropicError(`Failed to parse structured output: ${error}`);
72+
}
73+
},
74+
};
75+
}

0 commit comments

Comments
 (0)