Skip to content

Commit d98d9ba

Browse files
fix(anthropic, amazon-bedrock): migrate output_format to output_config.format (#12319)
## Background #12298 ## Summary - **`@ai-sdk/anthropic`**: Migrated the deprecated `output_format` request parameter to `output_config.format`, aligning with the [current Anthropic API](https://platform.claude.com/docs/en/build-with-claude/structured-outputs). The `effort` and `format` fields are now merged into a single `output_config` object to avoid one spread overwriting the other. - **`@ai-sdk/amazon-bedrock`**: Enabled `supportsNativeStructuredOutput: true` for Bedrock Anthropic models. Structured outputs are now GA on Bedrock and no longer require a beta header, so the JSON tool fallback is no longer necessary. ## Manual Verification verified the fix by running the following code snippet before and after the changes <Details> <summary> repro </summary> ```ts import { bedrockAnthropic } from '@ai-sdk/amazon-bedrock/anthropic'; import { generateText, Output } from 'ai'; import 'dotenv/config'; import { z } from 'zod'; import { run } from '../../lib/run'; run(async () => { const result = await generateText({ model: bedrockAnthropic('us.anthropic.claude-opus-4-6-v1'), output: Output.object({ schema: z.object({ recipe: z.object({ name: z.string(), ingredients: z.array( z.object({ name: z.string(), amount: z.string(), }), ), steps: z.array(z.string()), }), }), }), providerOptions: { anthropic: { structuredOutputMode: "outputFormat", thinking: { type: "adaptive" }, effort: "low", }, }, prompt: 'Generate a lasagna recipe.', }); console.log('Recipe:', JSON.stringify(result.output, null, 2)); console.log(); console.log('Finish reason:', result.finishReason); console.log('Usage:', result.usage); }); ``` </Details> ## Related Issues Fixes #12298 --------- Co-authored-by: Aayush Kapoor <83492835+aayush-kapoor@users.noreply.github.com> Co-authored-by: Aayush Kapoor <aayushkapoor34@gmail.com>
1 parent 97c7b9f commit d98d9ba

File tree

8 files changed

+297
-90
lines changed

8 files changed

+297
-90
lines changed

.changeset/plenty-jeans-train.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@ai-sdk/amazon-bedrock': patch
3+
'@ai-sdk/anthropic': patch
4+
---
5+
6+
Migrated deprecated `output_format` parameter to `output_config.format` for structured outputs + Enabled native structured output support for Bedrock Anthropic models via `output_config.format`.

packages/amazon-bedrock/src/anthropic/bedrock-anthropic-provider.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ describe('bedrock-anthropic-provider', () => {
7171
buildRequestUrl: expect.any(Function),
7272
transformRequestBody: expect.any(Function),
7373
supportedUrls: expect.any(Function),
74-
supportsNativeStructuredOutput: false,
74+
supportsNativeStructuredOutput: true,
7575
}),
7676
);
7777
});

packages/amazon-bedrock/src/anthropic/bedrock-anthropic-provider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,8 +319,8 @@ export function createBedrockAnthropic(
319319

320320
// Bedrock Anthropic doesn't support URL sources, force download and base64 conversion
321321
supportedUrls: () => ({}),
322-
// force the use of JSON tool fallback for structured outputs since beta header isn't supported
323-
supportsNativeStructuredOutput: false,
322+
// native structured output via output_config.format is supported on Bedrock
323+
supportsNativeStructuredOutput: true,
324324
});
325325

326326
const provider = function (modelId: BedrockAnthropicModelId) {

packages/amazon-bedrock/src/bedrock-chat-language-model.test.ts

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4327,6 +4327,184 @@ describe('doGenerate', () => {
43274327
});
43284328
});
43294329

