diff --git a/backend/crates/providers/openai/src/transport/request.rs b/backend/crates/providers/openai/src/transport/request.rs index 1bf6e2fd1..6f14c1e4a 100644 --- a/backend/crates/providers/openai/src/transport/request.rs +++ b/backend/crates/providers/openai/src/transport/request.rs @@ -12,6 +12,7 @@ use crate::transport::protocol::responses::CodexResponsesRequest; const PASSTHROUGH_HEADERS_CONTEXT_KEY: &str = "opaque_request_headers"; const THREAD_SPAWN_SUBAGENT_KIND: &str = "thread_spawn"; const THREAD_SPAWN_CONVERSATION_PREFIX: &str = "thread-spawn:"; +const UNSUPPORTED_CODEX_RESPONSES_FIELDS: &[&str] = &["max_output_tokens", "temperature"]; const CROSS_ACCOUNT_IDENTITY_KEYS: &[&str] = &[ "authorization", @@ -90,7 +91,7 @@ pub fn encode_generate_request( return Err(CodexRequestEncodeError::InvalidProtocolPayload); } let mut body = payload.body().clone(); - body.insert("model".to_owned(), Value::String(upstream_model.to_owned())); + adapt_codex_responses_body(&mut body, upstream_model); let mut encoded = CodexResponsesRequest::from_body(body); encoded.explicit_prompt_cache_key = encoded.prompt_cache_key().is_some(); @@ -99,6 +100,13 @@ pub fn encode_generate_request( Ok(encoded) } +fn adapt_codex_responses_body(body: &mut Map, upstream_model: &str) { + body.insert("model".to_owned(), Value::String(upstream_model.to_owned())); + for field in UNSUPPORTED_CODEX_RESPONSES_FIELDS { + body.remove(*field); + } +} + fn extract_request_context(request: &mut CodexResponsesRequest) { let context = ExtractedRequestContext::from_body(request.body()); request.turn_state = context.turn_state; diff --git a/backend/crates/providers/openai/tests/transport/request.rs b/backend/crates/providers/openai/tests/transport/request.rs index d858368cd..72702c558 100644 --- a/backend/crates/providers/openai/tests/transport/request.rs +++ b/backend/crates/providers/openai/tests/transport/request.rs @@ -70,7 +70,7 @@ fn encoder_should_never_hash_prompt_content_into_an_accountless_pool_identity() } #[test] -fn encoder_should_only_patch_model_and_preserve_raw_generate_semantics() { +fn encoder_should_patch_model_and_preserve_supported_generate_semantics() { let request = request(Map::from_iter([ ("model".to_owned(), json!("client-model")), ("input".to_owned(), json!("secret prompt")), @@ -78,7 +78,6 @@ fn encoder_should_only_patch_model_and_preserve_raw_generate_semantics() { "tools".to_owned(), json!([{"type": "function", "name": "lookup", "strict": true}]), ), - ("max_output_tokens".to_owned(), json!(512)), ("store".to_owned(), json!(false)), ( "reasoning".to_owned(), @@ -89,7 +88,6 @@ fn encoder_should_only_patch_model_and_preserve_raw_generate_semantics() { let encoded = encode_generate_request(&request, "gpt-test").expect("encode"); assert_eq!(encoded.body().get("model"), Some(&json!("gpt-test"))); assert!(encoded.body().get("stream").is_none()); - assert_eq!(encoded.body().get("max_output_tokens"), Some(&json!(512))); assert_eq!(encoded.body().get("store"), Some(&json!(false))); let body = Value::Object(encoded.body().clone()); assert_eq!(body.pointer("/tools/0/strict"), Some(&json!(true))); @@ -97,6 +95,28 @@ fn encoder_should_only_patch_model_and_preserve_raw_generate_semantics() { assert!(!encoded.force_http_sse); } +#[test] +fn encoder_should_remove_unsupported_fields_from_upstream_body() { + let request = request(Map::from_iter([ + ("model".to_owned(), json!("client-model")), + ("input".to_owned(), json!("hello")), + ("max_output_tokens".to_owned(), json!(512)), + ("max_tokens".to_owned(), json!(256)), + ("temperature".to_owned(), json!(0.2)), + ])); + + let encoded = encode_generate_request(&request, "gpt-test").expect("encode"); + + assert_eq!( + Value::Object(encoded.body().clone()), + json!({ + "model": "gpt-test", + "input": "hello", + "max_tokens": 256, + }) + ); +} + #[test] fn encoder_should_preserve_client_store_intent() { let request = request(Map::from_iter([