use std::collections::HashMap; use std::collections::HashSet; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use strum::IntoEnumIterator; use strum_macros::Display; use strum_macros::EnumIter; use ts_rs::TS; use crate::config_types::Verbosity; /// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning #[derive( Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS, EnumIter, Hash, )] #[serde(rename_all = "lowercase")] #[strum(serialize_all = "lowercase")] pub enum ReasoningEffort { None, Minimal, Low, #[default] Medium, High, XHigh, } /// 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 reasoning_effort_mapping: Option>, pub migration_config_key: String, pub model_link: Option, pub upgrade_copy: Option, pub migration_markdown: Option, } /// 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, /// 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, /// whether this model is supported in the api pub supported_in_api: bool, } /// 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, Function, } /// 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)] 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, #[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, pub upgrade: Option, pub base_instructions: String, pub supports_reasoning_summaries: bool, pub support_verbosity: bool, pub default_verbosity: Option, pub apply_patch_tool_type: Option, pub truncation_policy: TruncationPolicyConfig, pub supports_parallel_tool_calls: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub context_window: Option, /// Token threshold for automatic compaction. When omitted, core derives it /// from `context_window` (90%). #[serde(default, skip_serializing_if = "Option::is_none")] pub auto_compact_token_limit: 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, } impl ModelInfo { pub fn auto_compact_token_limit(&self) -> Option { self.auto_compact_token_limit.or_else(|| { self.context_window .map(|context_window| (context_window * 9) / 10) }) } } #[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, } // 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(), default_reasoning_effort: info .default_reasoning_level .unwrap_or(ReasoningEffort::None), supported_reasoning_efforts: info.supported_reasoning_levels.clone(), is_default: false, // default is the highest priority available model upgrade: info.upgrade.as_ref().map(|upgrade| ModelUpgrade { id: upgrade.model.clone(), reasoning_effort_mapping: reasoning_effort_mapping_from_presets( &info.supported_reasoning_levels, ), 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, supported_in_api: info.supported_in_api, } } } 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() } /// Merge remote presets with existing presets, preferring remote when slugs match. /// /// Remote presets take precedence. Existing presets not in remote are appended with `is_default` set to false. pub fn merge( remote_presets: Vec, existing_presets: Vec, ) -> Vec { if remote_presets.is_empty() { return existing_presets; } let remote_slugs: HashSet<&str> = remote_presets .iter() .map(|preset| preset.model.as_str()) .collect(); let mut merged_presets = remote_presets.clone(); for mut preset in existing_presets { if remote_slugs.contains(preset.model.as_str()) { continue; } preset.is_default = false; merged_presets.push(preset); } merged_presets } } fn reasoning_effort_mapping_from_presets( presets: &[ReasoningEffortPreset], ) -> Option> { if presets.is_empty() { return None; } // Map every canonical effort to the closest supported effort for the new model. let supported: Vec = presets.iter().map(|p| p.effort).collect(); let mut map = HashMap::new(); for effort in ReasoningEffort::iter() { let nearest = nearest_effort(effort, &supported); map.insert(effort, nearest); } Some(map) } fn effort_rank(effort: ReasoningEffort) -> i32 { match effort { ReasoningEffort::None => 0, ReasoningEffort::Minimal => 1, ReasoningEffort::Low => 2, ReasoningEffort::Medium => 3, ReasoningEffort::High => 4, ReasoningEffort::XHigh => 5, } } fn nearest_effort(target: ReasoningEffort, supported: &[ReasoningEffort]) -> ReasoningEffort { let target_rank = effort_rank(target); supported .iter() .copied() .min_by_key(|candidate| (effort_rank(*candidate) - target_rank).abs()) .unwrap_or(target) }