From 604061ce51d194a3aa6aad3b3170240d096e1725 Mon Sep 17 00:00:00 2001 From: Celia Chen Date: Sat, 3 Oct 2026 00:22:36 +0000 Subject: [PATCH] Enable Ultrafast service tiers for Amazon Bedrock Astra models (#50472) ## Why Bedrock catalogs cleared all service-tier metadata, and requests omitted every selected tier. This prevented users from selecting `ultrafast` or using tiers defined in custom catalogs. ## What changed - Advertise `ultrafast` for `openai.gpt-6-astra` on Mantle and the `global.openai.gpt-6-astra` and `us.openai.gpt-6-astra` Runtime variants, while retaining the implicit default tier unless users select otherwise. - Preserve custom catalog speed tiers, service-tier definitions, and defaults while continuing to normalize Bedrock web search compatibility. - Send selected service tiers only when the Bedrock model advertises them, including `flex`. ## Testing Add coverage for `model/list` tier discovery, custom catalog preservation, configured and per-turn HTTP request tiers, and TUI slash-command selection, toggling, and model switching. GitOrigin-RevId: 4793e7af7b9f604a6b7812611030da6adb8acd35 --- .../suite/v2/bedrock_service_tier_tests.rs | 148 ++++++++++++++++++ codex-rs/app-server/tests/suite/v2/mod.rs | 2 + codex-rs/core/src/client.rs | 17 +- .../tests/suite/bedrock_service_tier_tests.rs | 138 ++++++++++++++++ codex-rs/core/tests/suite/mod.rs | 2 + .../src/amazon_bedrock/catalog.rs | 54 +++++-- .../src/amazon_bedrock/runtime_catalog.rs | 4 +- codex-rs/model-provider/src/provider.rs | 65 ++++---- ...dget__tests__bedrock_mantle_ultrafast.snap | 17 ++ ...sts__bedrock_runtime_global_ultrafast.snap | 17 ++ ...__tests__bedrock_runtime_us_ultrafast.snap | 17 ++ .../chatwidget/tests/bedrock_catalog_tests.rs | 94 ++++++++++- 12 files changed, 527 insertions(+), 48 deletions(-) create mode 100644 codex-rs/app-server/tests/suite/v2/bedrock_service_tier_tests.rs create mode 100644 codex-rs/core/tests/suite/bedrock_service_tier_tests.rs create mode 100644 codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__bedrock_mantle_ultrafast.snap create mode 100644 codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__bedrock_runtime_global_ultrafast.snap create mode 100644 codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__bedrock_runtime_us_ultrafast.snap diff --git a/codex-rs/app-server/tests/suite/v2/bedrock_service_tier_tests.rs b/codex-rs/app-server/tests/suite/v2/bedrock_service_tier_tests.rs new file mode 100644 index 0000000000..d92f454f8a --- /dev/null +++ b/codex-rs/app-server/tests/suite/v2/bedrock_service_tier_tests.rs @@ -0,0 +1,148 @@ +//! Verifies Bedrock service-tier discovery through the public model/list API. + +use app_test_support::TestAppServer; +use codex_app_server_protocol::ClientRequest; +use codex_app_server_protocol::ModelListParams; +use codex_app_server_protocol::ModelListResponse; +use codex_app_server_protocol::ModelServiceTier; +use pretty_assertions::assert_eq; +use tempfile::TempDir; + +#[test_case::test_case("amazon-bedrock", vec!["openai.gpt-6-astra"]; "mantle")] +#[test_case::test_case("amazon-bedrock-runtime", vec!["global.openai.gpt-6-astra", "us.openai.gpt-6-astra"]; "runtime")] +#[tokio::test] +async fn bedrock_model_list_advertises_ultrafast_without_changing_default( + provider_id: &str, + expected_models: Vec<&str>, +) -> anyhow::Result<()> { + let home = TempDir::new()?; + std::fs::write( + home.path().join("config.toml"), + format!( + r#"model_provider = "{provider_id}" +[model_providers.{provider_id}.aws] +region = "us-west-2" +"# + ), + )?; + let mut app_server = TestAppServer::builder() + .with_codex_home(home.path()) + .build_initialized() + .await?; + let response: ModelListResponse = app_server + .request(|request_id| ClientRequest::ModelList { + request_id, + params: ModelListParams { + cursor: None, + limit: Some(100), + include_hidden: Some(true), + }, + }) + .await?; + + assert_eq!( + response + .data + .iter() + .filter(|model| !model.service_tiers.is_empty()) + .map(|model| ( + model.model.as_str(), + model.service_tiers.clone(), + model.default_service_tier.clone() + )) + .collect::>(), + expected_models + .into_iter() + .map(|model| ( + model, + vec![ModelServiceTier { + id: "ultrafast".to_string(), + name: "Ultrafast".to_string(), + description: "The fastest available responses for latency-sensitive work." + .to_string(), + }], + None + )) + .collect::>() + ); + Ok(()) +} + +#[test_case::test_case("amazon-bedrock", "openai.gpt-6-astra"; "mantle")] +#[test_case::test_case("amazon-bedrock-runtime", "global.openai.gpt-6-astra"; "runtime")] +#[tokio::test] +async fn bedrock_model_list_preserves_custom_service_tiers( + provider_id: &str, + model_id: &str, +) -> anyhow::Result<()> { + let home = TempDir::new()?; + let mut model = codex_models_manager::bundled_models_response()? + .models + .into_iter() + .find(|model| model.slug == "gpt-6-astra") + .expect("bundled Astra model"); + model.slug = model_id.to_string(); + model.additional_speed_tiers = vec!["fast".to_string()]; + model.service_tiers = vec![codex_protocol::openai_models::ModelServiceTier { + id: "ultrafast".to_string(), + name: "Express".to_string(), + description: "My custom Bedrock tier.".to_string(), + }]; + model.default_service_tier = Some("ultrafast".to_string()); + let catalog_path = home.path().join("models.json"); + std::fs::write( + &catalog_path, + serde_json::to_vec(&codex_protocol::openai_models::ModelsResponse { + models: vec![model], + })?, + )?; + let catalog_path = serde_json::to_string(&catalog_path)?; + std::fs::write( + home.path().join("config.toml"), + format!( + r#"model_provider = "{provider_id}" +model_catalog_json = {catalog_path} +[model_providers.{provider_id}.aws] +region = "us-west-2" +"# + ), + )?; + let mut app_server = TestAppServer::builder() + .with_codex_home(home.path()) + .build_initialized() + .await?; + let response: ModelListResponse = app_server + .request(|request_id| ClientRequest::ModelList { + request_id, + params: ModelListParams { + cursor: None, + limit: Some(100), + include_hidden: Some(true), + }, + }) + .await?; + + assert_eq!( + response + .data + .into_iter() + .map(|model| ( + model.model, + model.additional_speed_tiers, + model.service_tiers, + model.default_service_tier, + )) + .collect::>(), + vec![( + model_id.to_string(), + vec!["fast".to_string()], + vec![ModelServiceTier { + id: "ultrafast".to_string(), + name: "Express".to_string(), + description: "My custom Bedrock tier.".to_string(), + }], + Some("ultrafast".to_string()), + )] + ); + Ok(()) +} diff --git a/codex-rs/app-server/tests/suite/v2/mod.rs b/codex-rs/app-server/tests/suite/v2/mod.rs index f52cdac971..4eed47fe52 100644 --- a/codex-rs/app-server/tests/suite/v2/mod.rs +++ b/codex-rs/app-server/tests/suite/v2/mod.rs @@ -14,6 +14,8 @@ mod auth_storage_originator; mod auto_env; #[path = "bedrock_gov_cloud_tests.rs"] mod bedrock_gov_cloud; +#[path = "bedrock_service_tier_tests.rs"] +mod bedrock_service_tier; mod bedrock_setup; mod client_metadata; mod code_mode_host; diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 8a7fd4bc13..9a450c9d92 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -975,12 +975,17 @@ impl ModelClient { prompt.output_schema_strict, ); let prompt_cache_key = Some(self.prompt_cache_key(responses_metadata)); - let service_tier = if self.state.provider.info().is_amazon_bedrock() { - // Bedrock only supports the implicit default tier, including with custom catalogs. - None - } else { - model_info.service_tier_for_request(service_tier) - }; + let service_tier = model_info + .service_tier_for_request(service_tier) + .filter(|tier| { + // Bedrock requires an advertised tier, including for flex, which the + // generic OpenAI resolver permits without catalog support. + !self.state.provider.info().is_amazon_bedrock() + || model_info + .service_tiers + .iter() + .any(|supported| supported.id == *tier) + }); if !include_internal { for item in &mut input { item.clear_tool_result_metadata(); diff --git a/codex-rs/core/tests/suite/bedrock_service_tier_tests.rs b/codex-rs/core/tests/suite/bedrock_service_tier_tests.rs new file mode 100644 index 0000000000..0a04041286 --- /dev/null +++ b/codex-rs/core/tests/suite/bedrock_service_tier_tests.rs @@ -0,0 +1,138 @@ +//! Exercises Bedrock service-tier selection through provider catalogs and HTTP requests. + +use codex_login::CodexAuth; +use codex_login::auth::BedrockApiKeyAuth; +use codex_model_provider_info::AMAZON_BEDROCK_PROVIDER_ID; +use codex_model_provider_info::AMAZON_BEDROCK_RUNTIME_PROVIDER_ID; +use codex_model_provider_info::built_in_model_providers; +use codex_protocol::openai_models::ModelServiceTier; +use core_test_support::responses; +use core_test_support::skip_if_no_network; +use core_test_support::test_codex::test_codex; +use pretty_assertions::assert_eq; + +#[test_case::test_case(AMAZON_BEDROCK_PROVIDER_ID, "openai.gpt-6-astra"; "mantle")] +#[test_case::test_case(AMAZON_BEDROCK_RUNTIME_PROVIDER_ID, "global.openai.gpt-6-astra"; "runtime global")] +#[test_case::test_case(AMAZON_BEDROCK_RUNTIME_PROVIDER_ID, "us.openai.gpt-6-astra"; "runtime us")] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn bedrock_ultrafast_can_be_configured_and_toggled( + provider_id: &'static str, + model: &'static str, +) -> anyhow::Result<()> { + skip_if_no_network!(Ok(())); + let server = responses::start_mock_server().await; + let mock = responses::mount_sse_sequence( + &server, + ["configured", "standard", "ultrafast"] + .into_iter() + .map(|id| responses::sse(vec![responses::ev_completed(id)])) + .collect(), + ) + .await; + let test = test_codex() + .with_auth(CodexAuth::BedrockApiKey(BedrockApiKeyAuth { + api_key: "dummy".to_string(), + region: "us-west-2".to_string(), + })) + .with_model(model) + .with_config(move |config| { + let base_url = config.model_provider.base_url.clone(); + config.model_provider_id = provider_id.to_string(); + config.model_provider = built_in_model_providers(/*openai_base_url*/ None) + .remove(provider_id) + .expect("built-in Bedrock provider"); + config.model_provider.base_url = base_url; + config.service_tier = Some("ultrafast".to_string()); + }) + .build_with_auto_env(&server) + .await?; + assert!(test.config.model_catalog.is_none()); + + test.submit_turn("configured ultrafast").await?; + test.submit_turn_with_service_tier("standard", Some("default")) + .await?; + test.submit_turn_with_service_tier("ultrafast again", Some("ultrafast")) + .await?; + + assert_eq!( + mock.requests() + .iter() + .map(|request| request.body_json().get("service_tier").cloned()) + .collect::>(), + vec![ + Some(serde_json::json!("ultrafast")), + None, + Some(serde_json::json!("ultrafast")) + ] + ); + Ok(()) +} + +#[test_case::test_case(AMAZON_BEDROCK_PROVIDER_ID, "openai.gpt-6-astra", &["ultrafast"], Some("ultrafast"), Some("ultrafast"); "mantle astra")] +#[test_case::test_case(AMAZON_BEDROCK_RUNTIME_PROVIDER_ID, "global.openai.gpt-6-astra", &["ultrafast"], Some("ultrafast"), Some("ultrafast"); "runtime global astra")] +#[test_case::test_case(AMAZON_BEDROCK_RUNTIME_PROVIDER_ID, "us.openai.gpt-6-astra", &["ultrafast"], Some("ultrafast"), Some("ultrafast"); "runtime us astra")] +#[test_case::test_case(AMAZON_BEDROCK_PROVIDER_ID, "openai.gpt-6-astra", &["priority"], Some("priority"), Some("priority"); "mantle priority")] +#[test_case::test_case(AMAZON_BEDROCK_RUNTIME_PROVIDER_ID, "global.openai.gpt-6-astra", &["flex"], Some("flex"), Some("flex"); "runtime flex")] +#[test_case::test_case(AMAZON_BEDROCK_PROVIDER_ID, "openai.gpt-6-sol", &["custom-tier"], Some("custom-tier"), Some("custom-tier"); "mantle custom tier")] +#[test_case::test_case(AMAZON_BEDROCK_RUNTIME_PROVIDER_ID, "global.openai.gpt-6-sol", &["custom-tier"], Some("custom-tier"), Some("custom-tier"); "runtime custom tier")] +#[test_case::test_case(AMAZON_BEDROCK_PROVIDER_ID, "openai.gpt-6-astra", &[], Some("ultrafast"), None; "mantle empty tiers")] +#[test_case::test_case(AMAZON_BEDROCK_RUNTIME_PROVIDER_ID, "global.openai.gpt-6-astra", &[], Some("ultrafast"), None; "runtime empty tiers")] +#[test_case::test_case(AMAZON_BEDROCK_PROVIDER_ID, "openai.gpt-6-astra", &["ultrafast"], Some("priority"), None; "mantle unadvertised tier")] +#[test_case::test_case(AMAZON_BEDROCK_RUNTIME_PROVIDER_ID, "global.openai.gpt-6-astra", &["ultrafast"], Some("flex"), None; "runtime unadvertised tier")] +#[test_case::test_case(AMAZON_BEDROCK_PROVIDER_ID, "openai.gpt-6-astra", &["ultrafast"], None, None; "mantle core requires explicit selection")] +#[test_case::test_case(AMAZON_BEDROCK_RUNTIME_PROVIDER_ID, "global.openai.gpt-6-astra", &["ultrafast"], None, None; "runtime core requires explicit selection")] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn bedrock_custom_catalog_controls_service_tiers( + provider_id: &'static str, + model: &'static str, + advertised_tiers: &'static [&'static str], + tier: Option<&'static str>, + expected_tier: Option<&'static str>, +) -> anyhow::Result<()> { + skip_if_no_network!(Ok(())); + let server = responses::start_mock_server().await; + let mock = responses::mount_sse_once( + &server, + responses::sse(vec![responses::ev_completed("done")]), + ) + .await; + let test = test_codex() + .with_auth(CodexAuth::BedrockApiKey(BedrockApiKeyAuth { + api_key: "dummy".to_string(), + region: "us-west-2".to_string(), + })) + .with_model(model) + .with_model_info_override(model, move |info| { + info.service_tiers = advertised_tiers + .iter() + .map(|id| ModelServiceTier { + id: id.to_string(), + name: id.to_string(), + description: id.to_string(), + }) + .collect(); + info.default_service_tier = advertised_tiers.first().map(ToString::to_string); + }) + .with_config(move |config| { + let base_url = config.model_provider.base_url.clone(); + config.model_provider_id = provider_id.to_string(); + config.model_provider = built_in_model_providers(/*openai_base_url*/ None) + .remove(provider_id) + .expect("built-in Bedrock provider"); + config.model_provider.base_url = base_url; + config.service_tier = tier.map(str::to_string); + }) + .build_with_auto_env(&server) + .await?; + + test.submit_turn("hello").await?; + + assert_eq!( + mock.single_request() + .body_json() + .get("service_tier") + .cloned(), + expected_tier.map(serde_json::Value::from) + ); + Ok(()) +} diff --git a/codex-rs/core/tests/suite/mod.rs b/codex-rs/core/tests/suite/mod.rs index d0f0f0c8a8..8142ec5c11 100644 --- a/codex-rs/core/tests/suite/mod.rs +++ b/codex-rs/core/tests/suite/mod.rs @@ -57,6 +57,8 @@ mod auth_recovery_policy; mod auto_review; #[path = "bedrock_multi_agent_tests.rs"] mod bedrock_multi_agent; +#[path = "bedrock_service_tier_tests.rs"] +mod bedrock_service_tier; mod catalog_permission_messages; mod cli_stream; mod client; diff --git a/codex-rs/model-provider/src/amazon_bedrock/catalog.rs b/codex-rs/model-provider/src/amazon_bedrock/catalog.rs index 1de3b24436..2bfb33aa7e 100644 --- a/codex-rs/model-provider/src/amazon_bedrock/catalog.rs +++ b/codex-rs/model-provider/src/amazon_bedrock/catalog.rs @@ -8,10 +8,13 @@ use codex_model_provider_info::AMAZON_BEDROCK_GPT_6_LUNA_MODEL_ID; use codex_model_provider_info::AMAZON_BEDROCK_GPT_6_SOL_MODEL_ID; use codex_models_manager::bundled_models_response; use codex_protocol::openai_models::ModelInfo; +use codex_protocol::openai_models::ModelServiceTier; use codex_protocol::openai_models::ModelVisibility; use codex_protocol::openai_models::ModelsResponse; use codex_protocol::openai_models::WebSearchToolType; +use super::BedrockEndpoint; + const GPT_5_BEDROCK_CONTEXT_WINDOW: i64 = 272_000; const GPT_5_6_SOL_OPENAI_MODEL_ID: &str = "gpt-5.6-sol"; const GPT_5_6_TERRA_OPENAI_MODEL_ID: &str = "gpt-5.6-terra"; @@ -23,7 +26,7 @@ const GPT_6_ASTRA_OPENAI_MODEL_ID: &str = "gpt-6-astra"; const GPT_5_5_OPENAI_MODEL_ID: &str = "gpt-5.5"; pub(crate) fn static_model_catalog() -> ModelsResponse { - normalize_bedrock_catalog(ModelsResponse { + let catalog = ModelsResponse { models: vec![ bedrock_model( bundled_openai_model(GPT_6_1_SOL_OPENAI_MODEL_ID), @@ -74,7 +77,8 @@ pub(crate) fn static_model_catalog() -> ModelsResponse { /*priority*/ 7, ), ], - }) + }; + normalize_bundled_bedrock_catalog(catalog, BedrockEndpoint::Mantle) } pub(super) fn static_gov_model_catalog() -> ModelsResponse { @@ -88,12 +92,37 @@ pub(super) fn static_gov_model_catalog() -> ModelsResponse { catalog } -pub(crate) fn normalize_bedrock_catalog(mut catalog: ModelsResponse) -> ModelsResponse { +pub(super) fn normalize_bundled_bedrock_catalog( + mut catalog: ModelsResponse, + endpoint: BedrockEndpoint, +) -> ModelsResponse { for model in &mut catalog.models { - // Amazon Bedrock currently only supports the implicit "default" tier for GPT models. + // Bundled defaults must not inherit OpenAI-only tiers or opt into a premium tier. model.additional_speed_tiers.clear(); model.service_tiers.clear(); + if matches!( + (endpoint, model.slug.as_str()), + (BedrockEndpoint::Mantle, AMAZON_BEDROCK_GPT_6_ASTRA_MODEL_ID) + | ( + BedrockEndpoint::Runtime, + "global.openai.gpt-6-astra" | "us.openai.gpt-6-astra" + ) + ) { + model.service_tiers.push(ModelServiceTier { + id: "ultrafast".to_string(), + name: "Ultrafast".to_string(), + description: "The fastest available responses for latency-sensitive work." + .to_string(), + }); + } model.default_service_tier = None; + } + normalize_bedrock_catalog(catalog) +} + +pub(super) fn normalize_bedrock_catalog(mut catalog: ModelsResponse) -> ModelsResponse { + // Custom catalogs own their tier definitions; only normalize wire compatibility here. + for model in &mut catalog.models { // Bedrock rejects the `search_content_types` field used by multimodal search. model.web_search_tool_type = WebSearchToolType::Text; } @@ -260,9 +289,6 @@ mod tests { .collect::>(); let mut expected = models.clone(); for model in &mut expected { - model.additional_speed_tiers.clear(); - model.service_tiers.clear(); - model.default_service_tier = None; model.web_search_tool_type = WebSearchToolType::Text; } @@ -347,7 +373,16 @@ mod tests { expected.use_responses_lite = false; expected.tool_mode = None; expected.additional_speed_tiers.clear(); - expected.service_tiers.clear(); + expected.service_tiers = if slug == AMAZON_BEDROCK_GPT_6_ASTRA_MODEL_ID { + vec![ModelServiceTier { + id: "ultrafast".to_string(), + name: "Ultrafast".to_string(), + description: "The fastest available responses for latency-sensitive work." + .to_string(), + }] + } else { + Vec::new() + }; expected.default_service_tier = None; expected.web_search_tool_type = WebSearchToolType::Text; @@ -359,12 +394,11 @@ mod tests { } #[test] - fn gpt_5_bedrock_models_only_allow_default_service_tier() { + fn bedrock_models_do_not_enable_priority_or_explicit_default_tiers() { let catalog = static_model_catalog(); for model in catalog.models { assert_eq!(model.additional_speed_tiers, Vec::::new()); - assert_eq!(model.service_tiers, Vec::new()); assert_eq!(model.default_service_tier, None); assert_eq!( model.service_tier_for_request(Some("priority".to_string())), diff --git a/codex-rs/model-provider/src/amazon_bedrock/runtime_catalog.rs b/codex-rs/model-provider/src/amazon_bedrock/runtime_catalog.rs index 3f6eab5a89..399c7d8ed9 100644 --- a/codex-rs/model-provider/src/amazon_bedrock/runtime_catalog.rs +++ b/codex-rs/model-provider/src/amazon_bedrock/runtime_catalog.rs @@ -7,6 +7,8 @@ use codex_model_provider_info::AMAZON_BEDROCK_GPT_6_LUNA_MODEL_ID; use codex_model_provider_info::AMAZON_BEDROCK_GPT_6_SOL_MODEL_ID; use codex_protocol::openai_models::ModelsResponse; +use super::BedrockEndpoint; +use super::catalog::normalize_bundled_bedrock_catalog; use super::catalog::static_model_catalog; const ROUTING_VARIANTS: [(&str, &str, i32); 2] = @@ -40,7 +42,7 @@ pub(super) fn static_runtime_model_catalog() -> ModelsResponse { models.push(variant); } } - ModelsResponse { models } + normalize_bundled_bedrock_catalog(ModelsResponse { models }, BedrockEndpoint::Runtime) } #[cfg(test)] diff --git a/codex-rs/model-provider/src/provider.rs b/codex-rs/model-provider/src/provider.rs index 8d93a39de1..f8f0790dde 100644 --- a/codex-rs/model-provider/src/provider.rs +++ b/codex-rs/model-provider/src/provider.rs @@ -1298,43 +1298,48 @@ printf '%s\n' '{"AccessKeyId":"exported","SecretAccessKey":"secret"}' } #[tokio::test] - async fn configured_bedrock_catalog_only_allows_default_service_tier() { - let configured_model = codex_models_manager::bundled_models_response() + async fn configured_bedrock_catalog_preserves_service_tiers() { + let mut configured_model = codex_models_manager::bundled_models_response() .expect("bundled models should parse") .models .into_iter() .find(|model| model.slug == "gpt-5.5") .expect("bundled models should include GPT-5.5"); - assert!(!configured_model.additional_speed_tiers.is_empty()); - assert!(!configured_model.service_tiers.is_empty()); + configured_model.service_tiers = vec![codex_protocol::openai_models::ModelServiceTier { + id: "custom-tier".to_string(), + name: "Custom tier".to_string(), + description: "User-defined tier description.".to_string(), + }]; + configured_model.default_service_tier = Some("custom-tier".to_string()); + let configured_catalog = ModelsResponse { + models: vec![configured_model], + }; + let mut expected = configured_catalog.clone(); + expected.models[0].web_search_tool_type = + codex_protocol::openai_models::WebSearchToolType::Text; - let mut provider_info = - ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None); - provider_info.base_url = + let mut gov_provider = ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None); + gov_provider.base_url = Some("https://bedrock-mantle.us-gov-west-1.api.aws/openai/v1".to_string()); - let provider = create_model_provider(provider_info, /*auth_manager*/ None); - let manager = provider.models_manager( - test_codex_home(), - Some(ModelsResponse { - models: vec![configured_model], - }), - ); - - let catalog = manager - .raw_model_catalog( - RefreshStrategy::Online, - HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault), - ) - .await; - - assert_eq!(catalog.models.len(), 1); - assert_eq!(catalog.models[0].slug, "gpt-5.5"); - assert_eq!( - catalog.models[0].additional_speed_tiers, - Vec::::new() - ); - assert_eq!(catalog.models[0].service_tiers, Vec::new()); - assert_eq!(catalog.models[0].default_service_tier, None); + for provider_info in [ + ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None), + ModelProviderInfo::create_amazon_bedrock_runtime_provider(/*aws*/ None), + gov_provider, + ] { + let provider = create_model_provider(provider_info, /*auth_manager*/ None); + for manager in [ + provider.models_manager(test_codex_home(), Some(configured_catalog.clone())), + provider.models_manager_without_cache(Some(configured_catalog.clone())), + ] { + let catalog = manager + .raw_model_catalog( + RefreshStrategy::Online, + HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault), + ) + .await; + assert_eq!(catalog, expected); + } + } } #[tokio::test] diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__bedrock_mantle_ultrafast.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__bedrock_mantle_ultrafast.snap new file mode 100644 index 0000000000..e0809c1682 --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__bedrock_mantle_ultrafast.snap @@ -0,0 +1,17 @@ +--- +source: tui/src/chatwidget/tests/bedrock_catalog_tests.rs +expression: "format!(\"Bundled catalog:\\n{bundled_view}\\n\\nCustom catalog:\\n{custom_view}\")" +--- +Bundled catalog: +› /ultrafast The fastest available responses for latency-sensitive work. + +› /ultrafast + + GPT-6-Astra default · /tmp/project + +Custom catalog: +› /express My custom Bedrock tier. + +› /express + + GPT-6-Astra default · /tmp/project diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__bedrock_runtime_global_ultrafast.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__bedrock_runtime_global_ultrafast.snap new file mode 100644 index 0000000000..0a2120ba85 --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__bedrock_runtime_global_ultrafast.snap @@ -0,0 +1,17 @@ +--- +source: tui/src/chatwidget/tests/bedrock_catalog_tests.rs +expression: "format!(\"Bundled catalog:\\n{bundled_view}\\n\\nCustom catalog:\\n{custom_view}\")" +--- +Bundled catalog: +› /ultrafast The fastest available responses for latency-sensitive work. + +› /ultrafast + + GPT-6-Astra (Global) default · /tmp/project + +Custom catalog: +› /express My custom Bedrock tier. + +› /express + + GPT-6-Astra default · /tmp/project diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__bedrock_runtime_us_ultrafast.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__bedrock_runtime_us_ultrafast.snap new file mode 100644 index 0000000000..f08ee83308 --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__bedrock_runtime_us_ultrafast.snap @@ -0,0 +1,17 @@ +--- +source: tui/src/chatwidget/tests/bedrock_catalog_tests.rs +expression: "format!(\"Bundled catalog:\\n{bundled_view}\\n\\nCustom catalog:\\n{custom_view}\")" +--- +Bundled catalog: +› /ultrafast The fastest available responses for latency-sensitive work. + +› /ultrafast + + GPT-6-Astra (US cross-region) default · /tmp/project + +Custom catalog: +› /express My custom Bedrock tier. + +› /express + + GPT-6-Astra default · /tmp/project diff --git a/codex-rs/tui/src/chatwidget/tests/bedrock_catalog_tests.rs b/codex-rs/tui/src/chatwidget/tests/bedrock_catalog_tests.rs index 5f0cb91520..b85e27b065 100644 --- a/codex-rs/tui/src/chatwidget/tests/bedrock_catalog_tests.rs +++ b/codex-rs/tui/src/chatwidget/tests/bedrock_catalog_tests.rs @@ -1,4 +1,4 @@ -//! Renders Bedrock provider catalogs and Astra reasoning choices. +//! Renders Bedrock catalogs and exercises Astra reasoning and service-tier choices. use super::*; use codex_http_client::HttpClientFactory; @@ -115,3 +115,95 @@ async fn bedrock_govcloud_model_pickers() { ); } } + +#[tokio::test] +async fn bedrock_ultrafast_slash_command_selects_and_clears_tier() { + for (name, provider_info, model, standard_model) in [ + ( + "mantle", + ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None), + "openai.gpt-6-astra", + "openai.gpt-6-sol", + ), + ( + "runtime_global", + ModelProviderInfo::create_amazon_bedrock_runtime_provider(/*aws*/ None), + "global.openai.gpt-6-astra", + "global.openai.gpt-6-sol", + ), + ( + "runtime_us", + ModelProviderInfo::create_amazon_bedrock_runtime_provider(/*aws*/ None), + "us.openai.gpt-6-astra", + "us.openai.gpt-6-sol", + ), + ] { + let presets = create_model_provider(provider_info.clone(), /*auth_manager*/ None) + .models_manager_without_cache(/*config_model_catalog*/ None) + .list_models( + RefreshStrategy::Offline, + HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault), + ) + .await; + let (mut chat, mut events, _ops) = make_chatwidget_manual(Some(model)).await; + chat.model_catalog = Arc::new(ModelCatalog::new(presets)); + chat.set_feature_enabled(Feature::FastMode, /*enabled*/ true); + chat.set_service_tier(/*service_tier*/ None); + chat.bottom_pane + .set_composer_text("/ultrafast".to_string(), Vec::new(), Vec::new()); + let bundled_view = normalize_snapshot_paths(render_bottom_popup(&chat, /*width*/ 100)); + chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); + assert_eq!(chat.current_service_tier(), Some("ultrafast")); + assert!(std::iter::from_fn(|| events.try_recv().ok()).any(|event| { + matches!(event, AppEvent::PersistServiceTierSelection { service_tier } + if service_tier.as_deref() == Some("ultrafast")) + })); + + chat.set_model(standard_model); + assert_eq!(chat.current_service_tier(), None); + assert!(chat.current_model_service_tier_commands().is_empty()); + chat.set_model(model); + assert_eq!(chat.current_service_tier(), Some("ultrafast")); + chat.bottom_pane + .set_composer_text("/ultrafast".to_string(), Vec::new(), Vec::new()); + chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); + assert_eq!(chat.current_service_tier(), Some("default")); + + let mut custom_model = codex_models_manager::bundled_models_response() + .expect("bundled catalog") + .models + .into_iter() + .find(|model| model.slug == "gpt-6-astra") + .expect("bundled Astra model"); + custom_model.slug = model.to_string(); + custom_model.service_tiers = vec![codex_protocol::openai_models::ModelServiceTier { + id: "ultrafast".to_string(), + name: "Express".to_string(), + description: "My custom Bedrock tier.".to_string(), + }]; + custom_model.default_service_tier = Some("ultrafast".to_string()); + let presets = create_model_provider(provider_info, /*auth_manager*/ None) + .models_manager_without_cache(Some(codex_protocol::openai_models::ModelsResponse { + models: vec![custom_model], + })) + .list_models( + RefreshStrategy::Offline, + HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault), + ) + .await; + chat.model_catalog = Arc::new(ModelCatalog::new(presets)); + chat.set_service_tier(/*service_tier*/ None); + assert_eq!(chat.current_service_tier(), Some("ultrafast")); + chat.set_service_tier(Some("default".to_string())); + assert_eq!(chat.current_service_tier(), Some("default")); + chat.bottom_pane + .set_composer_text("/express".to_string(), Vec::new(), Vec::new()); + let custom_view = normalize_snapshot_paths(render_bottom_popup(&chat, /*width*/ 100)); + assert_chatwidget_snapshot!( + format!("bedrock_{name}_ultrafast"), + format!("Bundled catalog:\n{bundled_view}\n\nCustom catalog:\n{custom_view}") + ); + chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); + assert_eq!(chat.current_service_tier(), Some("ultrafast")); + } +}