diff --git a/src/provider/openai.rs b/src/provider/openai.rs index f3b793291..7c5881ceb 100644 --- a/src/provider/openai.rs +++ b/src/provider/openai.rs @@ -702,6 +702,20 @@ impl OpenAIProvider { format!("{}/compact", Self::responses_url(credentials)) } + /// Codex-family models (`gpt-5.x-codex*`, including the `[1m]` long-context + /// suffix variants) do not accept the native `image_generation` tool. The + /// OpenAI Responses API rejects requests with that tool attached and the + /// session aborts with a 400. Other ChatGPT-mode models (gpt-5.x non-codex, + /// gpt-4o, …) still support it, so we only suppress the tool for codex + /// model ids. + fn supports_native_image_generation(model_id: &str) -> bool { + let normalized = model_id + .strip_suffix("[1m]") + .unwrap_or(model_id) + .to_ascii_lowercase(); + !normalized.contains("codex") + } + #[expect( clippy::too_many_arguments, reason = "request construction threads explicit per-request OpenAI settings without hidden state" @@ -720,7 +734,7 @@ impl OpenAIProvider { native_compaction_threshold: Option, ) -> Value { let mut tools = api_tools.to_vec(); - if is_chatgpt_mode { + if is_chatgpt_mode && Self::supports_native_image_generation(model_id) { tools.push(serde_json::json!({ "type": "image_generation" })); } diff --git a/src/provider/openai_tests/payloads.rs b/src/provider/openai_tests/payloads.rs index 891dfba67..54070519c 100644 --- a/src/provider/openai_tests/payloads.rs +++ b/src/provider/openai_tests/payloads.rs @@ -17,6 +17,70 @@ fn test_build_response_request_includes_stream_for_http() { assert_eq!(request["store"], serde_json::json!(false)); } +#[test] +fn test_chatgpt_payload_includes_native_image_generation_for_non_codex_models() { + // Regression for issue #115: ChatGPT mode adds a native `image_generation` + // tool. Non-codex models (gpt-5.x, gpt-4o, …) accept it and benefit. + let request = OpenAIProvider::build_response_request( + "gpt-5.5", + "system".to_string(), + &[], + &[], + true, + Some(DEFAULT_MAX_OUTPUT_TOKENS), + None, + None, + None, + None, + None, + ); + + assert!( + request["tools"] + .as_array() + .expect("tools array") + .iter() + .any(|tool| tool["type"] == "image_generation"), + "non-codex models should still receive image_generation in ChatGPT mode" + ); +} + +#[test] +fn test_chatgpt_payload_excludes_native_image_generation_for_codex_models() { + // Regression for issue #115: codex-family models reject the native + // image_generation tool with a 400 from the OpenAI Responses API. Suppress + // it for any model id whose normalized form contains "codex", including + // the `[1m]` long-context suffix variant. + for model in [ + "gpt-5.3-codex", + "gpt-5.3-codex-spark", + "gpt-5.3-codex-spark[1m]", + ] { + let request = OpenAIProvider::build_response_request( + model, + "system".to_string(), + &[], + &[], + true, + Some(DEFAULT_MAX_OUTPUT_TOKENS), + None, + None, + None, + None, + None, + ); + + assert!( + request["tools"] + .as_array() + .expect("tools array") + .iter() + .all(|tool| tool["type"] != "image_generation"), + "{model} should not receive the unsupported native image_generation tool" + ); + } +} + #[test] fn test_websocket_payload_strips_stream_and_background() { let mut request = OpenAIProvider::build_response_request(