Codex Handbook
protocol/src/openai_models.rs 1122 lines
//! Shared model metadata types exchanged between Codex services and clients.//!//! These types are serialized across core, TUI, app-server, and SDK boundaries, so field defaults//! are used to preserve compatibility when older payloads omit newly introduced attributes.use std::fmt;use std::str::FromStr;use schemars::JsonSchema;use schemars::r#gen::SchemaGenerator;use schemars::schema::InstanceType;use schemars::schema::Metadata;use schemars::schema::Schema;use schemars::schema::SchemaObject;use schemars::schema::StringValidation;use serde::Deserialize;use serde::Deserializer;use serde::Serialize;use serde::Serializer;use serde::de::DeserializeOwned;use serde::de::Error;use strum_macros::Display;use strum_macros::EnumIter;use tracing::warn;use ts_rs::TS;use crate::config_types::Personality;use crate::config_types::ReasoningSummary;use crate::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;use crate::config_types::ServiceTier;use crate::config_types::Verbosity;use crate::protocol::MultiAgentVersion;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(Debug, Default, Clone, PartialEq, Eq, TS, Hash)]#[ts(type = "string")]pub enum ReasoningEffort {    None,    Minimal,    Low,    #[default]    Medium,    High,    XHigh,    /// A model-defined effort value that this client does not know yet.    Custom(String),}impl ReasoningEffort {    /// Returns the exact value used on the wire.    pub fn as_str(&self) -> &str {        match self {            Self::None => "none",            Self::Minimal => "minimal",            Self::Low => "low",            Self::Medium => "medium",            Self::High => "high",            Self::XHigh => "xhigh",            Self::Custom(effort) => effort,        }    }}impl fmt::Display for ReasoningEffort {    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {        f.write_str(self.as_str())    }}impl JsonSchema for ReasoningEffort {    fn schema_name() -> String {        "ReasoningEffort".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(                    "A non-empty reasoning effort value advertised by the model.".to_string(),                ),                ..Default::default()            })),            string: Some(Box::new(StringValidation {                min_length: Some(1),                ..Default::default()            })),            ..Default::default()        })    }}impl Serialize for ReasoningEffort {    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>    where        S: Serializer,    {        serializer.serialize_str(self.as_str())    }}impl<'de> Deserialize<'de> for ReasoningEffort {    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>    where        D: Deserializer<'de>,    {        let effort = String::deserialize(deserializer)?;        effort.parse().map_err(D::Error::custom)    }}impl FromStr for ReasoningEffort {    type Err = String;    fn from_str(s: &str) -> Result<Self, Self::Err> {        match s {            "none" => Ok(Self::None),            "minimal" => Ok(Self::Minimal),            "low" => Ok(Self::Low),            "medium" => Ok(Self::Medium),            "high" => Ok(Self::High),            "xhigh" => Ok(Self::XHigh),            "" => Err("reasoning_effort must not be empty".to_string()),            effort => Ok(Self::Custom(effort.to_string())),        }    }}/// Canonical user-input modality tags advertised by a model.#[derive(    Debug,    Serialize,    Deserialize,    Clone,    Copy,    PartialEq,    Eq,    Display,    JsonSchema,    TS,    EnumIter,    Hash,)]#[serde(rename_all = "lowercase")]#[strum(serialize_all = "lowercase")]pub enum InputModality {    /// Plain text turns and tool payloads.    Text,    /// Image attachments included in user turns.    Image,}/// Backward-compatible default when `input_modalities` is omitted on the wire.////// Legacy payloads predate modality metadata, so we conservatively assume both text and images are/// accepted unless a preset explicitly narrows support.pub fn default_input_modalities() -> Vec<InputModality> {    vec![InputModality::Text, InputModality::Image]}/// A reasoning effort option that can be surfaced for a model.#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)]pub struct ReasoningEffortPreset {    /// Effort level that the model supports.    pub effort: ReasoningEffort,    /// Short human description shown next to the effort in UIs.    pub description: String,}#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)]pub struct ModelUpgrade {    pub id: String,    pub migration_config_key: String,    pub model_link: Option<String>,    pub upgrade_copy: Option<String>,    pub migration_markdown: Option<String>,}#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)]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 {    /// Stable identifier for the preset.    pub id: String,    /// Model slug (e.g., "gpt-5").    pub model: String,    /// Display name shown in UIs.    pub display_name: String,    /// Short human description shown in UIs.    pub description: String,    /// Reasoning effort applied when none is explicitly chosen.    pub default_reasoning_effort: ReasoningEffort,    /// Supported reasoning effort options.    pub supported_reasoning_efforts: Vec<ReasoningEffortPreset>,    /// Whether this model supports personality-specific instructions.    #[serde(default)]    pub supports_personality: bool,    /// 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>,    /// Catalog default service tier id for this model.    #[serde(default, skip_serializing_if = "Option::is_none")]    pub default_service_tier: Option<String>,    /// Whether this is the default model for new users.    pub is_default: bool,    /// recommended upgrade model    pub upgrade: Option<ModelUpgrade>,    /// Whether this preset should appear in the picker UI.    pub show_in_picker: bool,    /// Availability NUX shown when this preset becomes accessible to the user.    pub availability_nux: Option<ModelAvailabilityNux>,    /// whether this model is supported in the api    pub supported_in_api: bool,    /// Input modalities accepted when composing user turns for this preset.    #[serde(default = "default_input_modalities")]    pub input_modalities: Vec<InputModality>,}/// Visibility of a model in the picker or APIs.#[derive(    Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, TS, JsonSchema, EnumIter, Display,)]#[serde(rename_all = "lowercase")]#[strum(serialize_all = "lowercase")]pub enum ModelVisibility {    List,    Hide,    None,}/// Shell execution capability for a model.#[derive(    Debug,    Serialize,    Deserialize,    Clone,    Copy,    PartialEq,    Eq,    TS,    JsonSchema,    EnumIter,    Display,    Hash,)]#[serde(rename_all = "snake_case")]#[strum(serialize_all = "snake_case")]pub enum ConfigShellToolType {    Default,    Local,    UnifiedExec,    Disabled,    ShellCommand,}#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, TS, JsonSchema)]#[serde(rename_all = "snake_case")]pub enum ApplyPatchToolType {    Freeform,}#[derive(    Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, TS, JsonSchema, Default,)]#[serde(rename_all = "snake_case")]pub enum WebSearchToolType {    #[default]    Text,    TextAndImage,}/// Server-provided truncation policy metadata for a model.#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, TS, JsonSchema)]#[serde(rename_all = "snake_case")]pub enum TruncationMode {    Bytes,    Tokens,}#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, TS, JsonSchema)]#[serde(rename_all = "snake_case")]pub enum ToolMode {    Direct,    CodeMode,    CodeModeOnly,}fn deserialize_optional_model_selector<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>where    D: Deserializer<'de>,    T: DeserializeOwned,{    let Some(value) = Option::<String>::deserialize(deserializer)? else {        return Ok(None);    };    Ok(serde_json::from_value(serde_json::Value::String(value)).ok())}#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, TS, JsonSchema)]pub struct TruncationPolicyConfig {    pub mode: TruncationMode,    pub limit: i64,}impl TruncationPolicyConfig {    pub const fn bytes(limit: i64) -> Self {        Self {            mode: TruncationMode::Bytes,            limit,        }    }    pub const fn tokens(limit: i64) -> Self {        Self {            mode: TruncationMode::Tokens,            limit,        }    }}/// Semantic version triple encoded as an array in JSON (e.g. [0, 62, 0]).#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, TS, JsonSchema)]pub struct ClientVersion(pub i32, pub i32, pub i32);const fn default_effective_context_window_percent() -> i64 {    95}/// Model metadata returned by the Codex backend `/models` endpoint.#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]pub struct ModelInfo {    pub slug: String,    pub display_name: String,    pub description: Option<String>,    #[serde(default, skip_serializing_if = "Option::is_none")]    pub default_reasoning_level: Option<ReasoningEffort>,    pub supported_reasoning_levels: Vec<ReasoningEffortPreset>,    pub shell_type: ConfigShellToolType,    pub visibility: ModelVisibility,    pub supported_in_api: bool,    pub priority: i32,    #[serde(default)]    pub additional_speed_tiers: Vec<String>,    #[serde(default)]    pub service_tiers: Vec<ModelServiceTier>,    #[serde(default, skip_serializing_if = "Option::is_none")]    pub default_service_tier: Option<String>,    pub availability_nux: Option<ModelAvailabilityNux>,    pub upgrade: Option<ModelInfoUpgrade>,    pub base_instructions: String,    #[serde(default, skip_serializing_if = "Option::is_none")]    pub model_messages: Option<ModelMessages>,    pub supports_reasoning_summaries: bool,    #[serde(default)]    pub default_reasoning_summary: ReasoningSummary,    pub support_verbosity: bool,    pub default_verbosity: Option<Verbosity>,    pub apply_patch_tool_type: Option<ApplyPatchToolType>,    #[serde(default)]    pub web_search_tool_type: WebSearchToolType,    pub truncation_policy: TruncationPolicyConfig,    pub supports_parallel_tool_calls: bool,    #[serde(default)]    pub supports_image_detail_original: bool,    #[serde(default, skip_serializing_if = "Option::is_none")]    pub context_window: Option<i64>,    /// Maximum context window allowed for config overrides.    #[serde(default, skip_serializing_if = "Option::is_none")]    pub max_context_window: Option<i64>,    /// Token threshold for automatic compaction. When omitted, core derives it    /// from `context_window` (90%). When provided, core clamps it to 90% of the    /// context window when available.    #[serde(default, skip_serializing_if = "Option::is_none")]    pub auto_compact_token_limit: Option<i64>,    /// Opaque identifier for compaction-compatible model configurations.    #[serde(default, skip_serializing_if = "Option::is_none")]    pub comp_hash: Option<String>,    /// Percentage of the context window considered usable for inputs, after    /// reserving headroom for system prompts, tool overhead, and model output.    #[serde(default = "default_effective_context_window_percent")]    pub effective_context_window_percent: i64,    pub experimental_supported_tools: Vec<String>,    /// Input modalities accepted by the backend for this model.    #[serde(default = "default_input_modalities")]    pub input_modalities: Vec<InputModality>,    /// Internal-only marker set by core when a model slug resolved to fallback metadata.    #[serde(default, skip_serializing, skip_deserializing)]    #[schemars(skip)]    #[ts(skip)]    pub used_fallback_model_metadata: bool,    #[serde(default)]    pub supports_search_tool: bool,    #[serde(default)]    pub use_responses_lite: bool,    #[serde(default, skip_serializing_if = "Option::is_none")]    pub auto_review_model_override: Option<String>,    #[serde(        default,        skip_serializing_if = "Option::is_none",        deserialize_with = "deserialize_optional_model_selector"    )]    pub tool_mode: Option<ToolMode>,    #[serde(        default,        skip_serializing_if = "Option::is_none",        deserialize_with = "deserialize_optional_model_selector"    )]    pub multi_agent_version: Option<MultiAgentVersion>,}impl ModelInfo {    pub fn resolved_context_window(&self) -> Option<i64> {        self.context_window.or(self.max_context_window)    }    pub fn auto_compact_token_limit(&self) -> Option<i64> {        let context_limit = self            .resolved_context_window()            .map(|context_window| (context_window * 9) / 10);        let config_limit = self.auto_compact_token_limit;        if let Some(context_limit) = context_limit {            return Some(                config_limit.map_or(context_limit, |limit| std::cmp::min(limit, context_limit)),            );        }        config_limit    }    pub fn supports_personality(&self) -> bool {        self.model_messages            .as_ref()            .is_some_and(ModelMessages::supports_personality)    }    pub fn get_model_instructions(&self, personality: Option<Personality>) -> String {        if let Some(model_messages) = &self.model_messages            && let Some(template) = &model_messages.instructions_template        {            // if we have a template, always use it            let personality_message = model_messages                .get_personality_message(personality)                .unwrap_or_default();            template.replace(PERSONALITY_PLACEHOLDER, personality_message.as_str())        } else if let Some(personality) = personality {            warn!(                model = %self.slug,                %personality,                "Model personality requested but model_messages is missing, falling back to base instructions."            );            self.base_instructions.clone()        } else {            self.base_instructions.clone()        }    }}/// A strongly-typed template for assembling model instructions and developer messages. If/// instructions_* is populated and valid, it will override base_instructions.#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]pub struct ModelMessages {    pub instructions_template: Option<String>,    pub instructions_variables: Option<ModelInstructionsVariables>,}impl ModelMessages {    fn has_personality_placeholder(&self) -> bool {        self.instructions_template            .as_ref()            .map(|spec| spec.contains(PERSONALITY_PLACEHOLDER))            .unwrap_or(false)    }    fn supports_personality(&self) -> bool {        self.has_personality_placeholder()            && self                .instructions_variables                .as_ref()                .is_some_and(ModelInstructionsVariables::is_complete)    }    pub fn get_personality_message(&self, personality: Option<Personality>) -> Option<String> {        self.instructions_variables            .as_ref()            .and_then(|variables| variables.get_personality_message(personality))    }}#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]pub struct ModelInstructionsVariables {    pub personality_default: Option<String>,    pub personality_friendly: Option<String>,    pub personality_pragmatic: Option<String>,}impl ModelInstructionsVariables {    pub fn is_complete(&self) -> bool {        self.personality_default.is_some()            && self.personality_friendly.is_some()            && self.personality_pragmatic.is_some()    }    pub fn get_personality_message(&self, personality: Option<Personality>) -> Option<String> {        if let Some(personality) = personality {            match personality {                Personality::None => Some(String::new()),                Personality::Friendly => self.personality_friendly.clone(),                Personality::Pragmatic => self.personality_pragmatic.clone(),            }        } else {            self.personality_default.clone()        }    }}#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]pub struct ModelInfoUpgrade {    pub model: String,    pub migration_markdown: String,}impl From<&ModelUpgrade> for ModelInfoUpgrade {    fn from(upgrade: &ModelUpgrade) -> Self {        ModelInfoUpgrade {            model: upgrade.id.clone(),            migration_markdown: upgrade.migration_markdown.clone().unwrap_or_default(),        }    }}/// Response wrapper for `/models`.#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema, Default)]pub struct ModelsResponse {    pub models: Vec<ModelInfo>,}// convert ModelInfo to ModelPresetimpl From<ModelInfo> for ModelPreset {    fn from(info: ModelInfo) -> Self {        let supports_personality = info.supports_personality();        ModelPreset {            id: info.slug.clone(),            model: info.slug.clone(),            display_name: info.display_name,            description: info.description.unwrap_or_default(),            default_reasoning_effort: info                .default_reasoning_level                .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,            default_service_tier: info.default_service_tier,            is_default: false, // default is the highest priority available model            upgrade: info.upgrade.as_ref().map(|upgrade| ModelUpgrade {                id: upgrade.model.clone(),                migration_config_key: info.slug.clone(),                // todo(aibrahim): add the model link here.                model_link: None,                upgrade_copy: None,                migration_markdown: Some(upgrade.migration_markdown.clone()),            }),            show_in_picker: info.visibility == ModelVisibility::List,            availability_nux: info.availability_nux,            supported_in_api: info.supported_in_api,            input_modalities: info.input_modalities,        }    }}impl ModelPreset {    pub fn supports_fast_mode(&self) -> bool {        self.service_tiers            .iter()            .any(|tier| tier.id == ServiceTier::Fast.request_value())            || self                .additional_speed_tiers                .iter()                .any(|tier| tier == SPEED_TIER_FAST)    }}impl ModelInfo {    pub fn supports_service_tier(&self, service_tier: &str) -> bool {        self.service_tiers            .iter()            .any(|tier| tier.id == service_tier)    }    pub fn service_tier_for_request(&self, service_tier: Option<String>) -> Option<String> {        service_tier.filter(|service_tier| {            service_tier != SERVICE_TIER_DEFAULT_REQUEST_VALUE                && self.supports_service_tier(service_tier)        })    }}impl ModelPreset {    /// Filter models based on authentication mode.    ///    /// In ChatGPT mode, all models are visible. Otherwise, only API-supported models are shown.    pub fn filter_by_auth(models: Vec<ModelPreset>, chatgpt_mode: bool) -> Vec<ModelPreset> {        models            .into_iter()            .filter(|model| chatgpt_mode || model.supported_in_api)            .collect()    }    /// Recompute the single default preset using picker visibility.    ///    /// The first picker-visible model wins; if none are picker-visible, the first model wins.    pub fn mark_default_by_picker_visibility(models: &mut [ModelPreset]) {        for preset in models.iter_mut() {            preset.is_default = false;        }        if let Some(default) = models.iter_mut().find(|preset| preset.show_in_picker) {            default.is_default = true;        } else if let Some(default) = models.first_mut() {            default.is_default = true;        }    }}#[cfg(test)]mod tests {    use super::*;    use pretty_assertions::assert_eq;    use serde_json::from_str;    use serde_json::to_string;    fn test_model(spec: Option<ModelMessages>) -> ModelInfo {        ModelInfo {            slug: "test-model".to_string(),            display_name: "Test Model".to_string(),            description: None,            default_reasoning_level: None,            supported_reasoning_levels: vec![],            shell_type: ConfigShellToolType::ShellCommand,            visibility: ModelVisibility::List,            supported_in_api: true,            priority: 1,            additional_speed_tiers: Vec::new(),            service_tiers: Vec::new(),            default_service_tier: None,            availability_nux: None,            upgrade: None,            base_instructions: "base".to_string(),            model_messages: spec,            supports_reasoning_summaries: false,            default_reasoning_summary: ReasoningSummary::Auto,            support_verbosity: false,            default_verbosity: None,            apply_patch_tool_type: None,            web_search_tool_type: WebSearchToolType::Text,            truncation_policy: TruncationPolicyConfig::bytes(/*limit*/ 10_000),            supports_parallel_tool_calls: false,            supports_image_detail_original: false,            context_window: None,            max_context_window: None,            auto_compact_token_limit: None,            comp_hash: None,            effective_context_window_percent: 95,            experimental_supported_tools: vec![],            input_modalities: default_input_modalities(),            used_fallback_model_metadata: false,            supports_search_tool: false,            use_responses_lite: false,            auto_review_model_override: None,            tool_mode: None,            multi_agent_version: None,        }    }    fn personality_variables() -> ModelInstructionsVariables {        ModelInstructionsVariables {            personality_default: Some("default".to_string()),            personality_friendly: Some("friendly".to_string()),            personality_pragmatic: Some("pragmatic".to_string()),        }    }    #[test]    fn reasoning_effort_accepts_known_and_custom_values() {        let custom = ReasoningEffort::Custom("max".to_string());        let deserialized = from_str::<ReasoningEffort>(r#""max""#)            .expect("custom reasoning effort should deserialize");        let serialized = to_string(&custom).expect("custom reasoning effort should serialize");        assert_eq!(            (                "high".parse(),                "max".parse(),                deserialized,                serialized,                custom.to_string(),            ),            (                Ok(ReasoningEffort::High),                Ok(custom.clone()),                custom,                r#""max""#.to_string(),                "max".to_string(),            )        );    }    #[test]    fn reasoning_effort_rejects_empty_values() {        assert_eq!(            "".parse::<ReasoningEffort>(),            Err("reasoning_effort must not be empty".to_string())        );    }    #[test]    fn reasoning_effort_json_schema_is_an_open_string() {        let mut effort_generator = SchemaGenerator::default();        assert_eq!(            ReasoningEffort::json_schema(&mut effort_generator),            Schema::Object(SchemaObject {                instance_type: Some(InstanceType::String.into()),                metadata: Some(Box::new(Metadata {                    description: Some(                        "A non-empty reasoning effort value advertised by the model.".to_string(),                    ),                    ..Default::default()                })),                string: Some(Box::new(StringValidation {                    min_length: Some(1),                    ..Default::default()                })),                ..Default::default()            })        );    }    #[test]    fn get_model_instructions_uses_template_when_placeholder_present() {        let model = test_model(Some(ModelMessages {            instructions_template: Some("Hello {{ personality }}".to_string()),            instructions_variables: Some(personality_variables()),        }));        let instructions = model.get_model_instructions(Some(Personality::Friendly));        assert_eq!(instructions, "Hello friendly");    }    #[test]    fn get_model_instructions_always_strips_placeholder() {        let model = test_model(Some(ModelMessages {            instructions_template: Some("Hello\n{{ personality }}".to_string()),            instructions_variables: Some(ModelInstructionsVariables {                personality_default: None,                personality_friendly: Some("friendly".to_string()),                personality_pragmatic: None,            }),        }));        assert_eq!(            model.get_model_instructions(Some(Personality::Friendly)),            "Hello\nfriendly"        );        assert_eq!(            model.get_model_instructions(Some(Personality::Pragmatic)),            "Hello\n"        );        assert_eq!(            model.get_model_instructions(Some(Personality::None)),            "Hello\n"        );        assert_eq!(            model.get_model_instructions(/*personality*/ None),            "Hello\n"        );        let model_no_personality = test_model(Some(ModelMessages {            instructions_template: Some("Hello\n{{ personality }}".to_string()),            instructions_variables: Some(ModelInstructionsVariables {                personality_default: None,                personality_friendly: None,                personality_pragmatic: None,            }),        }));        assert_eq!(            model_no_personality.get_model_instructions(Some(Personality::Friendly)),            "Hello\n"        );        assert_eq!(            model_no_personality.get_model_instructions(Some(Personality::Pragmatic)),            "Hello\n"        );        assert_eq!(            model_no_personality.get_model_instructions(Some(Personality::None)),            "Hello\n"        );        assert_eq!(            model_no_personality.get_model_instructions(/*personality*/ None),            "Hello\n"        );    }    #[test]    fn get_model_instructions_falls_back_when_template_is_missing() {        let model = test_model(Some(ModelMessages {            instructions_template: None,            instructions_variables: Some(ModelInstructionsVariables {                personality_default: None,                personality_friendly: None,                personality_pragmatic: None,            }),        }));        let instructions = model.get_model_instructions(Some(Personality::Friendly));        assert_eq!(instructions, "base");    }    #[test]    fn get_personality_message_returns_default_when_personality_is_none() {        let personality_template = personality_variables();        assert_eq!(            personality_template.get_personality_message(/*personality*/ None),            Some("default".to_string())        );    }    #[test]    fn get_personality_message() {        let personality_variables = personality_variables();        assert_eq!(            personality_variables.get_personality_message(Some(Personality::Friendly)),            Some("friendly".to_string())        );        assert_eq!(            personality_variables.get_personality_message(Some(Personality::Pragmatic)),            Some("pragmatic".to_string())        );        assert_eq!(            personality_variables.get_personality_message(Some(Personality::None)),            Some(String::new())        );        assert_eq!(            personality_variables.get_personality_message(/*personality*/ None),            Some("default".to_string())        );        let personality_variables = ModelInstructionsVariables {            personality_default: Some("default".to_string()),            personality_friendly: None,            personality_pragmatic: None,        };        assert_eq!(            personality_variables.get_personality_message(Some(Personality::Friendly)),            None        );        assert_eq!(            personality_variables.get_personality_message(Some(Personality::Pragmatic)),            None        );        assert_eq!(            personality_variables.get_personality_message(Some(Personality::None)),            Some(String::new())        );        assert_eq!(            personality_variables.get_personality_message(/*personality*/ None),            Some("default".to_string())        );        let personality_variables = ModelInstructionsVariables {            personality_default: None,            personality_friendly: Some("friendly".to_string()),            personality_pragmatic: Some("pragmatic".to_string()),        };        assert_eq!(            personality_variables.get_personality_message(Some(Personality::Friendly)),            Some("friendly".to_string())        );        assert_eq!(            personality_variables.get_personality_message(Some(Personality::Pragmatic)),            Some("pragmatic".to_string())        );        assert_eq!(            personality_variables.get_personality_message(Some(Personality::None)),            Some(String::new())        );        assert_eq!(            personality_variables.get_personality_message(/*personality*/ None),            None        );    }    #[test]    fn model_info_defaults_availability_nux_to_none_when_omitted() {        let model: ModelInfo = serde_json::from_value(serde_json::json!({            "slug": "test-model",            "display_name": "Test Model",            "description": null,            "supported_reasoning_levels": [],            "shell_type": "shell_command",            "visibility": "list",            "supported_in_api": true,            "priority": 1,            "upgrade": null,            "base_instructions": "base",            "model_messages": null,            "supports_reasoning_summaries": false,            "default_reasoning_summary": "auto",            "support_verbosity": false,            "default_verbosity": null,            "apply_patch_tool_type": null,            "truncation_policy": {                "mode": "bytes",                "limit": 10000            },            "supports_parallel_tool_calls": false,            "supports_image_detail_original": false,            "context_window": null,            "auto_compact_token_limit": null,            "effective_context_window_percent": 95,            "experimental_supported_tools": [],            "input_modalities": ["text", "image"]        }))        .expect("deserialize model info");        assert_eq!(model.availability_nux, None);        assert!(!model.supports_image_detail_original);        assert_eq!(model.web_search_tool_type, WebSearchToolType::Text);        assert!(!model.supports_search_tool);        assert!(!model.use_responses_lite);        assert_eq!(model.comp_hash, None);        assert_eq!(model.auto_review_model_override, None);        assert_eq!(model.tool_mode, None);    }    #[test]    fn model_info_deserializes_known_tool_mode() {        let mut value =            serde_json::to_value(test_model(/*spec*/ None)).expect("serialize test model");        let object = value            .as_object_mut()            .expect("model info should be an object");        object.insert(            "tool_mode".to_string(),            serde_json::Value::String("code_mode_only".to_string()),        );        let model = serde_json::from_value::<ModelInfo>(value).expect("deserialize model info");        assert_eq!(model.tool_mode, Some(ToolMode::CodeModeOnly));    }    #[test]    fn model_info_treats_unknown_tool_mode_as_omitted() {        let mut value =            serde_json::to_value(test_model(/*spec*/ None)).expect("serialize test model");        let object = value            .as_object_mut()            .expect("model info should be an object");        object.insert(            "tool_mode".to_string(),            serde_json::Value::String("future_tool_mode".to_string()),        );        let model = serde_json::from_value::<ModelInfo>(value).expect("deserialize model info");        assert_eq!(model.tool_mode, None);        let serialized = serde_json::to_value(model).expect("serialize model info");        let object = serialized            .as_object()            .expect("model info should be an object");        assert!(!object.contains_key("tool_mode"));    }    #[test]    fn model_info_treats_unknown_multi_agent_version_as_omitted() {        let mut value =            serde_json::to_value(test_model(/*spec*/ None)).expect("serialize test model");        let object = value            .as_object_mut()            .expect("model info should be an object");        object.insert(            "multi_agent_version".to_string(),            serde_json::Value::String("future_multi_agent_version".to_string()),        );        let model = serde_json::from_value::<ModelInfo>(value).expect("deserialize model info");        assert_eq!(model.multi_agent_version, None);    }    #[test]    fn resolved_context_window_prefers_context_window() {        let model = ModelInfo {            context_window: Some(273_000),            max_context_window: Some(400_000),            ..test_model(/*spec*/ None)        };        assert_eq!(model.resolved_context_window(), Some(273_000));    }    #[test]    fn resolved_context_window_falls_back_to_max_context_window() {        let model = ModelInfo {            context_window: None,            max_context_window: Some(400_000),            ..test_model(/*spec*/ None)        };        assert_eq!(model.resolved_context_window(), Some(400_000));        assert_eq!(model.auto_compact_token_limit(), Some(360_000));    }    #[test]    fn model_preset_preserves_availability_nux() {        let preset = ModelPreset::from(ModelInfo {            availability_nux: Some(ModelAvailabilityNux {                message: "Try Spark.".to_string(),            }),            additional_speed_tiers: vec![SPEED_TIER_FAST.to_string()],            default_service_tier: Some(ServiceTier::Fast.request_value().to_string()),            service_tiers: Vec::new(),            ..test_model(/*spec*/ None)        });        assert_eq!(            preset.availability_nux,            Some(ModelAvailabilityNux {                message: "Try Spark.".to_string(),            })        );        assert!(preset.supports_fast_mode());        assert_eq!(            preset.default_service_tier,            Some(ServiceTier::Fast.request_value().to_string())        );    }    #[test]    fn model_preset_supports_fast_mode_from_service_tiers() {        let preset = ModelPreset::from(ModelInfo {            service_tiers: vec![ModelServiceTier {                id: ServiceTier::Fast.request_value().to_string(),                name: "Fast".to_string(),                description: "Priority processing.".to_string(),            }],            ..test_model(/*spec*/ None)        });        assert!(preset.supports_fast_mode());    }    #[test]    fn service_tier_for_request_omits_explicit_default_tier() {        let model = ModelInfo {            default_service_tier: Some(ServiceTier::Fast.request_value().to_string()),            service_tiers: vec![ModelServiceTier {                id: ServiceTier::Fast.request_value().to_string(),                name: "Fast".to_string(),                description: "Priority processing.".to_string(),            }],            ..test_model(/*spec*/ None)        };        assert_eq!(            model.service_tier_for_request(Some(SERVICE_TIER_DEFAULT_REQUEST_VALUE.to_string())),            None        );    }    #[test]    fn service_tier_for_request_filters_unsupported_tiers() {        let model = ModelInfo {            default_service_tier: Some(ServiceTier::Fast.request_value().to_string()),            service_tiers: vec![ModelServiceTier {                id: ServiceTier::Fast.request_value().to_string(),                name: "Fast".to_string(),                description: "Priority processing.".to_string(),            }],            ..test_model(/*spec*/ None)        };        assert_eq!(            model.service_tier_for_request(Some(ServiceTier::Fast.request_value().to_string())),            Some(ServiceTier::Fast.request_value().to_string())        );        assert_eq!(            model.service_tier_for_request(Some("unsupported".to_string())),            None        );        assert_eq!(model.service_tier_for_request(/*service_tier*/ None), None);    }    #[test]    fn service_tier_for_request_does_not_apply_catalog_default() {        let model = ModelInfo {            default_service_tier: Some(ServiceTier::Fast.request_value().to_string()),            service_tiers: vec![ModelServiceTier {                id: ServiceTier::Fast.request_value().to_string(),                name: "Fast".to_string(),                description: "Priority processing.".to_string(),            }],            ..test_model(/*spec*/ None)        };        assert_eq!(model.service_tier_for_request(/*service_tier*/ None), None);    }}