use std::collections::HashMap; 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, } /// 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, } /// 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, } #[derive(Deserialize, Debug, Clone, PartialEq, Eq, Default, Hash, TS, JsonSchema, Serialize)] #[serde(rename_all = "snake_case")] pub enum ReasoningSummaryFormat { #[default] None, Experimental, } /// 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); /// 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, pub default_reasoning_level: ReasoningEffort, pub supported_reasoning_levels: Vec, pub shell_type: ConfigShellToolType, pub visibility: ModelVisibility, pub minimal_client_version: ClientVersion, pub supported_in_api: bool, pub priority: i32, pub upgrade: Option, pub base_instructions: Option, 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, pub context_window: Option, pub reasoning_summary_format: ReasoningSummaryFormat, pub experimental_supported_tools: Vec, } /// Response wrapper for `/models`. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema, Default)] pub struct ModelsResponse { pub models: Vec, #[serde(default)] pub etag: String, } // 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, 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_slug| ModelUpgrade { id: upgrade_slug.clone(), reasoning_effort_mapping: reasoning_effort_mapping_from_presets( &info.supported_reasoning_levels, ), migration_config_key: info.slug.clone(), }), show_in_picker: info.visibility == ModelVisibility::List, } } } 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) }