@@ -68,6 +68,12 @@ const fakeFetchWithAuth = injectFetchHeaders({ 'x-amz-auth': 'test-auth' });
6868
6969const modelId = 'anthropic.claude-3-haiku-20240307-v1:0' ;
7070const anthropicModelId = 'anthropic.claude-3-5-sonnet-20240620-v1:0' ; // Define at top level
71+ const supportedStructuredOutputModelId =
72+ 'anthropic.claude-sonnet-4-5-20250929-v1:0' ;
73+ const supportedStructuredOutput41ModelId =
74+ 'anthropic.claude-opus-4-1-20250805-v1:0' ;
75+ const dateBasedStructuredOutput4ModelId =
76+ 'anthropic.claude-sonnet-4-20250514-v1:0' ;
7177const baseUrl = 'https://bedrock-runtime.us-east-1.amazonaws.com' ;
7278
7379const streamUrl = `${ baseUrl } /model/${ encodeURIComponent (
@@ -77,6 +83,15 @@ const generateUrl = `${baseUrl}/model/${encodeURIComponent(modelId)}/converse`;
7783const anthropicGenerateUrl = `${ baseUrl } /model/${ encodeURIComponent (
7884 anthropicModelId ,
7985) } /converse`;
86+ const supportedStructuredOutputGenerateUrl = `${ baseUrl } /model/${ encodeURIComponent (
87+ supportedStructuredOutputModelId ,
88+ ) } /converse`;
89+ const supportedStructuredOutput41GenerateUrl = `${ baseUrl } /model/${ encodeURIComponent (
90+ supportedStructuredOutput41ModelId ,
91+ ) } /converse`;
92+ const dateBasedStructuredOutput4GenerateUrl = `${ baseUrl } /model/${ encodeURIComponent (
93+ dateBasedStructuredOutput4ModelId ,
94+ ) } /converse`;
8095
8196const novaModelId = 'us.amazon.nova-2-lite-v1:0' ;
8297const novaGenerateUrl = `${ baseUrl } /model/${ encodeURIComponent (
@@ -103,16 +118,22 @@ const server = createTestServer({
103118 } ,
104119 // Configure the server for the Anthropic model from the start
105120 [ anthropicGenerateUrl ] : { } ,
121+ [ supportedStructuredOutputGenerateUrl ] : { } ,
122+ [ supportedStructuredOutput41GenerateUrl ] : { } ,
123+ [ dateBasedStructuredOutput4GenerateUrl ] : { } ,
106124 [ novaGenerateUrl ] : { } ,
107125 [ openaiGenerateUrl ] : { } ,
108126 [ newerAnthropicGenerateUrl ] : { } ,
109127} ) ;
110128
111129function prepareJsonFixtureResponse (
112130 filename : string ,
113- { headers } : { headers ?: Record < string , string > } = { } ,
131+ {
132+ headers,
133+ url = generateUrl ,
134+ } : { headers ?: Record < string , string > ; url ?: string } = { } ,
114135) {
115- server . urls [ generateUrl ] . response = {
136+ server . urls [ url ] . response = {
116137 type : 'json-value' ,
117138 headers,
118139 body : JSON . parse (
@@ -149,6 +170,18 @@ beforeEach(() => {
149170 type : 'json-value' ,
150171 body : { } ,
151172 } ;
173+ server . urls [ supportedStructuredOutputGenerateUrl ] . response = {
174+ type : 'json-value' ,
175+ body : { } ,
176+ } ;
177+ server . urls [ supportedStructuredOutput41GenerateUrl ] . response = {
178+ type : 'json-value' ,
179+ body : { } ,
180+ } ;
181+ server . urls [ dateBasedStructuredOutput4GenerateUrl ] . response = {
182+ type : 'json-value' ,
183+ body : { } ,
184+ } ;
152185 mockPrepareAnthropicTools . mockClear ( ) ;
153186} ) ;
154187
@@ -183,6 +216,36 @@ const newerAnthropicModel = new BedrockChatLanguageModel(
183216 } ,
184217) ;
185218
219+ const supportedStructuredOutputModel = new BedrockChatLanguageModel (
220+ supportedStructuredOutputModelId ,
221+ {
222+ baseUrl : ( ) => baseUrl ,
223+ headers : { } ,
224+ fetch : fakeFetchWithAuth ,
225+ generateId : ( ) => 'test-id' ,
226+ } ,
227+ ) ;
228+
229+ const supportedStructuredOutput41Model = new BedrockChatLanguageModel (
230+ supportedStructuredOutput41ModelId ,
231+ {
232+ baseUrl : ( ) => baseUrl ,
233+ headers : { } ,
234+ fetch : fakeFetchWithAuth ,
235+ generateId : ( ) => 'test-id' ,
236+ } ,
237+ ) ;
238+
239+ const dateBasedStructuredOutput4Model = new BedrockChatLanguageModel (
240+ dateBasedStructuredOutput4ModelId ,
241+ {
242+ baseUrl : ( ) => baseUrl ,
243+ headers : { } ,
244+ fetch : fakeFetchWithAuth ,
245+ generateId : ( ) => 'test-id' ,
246+ } ,
247+ ) ;
248+
186249let mockOptions : { success : boolean ; errorValue ?: any } = { success : true } ;
187250
188251describe ( 'doStream' , ( ) => {
@@ -4403,9 +4466,11 @@ describe('doGenerate', () => {
44034466 } ) ;
44044467
44054468 it ( 'should use native output_config.format instead of json tool when thinking is enabled with structured output' , async ( ) => {
4406- prepareJsonFixtureResponse ( 'bedrock-text' ) ;
4469+ prepareJsonFixtureResponse ( 'bedrock-text' , {
4470+ url : supportedStructuredOutputGenerateUrl ,
4471+ } ) ;
44074472
4408- await model . doGenerate ( {
4473+ await supportedStructuredOutputModel . doGenerate ( {
44094474 prompt : [
44104475 {
44114476 role : 'user' ,
@@ -4467,9 +4532,11 @@ describe('doGenerate', () => {
44674532 } ) ;
44684533
44694534 it ( 'should merge output_config.effort and output_config.format when thinking with maxReasoningEffort and structured output' , async ( ) => {
4470- prepareJsonFixtureResponse ( 'bedrock-text' ) ;
4535+ prepareJsonFixtureResponse ( 'bedrock-text' , {
4536+ url : supportedStructuredOutputGenerateUrl ,
4537+ } ) ;
44714538
4472- await model . doGenerate ( {
4539+ await supportedStructuredOutputModel . doGenerate ( {
44734540 prompt : [
44744541 {
44754542 role : 'user' ,
@@ -4516,30 +4583,95 @@ describe('doGenerate', () => {
45164583 } ) ;
45174584 } ) ;
45184585
4519- it ( 'should still use json tool fallback for structured output without thinking enabled' , async ( ) => {
4520- server . urls [ generateUrl ] . response = {
4521- type : 'json-value' ,
4522- body : {
4523- output : {
4524- message : {
4525- role : 'assistant' ,
4526- content : [
4527- {
4528- toolUse : {
4529- toolUseId : 'json-tool-id' ,
4530- name : 'json' ,
4531- input : { name : 'Test' } ,
4532- } ,
4533- } ,
4534- ] ,
4586+ it ( 'should use native output_config.format for supported Anthropic models without thinking enabled' , async ( ) => {
4587+ prepareJsonFixtureResponse ( 'bedrock-text' , {
4588+ url : supportedStructuredOutputGenerateUrl ,
4589+ } ) ;
4590+
4591+ await supportedStructuredOutputModel . doGenerate ( {
4592+ prompt : [
4593+ {
4594+ role : 'user' ,
4595+ content : [ { type : 'text' , text : 'Generate a name' } ] ,
4596+ } ,
4597+ ] ,
4598+ responseFormat : {
4599+ type : 'json' ,
4600+ schema : {
4601+ type : 'object' ,
4602+ properties : {
4603+ name : { type : 'string' } ,
45354604 } ,
4605+ required : [ 'name' ] ,
45364606 } ,
4537- usage : { inputTokens : 4 , outputTokens : 10 , totalTokens : 14 } ,
4538- stopReason : 'tool_use' ,
45394607 } ,
4540- } ;
4608+ } ) ;
45414609
4542- const result = await model . doGenerate ( {
4610+ const requestBody = await server . calls [ 0 ] . requestBodyJson ;
4611+
4612+ expect ( requestBody . toolConfig ) . toBeUndefined ( ) ;
4613+
4614+ expect ( requestBody . additionalModelRequestFields ?. output_config ) . toEqual ( {
4615+ format : {
4616+ type : 'json_schema' ,
4617+ schema : {
4618+ type : 'object' ,
4619+ properties : {
4620+ name : { type : 'string' } ,
4621+ } ,
4622+ required : [ 'name' ] ,
4623+ } ,
4624+ } ,
4625+ } ) ;
4626+ } ) ;
4627+
4628+ it ( 'should use native output_config.format for Anthropic 4-1 models without thinking enabled' , async ( ) => {
4629+ prepareJsonFixtureResponse ( 'bedrock-text' , {
4630+ url : supportedStructuredOutput41GenerateUrl ,
4631+ } ) ;
4632+
4633+ await supportedStructuredOutput41Model . doGenerate ( {
4634+ prompt : [
4635+ {
4636+ role : 'user' ,
4637+ content : [ { type : 'text' , text : 'Generate a name' } ] ,
4638+ } ,
4639+ ] ,
4640+ responseFormat : {
4641+ type : 'json' ,
4642+ schema : {
4643+ type : 'object' ,
4644+ properties : {
4645+ name : { type : 'string' } ,
4646+ } ,
4647+ required : [ 'name' ] ,
4648+ } ,
4649+ } ,
4650+ } ) ;
4651+
4652+ const requestBody = await server . calls [ 0 ] . requestBodyJson ;
4653+
4654+ expect ( requestBody . toolConfig ) . toBeUndefined ( ) ;
4655+ expect ( requestBody . additionalModelRequestFields ?. output_config ) . toEqual ( {
4656+ format : {
4657+ type : 'json_schema' ,
4658+ schema : {
4659+ type : 'object' ,
4660+ properties : {
4661+ name : { type : 'string' } ,
4662+ } ,
4663+ required : [ 'name' ] ,
4664+ } ,
4665+ } ,
4666+ } ) ;
4667+ } ) ;
4668+
4669+ it ( 'should use json tool fallback for date-based Anthropic 4 models without thinking enabled' , async ( ) => {
4670+ prepareJsonFixtureResponse ( 'bedrock-text' , {
4671+ url : dateBasedStructuredOutput4GenerateUrl ,
4672+ } ) ;
4673+
4674+ await dateBasedStructuredOutput4Model . doGenerate ( {
45434675 prompt : [
45444676 {
45454677 role : 'user' ,
@@ -4564,20 +4696,9 @@ describe('doGenerate', () => {
45644696 expect ( requestBody . toolConfig . tools ) . toHaveLength ( 1 ) ;
45654697 expect ( requestBody . toolConfig . tools [ 0 ] . toolSpec . name ) . toBe ( 'json' ) ;
45664698 expect ( requestBody . toolConfig . toolChoice ) . toEqual ( { any : { } } ) ;
4567-
45684699 expect (
45694700 requestBody . additionalModelRequestFields ?. output_config ?. format ,
45704701 ) . toBeUndefined ( ) ;
4571-
4572- expect ( result . content ) . toMatchInlineSnapshot ( `
4573- [
4574- {
4575- "text": "{"name":"Test"}",
4576- "type": "text",
4577- },
4578- ]
4579- ` ) ;
4580- expect ( result . providerMetadata ?. bedrock ?. isJsonResponseFromTool ) . toBe ( true ) ;
45814702 } ) ;
45824703
45834704 it ( 'should extract reasoning text with signature' , async ( ) => {
0 commit comments