4330+
it('should use native output_config.format instead of json tool when thinking is enabled with structured output', async () => {
4331+
prepareJsonFixtureResponse('bedrock-text');
4332+
4333+
await model.doGenerate({
4334+
prompt: [
4335+
{
4336+
role: 'user',
4337+
content: [{ type: 'text', text: 'Generate a recipe' }],
4338+
},
4339+
],
4340+
responseFormat: {
4341+
type: 'json',
4342+
schema: {
4343+
type: 'object',
4344+
properties: {
4345+
recipe: {
4346+
type: 'object',
4347+
properties: {
4348+
name: { type: 'string' },
4349+
ingredients: { type: 'array', items: { type: 'string' } },
4350+
},
4351+
required: ['name', 'ingredients'],
4352+
},
4353+
},
4354+
required: ['recipe'],
4355+
},
4356+
},
4357+
providerOptions: {
4358+
bedrock: {
4359+
reasoningConfig: {
4360+
type: 'enabled',
4361+
budgetTokens: 2000,
4362+
},
4363+
},
4364+
},
4365+
});
4366+
4367+
const requestBody = await server.calls[0].requestBodyJson;
4368+
4369+
expect(requestBody.toolConfig).toBeUndefined();
4370+
4371+
expect(requestBody.additionalModelRequestFields?.output_config).toEqual({
4372+
format: {
4373+
type: 'json_schema',
4374+
schema: {
4375+
type: 'object',
4376+
properties: {
4377+
recipe: {
4378+
type: 'object',
4379+
properties: {
4380+
name: { type: 'string' },
4381+
ingredients: { type: 'array', items: { type: 'string' } },
4382+
},
4383+
required: ['name', 'ingredients'],
4384+
},
4385+
},
4386+
required: ['recipe'],
4387+
},
4388+
},
4389+
});
4390+
4391+
expect(requestBody.additionalModelRequestFields?.thinking).toBeDefined();
4392+
});
4393+
4394+
it('should merge output_config.effort and output_config.format when thinking with maxReasoningEffort and structured output', async () => {
4395+
prepareJsonFixtureResponse('bedrock-text');
4396+
4397+
await model.doGenerate({
4398+
prompt: [
4399+
{
4400+
role: 'user',
4401+
content: [{ type: 'text', text: 'Generate a recipe' }],
4402+
},
4403+
],
4404+
responseFormat: {
4405+
type: 'json',
4406+
schema: {
4407+
type: 'object',
4408+
properties: {
4409+
name: { type: 'string' },
4410+
},
4411+
required: ['name'],
4412+
},
4413+
},
4414+
providerOptions: {
4415+
bedrock: {
4416+
reasoningConfig: {
4417+
type: 'enabled',
4418+
budgetTokens: 2000,
4419+
maxReasoningEffort: 'medium',
4420+
},
4421+
},
4422+
},
4423+
});
4424+
4425+
const requestBody = await server.calls[0].requestBodyJson;
4426+
4427+
expect(requestBody.toolConfig).toBeUndefined();
4428+
4429+
expect(requestBody.additionalModelRequestFields?.output_config).toEqual({
4430+
effort: 'medium',
4431+
format: {
4432+
type: 'json_schema',
4433+
schema: {
4434+
type: 'object',
4435+
properties: {
4436+
name: { type: 'string' },
4437+
},
4438+
required: ['name'],
4439+
},
4440+
},
4441+
});
4442+
});
4443+
4444+
it('should still use json tool fallback for structured output without thinking enabled', async () => {
4445+
server.urls[generateUrl].response = {
4446+
type: 'json-value',
4447+
body: {
4448+
output: {
4449+
message: {
4450+
role: 'assistant',
4451+
content: [
4452+
{
4453+
toolUse: {
4454+
toolUseId: 'json-tool-id',
4455+
name: 'json',
4456+
input: { name: 'Test' },
4457+
},
4458+
},
4459+
],
4460+
},
4461+
},
4462+
usage: { inputTokens: 4, outputTokens: 10, totalTokens: 14 },
4463+
stopReason: 'tool_use',
4464+
},
4465+
};
4466+
4467+
const result = await model.doGenerate({
4468+
prompt: [
4469+
{
4470+
role: 'user',
4471+
content: [{ type: 'text', text: 'Generate a name' }],
4472+
},
4473+
],
4474+
responseFormat: {
4475+
type: 'json',
4476+
schema: {
4477+
type: 'object',
4478+
properties: {
4479+
name: { type: 'string' },
4480+
},
4481+
required: ['name'],
4482+
},
4483+
},
4484+
});
4485+
4486+
const requestBody = await server.calls[0].requestBodyJson;
4487+
4488+
expect(requestBody.toolConfig).toBeDefined();
4489+
expect(requestBody.toolConfig.tools).toHaveLength(1);
4490+
expect(requestBody.toolConfig.tools[0].toolSpec.name).toBe('json');
4491+
expect(requestBody.toolConfig.toolChoice).toEqual({ any: {} });
4492+
4493+
expect(
4494+
requestBody.additionalModelRequestFields?.output_config?.format,
4495+
).toBeUndefined();
4496+
4497+
expect(result.content).toMatchInlineSnapshot(`
4498+
[
4499+
{
4500+
"text": "{"name":"Test"}",
4501+
"type": "text",
4502+
},
4503+
]
4504+
`);
4505+
expect(result.providerMetadata?.bedrock?.isJsonResponseFromTool).toBe(true);
4506+
});
4507+
43304508
it('should extract reasoning text with signature', async () => {
43314509
server.urls[generateUrl].response = {
43324510
type: 'json-value',

packages/amazon-bedrock/src/bedrock-chat-language-model.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,21 @@ export class BedrockChatLanguageModel implements LanguageModelV3 {
136136
});
137137
}
138138

139+
const isAnthropicModel = this.modelId.includes('anthropic');
140+
const isThinkingEnabled =
141+
bedrockOptions.reasoningConfig?.type === 'enabled' ||
142+
bedrockOptions.reasoningConfig?.type === 'adaptive';
143+
144+
const useNativeStructuredOutput =
145+
isAnthropicModel &&
146+
isThinkingEnabled &&
147+
responseFormat?.type === 'json' &&
148+
responseFormat.schema != null;
149+
139150
const jsonResponseTool: LanguageModelV3FunctionTool | undefined =
140-
responseFormat?.type === 'json' && responseFormat.schema != null
151+
responseFormat?.type === 'json' &&
152+
responseFormat.schema != null &&
153+
!useNativeStructuredOutput
141154
? {
142155
type: 'function',
143156
name: 'json',
@@ -176,15 +189,12 @@ export class BedrockChatLanguageModel implements LanguageModelV3 {
176189
};
177190
}
178191

179-
const isAnthropicModel = this.modelId.includes('anthropic');
180192
const thinkingType = bedrockOptions.reasoningConfig?.type;
181-
const isThinkingRequested =
182-
thinkingType === 'enabled' || thinkingType === 'adaptive';
183193
const thinkingBudget =
184194
thinkingType === 'enabled'
185195
? bedrockOptions.reasoningConfig?.budgetTokens
186196
: undefined;
187-
const isAnthropicThinkingEnabled = isAnthropicModel && isThinkingRequested;
197+
const isAnthropicThinkingEnabled = isAnthropicModel && isThinkingEnabled;
188198

189199
const inferenceConfig = {
190200
...(maxOutputTokens != null && { maxTokens: maxOutputTokens }),
@@ -244,6 +254,7 @@ export class BedrockChatLanguageModel implements LanguageModelV3 {
244254
bedrockOptions.additionalModelRequestFields = {
245255
...bedrockOptions.additionalModelRequestFields,
246256
output_config: {
257+
...bedrockOptions.additionalModelRequestFields?.output_config,
247258
effort: maxReasoningEffort,
248259
},
249260
};
@@ -267,6 +278,19 @@ export class BedrockChatLanguageModel implements LanguageModelV3 {
267278
}
268279
}
269280

281+
if (useNativeStructuredOutput) {
282+
bedrockOptions.additionalModelRequestFields = {
283+
...bedrockOptions.additionalModelRequestFields,
284+
output_config: {
285+
...bedrockOptions.additionalModelRequestFields?.output_config,
286+
format: {
287+
type: 'json_schema',
288+
schema: responseFormat!.schema,
289+
},
290+
},
291+
};
292+
}
293+
270294
if (isAnthropicThinkingEnabled && inferenceConfig.temperature != null) {
271295
delete inferenceConfig.temperature;
272296
warnings.push({

0 commit comments

Comments
 (0)