diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 963ac69000..ea019d3b61 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -26,7 +26,7 @@ use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::SandboxMode as CoreSandboxMode; -use codex_protocol::config_types::ServiceTier; +use codex_protocol::config_types::ServiceTier as CoreServiceTier; use codex_protocol::config_types::Verbosity; use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WebSearchToolConfig; @@ -121,6 +121,47 @@ use serde_with::serde_as; use thiserror::Error; use ts_rs::TS; +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "lowercase")] +#[ts(export)] +pub enum ServiceTier { + Fast, + Flex, +} + +impl ServiceTier { + pub fn fast() -> Self { + Self::Fast + } + + pub fn flex() -> Self { + Self::Flex + } + + pub fn to_core(self) -> CoreServiceTier { + match self { + Self::Fast => CoreServiceTier::priority(), + Self::Flex => CoreServiceTier::flex(), + } + } + + pub fn from_core(service_tier: &CoreServiceTier) -> Option { + if service_tier.is_priority() { + Some(Self::Fast) + } else if service_tier.is_flex() { + Some(Self::Flex) + } else { + None + } + } +} + +impl From for CoreServiceTier { + fn from(value: ServiceTier) -> Self { + value.to_core() + } +} + // Macro to declare a camelCased API v2 enum mirroring a core enum which // tends to use either snake_case or kebab-case. macro_rules! v2_enum_from_core { diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index edc142c840..72dbf019d2 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -143,6 +143,7 @@ use codex_app_server_protocol::SendAddCreditsNudgeEmailParams; use codex_app_server_protocol::SendAddCreditsNudgeEmailResponse; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ServerRequestResolvedNotification; +use codex_app_server_protocol::ServiceTier; use codex_app_server_protocol::SkillSummary; use codex_app_server_protocol::SkillsConfigWriteParams; use codex_app_server_protocol::SkillsConfigWriteResponse; @@ -2538,7 +2539,7 @@ impl CodexMessageProcessor { let mut typesafe_overrides = self.build_thread_config_overrides( model, model_provider, - service_tier, + Self::resolve_service_tier_override(service_tier), cwd, approval_policy, approvals_reviewer, @@ -2919,7 +2920,7 @@ impl CodexMessageProcessor { thread: thread.clone(), model: config_snapshot.model, model_provider: config_snapshot.model_provider_id, - service_tier: config_snapshot.service_tier, + service_tier: config_snapshot.service_tier.as_ref().and_then(ServiceTier::from_core), cwd: config_snapshot.cwd, instruction_sources, approval_policy: config_snapshot.approval_policy.into(), @@ -3019,6 +3020,12 @@ impl CodexMessageProcessor { Ok(environment_selections) } + fn resolve_service_tier_override( + service_tier: Option>, + ) -> Option> { + service_tier.map(|service_tier| service_tier.map(ServiceTier::to_core)) + } + async fn thread_archive(&self, request_id: ConnectionRequestId, params: ThreadArchiveParams) { let _thread_list_state_permit = match self.acquire_thread_list_state_permit().await { Ok(permit) => permit, @@ -4381,7 +4388,7 @@ impl CodexMessageProcessor { let mut typesafe_overrides = self.build_thread_config_overrides( model, model_provider, - service_tier, + Self::resolve_service_tier_override(service_tier), cwd, approval_policy, approvals_reviewer, @@ -4499,7 +4506,10 @@ impl CodexMessageProcessor { thread, model: session_configured.model, model_provider: session_configured.model_provider_id, - service_tier: session_configured.service_tier, + service_tier: session_configured + .service_tier + .as_ref() + .and_then(ServiceTier::from_core), cwd: session_configured.cwd, instruction_sources, approval_policy: session_configured.approval_policy.into(), @@ -5013,7 +5023,7 @@ impl CodexMessageProcessor { let mut typesafe_overrides = self.build_thread_config_overrides( model, model_provider, - service_tier, + Self::resolve_service_tier_override(service_tier), cwd, approval_policy, approvals_reviewer, @@ -5134,7 +5144,10 @@ impl CodexMessageProcessor { thread: thread.clone(), model: session_configured.model, model_provider: session_configured.model_provider_id, - service_tier: session_configured.service_tier, + service_tier: session_configured + .service_tier + .as_ref() + .and_then(ServiceTier::from_core), cwd: session_configured.cwd, instruction_sources, approval_policy: session_configured.approval_policy.into(), @@ -6769,7 +6782,7 @@ impl CodexMessageProcessor { let model = params.model; let effort = params.effort.map(Some); let summary = params.summary; - let service_tier = params.service_tier; + let service_tier = Self::resolve_service_tier_override(params.service_tier); let personality = params.personality; // If any overrides are provided, validate them synchronously so the @@ -8492,7 +8505,7 @@ async fn handle_pending_thread_resume_request( thread, model, model_provider: model_provider_id, - service_tier, + service_tier: service_tier.as_ref().and_then(ServiceTier::from_core), cwd, instruction_sources, approval_policy: approval_policy.into(), @@ -8662,11 +8675,15 @@ fn collect_resume_override_mismatches( )); } if let Some(requested_service_tier) = request.service_tier.as_ref() - && requested_service_tier != &config_snapshot.service_tier + && requested_service_tier.map(ServiceTier::to_core).as_ref() + != config_snapshot.service_tier.as_ref() { + let active_service_tier = config_snapshot + .service_tier + .as_ref() + .and_then(ServiceTier::from_core); mismatch_details.push(format!( - "service_tier requested={requested_service_tier:?} active={:?}", - config_snapshot.service_tier + "service_tier requested={requested_service_tier:?} active={active_service_tier:?}" )); } if let Some(requested_cwd) = request.cwd.as_deref() { @@ -10048,6 +10065,7 @@ mod tests { use codex_model_provider_info::ModelProviderInfo; use codex_model_provider_info::WireApi; use codex_protocol::ThreadId; + use codex_protocol::config_types::ServiceTier as CoreServiceTier; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; @@ -10528,7 +10546,7 @@ mod tests { path: None, model: None, model_provider: None, - service_tier: Some(Some(codex_protocol::config_types::ServiceTier::Fast)), + service_tier: Some(Some(ServiceTier::fast())), cwd: None, approval_policy: None, approvals_reviewer: None, @@ -10544,7 +10562,7 @@ mod tests { let config_snapshot = ThreadConfigSnapshot { model: "gpt-5".to_string(), model_provider_id: "openai".to_string(), - service_tier: Some(codex_protocol::config_types::ServiceTier::Flex), + service_tier: Some(CoreServiceTier::flex()), approval_policy: codex_protocol::protocol::AskForApproval::OnRequest, approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User, permission_profile: codex_protocol::models::PermissionProfile::Disabled, diff --git a/codex-rs/app-server/src/models.rs b/codex-rs/app-server/src/models.rs index fd08098f74..74c4f2d066 100644 --- a/codex-rs/app-server/src/models.rs +++ b/codex-rs/app-server/src/models.rs @@ -6,6 +6,7 @@ use codex_app_server_protocol::ReasoningEffortOption; use codex_core::ThreadManager; use codex_models_manager::manager::RefreshStrategy; use codex_protocol::openai_models::ModelPreset; +use codex_protocol::openai_models::ModelServiceTier as CoreModelServiceTier; use codex_protocol::openai_models::ReasoningEffortPreset; pub async fn supported_models( @@ -22,6 +23,7 @@ pub async fn supported_models( } fn model_from_preset(preset: ModelPreset) -> Model { + let additional_speed_tiers = legacy_additional_speed_tiers(&preset.service_tiers); Model { id: preset.id.to_string(), model: preset.model.to_string(), @@ -42,11 +44,18 @@ fn model_from_preset(preset: ModelPreset) -> Model { default_reasoning_effort: preset.default_reasoning_effort, input_modalities: preset.input_modalities, supports_personality: preset.supports_personality, - additional_speed_tiers: preset.additional_speed_tiers, + additional_speed_tiers, is_default: preset.is_default, } } +fn legacy_additional_speed_tiers(service_tiers: &[CoreModelServiceTier]) -> Vec { + service_tiers + .iter() + .filter_map(|tier| tier.id.is_priority().then_some("fast".to_string())) + .collect() +} + fn reasoning_efforts_from_preset( efforts: Vec, ) -> Vec { diff --git a/codex-rs/app-server/tests/common/models_cache.rs b/codex-rs/app-server/tests/common/models_cache.rs index 3b4a58a7ab..af3e0da69f 100644 --- a/codex-rs/app-server/tests/common/models_cache.rs +++ b/codex-rs/app-server/tests/common/models_cache.rs @@ -28,7 +28,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo { }, supported_in_api: preset.supported_in_api, priority, - additional_speed_tiers: preset.additional_speed_tiers.clone(), + service_tiers: preset.service_tiers.clone(), upgrade: preset.upgrade.as_ref().map(Into::into), base_instructions: "base instructions".to_string(), model_messages: None, diff --git a/codex-rs/app-server/tests/suite/v2/model_list.rs b/codex-rs/app-server/tests/suite/v2/model_list.rs index 830ab0f783..ffb360e9c6 100644 --- a/codex-rs/app-server/tests/suite/v2/model_list.rs +++ b/codex-rs/app-server/tests/suite/v2/model_list.rs @@ -13,6 +13,7 @@ use codex_app_server_protocol::ModelUpgradeInfo; use codex_app_server_protocol::ReasoningEffortOption; use codex_app_server_protocol::RequestId; use codex_protocol::openai_models::ModelPreset; +use codex_protocol::openai_models::ModelServiceTier as CoreModelServiceTier; use pretty_assertions::assert_eq; use tempfile::TempDir; use tokio::time::timeout; @@ -50,11 +51,18 @@ fn model_from_preset(preset: &ModelPreset) -> Model { // cache report `supports_personality = false`. // todo(sayan): fix, maybe make roundtrip use ModelInfo only supports_personality: false, - additional_speed_tiers: preset.additional_speed_tiers.clone(), + additional_speed_tiers: legacy_additional_speed_tiers(&preset.service_tiers), is_default: preset.is_default, } } +fn legacy_additional_speed_tiers(service_tiers: &[CoreModelServiceTier]) -> Vec { + service_tiers + .iter() + .filter_map(|tier| tier.id.is_priority().then_some("fast".to_string())) + .collect() +} + fn expected_visible_models() -> Vec { // Filter by supported_in_api to support testing with both ChatGPT and non-ChatGPT auth modes. let mut presets = ModelPreset::filter_by_auth( diff --git a/codex-rs/app-server/tests/suite/v2/thread_start.rs b/codex-rs/app-server/tests/suite/v2/thread_start.rs index d8a50b88a4..cca4d085a6 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_start.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_start.rs @@ -14,6 +14,7 @@ use codex_app_server_protocol::McpServerStatusUpdatedNotification; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::SandboxMode; use codex_app_server_protocol::ServerNotification; +use codex_app_server_protocol::ServiceTier; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::ThreadStartedNotification; @@ -26,7 +27,6 @@ use codex_core::config::set_project_trust_level; use codex_exec_server::LOCAL_FS; use codex_git_utils::resolve_root_git_project_for_trust; use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR; -use codex_protocol::config_types::ServiceTier; use codex_protocol::config_types::TrustLevel; use codex_protocol::openai_models::ReasoningEffort; use pretty_assertions::assert_eq; @@ -340,7 +340,7 @@ async fn thread_start_accepts_flex_service_tier() -> Result<()> { let req_id = mcp .send_thread_start_request(ThreadStartParams { - service_tier: Some(Some(ServiceTier::Flex)), + service_tier: Some(Some(ServiceTier::flex())), ..Default::default() }) .await?; @@ -352,7 +352,7 @@ async fn thread_start_accepts_flex_service_tier() -> Result<()> { .await??; let ThreadStartResponse { service_tier, .. } = to_response::(resp)?; - assert_eq!(service_tier, Some(ServiceTier::Flex)); + assert_eq!(service_tier, Some(ServiceTier::flex())); Ok(()) } diff --git a/codex-rs/codex-api/tests/models_integration.rs b/codex-rs/codex-api/tests/models_integration.rs index 9f95c9441f..93e08af20d 100644 --- a/codex-rs/codex-api/tests/models_integration.rs +++ b/codex-rs/codex-api/tests/models_integration.rs @@ -74,7 +74,7 @@ async fn models_client_hits_models_endpoint() { visibility: ModelVisibility::List, supported_in_api: true, priority: 1, - additional_speed_tiers: Vec::new(), + service_tiers: Vec::new(), upgrade: None, base_instructions: "base instructions".to_string(), model_messages: None, diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index a30a3ed925..fa29de17fc 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -2151,10 +2151,7 @@ "type": "object" }, "ServiceTier": { - "enum": [ - "fast", - "flex" - ], + "description": "String-backed service tier identifier. Known values include `priority` and `flex`, but other backend-provided ids are allowed.", "type": "string" }, "ShellEnvironmentPolicyInherit": { @@ -4497,4 +4494,4 @@ }, "title": "ConfigToml", "type": "object" -} \ No newline at end of file +} diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index ba81b451a7..5ed1658d3d 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -889,11 +889,7 @@ impl ModelClientSession { store: provider.is_azure_responses_endpoint(), stream: true, include, - service_tier: match service_tier { - Some(ServiceTier::Fast) => Some("priority".to_string()), - Some(service_tier) => Some(service_tier.to_string()), - None => None, - }, + service_tier: service_tier.map(|service_tier| service_tier.to_string()), prompt_cache_key, text, client_metadata: Some(HashMap::from([( diff --git a/codex-rs/core/src/client_common_tests.rs b/codex-rs/core/src/client_common_tests.rs index f67e4f1fd8..732ae63e4d 100644 --- a/codex-rs/core/src/client_common_tests.rs +++ b/codex-rs/core/src/client_common_tests.rs @@ -155,7 +155,7 @@ fn serializes_flex_service_tier_when_set() { stream: true, include: vec![], prompt_cache_key: None, - service_tier: Some(ServiceTier::Flex.to_string()), + service_tier: Some(ServiceTier::flex().to_string()), text: None, client_metadata: None, }; diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index 58a2610fcb..9638086b78 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -518,7 +518,7 @@ async fn drain_to_completed( &turn_context.session_telemetry, turn_context.reasoning_effort, turn_context.reasoning_summary, - turn_context.config.service_tier, + turn_context.config.service_tier.clone(), turn_metadata_header, // Rollout tracing currently models remote compaction only; local compaction streams // are left untraced until the reducer has a first-class local compaction lifecycle. diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 20b2a923f8..e6a5bf09a9 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -2659,12 +2659,10 @@ impl Config { None => config_profile.service_tier.or(cfg.service_tier), }; let service_tier = match service_tier { - Some(ServiceTier::Fast) if features.enabled(Feature::FastMode) => { - Some(ServiceTier::Fast) + Some(service_tier) if service_tier.is_priority() && !features.enabled(Feature::FastMode) => { + None } - Some(ServiceTier::Fast) => None, - Some(ServiceTier::Flex) => Some(ServiceTier::Flex), - None => None, + other => other, }; let compact_prompt = compact_prompt.or(cfg.compact_prompt).and_then(|value| { diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index c18976fde1..2393eab5b3 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -585,7 +585,7 @@ impl Codex { .auth_cached() .and_then(|auth| auth.account_plan_type()); let service_tier = get_service_tier( - config.service_tier, + config.service_tier.clone(), config.notices.fast_default_opt_out.unwrap_or(false), account_plan_type, config.features.enabled(Feature::FastMode), @@ -788,7 +788,7 @@ fn get_service_tier( account_plan_type .is_some_and(is_enterprise_default_service_tier_plan) - .then_some(ServiceTier::Fast) + .then(ServiceTier::priority) } fn is_enterprise_default_service_tier_plan(plan_type: AccountPlanType) -> bool { diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 50b3345d61..73ba043c3d 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -132,7 +132,7 @@ impl SessionConfiguration { ThreadConfigSnapshot { model: self.collaboration_mode.model().to_string(), model_provider_id: self.original_config_do_not_use.model_provider_id.clone(), - service_tier: self.service_tier, + service_tier: self.service_tier.clone(), approval_policy: self.approval_policy.value(), approvals_reviewer: self.approvals_reviewer, permission_profile: self.permission_profile(), @@ -177,7 +177,7 @@ impl SessionConfiguration { if let Some(summary) = updates.reasoning_summary { next_configuration.model_reasoning_summary = Some(summary); } - if let Some(service_tier) = updates.service_tier { + if let Some(service_tier) = updates.service_tier.clone() { next_configuration.service_tier = service_tier; } if let Some(personality) = updates.personality { diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 89633daf35..4b7fa855ce 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -2801,7 +2801,7 @@ fn get_service_tier_defaults_enterprise_accounts_to_fast() { Some(AccountPlanType::Enterprise), /*fast_mode_enabled*/ true, ), - Some(ServiceTier::Fast) + Some(ServiceTier::priority()) ); assert_eq!( get_service_tier( @@ -2810,7 +2810,7 @@ fn get_service_tier_defaults_enterprise_accounts_to_fast() { Some(AccountPlanType::EnterpriseCbpUsageBased), /*fast_mode_enabled*/ true, ), - Some(ServiceTier::Fast) + Some(ServiceTier::priority()) ); assert_eq!( get_service_tier( @@ -2819,7 +2819,7 @@ fn get_service_tier_defaults_enterprise_accounts_to_fast() { Some(AccountPlanType::Business), /*fast_mode_enabled*/ true, ), - Some(ServiceTier::Fast) + Some(ServiceTier::priority()) ); assert_eq!( get_service_tier( @@ -2828,7 +2828,7 @@ fn get_service_tier_defaults_enterprise_accounts_to_fast() { Some(AccountPlanType::Team), /*fast_mode_enabled*/ true, ), - Some(ServiceTier::Fast) + Some(ServiceTier::priority()) ); assert_eq!( get_service_tier( @@ -2837,7 +2837,7 @@ fn get_service_tier_defaults_enterprise_accounts_to_fast() { Some(AccountPlanType::SelfServeBusinessUsageBased), /*fast_mode_enabled*/ true, ), - Some(ServiceTier::Fast) + Some(ServiceTier::priority()) ); } diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index 2b37372a33..f22aaab1d1 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -697,7 +697,7 @@ async fn track_turn_resolved_config_analytics( permission_profile_cwd: turn_context.cwd.to_path_buf(), reasoning_effort: turn_context.reasoning_effort, reasoning_summary: Some(turn_context.reasoning_summary), - service_tier: turn_context.config.service_tier, + service_tier: turn_context.config.service_tier.clone(), approval_policy: turn_context.approval_policy.value(), approvals_reviewer: turn_context.config.approvals_reviewer, sandbox_network_access: turn_context.network_sandbox_policy().is_enabled(), @@ -1839,7 +1839,7 @@ async fn try_run_sampling_request( &turn_context.session_telemetry, turn_context.reasoning_effort, turn_context.reasoning_summary, - turn_context.config.service_tier, + turn_context.config.service_tier.clone(), turn_metadata_header, &inference_trace, ) diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index a87d2ab178..e40f9f6563 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -397,7 +397,7 @@ impl Session { per_turn_config.model_reasoning_effort = session_configuration.collaboration_mode.reasoning_effort(); per_turn_config.model_reasoning_summary = session_configuration.model_reasoning_summary; - per_turn_config.service_tier = session_configuration.service_tier; + per_turn_config.service_tier = session_configuration.service_tier.clone(); per_turn_config.personality = session_configuration.personality; per_turn_config.approvals_reviewer = session_configuration.approvals_reviewer; per_turn_config.permissions.permission_profile = diff --git a/codex-rs/core/tests/suite/agent_websocket.rs b/codex-rs/core/tests/suite/agent_websocket.rs index 305346afac..f765c3c137 100644 --- a/codex-rs/core/tests/suite/agent_websocket.rs +++ b/codex-rs/core/tests/suite/agent_websocket.rs @@ -269,7 +269,7 @@ async fn websocket_v2_first_turn_uses_updated_fast_tier_after_startup_prewarm() assert_eq!(warmup["generate"].as_bool(), Some(false)); assert_eq!(warmup.get("service_tier"), None); - test.submit_turn_with_service_tier("hello", Some(ServiceTier::Fast)) + test.submit_turn_with_service_tier("hello", Some(ServiceTier::priority())) .await?; assert_eq!(server.handshakes().len(), 1); @@ -313,7 +313,7 @@ async fn websocket_v2_first_turn_drops_fast_tier_after_startup_prewarm() -> Resu .features .enable(Feature::ResponsesWebsocketsV2) .expect("test config should allow feature update"); - config.service_tier = Some(ServiceTier::Fast); + config.service_tier = Some(ServiceTier::priority()); }); let test = builder.build_with_websocket_server(&server).await?; @@ -385,7 +385,7 @@ async fn websocket_v2_next_turn_uses_updated_service_tier() -> Result<()> { assert_eq!(warmup["generate"].as_bool(), Some(false)); assert_eq!(warmup.get("service_tier"), None); - test.submit_turn_with_service_tier("first", Some(ServiceTier::Fast)) + test.submit_turn_with_service_tier("first", Some(ServiceTier::priority())) .await?; test.submit_turn_with_service_tier("second", /*service_tier*/ None) .await?; diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index 43ec50746e..aa8d6d8ceb 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -105,7 +105,7 @@ fn test_model_info( used_fallback_model_metadata: false, supports_search_tool: false, priority: 1, - additional_speed_tiers: Vec::new(), + service_tiers: Vec::new(), upgrade: None, base_instructions: "base instructions".to_string(), model_messages: None, @@ -297,7 +297,7 @@ async fn service_tier_change_is_applied_on_next_http_turn() -> Result<()> { let test = test_codex().build(&server).await?; - test.submit_turn_with_service_tier("fast turn", Some(ServiceTier::Fast)) + test.submit_turn_with_service_tier("fast turn", Some(ServiceTier::priority())) .await?; test.submit_turn_with_service_tier("standard turn", /*service_tier*/ None) .await?; @@ -323,7 +323,7 @@ async fn flex_service_tier_is_applied_to_http_turn() -> Result<()> { let test = test_codex().build(&server).await?; - test.submit_turn_with_service_tier("flex turn", Some(ServiceTier::Flex)) + test.submit_turn_with_service_tier("flex turn", Some(ServiceTier::flex())) .await?; let request = resp_mock.single_request(); @@ -817,7 +817,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result< used_fallback_model_metadata: false, supports_search_tool: false, priority: 1, - additional_speed_tiers: Vec::new(), + service_tiers: Vec::new(), upgrade: None, base_instructions: "base instructions".to_string(), model_messages: None, diff --git a/codex-rs/core/tests/suite/models_cache_ttl.rs b/codex-rs/core/tests/suite/models_cache_ttl.rs index e2688afc97..207027bfdd 100644 --- a/codex-rs/core/tests/suite/models_cache_ttl.rs +++ b/codex-rs/core/tests/suite/models_cache_ttl.rs @@ -340,7 +340,7 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo { visibility: ModelVisibility::List, supported_in_api: true, priority, - additional_speed_tiers: Vec::new(), + service_tiers: Vec::new(), upgrade: None, base_instructions: "base instructions".to_string(), model_messages: None, diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index dde6d2ca51..de43ad7869 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -578,7 +578,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow visibility: ModelVisibility::List, supported_in_api: true, priority: 1, - additional_speed_tiers: Vec::new(), + service_tiers: Vec::new(), upgrade: None, base_instructions: "base instructions".to_string(), model_messages: Some(ModelMessages { @@ -686,7 +686,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - visibility: ModelVisibility::List, supported_in_api: true, priority: 1, - additional_speed_tiers: Vec::new(), + service_tiers: Vec::new(), upgrade: None, base_instructions: "base instructions".to_string(), model_messages: Some(ModelMessages { diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index 49218c78d2..4431d9e2c9 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -532,7 +532,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { used_fallback_model_metadata: false, supports_search_tool: false, priority: 1, - additional_speed_tiers: Vec::new(), + service_tiers: Vec::new(), upgrade: None, base_instructions: "base instructions".to_string(), model_messages: None, @@ -788,7 +788,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { used_fallback_model_metadata: false, supports_search_tool: false, priority: 1, - additional_speed_tiers: Vec::new(), + service_tiers: Vec::new(), upgrade: None, base_instructions: remote_base.to_string(), model_messages: None, @@ -1278,7 +1278,7 @@ fn test_remote_model_with_policy( used_fallback_model_metadata: false, supports_search_tool: false, priority, - additional_speed_tiers: Vec::new(), + service_tiers: Vec::new(), upgrade: None, base_instructions: "base instructions".to_string(), model_messages: None, diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index 0947f4fba7..79469c20a5 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -1282,7 +1282,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re visibility: ModelVisibility::List, supported_in_api: true, priority: 1, - additional_speed_tiers: Vec::new(), + service_tiers: Vec::new(), upgrade: None, base_instructions: "base instructions".to_string(), model_messages: None, diff --git a/codex-rs/core/tests/suite/spawn_agent_description.rs b/codex-rs/core/tests/suite/spawn_agent_description.rs index 031c3135e8..12845a0188 100644 --- a/codex-rs/core/tests/suite/spawn_agent_description.rs +++ b/codex-rs/core/tests/suite/spawn_agent_description.rs @@ -66,7 +66,7 @@ fn test_model_info( used_fallback_model_metadata: false, supports_search_tool: false, priority: 1, - additional_speed_tiers: Vec::new(), + service_tiers: Vec::new(), upgrade: None, base_instructions: "base instructions".to_string(), model_messages: None, diff --git a/codex-rs/core/tests/suite/view_image.rs b/codex-rs/core/tests/suite/view_image.rs index 29c660d3af..a06f5e555f 100644 --- a/codex-rs/core/tests/suite/view_image.rs +++ b/codex-rs/core/tests/suite/view_image.rs @@ -1084,7 +1084,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an used_fallback_model_metadata: false, supports_search_tool: false, priority: 1, - additional_speed_tiers: Vec::new(), + service_tiers: Vec::new(), upgrade: None, base_instructions: "base instructions".to_string(), model_messages: None, diff --git a/codex-rs/memories/write/src/startup_tests.rs b/codex-rs/memories/write/src/startup_tests.rs index d89b68825e..1b1053fa44 100644 --- a/codex-rs/memories/write/src/startup_tests.rs +++ b/codex-rs/memories/write/src/startup_tests.rs @@ -253,14 +253,14 @@ async fn memories_startup_phase1_uses_live_thread_service_tier() -> anyhow::Resu model: None, effort: None, summary: None, - service_tier: Some(Some(ServiceTier::Fast)), + service_tier: Some(Some(ServiceTier::priority())), collaboration_mode: None, personality: None, }) .await?; - let config_snapshot = wait_for_service_tier(&test, Some(ServiceTier::Fast)).await?; - assert_eq!(config_snapshot.service_tier, Some(ServiceTier::Fast)); + let config_snapshot = wait_for_service_tier(&test, Some(ServiceTier::priority())).await?; + assert_eq!(config_snapshot.service_tier, Some(ServiceTier::priority())); let context = crate::runtime::MemoryStartupContext::new( Arc::clone(&test.thread_manager), @@ -277,7 +277,7 @@ async fn memories_startup_phase1_uses_live_thread_service_tier() -> anyhow::Resu ReasoningEffort::Low, ) .await; - assert_eq!(request_context.service_tier, Some(ServiceTier::Fast)); + assert_eq!(request_context.service_tier, Some(ServiceTier::priority())); shutdown_test_codex(&test).await?; Ok(()) diff --git a/codex-rs/model-provider/src/amazon_bedrock/catalog.rs b/codex-rs/model-provider/src/amazon_bedrock/catalog.rs index 4ca2cb891e..47d2c75619 100644 --- a/codex-rs/model-provider/src/amazon_bedrock/catalog.rs +++ b/codex-rs/model-provider/src/amazon_bedrock/catalog.rs @@ -1,10 +1,12 @@ use codex_models_manager::model_info::BASE_INSTRUCTIONS; use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::config_types::ServiceTier; use codex_protocol::config_types::Verbosity; use codex_protocol::openai_models::ApplyPatchToolType; use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::InputModality; 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::ReasoningEffort; @@ -46,7 +48,11 @@ fn gpt_5_4_cmb_bedrock_model(priority: i32) -> ModelInfo { visibility: ModelVisibility::List, supported_in_api: true, priority, - additional_speed_tiers: vec!["fast".to_string()], + service_tiers: vec![ModelServiceTier { + id: ServiceTier::priority(), + name: "Fast".to_string(), + description: "Fast tier".to_string(), + }], availability_nux: None, upgrade: None, base_instructions: BASE_INSTRUCTIONS.to_string(), @@ -86,7 +92,7 @@ fn bedrock_oss_model(slug: &str, display_name: &str, priority: i32) -> ModelInfo visibility: ModelVisibility::List, supported_in_api: true, priority, - additional_speed_tiers: Vec::new(), + service_tiers: Vec::new(), availability_nux: None, upgrade: None, base_instructions: BASE_INSTRUCTIONS.to_string(), diff --git a/codex-rs/models-manager/models.json b/codex-rs/models-manager/models.json index c025fe36ad..ce06a3dd35 100644 --- a/codex-rs/models-manager/models.json +++ b/codex-rs/models-manager/models.json @@ -82,8 +82,12 @@ "team" ], "supports_search_tool": true, - "additional_speed_tiers": [ - "fast" + "service_tiers": [ + { + "id": "priority", + "name": "Fast", + "description": "Fast tier" + } ], "supports_reasoning_summaries": true }, @@ -164,8 +168,12 @@ "team" ], "supports_search_tool": true, - "additional_speed_tiers": [ - "fast" + "service_tiers": [ + { + "id": "priority", + "name": "Fast", + "description": "Fast tier" + } ], "supports_reasoning_summaries": true }, @@ -249,7 +257,7 @@ "team" ], "supports_search_tool": true, - "additional_speed_tiers": [], + "service_tiers": [], "supports_reasoning_summaries": true }, { @@ -332,7 +340,7 @@ "team" ], "supports_search_tool": true, - "additional_speed_tiers": [], + "service_tiers": [], "supports_reasoning_summaries": true }, { @@ -411,7 +419,7 @@ "team" ], "supports_search_tool": true, - "additional_speed_tiers": [], + "service_tiers": [], "supports_reasoning_summaries": true }, { @@ -491,7 +499,7 @@ "team" ], "supports_search_tool": true, - "additional_speed_tiers": [], + "service_tiers": [], "supports_reasoning_summaries": true } ] diff --git a/codex-rs/models-manager/src/model_info.rs b/codex-rs/models-manager/src/model_info.rs index 8e8abae549..5afcaf7ce2 100644 --- a/codex-rs/models-manager/src/model_info.rs +++ b/codex-rs/models-manager/src/model_info.rs @@ -75,7 +75,7 @@ pub fn model_info_from_slug(slug: &str) -> ModelInfo { visibility: ModelVisibility::None, supported_in_api: true, priority: 99, - additional_speed_tiers: Vec::new(), + service_tiers: Vec::new(), availability_nux: None, upgrade: None, base_instructions: BASE_INSTRUCTIONS.to_string(), diff --git a/codex-rs/protocol/src/config_types.rs b/codex-rs/protocol/src/config_types.rs index da83ee858a..b4433aeae2 100644 --- a/codex-rs/protocol/src/config_types.rs +++ b/codex-rs/protocol/src/config_types.rs @@ -347,12 +347,88 @@ impl From for WebSearchConfig { } } -#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)] -#[serde(rename_all = "lowercase")] -#[strum(serialize_all = "lowercase")] -pub enum ServiceTier { - Fast, - Flex, +pub const SERVICE_TIER_PRIORITY: &str = "priority"; +pub const SERVICE_TIER_FLEX: &str = "flex"; + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, TS)] +#[serde(transparent)] +#[ts(type = "string")] +pub struct ServiceTier(String); + +impl ServiceTier { + pub fn new(id: impl Into) -> Self { + Self(id.into()) + } + + pub fn as_str(&self) -> &str { + self.0.as_str() + } + + pub fn priority() -> Self { + Self::new(SERVICE_TIER_PRIORITY) + } + + pub fn flex() -> Self { + Self::new(SERVICE_TIER_FLEX) + } + + pub fn is_priority(&self) -> bool { + self.as_str() == SERVICE_TIER_PRIORITY + } + + pub fn is_flex(&self) -> bool { + self.as_str() == SERVICE_TIER_FLEX + } +} + +impl AsRef for ServiceTier { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl From<&str> for ServiceTier { + fn from(value: &str) -> Self { + Self::new(value) + } +} + +impl From for ServiceTier { + fn from(value: String) -> Self { + Self::new(value) + } +} + +impl From for String { + fn from(value: ServiceTier) -> Self { + value.0 + } +} + +impl std::fmt::Display for ServiceTier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +impl JsonSchema for ServiceTier { + fn schema_name() -> String { + "ServiceTier".to_string() + } + + fn json_schema(_generator: &mut SchemaGenerator) -> Schema { + Schema::Object(SchemaObject { + instance_type: Some(InstanceType::String.into()), + metadata: Some(Box::new(Metadata { + description: Some( + "String-backed service tier identifier. Known values include `priority` and `flex`, but other backend-provided ids are allowed." + .to_string(), + ), + ..Default::default() + })), + ..Default::default() + }) + } } #[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)] diff --git a/codex-rs/protocol/src/openai_models.rs b/codex-rs/protocol/src/openai_models.rs index 41275e6a6b..6f2daccea9 100644 --- a/codex-rs/protocol/src/openai_models.rs +++ b/codex-rs/protocol/src/openai_models.rs @@ -17,10 +17,10 @@ use ts_rs::TS; use crate::config_types::Personality; use crate::config_types::ReasoningSummary; +use crate::config_types::ServiceTier; use crate::config_types::Verbosity; const PERSONALITY_PLACEHOLDER: &str = "{{ personality }}"; -pub const SPEED_TIER_FAST: &str = "fast"; /// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning #[derive( @@ -115,6 +115,13 @@ pub struct ModelAvailabilityNux { pub message: String, } +#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)] +pub struct ModelServiceTier { + pub id: ServiceTier, + pub name: String, + pub description: String, +} + /// Metadata describing a Codex-supported model. #[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)] pub struct ModelPreset { @@ -133,9 +140,9 @@ pub struct ModelPreset { /// Whether this model supports personality-specific instructions. #[serde(default)] pub supports_personality: bool, - /// Additional speed tiers this model can run with beyond the standard path. + /// User-selectable service tiers supported by this model. #[serde(default)] - pub additional_speed_tiers: Vec, + pub service_tiers: Vec, /// Whether this is the default model for new users. pub is_default: bool, /// recommended upgrade model @@ -257,7 +264,7 @@ pub struct ModelInfo { pub supported_in_api: bool, pub priority: i32, #[serde(default)] - pub additional_speed_tiers: Vec, + pub service_tiers: Vec, pub availability_nux: Option, pub upgrade: Option, pub base_instructions: String, @@ -441,7 +448,7 @@ impl From for ModelPreset { .unwrap_or(ReasoningEffort::None), supported_reasoning_efforts: info.supported_reasoning_levels.clone(), supports_personality, - additional_speed_tiers: info.additional_speed_tiers, + service_tiers: info.service_tiers, is_default: false, // default is the highest priority available model upgrade: info.upgrade.as_ref().map(|upgrade| ModelUpgrade { id: upgrade.model.clone(), @@ -463,10 +470,16 @@ impl From for ModelPreset { } impl ModelPreset { - pub fn supports_fast_mode(&self) -> bool { - self.additional_speed_tiers + pub fn supports_service_tier(&self, service_tier: &ServiceTier) -> bool { + self.service_tiers .iter() - .any(|tier| tier == SPEED_TIER_FAST) + .any(|tier| tier.id == *service_tier) + } + + pub fn service_tier(&self, service_tier: &ServiceTier) -> Option<&ModelServiceTier> { + self.service_tiers + .iter() + .find(|tier| tier.id == *service_tier) } /// Filter models based on authentication mode. @@ -547,7 +560,7 @@ mod tests { visibility: ModelVisibility::List, supported_in_api: true, priority: 1, - additional_speed_tiers: Vec::new(), + service_tiers: Vec::new(), availability_nux: None, upgrade: None, base_instructions: "base".to_string(), @@ -817,7 +830,11 @@ mod tests { availability_nux: Some(ModelAvailabilityNux { message: "Try Spark.".to_string(), }), - additional_speed_tiers: vec![SPEED_TIER_FAST.to_string()], + service_tiers: vec![ModelServiceTier { + id: ServiceTier::priority(), + name: "Fast".to_string(), + description: "Fast tier".to_string(), + }], ..test_model(/*spec*/ None) }); @@ -827,6 +844,6 @@ mod tests { message: "Try Spark.".to_string(), }) ); - assert!(preset.supports_fast_mode()); + assert!(preset.supports_service_tier(&ServiceTier::priority())); } } diff --git a/codex-rs/tools/src/agent_tool_tests.rs b/codex-rs/tools/src/agent_tool_tests.rs index 3157cfc547..46fed4db23 100644 --- a/codex-rs/tools/src/agent_tool_tests.rs +++ b/codex-rs/tools/src/agent_tool_tests.rs @@ -19,7 +19,7 @@ fn model_preset(id: &str, show_in_picker: bool) -> ModelPreset { description: "Balanced".to_string(), }], supports_personality: false, - additional_speed_tiers: Vec::new(), + service_tiers: Vec::new(), is_default: false, upgrade: None, show_in_picker,