//! 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 chrono::DateTime; use chrono::Utc; 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; mod access_programs; #[path = "openai_models/guardian.rs"] mod guardian; pub use guardian::GuardianModelPolicy; pub use guardian::GuardianReviewMode; pub use guardian::GuardianScope; #[path = "openai_models/guardian_v2.rs"] mod guardian_v2; #[path = "openai_models/reasoning_effort.rs"] mod reasoning_effort; pub use access_programs::ModelAccessPrograms; pub use guardian_v2::GuardianV2ModelConfig; pub use guardian_v2::GuardianV2TranscriptModelConfig; /// Backend model-catalog specialty identifying cybersecurity-focused models. pub const MODEL_SPECIALTY_CYBER: &str = "cyber"; 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, Max, Ultra, Persistent, /// 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::Max => "max", Self::Ultra => "ultra", Self::Persistent => "persistent", 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(&self, serializer: S) -> Result where S: Serializer, { serializer.serialize_str(self.as_str()) } } impl<'de> Deserialize<'de> for ReasoningEffort { fn deserialize(deserializer: D) -> Result 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 { 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), "max" => Ok(Self::Max), "ultra" => Ok(Self::Ultra), "persistent" => Ok(Self::Persistent), "" => 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, /// Audio attachments included in user turns. Audio, } /// 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 { 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, pub upgrade_copy: Option, pub migration_markdown: Option, /// Informational time when the model associated with this upgrade is scheduled to retire. #[serde( default, skip_serializing_if = "Option::is_none", deserialize_with = "deserialize_optional_rfc3339_timestamp" )] #[schemars(with = "Option")] #[ts(type = "string", optional)] pub retirement_at: Option>, } #[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, #[serde(default, skip_serializing_if = "Option::is_none")] pub model_specialty: Option, /// Reasoning effort applied when none is explicitly chosen. pub default_reasoning_effort: ReasoningEffort, /// Supported reasoning effort options. pub supported_reasoning_efforts: Vec, /// Deprecated catalog field, always false for new model presets. #[serde(default)] pub supports_personality: bool, /// Deprecated: use `service_tiers` instead. #[serde(default)] pub additional_speed_tiers: Vec, /// Service tiers this model can run with. #[serde(default)] pub service_tiers: Vec, /// Catalog default service tier id for this model. #[serde(default, skip_serializing_if = "Option::is_none")] pub default_service_tier: Option, /// Caller-specific explicit access programs, when discovery resolved them. #[serde(default, skip_serializing_if = "Option::is_none")] pub available_access_programs: Option, /// Whether this is the default model for new users. pub is_default: bool, /// recommended upgrade model pub upgrade: Option, /// Whether this preset should appear in the picker UI. pub show_in_picker: bool, /// Multi-agent backend selected when this model starts a new thread. #[serde(default, skip_serializing, skip_deserializing)] #[schemars(skip)] #[ts(skip)] pub multi_agent_version: Option, /// Availability NUX shown when this preset becomes accessible to the user. pub availability_nux: Option, /// 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, } /// 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 { #[serde(alias = "default", alias = "local", alias = "shell_command")] UnifiedExec, Disabled, } #[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, D::Error> where D: Deserializer<'de>, T: DeserializeOwned, { let Some(value) = Option::::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 } const fn default_true() -> bool { true } #[allow(clippy::trivially_copy_pass_by_ref)] const fn is_true(value: &bool) -> bool { *value } /// Model metadata returned by the Codex backend `/models` endpoint. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)] pub struct ModelInfo { /// Model-owned approval coverage. Absent preserves legacy settings; an empty map disables /// ordinary Guardian review. Keys are computer_use, shell, file_changes, mcp, network, /// and permissions. This does not override mandatory safety or administrator requirements. #[serde(default, skip_serializing_if = "Option::is_none")] pub guardian: Option, pub slug: String, pub display_name: String, pub description: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub default_reasoning_level: Option, pub supported_reasoning_levels: Vec, pub shell_type: ConfigShellToolType, pub visibility: ModelVisibility, pub supported_in_api: bool, pub priority: i32, #[serde(default)] pub additional_speed_tiers: Vec, #[serde(default)] pub service_tiers: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub default_service_tier: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub available_access_programs: Option, pub availability_nux: Option, pub upgrade: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub model_messages: Option, #[serde(default)] pub include_skills_usage_instructions: bool, #[serde(default)] pub include_plugin_usage_instructions: bool, #[serde(default = "default_true")] pub include_apps_usage_instructions: bool, /// Whether the model accepts the Responses API `reasoning.summary` parameter. #[serde(default = "default_true", skip_serializing_if = "is_true")] pub supports_reasoning_summary_parameter: bool, #[serde(default)] pub default_reasoning_summary: ReasoningSummary, pub support_verbosity: bool, pub default_verbosity: Option, pub apply_patch_tool_type: Option, #[serde(default)] pub web_search_tool_type: WebSearchToolType, pub truncation_policy: TruncationPolicyConfig, #[serde(default)] pub supports_image_detail_original: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub context_window: Option, /// Maximum context window allowed for config overrides. #[serde(default, skip_serializing_if = "Option::is_none")] pub max_context_window: Option, /// 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, /// Opaque identifier for compaction-compatible model configurations. #[serde(default, skip_serializing_if = "Option::is_none")] pub comp_hash: Option, /// 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, /// Input modalities accepted by the backend for this model. #[serde(default = "default_input_modalities")] pub input_modalities: Vec, /// 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, /// Whether experimental context management may be activated at session startup. #[serde(default)] pub supports_experimental_context: bool, #[serde(default)] pub use_responses_lite: bool, #[serde(default)] pub node_repl_auto_review_required: bool, #[serde(default)] pub node_repl_disabled: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub auto_review_model_override: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub model_specialty: Option, #[serde( default, skip_serializing_if = "Option::is_none", deserialize_with = "deserialize_optional_model_selector" )] pub tool_mode: Option, #[serde( default, skip_serializing_if = "Option::is_none", deserialize_with = "deserialize_optional_model_selector" )] pub multi_agent_version: Option, /// Reasoning effort used for multi-agent work when the user selects Ultra. #[serde(default, skip_serializing_if = "Option::is_none")] pub multi_agent_reasoning_effort: Option, } impl ModelInfo { pub fn resolved_context_window(&self) -> Option { self.context_window.or(self.max_context_window) } /// Context available to inference after reserving this model's configured headroom. pub fn usable_context_window(&self) -> Option { self.resolved_context_window().map(|context_window| { context_window.saturating_mul(self.effective_context_window_percent) / 100 }) } pub fn auto_compact_token_limit(&self) -> Option { 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 } /// Returns the literal instruction template. The personality argument remains for older /// callers; the `None` opt-out is applied separately by models-manager. pub fn get_model_instructions(&self, _personality: Option) -> String { if let Some(model_messages) = &self.model_messages && let Some(template) = &model_messages.instructions_template { template.clone() } else { warn!( model = %self.slug, "Model has no instruction template; returning empty instructions." ); String::new() } } } /// A strongly-typed template for assembling model instructions and developer messages. /// /// `instructions_template` is literal text. The deprecated `instructions_variables` field is /// retained to decode catalogs produced before personality selection was removed. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)] pub struct ModelMessages { /// Additional developer instructions for persistent mode. Missing or null uses the built-in /// instructions; an empty string disables them. #[serde(default, skip_serializing_if = "Option::is_none")] pub persistent_instructions: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub tools: Option, pub instructions_template: Option, pub instructions_variables: Option, pub approvals: Option, pub collaboration_modes: Option, pub auto_review: Option, pub permissions: Option, pub multi_agent: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub token_budget: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub guardian_v2: Option, /// Replacement confirmation-policy documents forwarded in actor MCP request metadata. #[serde(default, skip_serializing_if = "Option::is_none")] pub confirmation_policies: Option, } /// Model-owned confirmation-policy Markdown, forwarded unchanged to actor tools. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)] pub struct ConfirmationPolicies { /// Replacement Markdown for the Browser Use confirmation-policy document. #[serde(default, skip_serializing_if = "Option::is_none")] pub browser_use: Option, /// Replacement Markdown for the native Computer Use confirmation policy. #[serde(default, skip_serializing_if = "Option::is_none")] pub computer_use: Option, } /// Model-owned messages for built-in tools. #[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)] pub struct ToolMessages { #[serde(default, skip_serializing_if = "Option::is_none")] pub send_user_message_async: Option, } /// Model-owned messages for a built-in tool. #[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)] pub struct ToolMessage { /// Missing or null uses the built-in description; an empty string leaves the description /// empty without disabling the tool. #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, } /// Model-owned defaults for the context-window token-budget feature. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)] pub struct ModelTokenBudgetConfig { #[serde(default)] pub enabled: bool, #[serde(default)] pub use_history_notes_extension: bool, pub reminder_threshold_tokens: i64, pub reminder_message_template: String, pub guidance_message: String, pub auto_compact_fallback_prompt: String, pub auto_compact_fallback_buffer_tokens: i64, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)] pub struct ApprovalMessages { pub on_request: Option, pub on_request_auto_review: Option, pub never: Option, pub unless_trusted: Option, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)] pub struct CollaborationModeMessages { pub default: Option, pub plan: Option, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)] pub struct AutoReviewMessages { pub policy: Option, pub policy_template: Option, /// Extra developer policy for `node_repl` and `cua_repl` reviews. #[serde(default, skip_serializing_if = "Option::is_none")] pub node_repl_policy: Option, pub rejection_instructions: Option, pub timeout_instructions: Option, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)] pub struct PermissionMessages { pub danger_full_access: Option, pub workspace_write: Option, pub read_only: Option, } #[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)] pub struct MultiAgentMessages { pub role: Option, pub mode: Option, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)] pub struct MultiAgentRoleMessages { pub root: Option, pub subagent: Option, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)] pub struct MultiAgentModeMessages { pub explicit: Option, /// Ultra-only mode instructions. Missing or null uses the built-in proactive hint; /// an empty string suppresses the mode message. `hint_text` takes precedence. pub proactive: Option, pub hint_text: Option, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)] pub struct ModelInstructionsVariables { pub personality_default: Option, pub personality_friendly: Option, pub personality_pragmatic: Option, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)] pub struct ModelInfoUpgrade { pub model: String, pub migration_markdown: String, /// Informational time when the model associated with this upgrade is scheduled to retire. #[serde( default, skip_serializing_if = "Option::is_none", deserialize_with = "deserialize_optional_rfc3339_timestamp" )] #[schemars(with = "Option")] #[ts(type = "string", optional)] pub retirement_at: Option>, } fn deserialize_optional_rfc3339_timestamp<'de, D>( deserializer: D, ) -> Result>, D::Error> where D: Deserializer<'de>, { let value = Option::::deserialize(deserializer)?; Ok(value .as_ref() .and_then(serde_json::Value::as_str) .and_then(|value| DateTime::parse_from_rfc3339(value).ok()) .map(|value| value.with_timezone(&Utc))) } impl From<&ModelUpgrade> for ModelInfoUpgrade { fn from(upgrade: &ModelUpgrade) -> Self { ModelInfoUpgrade { model: upgrade.id.clone(), migration_markdown: upgrade.migration_markdown.clone().unwrap_or_default(), retirement_at: upgrade.retirement_at, } } } /// Response wrapper for `/models`. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema, Default)] pub struct ModelsResponse { #[serde( serialize_with = "serialize_model_infos_with_legacy_base", deserialize_with = "deserialize_model_infos_with_legacy_base" )] pub models: Vec, } #[derive(Serialize)] struct ModelInfoWithLegacyBaseInstructionsRef<'a> { #[serde(flatten)] model: &'a ModelInfo, base_instructions: String, } #[derive(Deserialize)] struct ModelInfoWithLegacyBaseInstructions { #[serde(default)] base_instructions: Option, #[serde(flatten)] model: ModelInfo, } /// Serializes catalog models with the deprecated top-level instruction field required by older /// clients. #[doc(hidden)] pub fn serialize_model_infos_with_legacy_base( models: &[ModelInfo], serializer: S, ) -> Result where S: Serializer, { models .iter() .map(|model| ModelInfoWithLegacyBaseInstructionsRef { model, base_instructions: model.get_model_instructions(/*personality*/ None), }) .collect::>() .serialize(serializer) } /// Deserializes catalog models while promoting the legacy top-level instruction field into Model /// Messages V2 when no canonical instruction template is present. #[doc(hidden)] pub fn deserialize_model_infos_with_legacy_base<'de, D>( deserializer: D, ) -> Result, D::Error> where D: Deserializer<'de>, { let models = Vec::::deserialize(deserializer)?; models .into_iter() .map(|legacy_model| { let ModelInfoWithLegacyBaseInstructions { base_instructions, mut model, } = legacy_model; if let Some(base_instructions) = base_instructions && model .model_messages .as_ref() .and_then(|messages| messages.instructions_template.as_ref()) .is_none() { let messages = model.model_messages.get_or_insert(ModelMessages { persistent_instructions: None, tools: None, instructions_template: None, instructions_variables: None, approvals: None, collaboration_modes: None, auto_review: None, permissions: None, multi_agent: None, token_budget: None, confirmation_policies: None, guardian_v2: None, }); messages.instructions_template = Some(base_instructions); } if model .model_messages .as_ref() .and_then(|messages| messages.instructions_template.as_ref()) .is_none() { let model_slug = &model.slug; return Err(D::Error::custom(format!( "model `{model_slug}` is missing both `base_instructions` and \ `model_messages.instructions_template`" ))); } Ok(model) }) .collect() } // convert ModelInfo to ModelPreset impl From for ModelPreset { fn from(info: ModelInfo) -> Self { ModelPreset { id: info.slug.clone(), model: info.slug.clone(), display_name: info.display_name, description: info.description.unwrap_or_default(), model_specialty: info.model_specialty, default_reasoning_effort: info .default_reasoning_level .unwrap_or(ReasoningEffort::None), supported_reasoning_efforts: info.supported_reasoning_levels.clone(), supports_personality: false, additional_speed_tiers: info.additional_speed_tiers, service_tiers: info.service_tiers, default_service_tier: info.default_service_tier, available_access_programs: info.available_access_programs, 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()), retirement_at: upgrade.retirement_at, }), show_in_picker: info.visibility == ModelVisibility::List, multi_agent_version: info.multi_agent_version, 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) -> Option { 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, chatgpt_mode: bool) -> Vec { 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; #[test] fn legacy_shell_model_metadata_deserializes_as_unified_exec() { for legacy_shell_type in ["default", "local", "shell_command"] { assert_eq!( from_str::(&format!("\"{legacy_shell_type}\"")) .expect("legacy shell type"), ConfigShellToolType::UnifiedExec ); } assert_eq!( to_string(&ConfigShellToolType::UnifiedExec).expect("serialize unified shell type"), "\"unified_exec\"" ); } pub(super) fn test_model(spec: Option) -> 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::UnifiedExec, visibility: ModelVisibility::List, supported_in_api: true, priority: 1, additional_speed_tiers: Vec::new(), service_tiers: Vec::new(), default_service_tier: None, available_access_programs: None, availability_nux: None, upgrade: None, model_messages: spec, include_skills_usage_instructions: false, include_plugin_usage_instructions: false, include_apps_usage_instructions: false, supports_reasoning_summary_parameter: true, 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_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, supports_experimental_context: false, use_responses_lite: false, guardian: None, node_repl_auto_review_required: false, node_repl_disabled: false, auto_review_model_override: None, model_specialty: None, tool_mode: None, multi_agent_version: None, multi_agent_reasoning_effort: None, } } #[test] fn model_messages_deserialize_without_optional_sections() { let messages: ModelMessages = from_str( r#"{"instructions_template":null,"instructions_variables":null,"persistent_instructions":null}"#, ) .expect("model messages should deserialize"); assert_eq!( messages, ModelMessages { persistent_instructions: None, tools: None, instructions_template: None, instructions_variables: None, approvals: None, collaboration_modes: None, auto_review: None, permissions: None, multi_agent: None, token_budget: None, confirmation_policies: None, guardian_v2: None, } ); } #[test] fn send_user_message_async_description_preserves_missing_null_and_empty_values() { for (value, expected) in [ (serde_json::json!({}), None), (serde_json::json!({"tools": null}), None), (serde_json::json!({"tools": {}}), Some(None)), ( serde_json::json!({"tools": {"send_user_message_async": null}}), Some(None), ), ( serde_json::json!({"tools": {"send_user_message_async": {}}}), Some(Some(ToolMessage::default())), ), ( serde_json::json!({"tools": {"send_user_message_async": {"description": null}}}), Some(Some(ToolMessage::default())), ), ( serde_json::json!({"tools": {"send_user_message_async": {"description": ""}}}), Some(Some(ToolMessage { description: Some(String::new()), })), ), ( serde_json::json!({"tools": {"send_user_message_async": {"description": "Catalog description"}}}), Some(Some(ToolMessage { description: Some("Catalog description".to_string()), })), ), ] { let messages: ModelMessages = serde_json::from_value(value).expect("model messages should deserialize"); let serialized = serde_json::to_value(&messages).expect("model messages should serialize"); assert_eq!( ( messages.tools, serialized .get("tools") .map(|tools| tools.get("send_user_message_async").cloned()), ), ( expected.as_ref().map(|tool| ToolMessages { send_user_message_async: tool.clone(), }), expected.map(|tool| tool.map(|tool| match tool.description { Some(description) => serde_json::json!({"description": description}), None => serde_json::json!({}), })), ) ); } } #[test] fn approval_messages_preserve_missing_and_empty_values() { let messages: ModelMessages = from_str( r#"{ "instructions_template": null, "instructions_variables": null, "approvals": { "on_request": "", "never": "" } }"#, ) .expect("approval messages should deserialize"); assert_eq!( messages.approvals, Some(ApprovalMessages { on_request: Some(String::new()), on_request_auto_review: None, never: Some(String::new()), unless_trusted: None, }) ); } #[test] fn auto_review_messages_preserve_missing_and_empty_values() { let missing_template: ModelMessages = from_str( r#"{ "instructions_template": null, "instructions_variables": null, "auto_review": { "policy": "policy" } }"#, ) .expect("auto-review messages should deserialize without a policy template"); let empty_template: ModelMessages = from_str( r#"{ "instructions_template": null, "instructions_variables": null, "auto_review": { "policy": "policy", "policy_template": "", "node_repl_policy": "", "rejection_instructions": "", "timeout_instructions": "" } }"#, ) .expect("auto-review messages should deserialize with an empty policy template"); assert_eq!( missing_template.auto_review, Some(AutoReviewMessages { policy: Some("policy".to_string()), policy_template: None, node_repl_policy: None, rejection_instructions: None, timeout_instructions: None, }) ); assert_eq!( empty_template.auto_review, Some(AutoReviewMessages { policy: Some("policy".to_string()), policy_template: Some(String::new()), node_repl_policy: Some(String::new()), rejection_instructions: Some(String::new()), timeout_instructions: Some(String::new()), }) ); } #[test] fn permission_messages_preserve_missing_and_empty_values() { let messages: ModelMessages = from_str( r#"{ "instructions_template": null, "instructions_variables": null, "permissions": { "workspace_write": "" } }"#, ) .expect("permission messages should deserialize"); assert_eq!( messages.permissions, Some(PermissionMessages { danger_full_access: None, workspace_write: Some(String::new()), read_only: None, }) ); } #[test] fn multi_agent_messages_preserve_missing_and_empty_values() { let messages: ModelMessages = from_str( r#"{"instructions_template":null,"instructions_variables":null,"multi_agent":{"role":{"root":"","subagent":"subagent base"},"mode":{"explicit":"explicit mode","proactive":"","hint_text":""}}}"#, ) .expect("multi-agent messages should deserialize"); assert_eq!( messages.multi_agent, Some(MultiAgentMessages { role: Some(MultiAgentRoleMessages { root: Some(String::new()), subagent: Some("subagent base".to_string()), }), mode: Some(MultiAgentModeMessages { explicit: Some("explicit mode".to_string()), proactive: Some(String::new()), hint_text: Some(String::new()), }), }) ); } #[test] fn collaboration_mode_messages_preserve_missing_and_empty_values() { let messages: ModelMessages = from_str( r#"{ "instructions_template": null, "instructions_variables": null, "collaboration_modes": { "default": "" } }"#, ) .expect("collaboration mode messages should deserialize"); assert_eq!( messages, ModelMessages { persistent_instructions: None, tools: None, instructions_template: None, instructions_variables: None, approvals: None, collaboration_modes: Some(CollaborationModeMessages { default: Some(String::new()), plan: None, }), auto_review: None, permissions: None, multi_agent: None, token_budget: None, confirmation_policies: None, guardian_v2: None, } ); } #[test] fn reasoning_effort_accepts_known_and_custom_values() { let custom = ReasoningEffort::Custom("future".to_string()); let deserialized = from_str::(r#""future""#) .expect("custom reasoning effort should deserialize"); let serialized = to_string(&custom).expect("custom reasoning effort should serialize"); let serialized_max = to_string(&ReasoningEffort::Max).expect("Max should serialize"); let serialized_ultra = to_string(&ReasoningEffort::Ultra).expect("Ultra should serialize"); let serialized_persistent = to_string(&ReasoningEffort::Persistent).expect("Persistent should serialize"); assert_eq!( ( "high".parse(), "max".parse(), "ultra".parse(), "persistent".parse(), "future".parse(), deserialized, serialized, serialized_max, serialized_ultra, serialized_persistent, custom.to_string(), ), ( Ok(ReasoningEffort::High), Ok(ReasoningEffort::Max), Ok(ReasoningEffort::Ultra), Ok(ReasoningEffort::Persistent), Ok(custom.clone()), custom, r#""future""#.to_string(), r#""max""#.to_string(), r#""ultra""#.to_string(), r#""persistent""#.to_string(), "future".to_string(), ) ); } #[test] fn reasoning_effort_rejects_empty_values() { assert_eq!( "".parse::(), 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_ignores_legacy_personality_variables() { let model = test_model(Some(ModelMessages { persistent_instructions: None, tools: None, instructions_template: Some("Hello {{ personality }}".to_string()), instructions_variables: Some(ModelInstructionsVariables { personality_default: Some("default".to_string()), personality_friendly: Some("friendly".to_string()), personality_pragmatic: Some("pragmatic".to_string()), }), approvals: None, collaboration_modes: None, auto_review: None, permissions: None, multi_agent: None, token_budget: None, confirmation_policies: None, guardian_v2: None, })); let instructions = "Hello {{ personality }}"; for personality in [ Some(Personality::Friendly), Some(Personality::Pragmatic), Some(Personality::None), None, ] { assert_eq!(model.get_model_instructions(personality), instructions); } } #[test] fn get_model_instructions_is_empty_when_template_is_missing() { let model = test_model(Some(ModelMessages { persistent_instructions: None, tools: None, instructions_template: None, instructions_variables: Some(ModelInstructionsVariables { personality_default: None, personality_friendly: None, personality_pragmatic: None, }), approvals: None, collaboration_modes: None, auto_review: None, permissions: None, multi_agent: None, token_budget: None, confirmation_policies: None, guardian_v2: None, })); let instructions = model.get_model_instructions(Some(Personality::Friendly)); assert_eq!(instructions, ""); } #[test] fn get_model_instructions_is_empty_when_model_messages_is_missing() { let model = test_model(/*spec*/ None); assert_eq!( model.get_model_instructions(Some(Personality::Friendly)), "" ); } #[test] fn models_response_promotes_legacy_base_instructions() { let mut value = serde_json::to_value(ModelsResponse { models: vec![test_model(/*spec*/ None)], }) .expect("serialize models response"); value["models"][0]["base_instructions"] = serde_json::json!("legacy instructions"); let response: ModelsResponse = serde_json::from_value(value).expect("deserialize legacy models response"); let model = &response.models[0]; assert_eq!( model.model_messages, Some(ModelMessages { persistent_instructions: None, tools: None, instructions_template: Some("legacy instructions".to_string()), instructions_variables: None, approvals: None, collaboration_modes: None, auto_review: None, permissions: None, multi_agent: None, token_budget: None, confirmation_policies: None, guardian_v2: None, }) ); assert_eq!( model.get_model_instructions(/*personality*/ None), "legacy instructions" ); let serialized = serde_json::to_value(response).expect("serialize canonical response"); assert_eq!( serialized["models"][0]["base_instructions"], "legacy instructions" ); } #[test] fn models_response_rejects_model_without_instruction_source() { let mut value = serde_json::to_value(ModelsResponse { models: vec![test_model(/*spec*/ None)], }) .expect("serialize models response"); value["models"][0] .as_object_mut() .expect("model should serialize as an object") .remove("base_instructions") .expect("serialized model should include legacy base instructions"); let error = serde_json::from_value::(value) .expect_err("model without instructions should be rejected"); assert_eq!( error.to_string(), "model `test-model` is missing both `base_instructions` and \ `model_messages.instructions_template`" ); } #[test] fn models_response_serializes_literal_legacy_base_instructions() { let response = ModelsResponse { models: vec![test_model(Some(ModelMessages { persistent_instructions: None, tools: None, instructions_template: Some("before {{ personality }} after".to_string()), instructions_variables: Some(ModelInstructionsVariables { personality_default: Some("default".to_string()), personality_friendly: Some("friendly".to_string()), personality_pragmatic: Some("pragmatic".to_string()), }), approvals: None, collaboration_modes: None, auto_review: None, permissions: None, multi_agent: None, token_budget: None, confirmation_policies: None, guardian_v2: None, }))], }; let serialized = serde_json::to_value(response).expect("serialize models response"); assert_eq!( serialized["models"][0]["base_instructions"], "before {{ personality }} after" ); } #[test] fn models_response_prefers_template_and_preserves_message_siblings() { let messages = ModelMessages { persistent_instructions: Some("Persistent catalog instructions".to_string()), tools: Some(ToolMessages { send_user_message_async: Some(ToolMessage { description: Some("Catalog description".to_string()), }), }), instructions_template: None, instructions_variables: None, approvals: Some(ApprovalMessages { on_request: Some("approval".to_string()), on_request_auto_review: None, never: Some("never approval".to_string()), unless_trusted: Some("unless-trusted approval".to_string()), }), collaboration_modes: Some(CollaborationModeMessages { default: Some("default collaboration".to_string()), plan: Some("plan collaboration".to_string()), }), auto_review: Some(AutoReviewMessages { policy: Some("policy".to_string()), policy_template: None, node_repl_policy: None, rejection_instructions: Some("rejection instructions".to_string()), timeout_instructions: Some("timeout instructions".to_string()), }), permissions: Some(PermissionMessages { danger_full_access: None, workspace_write: Some("workspace".to_string()), read_only: None, }), multi_agent: None, token_budget: None, confirmation_policies: Some(ConfirmationPolicies { browser_use: Some( "# Browser confirmations\n\nKeep {{literal_markdown}}.\n".to_string(), ), computer_use: Some( " # Native confirmations\r\n\nKeep ${native_markdown}.\n".to_string(), ), }), guardian_v2: Some(GuardianV2ModelConfig { classifier_instructions: Some("Guardian classification".to_string()), review_threshold_basis_points: Some(7_500), reasoning_effort: Some(ReasoningEffort::Minimal), transcript: Some(GuardianV2TranscriptModelConfig { sources: Some(vec!["reasoning".to_string()]), max_tool_entry_tokens: Some(500), ..Default::default() }), ..Default::default() }), }; let mut value = serde_json::to_value(ModelsResponse { models: vec![test_model(Some(messages.clone()))], }) .expect("serialize models response"); assert_eq!( value["models"][0]["model_messages"]["confirmation_policies"], serde_json::json!({ "browser_use": "# Browser confirmations\n\nKeep {{literal_markdown}}.\n", "computer_use": " # Native confirmations\r\n\nKeep ${native_markdown}.\n", }) ); value["models"][0]["base_instructions"] = serde_json::json!("legacy instructions"); let response: ModelsResponse = serde_json::from_value(value).expect("deserialize legacy models response"); let mut expected_messages = messages; expected_messages.instructions_template = Some("legacy instructions".to_string()); assert_eq!(response.models[0].model_messages, Some(expected_messages)); let canonical_messages = ModelMessages { persistent_instructions: Some(String::new()), tools: Some(ToolMessages { send_user_message_async: Some(ToolMessage { description: Some(String::new()), }), }), instructions_template: Some("canonical instructions".to_string()), instructions_variables: None, approvals: None, collaboration_modes: None, auto_review: None, permissions: None, multi_agent: None, token_budget: None, confirmation_policies: None, guardian_v2: None, }; let mut value = serde_json::to_value(ModelsResponse { models: vec![test_model(Some(canonical_messages.clone()))], }) .expect("serialize models response"); value["models"][0]["base_instructions"] = serde_json::json!("legacy instructions"); let response: ModelsResponse = serde_json::from_value(value).expect("deserialize mixed models response"); assert_eq!(response.models[0].model_messages, Some(canonical_messages)); } #[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": "unified_exec", "visibility": "list", "supported_in_api": true, "priority": 1, "upgrade": null, "model_messages": null, "default_reasoning_summary": "auto", "support_verbosity": false, "default_verbosity": null, "apply_patch_tool_type": null, "truncation_policy": { "mode": "bytes", "limit": 10000 }, "supports_image_detail_original": false, "context_window": null, "auto_compact_token_limit": null, "effective_context_window_percent": 95, "experimental_supported_tools": [] })) .expect("deserialize model info"); assert_eq!(model.availability_nux, None); assert_eq!( model.input_modalities, vec![InputModality::Text, InputModality::Image] ); assert!(!model.include_skills_usage_instructions); assert!(!model.include_plugin_usage_instructions); assert!(model.include_apps_usage_instructions); assert!(model.supports_reasoning_summary_parameter); assert!(!model.supports_image_detail_original); assert_eq!(model.web_search_tool_type, WebSearchToolType::Text); assert!(!model.supports_search_tool); assert!(!model.supports_experimental_context); assert!(!model.use_responses_lite); assert!(!model.node_repl_auto_review_required); assert!(!model.node_repl_disabled); assert_eq!(model.comp_hash, None); assert_eq!(model.auto_review_model_override, None); assert_eq!(model.tool_mode, None); assert_eq!(model.multi_agent_reasoning_effort, None); } #[test] fn model_info_deserializes_multi_agent_reasoning_effort() { let mut value = serde_json::to_value(test_model(/*spec*/ None)).expect("serialize test model"); value .as_object_mut() .expect("model info should be an object") .insert( "multi_agent_reasoning_effort".to_string(), serde_json::Value::String("high".to_string()), ); let model = serde_json::from_value::(value) .expect("deserialize multi-agent reasoning effort"); assert_eq!( model.multi_agent_reasoning_effort, Some(ReasoningEffort::High) ); } #[test] fn model_info_deserializes_optional_upgrade_retirement_at() { let base = serde_json::to_value(test_model(/*spec*/ None)) .expect("serialize test model without retirement time"); let mut absent = base.clone(); absent .as_object_mut() .expect("model info should be an object") .insert( "upgrade".to_string(), serde_json::json!({ "model": "replacement-model", "migration_markdown": "Use the replacement model." }), ); let absent = serde_json::from_value::(absent) .expect("deserialize model info without upgrade retirement time"); assert_eq!( absent .upgrade .as_ref() .and_then(|upgrade| upgrade.retirement_at.as_ref()) .map(DateTime::timestamp), None ); let mut null = base.clone(); null.as_object_mut() .expect("model info should be an object") .insert( "upgrade".to_string(), serde_json::json!({ "model": "replacement-model", "migration_markdown": "Use the replacement model.", "retirement_at": null }), ); let null = serde_json::from_value::(null) .expect("deserialize model info with null upgrade retirement time"); assert_eq!( null.upgrade .as_ref() .and_then(|upgrade| upgrade.retirement_at.as_ref()) .map(DateTime::timestamp), None ); let mut populated = base; populated .as_object_mut() .expect("model info should be an object") .insert( "upgrade".to_string(), serde_json::json!({ "model": "replacement-model", "migration_markdown": "Use the replacement model.", "retirement_at": "2030-01-01T00:00:00Z" }), ); let populated = serde_json::from_value::(populated) .expect("deserialize model info with upgrade retirement time"); assert_eq!( populated .upgrade .as_ref() .and_then(|upgrade| upgrade.retirement_at.as_ref()) .map(DateTime::timestamp), Some(1_893_456_000) ); let mut malformed = serde_json::to_value(test_model(/*spec*/ None)) .expect("serialize test model for malformed retirement time"); malformed .as_object_mut() .expect("model info should be an object") .insert( "upgrade".to_string(), serde_json::json!({ "model": "replacement-model", "migration_markdown": "Use the replacement model.", "retirement_at": "not-a-timestamp" }), ); let malformed = serde_json::from_value::(malformed) .expect("tolerate malformed upgrade retirement time"); assert_eq!( malformed .upgrade .as_ref() .and_then(|upgrade| upgrade.retirement_at.as_ref()) .map(DateTime::timestamp), None ); } #[test] fn model_info_preserves_explicit_apps_guidance_opt_out() { let value = serde_json::to_value(test_model(/*spec*/ None)) .expect("serialize model info with explicit apps guidance opt-out"); assert_eq!(value["include_apps_usage_instructions"], false); let model = serde_json::from_value::(value).expect("deserialize model info"); assert!(!model.include_apps_usage_instructions); } #[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::(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::(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::(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.usable_context_window(), Some(380_000)); assert_eq!(model.auto_compact_token_limit(), Some(360_000)); } #[test] fn model_context_window_limits_preserve_their_distinct_meanings() { let model = ModelInfo { context_window: Some(272_000), max_context_window: Some(400_000), auto_compact_token_limit: Some(250_000), effective_context_window_percent: 95, ..test_model(/*spec*/ None) }; assert_eq!( ( model.resolved_context_window(), model.usable_context_window(), model.auto_compact_token_limit(), ), (Some(272_000), Some(258_400), Some(244_800)) ); } #[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_access_programs_preserve_explicit_selection_availability() { use crate::turn_input::CyberAccessProgram; for (metadata, expected) in [ (None, None), (Some(serde_json::Value::Null), None), ( Some(serde_json::json!({ "cyber": [] })), Some(ModelAccessPrograms { cyber: vec![] }), ), ( Some(serde_json::json!({ "cyber": ["standard", "daybreak_blue", "future_program", "daybreak_red"] })), Some(ModelAccessPrograms { cyber: vec![ CyberAccessProgram::Standard, CyberAccessProgram::DaybreakBlue, CyberAccessProgram::DaybreakRed, ], }), ), ] { let mut value = serde_json::to_value(test_model(/*spec*/ None)).unwrap(); if let Some(metadata) = metadata { value["available_access_programs"] = metadata; } let model: ModelInfo = serde_json::from_value(value).unwrap(); assert_eq!(model.available_access_programs, expected); assert_eq!(ModelPreset::from(model).available_access_programs, expected); } } #[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); } }