1- Add model service tiers metadata (#20969)

## Why

The model list needs to carry display-ready service tier metadata so
clients can render tier choices with stable IDs, names, and
descriptions. A raw speed-tier string list is not enough for richer UI
copy or future tier labels.

## What changed

- Added `ModelServiceTier` to shared model metadata with string `id`,
`name`, and `description` fields.
- Added `service_tiers` to `ModelInfo` and `ModelPreset`, preserving
empty defaults for older cached model payloads.
- Exposed `serviceTiers` on app-server v2 `Model` responses and threaded
it through TUI app-server model conversion.
- Marked legacy `additional_speed_tiers` / `additionalSpeedTiers`
metadata as deprecated in source and generated schema output.
- Regenerated app-server protocol JSON schema and TypeScript fixtures,
including `ModelServiceTier.ts`.

## Verification

- Ran `just write-app-server-schema`.
- Did not run local tests per repo instruction; relying on PR CI.

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Ahmed Ibrahim
2026-05-05 09:51:18 +03:00
committed by GitHub
Unverified
parent dca105cf99
commit 9d579813bb
24 changed files with 190 additions and 4 deletions
@@ -11266,6 +11266,7 @@
"properties": {
"additionalSpeedTiers": {
"default": [],
"description": "Deprecated: use `serviceTiers` instead.",
"items": {
"type": "string"
},
@@ -11312,6 +11313,13 @@
"model": {
"type": "string"
},
"serviceTiers": {
"default": [],
"items": {
"$ref": "#/definitions/v2/ModelServiceTier"
},
"type": "array"
},
"supportedReasoningEfforts": {
"items": {
"$ref": "#/definitions/v2/ReasoningEffortOption"
@@ -11476,6 +11484,25 @@
"title": "ModelReroutedNotification",
"type": "object"
},
"ModelServiceTier": {
"properties": {
"description": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"description",
"id",
"name"
],
"type": "object"
},
"ModelUpgradeInfo": {
"properties": {
"migrationMarkdown": {
@@ -7877,6 +7877,7 @@
"properties": {
"additionalSpeedTiers": {
"default": [],
"description": "Deprecated: use `serviceTiers` instead.",
"items": {
"type": "string"
},
@@ -7923,6 +7924,13 @@
"model": {
"type": "string"
},
"serviceTiers": {
"default": [],
"items": {
"$ref": "#/definitions/ModelServiceTier"
},
"type": "array"
},
"supportedReasoningEfforts": {
"items": {
"$ref": "#/definitions/ReasoningEffortOption"
@@ -8087,6 +8095,25 @@
"title": "ModelReroutedNotification",
"type": "object"
},
"ModelServiceTier": {
"properties": {
"description": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"description",
"id",
"name"
],
"type": "object"
},
"ModelUpgradeInfo": {
"properties": {
"migrationMarkdown": {
@@ -24,6 +24,7 @@
"properties": {
"additionalSpeedTiers": {
"default": [],
"description": "Deprecated: use `serviceTiers` instead.",
"items": {
"type": "string"
},
@@ -70,6 +71,13 @@
"model": {
"type": "string"
},
"serviceTiers": {
"default": [],
"items": {
"$ref": "#/definitions/ModelServiceTier"
},
"type": "array"
},
"supportedReasoningEfforts": {
"items": {
"$ref": "#/definitions/ReasoningEffortOption"
@@ -120,6 +128,25 @@
],
"type": "object"
},
"ModelServiceTier": {
"properties": {
"description": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"description",
"id",
"name"
],
"type": "object"
},
"ModelUpgradeInfo": {
"properties": {
"migrationMarkdown": {
+6 -1
View File
@@ -4,7 +4,12 @@
import type { InputModality } from "../InputModality";
import type { ReasoningEffort } from "../ReasoningEffort";
import type { ModelAvailabilityNux } from "./ModelAvailabilityNux";
import type { ModelServiceTier } from "./ModelServiceTier";
import type { ModelUpgradeInfo } from "./ModelUpgradeInfo";
import type { ReasoningEffortOption } from "./ReasoningEffortOption";
export type Model = { id: string, model: string, upgrade: string | null, upgradeInfo: ModelUpgradeInfo | null, availabilityNux: ModelAvailabilityNux | null, displayName: string, description: string, hidden: boolean, supportedReasoningEfforts: Array<ReasoningEffortOption>, defaultReasoningEffort: ReasoningEffort, inputModalities: Array<InputModality>, supportsPersonality: boolean, additionalSpeedTiers: Array<string>, isDefault: boolean, };
export type Model = { id: string, model: string, upgrade: string | null, upgradeInfo: ModelUpgradeInfo | null, availabilityNux: ModelAvailabilityNux | null, displayName: string, description: string, hidden: boolean, supportedReasoningEfforts: Array<ReasoningEffortOption>, defaultReasoningEffort: ReasoningEffort, inputModalities: Array<InputModality>, supportsPersonality: boolean,
/**
* Deprecated: use `serviceTiers` instead.
*/
additionalSpeedTiers: Array<string>, serviceTiers: Array<ModelServiceTier>, isDefault: boolean, };
@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ModelServiceTier = { id: string, name: string, description: string, };
@@ -245,6 +245,7 @@ export type { ModelProviderCapabilitiesReadParams } from "./ModelProviderCapabil
export type { ModelProviderCapabilitiesReadResponse } from "./ModelProviderCapabilitiesReadResponse";
export type { ModelRerouteReason } from "./ModelRerouteReason";
export type { ModelReroutedNotification } from "./ModelReroutedNotification";
export type { ModelServiceTier } from "./ModelServiceTier";
export type { ModelUpgradeInfo } from "./ModelUpgradeInfo";
export type { ModelVerification } from "./ModelVerification";
export type { ModelVerificationNotification } from "./ModelVerificationNotification";
@@ -2511,6 +2511,15 @@ impl From<CoreModelAvailabilityNux> for ModelAvailabilityNux {
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ModelServiceTier {
pub id: String,
pub name: String,
pub description: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -2529,8 +2538,11 @@ pub struct Model {
pub input_modalities: Vec<InputModality>,
#[serde(default)]
pub supports_personality: bool,
/// Deprecated: use `serviceTiers` instead.
#[serde(default)]
pub additional_speed_tiers: Vec<String>,
#[serde(default)]
pub service_tiers: Vec<ModelServiceTier>,
// Only one model should be marked as default.
pub is_default: bool,
}
+10
View File
@@ -1,6 +1,7 @@
use std::sync::Arc;
use codex_app_server_protocol::Model;
use codex_app_server_protocol::ModelServiceTier;
use codex_app_server_protocol::ModelUpgradeInfo;
use codex_app_server_protocol::ReasoningEffortOption;
use codex_core::ThreadManager;
@@ -43,6 +44,15 @@ fn model_from_preset(preset: ModelPreset) -> Model {
input_modalities: preset.input_modalities,
supports_personality: preset.supports_personality,
additional_speed_tiers: preset.additional_speed_tiers,
service_tiers: preset
.service_tiers
.into_iter()
.map(|service_tier| ModelServiceTier {
id: service_tier.id,
name: service_tier.name,
description: service_tier.description,
})
.collect(),
is_default: preset.is_default,
}
}
@@ -29,6 +29,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,
@@ -9,6 +9,7 @@ use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::Model;
use codex_app_server_protocol::ModelListParams;
use codex_app_server_protocol::ModelListResponse;
use codex_app_server_protocol::ModelServiceTier;
use codex_app_server_protocol::ModelUpgradeInfo;
use codex_app_server_protocol::ReasoningEffortOption;
use codex_app_server_protocol::RequestId;
@@ -51,6 +52,15 @@ fn model_from_preset(preset: &ModelPreset) -> Model {
// todo(sayan): fix, maybe make roundtrip use ModelInfo only
supports_personality: false,
additional_speed_tiers: preset.additional_speed_tiers.clone(),
service_tiers: preset
.service_tiers
.iter()
.map(|service_tier| ModelServiceTier {
id: service_tier.id.clone(),
name: service_tier.name.clone(),
description: service_tier.description.clone(),
})
.collect(),
is_default: preset.is_default,
}
}
@@ -75,6 +75,7 @@ async fn models_client_hits_models_endpoint() {
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,
@@ -106,6 +106,7 @@ fn test_model_info(
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,
@@ -818,6 +819,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result<
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,
@@ -341,6 +341,7 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo {
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
View File
@@ -579,6 +579,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow
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 {
@@ -687,6 +688,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
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 {
@@ -533,6 +533,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> {
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,
@@ -789,6 +790,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
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,
@@ -1279,6 +1281,7 @@ fn test_remote_model_with_policy(
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
View File
@@ -1283,6 +1283,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re
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,
@@ -67,6 +67,7 @@ fn test_model_info(
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
View File
@@ -1085,6 +1085,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an
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,
@@ -47,6 +47,7 @@ fn gpt_5_4_cmb_bedrock_model(priority: i32) -> ModelInfo {
supported_in_api: true,
priority,
additional_speed_tiers: vec!["fast".to_string()],
service_tiers: Vec::new(),
availability_nux: None,
upgrade: None,
base_instructions: BASE_INSTRUCTIONS.to_string(),
@@ -87,6 +88,7 @@ fn bedrock_oss_model(slug: &str, display_name: &str, priority: i32) -> ModelInfo
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(),
@@ -76,6 +76,7 @@ pub fn model_info_from_slug(slug: &str) -> ModelInfo {
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(),
+36 -3
View File
@@ -115,6 +115,13 @@ pub struct ModelAvailabilityNux {
pub message: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)]
pub struct ModelServiceTier {
pub id: String,
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,12 @@ 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.
/// Deprecated: use `service_tiers` instead.
#[serde(default)]
pub additional_speed_tiers: Vec<String>,
/// Service tiers this model can run with.
#[serde(default)]
pub service_tiers: Vec<ModelServiceTier>,
/// Whether this is the default model for new users.
pub is_default: bool,
/// recommended upgrade model
@@ -258,6 +268,8 @@ pub struct ModelInfo {
pub priority: i32,
#[serde(default)]
pub additional_speed_tiers: Vec<String>,
#[serde(default)]
pub service_tiers: Vec<ModelServiceTier>,
pub availability_nux: Option<ModelAvailabilityNux>,
pub upgrade: Option<ModelInfoUpgrade>,
pub base_instructions: String,
@@ -442,6 +454,7 @@ impl From<ModelInfo> for ModelPreset {
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(),
@@ -464,9 +477,13 @@ impl From<ModelInfo> for ModelPreset {
impl ModelPreset {
pub fn supports_fast_mode(&self) -> bool {
self.additional_speed_tiers
self.service_tiers
.iter()
.any(|tier| tier == SPEED_TIER_FAST)
.any(|tier| tier.id == SPEED_TIER_FAST)
|| self
.additional_speed_tiers
.iter()
.any(|tier| tier == SPEED_TIER_FAST)
}
/// Filter models based on authentication mode.
@@ -548,6 +565,7 @@ mod tests {
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(),
@@ -818,6 +836,7 @@ mod tests {
message: "Try Spark.".to_string(),
}),
additional_speed_tiers: vec![SPEED_TIER_FAST.to_string()],
service_tiers: Vec::new(),
..test_model(/*spec*/ None)
});
@@ -829,4 +848,18 @@ mod tests {
);
assert!(preset.supports_fast_mode());
}
#[test]
fn model_preset_supports_fast_mode_from_service_tiers() {
let preset = ModelPreset::from(ModelInfo {
service_tiers: vec![ModelServiceTier {
id: SPEED_TIER_FAST.to_string(),
name: "Fast".to_string(),
description: "Priority processing.".to_string(),
}],
..test_model(/*spec*/ None)
});
assert!(preset.supports_fast_mode());
}
}
+1
View File
@@ -20,6 +20,7 @@ fn model_preset(id: &str, show_in_picker: bool) -> ModelPreset {
}],
supports_personality: false,
additional_speed_tiers: Vec::new(),
service_tiers: Vec::new(),
is_default: false,
upgrade: None,
show_in_picker,
+10
View File
@@ -111,6 +111,7 @@ use codex_protocol::models::PermissionProfile;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ModelAvailabilityNux;
use codex_protocol::openai_models::ModelPreset;
use codex_protocol::openai_models::ModelServiceTier;
use codex_protocol::openai_models::ModelUpgrade;
use codex_protocol::openai_models::ReasoningEffortPreset;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -1046,6 +1047,15 @@ fn model_preset_from_api_model(model: ApiModel) -> ModelPreset {
.collect(),
supports_personality: model.supports_personality,
additional_speed_tiers: model.additional_speed_tiers,
service_tiers: model
.service_tiers
.into_iter()
.map(|service_tier| ModelServiceTier {
id: service_tier.id,
name: service_tier.name,
description: service_tier.description,
})
.collect(),
is_default: model.is_default,
upgrade,
show_in_picker: !model.hidden,
@@ -2453,6 +2453,7 @@ async fn model_picker_hides_show_in_picker_false_models_from_cache() {
}],
supports_personality: false,
additional_speed_tiers: Vec::new(),
service_tiers: Vec::new(),
is_default: false,
upgrade: None,
show_in_picker,
@@ -2673,6 +2674,7 @@ async fn single_reasoning_option_skips_selection() {
supported_reasoning_efforts: single_effort,
supports_personality: false,
additional_speed_tiers: Vec::new(),
service_tiers: Vec::new(),
is_default: false,
upgrade: None,
show_in_picker: true,