@@ -11,6 +11,21 @@ use super::tools::ResponsesTool;
1111use crate :: tool:: { CodexNamespaceHandler , CustomHandler , ToolError } ;
1212use crate :: utils:: common:: serialize_to_string;
1313
14+ /// Standard Responses API reasoning generation settings.
15+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
16+ pub struct ReasoningConfig {
17+ #[ serde( default , skip_serializing_if = "Option::is_none" ) ]
18+ pub context : Option < String > ,
19+ #[ serde( default , skip_serializing_if = "Option::is_none" ) ]
20+ pub effort : Option < String > ,
21+ #[ serde( default , skip_serializing_if = "Option::is_none" ) ]
22+ pub generate_summary : Option < String > ,
23+ #[ serde( default , skip_serializing_if = "Option::is_none" ) ]
24+ pub mode : Option < String > ,
25+ #[ serde( default , skip_serializing_if = "Option::is_none" ) ]
26+ pub summary : Option < String > ,
27+ }
28+
1429#[ derive( Debug , Clone , Serialize , Deserialize ) ]
1530pub struct RequestPayload {
1631 pub model : String ,
@@ -26,6 +41,8 @@ pub struct RequestPayload {
2641 #[ serde( default = "default_true" ) ]
2742 pub store : bool ,
2843 pub include : Option < Vec < String > > ,
44+ #[ serde( default , skip_serializing_if = "Option::is_none" ) ]
45+ pub reasoning : Option < Box < ReasoningConfig > > ,
2946 pub temperature : Option < f64 > ,
3047 pub top_p : Option < f64 > ,
3148 pub max_output_tokens : Option < u32 > ,
@@ -62,6 +79,8 @@ pub struct UpstreamRequest<'a> {
6279 #[ serde( skip_serializing_if = "Option::is_none" ) ]
6380 pub include : Option < & ' a Vec < String > > ,
6481 #[ serde( skip_serializing_if = "Option::is_none" ) ]
82+ pub reasoning : Option < & ' a ReasoningConfig > ,
83+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
6584 pub temperature : Option < f64 > ,
6685 #[ serde( skip_serializing_if = "Option::is_none" ) ]
6786 pub top_p : Option < f64 > ,
@@ -73,6 +92,7 @@ pub struct UpstreamRequest<'a> {
7392 pub metadata : Option < & ' a Value > ,
7493 #[ serde( skip_serializing_if = "Option::is_none" ) ]
7594 pub parallel_tool_calls : Option < bool > ,
95+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
7696 pub cache_salt : Option < & ' a str > ,
7797}
7898
@@ -155,6 +175,7 @@ impl RequestPayload {
155175 tools,
156176 tool_choice : Some ( tool_choice) ,
157177 include : self . include . as_ref ( ) ,
178+ reasoning : self . reasoning . as_deref ( ) ,
158179 temperature : self . temperature ,
159180 top_p : self . top_p ,
160181 max_output_tokens : self . max_output_tokens ,
@@ -329,7 +350,18 @@ mod tests {
329350 }
330351
331352 #[ test]
332- fn request_payload_forwards_cache_salt_upstream ( ) {
353+ fn request_payload_omits_absent_and_forwards_present_cache_salt_upstream ( ) {
354+ let payload: RequestPayload = serde_json:: from_value ( serde_json:: json!( {
355+ "model" : "test-model" ,
356+ "input" : "hello"
357+ } ) )
358+ . expect ( "request should deserialize" ) ;
359+
360+ let upstream = serde_json:: to_value ( payload. to_upstream_request ( false ) . expect ( "request should normalize" ) )
361+ . expect ( "upstream request should serialize" ) ;
362+
363+ assert ! ( upstream. get( "cache_salt" ) . is_none( ) ) ;
364+
333365 let payload: RequestPayload = serde_json:: from_value ( serde_json:: json!( {
334366 "model" : "test-model" ,
335367 "input" : "hello" ,
@@ -343,6 +375,72 @@ mod tests {
343375 assert_eq ! ( upstream[ "cache_salt" ] , "tenant-a" ) ;
344376 }
345377
378+ #[ test]
379+ fn request_payload_forwards_reasoning_configuration_upstream ( ) {
380+ let reasoning = serde_json:: json!( {
381+ "context" : "all_turns" ,
382+ "effort" : "high" ,
383+ "generate_summary" : "concise" ,
384+ "mode" : "pro" ,
385+ "summary" : "detailed"
386+ } ) ;
387+ let payload: RequestPayload = serde_json:: from_value ( serde_json:: json!( {
388+ "model" : "test-model" ,
389+ "input" : "hello" ,
390+ "reasoning" : reasoning
391+ } ) )
392+ . expect ( "request should deserialize" ) ;
393+
394+ for stream in [ false , true ] {
395+ let upstream = serde_json:: to_value ( payload. to_upstream_request ( stream) . expect ( "request should normalize" ) )
396+ . expect ( "upstream request should serialize" ) ;
397+
398+ assert_eq ! ( upstream[ "reasoning" ] , reasoning) ;
399+ assert_eq ! ( upstream[ "stream" ] , stream) ;
400+ }
401+ }
402+
403+ #[ test]
404+ fn request_payload_handles_reasoning_boundaries ( ) {
405+ for reasoning in [ serde_json:: json!( { } ) , serde_json:: json!( { "effort" : "minimal" } ) ] {
406+ let payload: RequestPayload = serde_json:: from_value ( serde_json:: json!( {
407+ "model" : "test-model" ,
408+ "input" : "hello" ,
409+ "reasoning" : reasoning
410+ } ) )
411+ . expect ( "valid reasoning object should deserialize" ) ;
412+ let upstream = serde_json:: to_value ( payload. to_upstream_request ( false ) . expect ( "request should normalize" ) )
413+ . expect ( "upstream request should serialize" ) ;
414+
415+ assert_eq ! ( upstream[ "reasoning" ] , reasoning) ;
416+ }
417+
418+ for reasoning in [
419+ serde_json:: Value :: Null ,
420+ serde_json:: json!( "high" ) ,
421+ serde_json:: json!( { "effort" : 3 } ) ,
422+ ] {
423+ let parsed = serde_json:: from_value :: < RequestPayload > ( serde_json:: json!( {
424+ "model" : "test-model" ,
425+ "input" : "hello" ,
426+ "reasoning" : reasoning
427+ } ) ) ;
428+
429+ if reasoning. is_null ( ) {
430+ let upstream = serde_json:: to_value (
431+ parsed
432+ . expect ( "null should be treated as absent" )
433+ . to_upstream_request ( false )
434+ . expect ( "request should normalize" ) ,
435+ )
436+ . expect ( "upstream request should serialize" ) ;
437+ assert ! ( upstream. get( "reasoning" ) . is_none( ) ) ;
438+ } else {
439+ assert ! ( parsed. is_err( ) , "non-object reasoning configuration should be rejected" ) ;
440+ }
441+ }
442+ }
443+
346444 #[ test]
347445 fn request_payload_uses_option_tool_choice_for_missing_vs_explicit ( ) {
348446 let absent: RequestPayload = serde_json:: from_value ( serde_json:: json!( {
0 commit comments