Download codex-rs/protocol/src/openai_models.rs from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 71.1 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/protocol/src/openai_models.rs
- Command line
-
hf download hf://SaylorTwift/codex/codex-rs/protocol/src/openai_models.rs
-
curl -L -o openai_models.rs https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/protocol/src/openai_models.rs
71.1 kB
| //! 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; | |
| mod guardian; | |
| pub use guardian::GuardianModelPolicy; | |
| pub use guardian::GuardianReviewMode; | |
| pub use guardian::GuardianScope; | |
| mod guardian_v2; | |
| 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 | |
| pub enum ReasoningEffort { | |
| None, | |
| Minimal, | |
| Low, | |
| 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<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), | |
| "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. | |
| 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<InputModality> { | |
| vec![InputModality::Text, InputModality::Image] | |
| } | |
| /// A reasoning effort option that can be surfaced for a model. | |
| 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, | |
| } | |
| 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>, | |
| /// Informational time when the model associated with this upgrade is scheduled to retire. | |
| pub retirement_at: Option<DateTime<Utc>>, | |
| } | |
| pub struct ModelAvailabilityNux { | |
| pub message: String, | |
| } | |
| pub struct ModelServiceTier { | |
| pub id: String, | |
| pub name: String, | |
| pub description: String, | |
| } | |
| /// Metadata describing a Codex-supported model. | |
| 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, | |
| pub model_specialty: Option<String>, | |
| /// Reasoning effort applied when none is explicitly chosen. | |
| pub default_reasoning_effort: ReasoningEffort, | |
| /// Supported reasoning effort options. | |
| pub supported_reasoning_efforts: Vec<ReasoningEffortPreset>, | |
| /// Deprecated catalog field, always false for new model presets. | |
| pub supports_personality: bool, | |
| /// Deprecated: use `service_tiers` instead. | |
| pub additional_speed_tiers: Vec<String>, | |
| /// Service tiers this model can run with. | |
| pub service_tiers: Vec<ModelServiceTier>, | |
| /// Catalog default service tier id for this model. | |
| pub default_service_tier: Option<String>, | |
| /// Caller-specific explicit access programs, when discovery resolved them. | |
| pub available_access_programs: Option<ModelAccessPrograms>, | |
| /// 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, | |
| /// Multi-agent backend selected when this model starts a new thread. | |
| pub multi_agent_version: Option<MultiAgentVersion>, | |
| /// 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. | |
| pub input_modalities: Vec<InputModality>, | |
| } | |
| /// Visibility of a model in the picker or APIs. | |
| pub enum ModelVisibility { | |
| List, | |
| Hide, | |
| None, | |
| } | |
| /// Shell execution capability for a model. | |
| pub enum ConfigShellToolType { | |
| UnifiedExec, | |
| Disabled, | |
| } | |
| pub enum ApplyPatchToolType { | |
| Freeform, | |
| } | |
| pub enum WebSearchToolType { | |
| Text, | |
| TextAndImage, | |
| } | |
| /// Server-provided truncation policy metadata for a model. | |
| pub enum TruncationMode { | |
| Bytes, | |
| Tokens, | |
| } | |
| 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()) | |
| } | |
| 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]). | |
| pub struct ClientVersion(pub i32, pub i32, pub i32); | |
| const fn default_effective_context_window_percent() -> i64 { | |
| 95 | |
| } | |
| const fn default_true() -> bool { | |
| true | |
| } | |
| const fn is_true(value: &bool) -> bool { | |
| *value | |
| } | |
| /// Model metadata returned by the Codex backend `/models` endpoint. | |
| 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. | |
| pub guardian: Option<GuardianModelPolicy>, | |
| pub slug: String, | |
| pub display_name: String, | |
| pub description: Option<String>, | |
| 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, | |
| pub additional_speed_tiers: Vec<String>, | |
| pub service_tiers: Vec<ModelServiceTier>, | |
| pub default_service_tier: Option<String>, | |
| pub available_access_programs: Option<ModelAccessPrograms>, | |
| pub availability_nux: Option<ModelAvailabilityNux>, | |
| pub upgrade: Option<ModelInfoUpgrade>, | |
| pub model_messages: Option<ModelMessages>, | |
| pub include_skills_usage_instructions: bool, | |
| pub include_plugin_usage_instructions: bool, | |
| pub include_apps_usage_instructions: bool, | |
| /// Whether the model accepts the Responses API `reasoning.summary` parameter. | |
| pub supports_reasoning_summary_parameter: bool, | |
| pub default_reasoning_summary: ReasoningSummary, | |
| pub support_verbosity: bool, | |
| pub default_verbosity: Option<Verbosity>, | |
| pub apply_patch_tool_type: Option<ApplyPatchToolType>, | |
| pub web_search_tool_type: WebSearchToolType, | |
| pub truncation_policy: TruncationPolicyConfig, | |
| pub supports_image_detail_original: bool, | |
| pub context_window: Option<i64>, | |
| /// Maximum context window allowed for config overrides. | |
| 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. | |
| pub auto_compact_token_limit: Option<i64>, | |
| /// Opaque identifier for compaction-compatible model configurations. | |
| 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. | |
| pub effective_context_window_percent: i64, | |
| pub experimental_supported_tools: Vec<String>, | |
| /// Input modalities accepted by the backend for this model. | |
| pub input_modalities: Vec<InputModality>, | |
| /// Internal-only marker set by core when a model slug resolved to fallback metadata. | |
| pub used_fallback_model_metadata: bool, | |
| pub supports_search_tool: bool, | |
| /// Whether experimental context management may be activated at session startup. | |
| pub supports_experimental_context: bool, | |
| pub use_responses_lite: bool, | |
| pub node_repl_auto_review_required: bool, | |
| pub node_repl_disabled: bool, | |
| pub auto_review_model_override: Option<String>, | |
| pub model_specialty: Option<String>, | |
| pub tool_mode: Option<ToolMode>, | |
| pub multi_agent_version: Option<MultiAgentVersion>, | |
| /// Reasoning effort used for multi-agent work when the user selects Ultra. | |
| pub multi_agent_reasoning_effort: Option<ReasoningEffort>, | |
| } | |
| impl ModelInfo { | |
| pub fn resolved_context_window(&self) -> Option<i64> { | |
| 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<i64> { | |
| 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<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 | |
| } | |
| /// 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<Personality>) -> 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. | |
| pub struct ModelMessages { | |
| /// Additional developer instructions for persistent mode. Missing or null uses the built-in | |
| /// instructions; an empty string disables them. | |
| pub persistent_instructions: Option<String>, | |
| pub tools: Option<ToolMessages>, | |
| pub instructions_template: Option<String>, | |
| pub instructions_variables: Option<ModelInstructionsVariables>, | |
| pub approvals: Option<ApprovalMessages>, | |
| pub collaboration_modes: Option<CollaborationModeMessages>, | |
| pub auto_review: Option<AutoReviewMessages>, | |
| pub permissions: Option<PermissionMessages>, | |
| pub multi_agent: Option<MultiAgentMessages>, | |
| pub token_budget: Option<ModelTokenBudgetConfig>, | |
| pub guardian_v2: Option<GuardianV2ModelConfig>, | |
| /// Replacement confirmation-policy documents forwarded in actor MCP request metadata. | |
| pub confirmation_policies: Option<ConfirmationPolicies>, | |
| } | |
| /// Model-owned confirmation-policy Markdown, forwarded unchanged to actor tools. | |
| pub struct ConfirmationPolicies { | |
| /// Replacement Markdown for the Browser Use confirmation-policy document. | |
| pub browser_use: Option<String>, | |
| /// Replacement Markdown for the native Computer Use confirmation policy. | |
| pub computer_use: Option<String>, | |
| } | |
| /// Model-owned messages for built-in tools. | |
| pub struct ToolMessages { | |
| pub send_user_message_async: Option<ToolMessage>, | |
| } | |
| /// Model-owned messages for a built-in tool. | |
| pub struct ToolMessage { | |
| /// Missing or null uses the built-in description; an empty string leaves the description | |
| /// empty without disabling the tool. | |
| pub description: Option<String>, | |
| } | |
| /// Model-owned defaults for the context-window token-budget feature. | |
| pub struct ModelTokenBudgetConfig { | |
| pub enabled: bool, | |
| 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, | |
| } | |
| pub struct ApprovalMessages { | |
| pub on_request: Option<String>, | |
| pub on_request_auto_review: Option<String>, | |
| pub never: Option<String>, | |
| pub unless_trusted: Option<String>, | |
| } | |
| pub struct CollaborationModeMessages { | |
| pub default: Option<String>, | |
| pub plan: Option<String>, | |
| } | |
| pub struct AutoReviewMessages { | |
| pub policy: Option<String>, | |
| pub policy_template: Option<String>, | |
| /// Extra developer policy for `node_repl` and `cua_repl` reviews. | |
| pub node_repl_policy: Option<String>, | |
| pub rejection_instructions: Option<String>, | |
| pub timeout_instructions: Option<String>, | |
| } | |
| pub struct PermissionMessages { | |
| pub danger_full_access: Option<String>, | |
| pub workspace_write: Option<String>, | |
| pub read_only: Option<String>, | |
| } | |
| pub struct MultiAgentMessages { | |
| pub role: Option<MultiAgentRoleMessages>, | |
| pub mode: Option<MultiAgentModeMessages>, | |
| } | |
| pub struct MultiAgentRoleMessages { | |
| pub root: Option<String>, | |
| pub subagent: Option<String>, | |
| } | |
| pub struct MultiAgentModeMessages { | |
| pub explicit: Option<String>, | |
| /// 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<String>, | |
| pub hint_text: Option<String>, | |
| } | |
| pub struct ModelInstructionsVariables { | |
| pub personality_default: Option<String>, | |
| pub personality_friendly: Option<String>, | |
| pub personality_pragmatic: Option<String>, | |
| } | |
| pub struct ModelInfoUpgrade { | |
| pub model: String, | |
| pub migration_markdown: String, | |
| /// Informational time when the model associated with this upgrade is scheduled to retire. | |
| pub retirement_at: Option<DateTime<Utc>>, | |
| } | |
| fn deserialize_optional_rfc3339_timestamp<'de, D>( | |
| deserializer: D, | |
| ) -> Result<Option<DateTime<Utc>>, D::Error> | |
| where | |
| D: Deserializer<'de>, | |
| { | |
| let value = Option::<serde_json::Value>::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`. | |
| pub struct ModelsResponse { | |
| pub models: Vec<ModelInfo>, | |
| } | |
| struct ModelInfoWithLegacyBaseInstructionsRef<'a> { | |
| model: &'a ModelInfo, | |
| base_instructions: String, | |
| } | |
| struct ModelInfoWithLegacyBaseInstructions { | |
| base_instructions: Option<String>, | |
| model: ModelInfo, | |
| } | |
| /// Serializes catalog models with the deprecated top-level instruction field required by older | |
| /// clients. | |
| pub fn serialize_model_infos_with_legacy_base<S>( | |
| models: &[ModelInfo], | |
| serializer: S, | |
| ) -> Result<S::Ok, S::Error> | |
| where | |
| S: Serializer, | |
| { | |
| models | |
| .iter() | |
| .map(|model| ModelInfoWithLegacyBaseInstructionsRef { | |
| model, | |
| base_instructions: model.get_model_instructions(/*personality*/ None), | |
| }) | |
| .collect::<Vec<_>>() | |
| .serialize(serializer) | |
| } | |
| /// Deserializes catalog models while promoting the legacy top-level instruction field into Model | |
| /// Messages V2 when no canonical instruction template is present. | |
| pub fn deserialize_model_infos_with_legacy_base<'de, D>( | |
| deserializer: D, | |
| ) -> Result<Vec<ModelInfo>, D::Error> | |
| where | |
| D: Deserializer<'de>, | |
| { | |
| let models = Vec::<ModelInfoWithLegacyBaseInstructions>::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<ModelInfo> 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<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; | |
| } | |
| } | |
| } | |
| mod tests { | |
| use super::*; | |
| use pretty_assertions::assert_eq; | |
| use serde_json::from_str; | |
| use serde_json::to_string; | |
| fn legacy_shell_model_metadata_deserializes_as_unified_exec() { | |
| for legacy_shell_type in ["default", "local", "shell_command"] { | |
| assert_eq!( | |
| from_str::<ConfigShellToolType>(&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<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::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, | |
| } | |
| } | |
| 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, | |
| } | |
| ); | |
| } | |
| 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!({}), | |
| })), | |
| ) | |
| ); | |
| } | |
| } | |
| 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, | |
| }) | |
| ); | |
| } | |
| 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()), | |
| }) | |
| ); | |
| } | |
| 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, | |
| }) | |
| ); | |
| } | |
| 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()), | |
| }), | |
| }) | |
| ); | |
| } | |
| 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, | |
| } | |
| ); | |
| } | |
| fn reasoning_effort_accepts_known_and_custom_values() { | |
| let custom = ReasoningEffort::Custom("future".to_string()); | |
| let deserialized = from_str::<ReasoningEffort>(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(), | |
| ) | |
| ); | |
| } | |
| fn reasoning_effort_rejects_empty_values() { | |
| assert_eq!( | |
| "".parse::<ReasoningEffort>(), | |
| Err("reasoning_effort must not be empty".to_string()) | |
| ); | |
| } | |
| 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() | |
| }) | |
| ); | |
| } | |
| 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); | |
| } | |
| } | |
| 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, ""); | |
| } | |
| 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)), | |
| "" | |
| ); | |
| } | |
| 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" | |
| ); | |
| } | |
| 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::<ModelsResponse>(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`" | |
| ); | |
| } | |
| 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" | |
| ); | |
| } | |
| 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)); | |
| } | |
| 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); | |
| } | |
| 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::<ModelInfo>(value) | |
| .expect("deserialize multi-agent reasoning effort"); | |
| assert_eq!( | |
| model.multi_agent_reasoning_effort, | |
| Some(ReasoningEffort::High) | |
| ); | |
| } | |
| 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::<ModelInfo>(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::<ModelInfo>(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::<ModelInfo>(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::<ModelInfo>(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 | |
| ); | |
| } | |
| 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::<ModelInfo>(value).expect("deserialize model info"); | |
| assert!(!model.include_apps_usage_instructions); | |
| } | |
| 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)); | |
| } | |
| 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")); | |
| } | |
| 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); | |
| } | |
| 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)); | |
| } | |
| 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)); | |
| } | |
| 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)) | |
| ); | |
| } | |
| 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()) | |
| ); | |
| } | |
| 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); | |
| } | |
| } | |
| 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()); | |
| } | |
| 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 | |
| ); | |
| } | |
| 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); | |
| } | |
| } | |