@@ -28,6 +28,7 @@ pub struct RequestPayload {
2828 pub max_output_tokens : Option < u32 > ,
2929 pub truncation : Option < String > ,
3030 pub metadata : Option < Value > ,
31+ pub parallel_tool_calls : Option < bool > ,
3132 #[ serde( default , skip_serializing_if = "Option::is_none" ) ]
3233 pub cache_salt : Option < String > ,
3334}
@@ -64,6 +65,7 @@ pub struct UpstreamRequest<'a> {
6465 #[ serde( skip_serializing_if = "Option::is_none" ) ]
6566 pub metadata : Option < & ' a Value > ,
6667 #[ serde( skip_serializing_if = "Option::is_none" ) ]
68+ pub parallel_tool_calls : Option < bool > ,
6769 pub cache_salt : Option < & ' a str > ,
6870}
6971
@@ -128,6 +130,18 @@ impl RequestPayload {
128130 /// flat name collides with a top-level function tool or another namespace
129131 /// member.
130132 pub fn to_upstream_request ( & self , stream : bool ) -> Result < UpstreamRequest < ' _ > , ToolError > {
133+ let has_built_in_tool = self . declares_built_in_tool ( ) ;
134+ if has_built_in_tool && self . parallel_tool_calls == Some ( true ) {
135+ return Err ( ToolError :: Config (
136+ "parallel_tool_calls must be false when using built-in tools" . into ( ) ,
137+ ) ) ;
138+ }
139+ let parallel_tool_calls = if has_built_in_tool {
140+ Some ( false )
141+ } else {
142+ self . parallel_tool_calls
143+ } ;
144+
131145 let renamed_tools = self
132146 . tools
133147 . as_deref ( )
@@ -151,9 +165,16 @@ impl RequestPayload {
151165 max_output_tokens : self . max_output_tokens ,
152166 truncation : self . truncation . as_deref ( ) ,
153167 metadata : self . metadata . as_ref ( ) ,
168+ parallel_tool_calls,
154169 cache_salt : self . cache_salt . as_deref ( ) ,
155170 } )
156171 }
172+
173+ fn declares_built_in_tool ( & self ) -> bool {
174+ self . tools
175+ . as_deref ( )
176+ . is_some_and ( |tools| tools. iter ( ) . any ( ResponsesTool :: is_gateway_owned) )
177+ }
157178}
158179
159180fn upstream_tools ( tool : ResponsesTool ) -> Vec < UpstreamTool > {
@@ -302,6 +323,144 @@ mod tests {
302323 assert_eq ! ( value[ "input" ] , "hi" ) ;
303324 }
304325
326+ #[ test]
327+ fn to_upstream_request_preserves_parallel_tool_calls ( ) {
328+ let payload: RequestPayload = serde_json:: from_value ( serde_json:: json!( {
329+ "model" : "test" ,
330+ "input" : "hi" ,
331+ "parallel_tool_calls" : false
332+ } ) )
333+ . unwrap ( ) ;
334+
335+ let upstream = payload. to_upstream_request ( false ) . expect ( "valid upstream request" ) ;
336+ let value = serde_json:: to_value ( upstream) . unwrap ( ) ;
337+ assert_eq ! ( value[ "parallel_tool_calls" ] , false ) ;
338+ }
339+
340+ #[ test]
341+ fn to_upstream_request_allows_parallel_tool_calls_for_client_function_tools ( ) {
342+ let payload: RequestPayload = serde_json:: from_value ( serde_json:: json!( {
343+ "model" : "test" ,
344+ "input" : "hi" ,
345+ "parallel_tool_calls" : true ,
346+ "tools" : [ { "type" : "function" , "name" : "get_weather" } ]
347+ } ) )
348+ . unwrap ( ) ;
349+
350+ let upstream = payload
351+ . to_upstream_request ( false )
352+ . expect ( "function tools allow parallel calls" ) ;
353+ let value = serde_json:: to_value ( upstream) . unwrap ( ) ;
354+ assert_eq ! ( value[ "parallel_tool_calls" ] , true ) ;
355+ }
356+
357+ #[ test]
358+ fn to_upstream_request_validates_parallel_tool_calls_for_mixed_tools ( ) {
359+ for built_in_tool in builtin_tool_declarations ( ) {
360+ for ( parallel_tool_calls, should_reject) in [ ( false , false ) , ( true , true ) ] {
361+ let payload: RequestPayload = serde_json:: from_value ( serde_json:: json!( {
362+ "model" : "test" ,
363+ "input" : "hi" ,
364+ "parallel_tool_calls" : parallel_tool_calls,
365+ "tools" : [
366+ { "type" : "function" , "name" : "get_weather" } ,
367+ built_in_tool. clone( )
368+ ]
369+ } ) )
370+ . unwrap ( ) ;
371+
372+ let result = payload. to_upstream_request ( false ) ;
373+ if should_reject {
374+ let err = result. expect_err ( "built-in tools should reject parallel tool calls" ) ;
375+ assert ! ( err. to_string( ) . contains( "parallel_tool_calls must be false" ) ) ;
376+ } else {
377+ let value =
378+ serde_json:: to_value ( result. expect ( "mixed built-in and function tools allow serial calls" ) )
379+ . unwrap ( ) ;
380+ assert_eq ! ( value[ "parallel_tool_calls" ] , false ) ;
381+ }
382+ }
383+ }
384+ }
385+
386+ #[ test]
387+ fn to_upstream_request_sets_serial_tool_calls_for_builtin_tools ( ) {
388+ for tool in builtin_tool_declarations ( ) {
389+ let payload: RequestPayload = serde_json:: from_value ( serde_json:: json!( {
390+ "model" : "test" ,
391+ "input" : "hi" ,
392+ "tools" : [ tool]
393+ } ) )
394+ . unwrap ( ) ;
395+
396+ let upstream = payload
397+ . to_upstream_request ( false )
398+ . expect ( "built-in tools default to serial tool calls" ) ;
399+ let value = serde_json:: to_value ( upstream) . unwrap ( ) ;
400+ assert_eq ! ( value[ "parallel_tool_calls" ] , false ) ;
401+ }
402+ }
403+
404+ #[ test]
405+ fn to_upstream_request_rejects_parallel_tool_calls_for_builtin_tools ( ) {
406+ for tool in builtin_tool_declarations ( ) {
407+ let payload: RequestPayload = serde_json:: from_value ( serde_json:: json!( {
408+ "model" : "test" ,
409+ "input" : "hi" ,
410+ "parallel_tool_calls" : true ,
411+ "tools" : [ tool]
412+ } ) )
413+ . unwrap ( ) ;
414+
415+ let Err ( err) = payload. to_upstream_request ( false ) else {
416+ panic ! ( "built-in tools should reject parallel_tool_calls=true" ) ;
417+ } ;
418+
419+ assert ! ( err. to_string( ) . contains( "parallel_tool_calls must be false" ) ) ;
420+ }
421+ }
422+
423+ #[ test]
424+ fn to_upstream_request_allows_builtin_tools_with_serial_tool_calls ( ) {
425+ for tool in builtin_tool_declarations ( ) {
426+ let payload: RequestPayload = serde_json:: from_value ( serde_json:: json!( {
427+ "model" : "test" ,
428+ "input" : "hi" ,
429+ "parallel_tool_calls" : false ,
430+ "tools" : [ tool]
431+ } ) )
432+ . unwrap ( ) ;
433+
434+ let upstream = payload
435+ . to_upstream_request ( false )
436+ . expect ( "serial built-in tool request is valid" ) ;
437+ let value = serde_json:: to_value ( upstream) . unwrap ( ) ;
438+ assert_eq ! ( value[ "parallel_tool_calls" ] , false ) ;
439+ }
440+ }
441+
442+ fn builtin_tool_declarations ( ) -> Vec < Value > {
443+ vec ! [
444+ serde_json:: json!( {
445+ "type" : "function" ,
446+ "name" : "read_mcp_resource" ,
447+ "metadata" : {
448+ "server_label" : "repo" ,
449+ "server_url" : "http://localhost:9001/mcp"
450+ }
451+ } ) ,
452+ serde_json:: json!( {
453+ "type" : "mcp" ,
454+ "name" : "read_mcp_resource" ,
455+ "server_label" : "repo" ,
456+ "server_url" : "http://localhost:9001/mcp"
457+ } ) ,
458+ serde_json:: json!( { "type" : "web_search_preview" } ) ,
459+ serde_json:: json!( { "type" : "file_search" , "vector_store_ids" : [ "vs_abc" ] } ) ,
460+ serde_json:: json!( { "type" : "code_interpreter" } ) ,
461+ ]
462+ }
463+
305464 #[ test]
306465 fn to_upstream_request_flattens_namespace_and_skips_unknown_tools ( ) {
307466 let payload: RequestPayload = serde_json:: from_value ( serde_json:: json!( {
0 commit comments