diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs index 6406f9e1ff..139cd94966 100644 --- a/codex-rs/config/src/config_toml.rs +++ b/codex-rs/config/src/config_toml.rs @@ -304,7 +304,13 @@ pub struct ConfigToml { /// Optionally specify a personality for the model pub personality: Option, - /// Optional explicit service tier preference for new turns (`fast` or `flex`). + /// Optional explicit service tier id preference for new turns. + /// + /// Takes precedence over deprecated `service_tier`. + pub service_tier_id: Option, + + /// Deprecated legacy service tier preference for new turns (`fast` or `flex`). + #[deprecated(note = "use service_tier_id instead")] pub service_tier: Option, /// Base URL for requests to ChatGPT (as opposed to the OpenAI API). diff --git a/codex-rs/config/src/profile_toml.rs b/codex-rs/config/src/profile_toml.rs index f6f63191b5..0ce157b551 100644 --- a/codex-rs/config/src/profile_toml.rs +++ b/codex-rs/config/src/profile_toml.rs @@ -23,7 +23,12 @@ use codex_protocol::protocol::AskForApproval; #[schemars(deny_unknown_fields)] pub struct ConfigProfile { pub model: Option, - /// Optional explicit service tier preference for new turns (`fast` or `flex`). + /// Optional explicit service tier id preference for new turns. + /// + /// Takes precedence over deprecated `service_tier`. + pub service_tier_id: Option, + /// Deprecated legacy service tier preference for new turns (`fast` or `flex`). + #[deprecated(note = "use service_tier_id instead")] pub service_tier: Option, /// The key in the `model_providers` map identifying the /// [`ModelProviderInfo`] to use. diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index fa29de17fc..39683dedaf 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -663,7 +663,16 @@ "$ref": "#/definitions/ServiceTier" } ], - "description": "Optional explicit service tier preference for new turns (`fast` or `flex`)." + "deprecated": true, + "description": "Deprecated legacy service tier preference for new turns (`fast` or `flex`)." + }, + "service_tier_id": { + "allOf": [ + { + "$ref": "#/definitions/ServiceTier" + } + ], + "description": "Optional explicit service tier id preference for new turns.\n\nTakes precedence over deprecated `service_tier`." }, "tools": { "$ref": "#/definitions/ToolsToml" @@ -4391,7 +4400,16 @@ "$ref": "#/definitions/ServiceTier" } ], - "description": "Optional explicit service tier preference for new turns (`fast` or `flex`)." + "deprecated": true, + "description": "Deprecated legacy service tier preference for new turns (`fast` or `flex`)." + }, + "service_tier_id": { + "allOf": [ + { + "$ref": "#/definitions/ServiceTier" + } + ], + "description": "Optional explicit service tier id preference for new turns.\n\nTakes precedence over deprecated `service_tier`." }, "shell_environment_policy": { "allOf": [ @@ -4494,4 +4512,4 @@ }, "title": "ConfigToml", "type": "object" -} +} \ No newline at end of file diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 1352de991e..7f75352654 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -60,6 +60,9 @@ use codex_model_provider_info::LMSTUDIO_OSS_PROVIDER_ID; use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID; use codex_model_provider_info::WireApi; use codex_models_manager::bundled_models_response; +use codex_protocol::config_types::SERVICE_TIER_FAST_LEGACY; +use codex_protocol::config_types::SERVICE_TIER_PRIORITY; +use codex_protocol::config_types::ServiceTier; use codex_protocol::models::ActivePermissionProfile; use codex_protocol::models::ActivePermissionProfileModification; use codex_protocol::models::ManagedFileSystemPermissions; @@ -6506,6 +6509,56 @@ async fn explicit_null_service_tier_override_sets_fast_default_opt_out() -> std: Ok(()) } +#[tokio::test] +async fn service_tier_id_takes_precedence_over_deprecated_service_tier() -> std::io::Result<()> { + let fixture = create_test_fixture()?; + let mut cfg = fixture.cfg.clone(); + cfg.service_tier_id = Some(ServiceTier::from("premium")); + #[allow(deprecated)] + { + cfg.service_tier = Some(ServiceTier::from(SERVICE_TIER_FAST_LEGACY)); + } + + let config = Config::load_from_base_config_with_overrides( + cfg, + ConfigOverrides { + cwd: Some(fixture.cwd_path()), + ..Default::default() + }, + fixture.codex_home(), + ) + .await?; + + assert_eq!(config.service_tier, Some(ServiceTier::from("premium"))); + Ok(()) +} + +#[tokio::test] +async fn deprecated_fast_service_tier_maps_to_priority_id() -> std::io::Result<()> { + let fixture = create_test_fixture()?; + let mut cfg = fixture.cfg.clone(); + #[allow(deprecated)] + { + cfg.service_tier = Some(ServiceTier::from(SERVICE_TIER_FAST_LEGACY)); + } + + let config = Config::load_from_base_config_with_overrides( + cfg, + ConfigOverrides { + cwd: Some(fixture.cwd_path()), + ..Default::default() + }, + fixture.codex_home(), + ) + .await?; + + assert_eq!( + config.service_tier, + Some(ServiceTier::from(SERVICE_TIER_PRIORITY)) + ); + Ok(()) +} + #[tokio::test] async fn fast_default_opt_out_notice_config_is_respected() -> std::io::Result<()> { let fixture = create_test_fixture()?; diff --git a/codex-rs/core/src/config/edit.rs b/codex-rs/core/src/config/edit.rs index a047592813..05b58f077d 100644 --- a/codex-rs/core/src/config/edit.rs +++ b/codex-rs/core/src/config/edit.rs @@ -6,6 +6,9 @@ use codex_config::types::McpServerConfig; use codex_config::types::ToolSuggestDisabledTool; use codex_features::FEATURES; use codex_protocol::config_types::Personality; +use codex_protocol::config_types::SERVICE_TIER_FAST_LEGACY; +use codex_protocol::config_types::SERVICE_TIER_FLEX; +use codex_protocol::config_types::SERVICE_TIER_PRIORITY; use codex_protocol::config_types::ServiceTier; use codex_protocol::config_types::TrustLevel; use codex_protocol::openai_models::ReasoningEffort; @@ -525,12 +528,43 @@ impl ConfigDocument { ); mutated }), - ConfigEdit::SetServiceTier { service_tier } => Ok(self.write_profile_value( - &["service_tier"], - service_tier - .as_ref() - .map(|service_tier| value(service_tier.to_string())), - )), + ConfigEdit::SetServiceTier { service_tier } => { + let mut mutated = false; + match service_tier.as_ref().map(AsRef::as_ref) { + Some(service_tier_id) if service_tier_id == SERVICE_TIER_PRIORITY => { + mutated |= self.write_profile_value( + &["service_tier_id"], + Some(value(SERVICE_TIER_PRIORITY)), + ); + mutated |= self.write_profile_value( + &["service_tier"], + Some(value(SERVICE_TIER_FAST_LEGACY)), + ); + } + Some(service_tier_id) if service_tier_id == SERVICE_TIER_FLEX => { + mutated |= self.write_profile_value( + &["service_tier_id"], + Some(value(SERVICE_TIER_FLEX)), + ); + mutated |= self + .write_profile_value(&["service_tier"], Some(value(SERVICE_TIER_FLEX))); + } + Some(_) => { + mutated |= self.write_profile_value( + &["service_tier_id"], + service_tier + .as_ref() + .map(|service_tier| value(service_tier.to_string())), + ); + mutated |= self.write_profile_value(&["service_tier"], None); + } + None => { + mutated |= self.write_profile_value(&["service_tier_id"], None); + mutated |= self.write_profile_value(&["service_tier"], None); + } + } + Ok(mutated) + } ConfigEdit::SetModelPersonality { personality } => Ok(self.write_profile_value( &["personality"], personality.map(|personality| value(personality.to_string())), diff --git a/codex-rs/core/src/config/edit_tests.rs b/codex-rs/core/src/config/edit_tests.rs index 376632a93a..e1d65ede47 100644 --- a/codex-rs/core/src/config/edit_tests.rs +++ b/codex-rs/core/src/config/edit_tests.rs @@ -2,6 +2,9 @@ use super::*; use codex_config::types::AppToolApproval; use codex_config::types::McpServerToolConfig; use codex_config::types::McpServerTransportConfig; +use codex_protocol::config_types::SERVICE_TIER_FLEX; +use codex_protocol::config_types::SERVICE_TIER_PRIORITY; +use codex_protocol::config_types::ServiceTier; use codex_protocol::openai_models::ReasoningEffort; use pretty_assertions::assert_eq; #[cfg(unix)] @@ -31,6 +34,89 @@ model_reasoning_effort = "high" assert_eq!(contents, expected); } +#[test] +fn set_service_tier_writes_legacy_fast_for_priority_id() { + let tmp = tempdir().expect("tmpdir"); + let codex_home = tmp.path(); + std::fs::write( + codex_home.join(CONFIG_TOML_FILE), + r#"service_tier_id = "premium" +"#, + ) + .expect("seed config"); + + ConfigEditsBuilder::new(codex_home) + .set_service_tier(Some(SERVICE_TIER_PRIORITY.into())) + .apply_blocking() + .expect("persist"); + + let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); + let expected = r#"service_tier_id = "priority" +service_tier = "fast" +"#; + assert_eq!(contents, expected); +} + +#[test] +fn set_service_tier_writes_both_keys_for_flex_id() { + let tmp = tempdir().expect("tmpdir"); + let codex_home = tmp.path(); + + ConfigEditsBuilder::new(codex_home) + .set_service_tier(Some(SERVICE_TIER_FLEX.into())) + .apply_blocking() + .expect("persist"); + + let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); + let expected = r#"service_tier_id = "flex" +service_tier = "flex" +"#; + assert_eq!(contents, expected); +} + +#[test] +fn set_service_tier_writes_service_tier_id_for_arbitrary_id() { + let tmp = tempdir().expect("tmpdir"); + let codex_home = tmp.path(); + std::fs::write( + codex_home.join(CONFIG_TOML_FILE), + r#"service_tier = "fast" +"#, + ) + .expect("seed config"); + + ConfigEditsBuilder::new(codex_home) + .set_service_tier(Some(ServiceTier::from("premium"))) + .apply_blocking() + .expect("persist"); + + let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); + let expected = r#"service_tier_id = "premium" +"#; + assert_eq!(contents, expected); +} + +#[test] +fn clear_service_tier_removes_legacy_and_id_keys() { + let tmp = tempdir().expect("tmpdir"); + let codex_home = tmp.path(); + std::fs::write( + codex_home.join(CONFIG_TOML_FILE), + r#"service_tier_id = "premium" +service_tier = "flex" +"#, + ) + .expect("seed config"); + + ConfigEditsBuilder::new(codex_home) + .set_service_tier(None) + .apply_blocking() + .expect("persist"); + + let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); + assert_eq!(contents, ""); +} + #[test] fn builder_with_edits_applies_custom_paths() { let tmp = tempdir().expect("tmpdir"); diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 7c0e163e8a..de107627fd 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -81,6 +81,7 @@ use codex_protocol::config_types::AltScreenMode; use codex_protocol::config_types::ForcedLoginMethod; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::config_types::SERVICE_TIER_FAST_LEGACY; use codex_protocol::config_types::SERVICE_TIER_PRIORITY; use codex_protocol::config_types::SandboxMode; use codex_protocol::config_types::ServiceTier; @@ -1991,6 +1992,14 @@ pub(crate) fn resolve_web_search_mode_for_turn( WebSearchMode::Disabled } +fn normalize_deprecated_service_tier(service_tier: ServiceTier) -> ServiceTier { + if service_tier.as_ref() == SERVICE_TIER_FAST_LEGACY { + SERVICE_TIER_PRIORITY.into() + } else { + service_tier + } +} + impl Config { #[cfg(test)] async fn load_from_base_config_with_overrides( @@ -2649,6 +2658,14 @@ impl Config { let model = model.or(config_profile.model).or(cfg.model); let mut notices = cfg.notice.unwrap_or_default(); + #[allow(deprecated)] + let deprecated_profile_service_tier = config_profile + .service_tier + .clone() + .map(normalize_deprecated_service_tier); + #[allow(deprecated)] + let deprecated_config_service_tier = + cfg.service_tier.clone().map(normalize_deprecated_service_tier); let service_tier = match service_tier_override { Some(Some(service_tier)) => Some(service_tier), Some(None) => { @@ -2657,7 +2674,11 @@ impl Config { notices.fast_default_opt_out = Some(true); None } - None => config_profile.service_tier.or(cfg.service_tier), + None => config_profile + .service_tier_id + .or(cfg.service_tier_id) + .or(deprecated_profile_service_tier) + .or(deprecated_config_service_tier), }; let service_tier = match service_tier { Some(service_tier) diff --git a/codex-rs/protocol/src/config_types.rs b/codex-rs/protocol/src/config_types.rs index cc2c55129b..94c2f2e755 100644 --- a/codex-rs/protocol/src/config_types.rs +++ b/codex-rs/protocol/src/config_types.rs @@ -349,6 +349,9 @@ impl From for WebSearchConfig { pub const SERVICE_TIER_PRIORITY: &str = "priority"; pub const SERVICE_TIER_FLEX: &str = "flex"; +/// Legacy config value accepted by older clients. New clients map this to +/// `priority` when reading deprecated `service_tier`. +pub const SERVICE_TIER_FAST_LEGACY: &str = "fast"; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, TS)] #[serde(transparent)] diff --git a/codex-rs/protocol/src/openai_models.rs b/codex-rs/protocol/src/openai_models.rs index c4167d8416..641d7a2048 100644 --- a/codex-rs/protocol/src/openai_models.rs +++ b/codex-rs/protocol/src/openai_models.rs @@ -17,6 +17,7 @@ use ts_rs::TS; use crate::config_types::Personality; use crate::config_types::ReasoningSummary; +#[cfg(test)] use crate::config_types::SERVICE_TIER_PRIORITY; use crate::config_types::ServiceTier; use crate::config_types::Verbosity;