From 2750da01c6442cd2d67a8d34552b819b1ea46f32 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 27 Feb 2026 21:40:01 -0500 Subject: [PATCH] Add gpt-5.3-codex model and make it the default codex alias - Add gpt-5.3-codex to catalog (API model, 1047576 context, 128K output) - Move "codex" alias from gpt-5.2-codex to gpt-5.3-codex - Add e2e integration test for gpt-5.3-codex via OpenAI API - Remove gpt-5.3-codex-spark (not yet available) - Empty CLI_ONLY_MODELS list and update related tests Co-Authored-By: Claude Opus 4.6 --- crates/attractor/src/cli/cli_backend.rs | 34 ++++--------------------- crates/attractor/tests/integration.rs | 16 ++++++------ crates/llm/src/catalog.json | 13 ++++++++++ crates/llm/src/catalog.rs | 6 ++--- crates/llm/tests/integration.rs | 14 ++++++++++ 5 files changed, 43 insertions(+), 40 deletions(-) diff --git a/crates/attractor/src/cli/cli_backend.rs b/crates/attractor/src/cli/cli_backend.rs index 03c0d5124..8450df7bc 100644 --- a/crates/attractor/src/cli/cli_backend.rs +++ b/crates/attractor/src/cli/cli_backend.rs @@ -12,7 +12,7 @@ use crate::handler::codergen::{CodergenBackend, CodergenResult}; use crate::outcome::StageUsage; /// Models that are only available through CLI tools (not via API). -const CLI_ONLY_MODELS: &[&str] = &["gpt-5.3-codex-spark"]; +const CLI_ONLY_MODELS: &[&str] = &[]; /// Returns true if the given model is only available through a CLI tool. #[must_use] @@ -410,9 +410,9 @@ mod tests { #[test] fn cli_command_for_codex() { - let cmd = cli_command_for_provider("openai", "gpt-5.3-codex-spark", "/tmp/prompt.txt"); + let cmd = cli_command_for_provider("openai", "gpt-5.3-codex", "/tmp/prompt.txt"); assert!(cmd.starts_with("codex exec --json --full-auto")); - assert!(cmd.contains("-m gpt-5.3-codex-spark")); + assert!(cmd.contains("-m gpt-5.3-codex")); assert!(cmd.ends_with("< /tmp/prompt.txt")); } @@ -455,17 +455,9 @@ mod tests { // -- Cycle 2: is_cli_only_model -- #[test] - fn codex_spark_is_cli_only() { - assert!(is_cli_only_model("gpt-5.3-codex-spark")); - } - - #[test] - fn claude_opus_is_not_cli_only() { + fn no_models_are_currently_cli_only() { + assert!(!is_cli_only_model("gpt-5.3-codex")); assert!(!is_cli_only_model("claude-opus-4-6")); - } - - #[test] - fn gemini_is_not_cli_only() { assert!(!is_cli_only_model("gemini-3.1-pro-preview")); } @@ -583,22 +575,6 @@ mod tests { assert!(router.should_use_cli(&node)); } - #[test] - fn router_uses_cli_for_cli_only_model() { - let mut node = Node::new("test"); - node.attrs.insert( - "llm_model".to_string(), - AttrValue::String("gpt-5.3-codex-spark".to_string()), - ); - - let cli_backend = CliBackend::new("model".into(), "openai".into()); - let router = BackendRouter::new( - Box::new(StubBackend), - cli_backend, - ); - assert!(router.should_use_cli(&node)); - } - #[test] fn router_uses_api_by_default() { let node = Node::new("test"); diff --git a/crates/attractor/tests/integration.rs b/crates/attractor/tests/integration.rs index ab7d97322..9a766533d 100644 --- a/crates/attractor/tests/integration.rs +++ b/crates/attractor/tests/integration.rs @@ -7401,7 +7401,7 @@ async fn cli_backend_run_with_codex_provider() { let codex_output = "{\"type\":\"item.completed\",\"item\":{\"id\":\"item_0\",\"type\":\"agent_message\",\"text\":\"Implemented the feature.\"}}\n{\"type\":\"turn.completed\",\"usage\":{\"input_tokens\":300,\"output_tokens\":150}}"; let test_env = Arc::new(CliTestEnv::new(codex_output)); let env: Arc = test_env.clone(); - let backend = CliBackend::new("gpt-5.3-codex-spark".into(), "openai".into()); + let backend = CliBackend::new("gpt-5.3-codex".into(), "openai".into()); let node = Node::new("implement"); let context = Context::new(); @@ -7417,7 +7417,7 @@ async fn cli_backend_run_with_codex_provider() { let commands = test_env.recorded_commands(); let cli_cmd = commands.iter().find(|c| c.contains("codex")).expect("should call codex CLI"); assert!(cli_cmd.contains("exec --json"), "should use exec mode"); - assert!(cli_cmd.contains("gpt-5.3-codex-spark"), "should use correct model"); + assert!(cli_cmd.contains("gpt-5.3-codex"), "should use correct model"); match result { CodergenResult::Text { text, usage, .. } => { @@ -7536,7 +7536,7 @@ async fn cli_backend_run_uses_node_provider_override() { let mut node = Node::new("step"); node.attrs.insert("llm_provider".to_string(), AttrValue::String("openai".to_string())); - node.attrs.insert("llm_model".to_string(), AttrValue::String("gpt-5.3-codex-spark".to_string())); + node.attrs.insert("llm_model".to_string(), AttrValue::String("gpt-5.3-codex".to_string())); let context = Context::new(); let emitter = Arc::new(EventEmitter::new()); @@ -7549,7 +7549,7 @@ async fn cli_backend_run_uses_node_provider_override() { let commands = test_env.recorded_commands(); let cli_cmd = commands.iter().find(|c| c.contains("codex")).expect("should call codex based on provider override"); - assert!(cli_cmd.contains("gpt-5.3-codex-spark")); + assert!(cli_cmd.contains("gpt-5.3-codex")); } #[tokio::test] @@ -7640,16 +7640,16 @@ async fn backend_router_delegates_to_api_for_normal_node() { } #[tokio::test] -async fn backend_router_delegates_to_cli_for_cli_only_model() { +async fn backend_router_delegates_to_cli_for_backend_attr() { let codex_output = "{\"type\":\"item.completed\",\"item\":{\"id\":\"item_0\",\"type\":\"agent_message\",\"text\":\"Codex did it\"}}\n{\"type\":\"turn.completed\",\"usage\":{\"input_tokens\":10,\"output_tokens\":5}}"; let env: Arc = Arc::new(CliTestEnv::new(codex_output)); let api_backend = Box::new(MockCodergenBackend); - let cli = CliBackend::new("gpt-5.3-codex-spark".into(), "openai".into()); + let cli = CliBackend::new("gpt-5.3-codex".into(), "openai".into()); let router = BackendRouter::new(api_backend, cli); let mut node = Node::new("codex_step"); - node.attrs.insert("llm_model".to_string(), AttrValue::String("gpt-5.3-codex-spark".to_string())); + node.attrs.insert("backend".to_string(), AttrValue::String("cli".to_string())); node.attrs.insert("llm_provider".to_string(), AttrValue::String("openai".to_string())); let context = Context::new(); @@ -7663,7 +7663,7 @@ async fn backend_router_delegates_to_cli_for_cli_only_model() { match result { CodergenResult::Text { text, .. } => { - assert_eq!(text, "Codex did it", "should route to CLI backend for CLI-only model"); + assert_eq!(text, "Codex did it", "should route to CLI backend for backend=cli"); } CodergenResult::Full(_) => panic!("expected Text result"), } diff --git a/crates/llm/src/catalog.json b/crates/llm/src/catalog.json index 7158668a4..5a88b35b3 100644 --- a/crates/llm/src/catalog.json +++ b/crates/llm/src/catalog.json @@ -75,6 +75,19 @@ "supports_reasoning": true, "input_cost_per_million": null, "output_cost_per_million": null, + "aliases": [] + }, + { + "id": "gpt-5.3-codex", + "provider": "openai", + "display_name": "GPT-5.3 Codex", + "context_window": 1047576, + "max_output": 128000, + "supports_tools": true, + "supports_vision": true, + "supports_reasoning": true, + "input_cost_per_million": null, + "output_cost_per_million": null, "aliases": ["codex"] }, { diff --git a/crates/llm/src/catalog.rs b/crates/llm/src/catalog.rs index 751e1a85a..b263d5b2f 100644 --- a/crates/llm/src/catalog.rs +++ b/crates/llm/src/catalog.rs @@ -51,7 +51,7 @@ mod tests { assert_eq!(info.id, "claude-sonnet-4-5"); let info = get_model_info("codex").unwrap(); - assert_eq!(info.id, "gpt-5.2-codex"); + assert_eq!(info.id, "gpt-5.3-codex"); } #[test] @@ -62,7 +62,7 @@ mod tests { #[test] fn list_models_all() { let models = list_models(None); - assert_eq!(models.len(), 8); + assert_eq!(models.len(), 9); } #[test] @@ -72,7 +72,7 @@ mod tests { assert!(anthropic.iter().all(|m| m.provider == "anthropic")); let openai = list_models(Some("openai")); - assert_eq!(openai.len(), 3); + assert_eq!(openai.len(), 4); let gemini = list_models(Some("gemini")); assert_eq!(gemini.len(), 2); diff --git a/crates/llm/tests/integration.rs b/crates/llm/tests/integration.rs index 27466aaf8..8443eadaa 100644 --- a/crates/llm/tests/integration.rs +++ b/crates/llm/tests/integration.rs @@ -52,6 +52,20 @@ async fn openai_complete() { assert_eq!(response.provider, "openai"); } +#[tokio::test] +#[ignore = "requires OPENAI_API_KEY"] +async fn openai_gpt_5_3_codex_complete() { + let api_key = std::env::var("OPENAI_API_KEY").expect("OPENAI_API_KEY must be set"); + let adapter = OpenAiAdapter::new(api_key); + let request = make_request("gpt-5.3-codex"); + let response = adapter.complete(&request).await.unwrap(); + + assert!(!response.text().is_empty(), "response text should not be empty"); + assert!(response.usage.input_tokens > 0); + assert!(response.usage.output_tokens > 0); + assert_eq!(response.provider, "openai"); +} + #[tokio::test] #[ignore = "requires GEMINI_API_KEY"] async fn gemini_complete() {