Use structured service tiers in core model metadata

This commit is contained in:
Ahmed Ibrahim
2026-05-03 07:24:16 +03:00
parent 35aaa5d9fc
commit e9a3e91f3c
32 changed files with 273 additions and 99 deletions
@@ -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<Self> {
if service_tier.is_priority() {
Some(Self::Fast)
} else if service_tier.is_flex() {
Some(Self::Flex)
} else {
None
}
}
}
impl From<ServiceTier> 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 {
@@ -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<ServiceTier>>,
) -> Option<Option<codex_protocol::config_types::ServiceTier>> {
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,
+10 -1
View File
@@ -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<String> {
service_tiers
.iter()
.filter_map(|tier| tier.id.is_priority().then_some("fast".to_string()))
.collect()
}
fn reasoning_efforts_from_preset(
efforts: Vec<ReasoningEffortPreset>,
) -> Vec<ReasoningEffortOption> {
@@ -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,
@@ -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<String> {
service_tiers
.iter()
.filter_map(|tier| tier.id.is_priority().then_some("fast".to_string()))
.collect()
}
fn expected_visible_models() -> Vec<Model> {
// Filter by supported_in_api to support testing with both ChatGPT and non-ChatGPT auth modes.
let mut presets = ModelPreset::filter_by_auth(
@@ -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::<ThreadStartResponse>(resp)?;
assert_eq!(service_tier, Some(ServiceTier::Flex));
assert_eq!(service_tier, Some(ServiceTier::flex()));
Ok(())
}
@@ -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,
+2 -5
View File
@@ -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"
}
}
+1 -5
View File
@@ -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([(
+1 -1
View File
@@ -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,
};
+1 -1
View File
@@ -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.
+3 -5
View File
@@ -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| {
+2 -2
View File
@@ -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 {
+2 -2
View File
@@ -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 {
+5 -5
View File
@@ -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())
);
}
+2 -2
View File
@@ -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,
)
+1 -1
View File
@@ -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 =
+3 -3
View File
@@ -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?;
+4 -4
View File
@@ -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,
@@ -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,
+2 -2
View File
@@ -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 {
+3 -3
View File
@@ -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,
+1 -1
View File
@@ -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,
@@ -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,
+1 -1
View File
@@ -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,
+4 -4
View File
@@ -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(())
@@ -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(),
+16 -8
View File
@@ -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
}
]
+1 -1
View File
@@ -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(),
+82 -6
View File
@@ -347,12 +347,88 @@ impl From<WebSearchToolConfig> 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<String>) -> 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<str> 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<String> for ServiceTier {
fn from(value: String) -> Self {
Self::new(value)
}
}
impl From<ServiceTier> 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)]
+28 -11
View File
@@ -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<String>,
pub service_tiers: Vec<ModelServiceTier>,
/// 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<String>,
pub service_tiers: Vec<ModelServiceTier>,
pub availability_nux: Option<ModelAvailabilityNux>,
pub upgrade: Option<ModelInfoUpgrade>,
pub base_instructions: String,
@@ -441,7 +448,7 @@ impl From<ModelInfo> 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<ModelInfo> 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()));
}
}
+1 -1
View File
@@ -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,