codex / codex-rs /protocol /src /openai_models.rs
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
afa0cbf verified
Raw
History Blame Contribute Delete
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;
#[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<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.
#[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<InputModality> {
vec![InputModality::Text, InputModality::Image]
}
/// A reasoning effort option that can be surfaced for a model.
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)]
pub struct ReasoningEffortPreset {
/// Effort level that the model supports.
pub effort: ReasoningEffort,
/// Short human description shown next to the effort in UIs.
pub description: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)]
pub struct ModelUpgrade {
pub id: String,
pub migration_config_key: String,
pub model_link: Option<String>,
pub upgrade_copy: Option<String>,
pub migration_markdown: Option<String>,
/// 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<String>")]
#[ts(type = "string", optional)]
pub retirement_at: Option<DateTime<Utc>>,
}
#[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<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.
#[serde(default)]
pub supports_personality: bool,
/// Deprecated: use `service_tiers` instead.
#[serde(default)]
pub additional_speed_tiers: Vec<String>,
/// Service tiers this model can run with.
#[serde(default)]
pub service_tiers: Vec<ModelServiceTier>,
/// Catalog default service tier id for this model.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_service_tier: Option<String>,
/// Caller-specific explicit access programs, when discovery resolved them.
#[serde(default, skip_serializing_if = "Option::is_none")]
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.
#[serde(default, skip_serializing, skip_deserializing)]
#[schemars(skip)]
#[ts(skip)]
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.
#[serde(default = "default_input_modalities")]
pub input_modalities: Vec<InputModality>,
}
/// Visibility of a model in the picker or APIs.
#[derive(
Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, TS, JsonSchema, EnumIter, Display,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum ModelVisibility {
List,
Hide,
None,
}
/// Shell execution capability for a model.
#[derive(
Debug,
Serialize,
Deserialize,
Clone,
Copy,
PartialEq,
Eq,
TS,
JsonSchema,
EnumIter,
Display,
Hash,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum ConfigShellToolType {
#[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<Option<T>, D::Error>
where
D: Deserializer<'de>,
T: DeserializeOwned,
{
let Some(value) = Option::<String>::deserialize(deserializer)? else {
return Ok(None);
};
Ok(serde_json::from_value(serde_json::Value::String(value)).ok())
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, TS, JsonSchema)]
pub struct TruncationPolicyConfig {
pub mode: TruncationMode,
pub limit: i64,
}
impl TruncationPolicyConfig {
pub const fn bytes(limit: i64) -> Self {
Self {
mode: TruncationMode::Bytes,
limit,
}
}
pub const fn tokens(limit: i64) -> Self {
Self {
mode: TruncationMode::Tokens,
limit,
}
}
}
/// Semantic version triple encoded as an array in JSON (e.g. [0, 62, 0]).
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, TS, JsonSchema)]
pub struct ClientVersion(pub i32, pub i32, pub i32);
const fn default_effective_context_window_percent() -> i64 {
95
}
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<GuardianModelPolicy>,
pub slug: String,
pub display_name: String,
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_reasoning_level: Option<ReasoningEffort>,
pub supported_reasoning_levels: Vec<ReasoningEffortPreset>,
pub shell_type: ConfigShellToolType,
pub visibility: ModelVisibility,
pub supported_in_api: bool,
pub priority: i32,
#[serde(default)]
pub additional_speed_tiers: Vec<String>,
#[serde(default)]
pub service_tiers: Vec<ModelServiceTier>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_service_tier: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub available_access_programs: Option<ModelAccessPrograms>,
pub availability_nux: Option<ModelAvailabilityNux>,
pub upgrade: Option<ModelInfoUpgrade>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_messages: Option<ModelMessages>,
#[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<Verbosity>,
pub apply_patch_tool_type: Option<ApplyPatchToolType>,
#[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<i64>,
/// Maximum context window allowed for config overrides.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_context_window: Option<i64>,
/// Token threshold for automatic compaction. When omitted, core derives it
/// from `context_window` (90%). When provided, core clamps it to 90% of the
/// context window when available.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auto_compact_token_limit: Option<i64>,
/// Opaque identifier for compaction-compatible model configurations.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub comp_hash: Option<String>,
/// Percentage of the context window considered usable for inputs, after
/// reserving headroom for system prompts, tool overhead, and model output.
#[serde(default = "default_effective_context_window_percent")]
pub effective_context_window_percent: i64,
pub experimental_supported_tools: Vec<String>,
/// Input modalities accepted by the backend for this model.
#[serde(default = "default_input_modalities")]
pub input_modalities: Vec<InputModality>,
/// Internal-only marker set by core when a model slug resolved to fallback metadata.
#[serde(default, skip_serializing, skip_deserializing)]
#[schemars(skip)]
#[ts(skip)]
pub used_fallback_model_metadata: bool,
#[serde(default)]
pub supports_search_tool: bool,
/// 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<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model_specialty: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_optional_model_selector"
)]
pub tool_mode: Option<ToolMode>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_optional_model_selector"
)]
pub multi_agent_version: Option<MultiAgentVersion>,
/// 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<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.
#[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<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
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>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub token_budget: Option<ModelTokenBudgetConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub guardian_v2: Option<GuardianV2ModelConfig>,
/// Replacement confirmation-policy documents forwarded in actor MCP request metadata.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub confirmation_policies: Option<ConfirmationPolicies>,
}
/// 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<String>,
/// Replacement Markdown for the native Computer Use confirmation policy.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub computer_use: Option<String>,
}
/// 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<ToolMessage>,
}
/// 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<String>,
}
/// 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<String>,
pub on_request_auto_review: Option<String>,
pub never: Option<String>,
pub unless_trusted: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
pub struct CollaborationModeMessages {
pub default: Option<String>,
pub plan: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
pub struct AutoReviewMessages {
pub policy: Option<String>,
pub policy_template: Option<String>,
/// Extra developer policy for `node_repl` and `cua_repl` reviews.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub node_repl_policy: Option<String>,
pub rejection_instructions: Option<String>,
pub timeout_instructions: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
pub struct PermissionMessages {
pub danger_full_access: Option<String>,
pub workspace_write: Option<String>,
pub read_only: Option<String>,
}
#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
pub struct MultiAgentMessages {
pub role: Option<MultiAgentRoleMessages>,
pub mode: Option<MultiAgentModeMessages>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
pub struct MultiAgentRoleMessages {
pub root: Option<String>,
pub subagent: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
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>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)]
pub struct ModelInstructionsVariables {
pub personality_default: Option<String>,
pub personality_friendly: Option<String>,
pub personality_pragmatic: Option<String>,
}
#[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<String>")]
#[ts(type = "string", optional)]
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`.
#[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<ModelInfo>,
}
#[derive(Serialize)]
struct ModelInfoWithLegacyBaseInstructionsRef<'a> {
#[serde(flatten)]
model: &'a ModelInfo,
base_instructions: String,
}
#[derive(Deserialize)]
struct ModelInfoWithLegacyBaseInstructions {
#[serde(default)]
base_instructions: Option<String>,
#[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<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.
#[doc(hidden)]
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;
}
}
}
#[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::<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,
}
}
#[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::<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(),
)
);
}
#[test]
fn reasoning_effort_rejects_empty_values() {
assert_eq!(
"".parse::<ReasoningEffort>(),
Err("reasoning_effort must not be empty".to_string())
);
}
#[test]
fn reasoning_effort_json_schema_is_an_open_string() {
let mut effort_generator = SchemaGenerator::default();
assert_eq!(
ReasoningEffort::json_schema(&mut effort_generator),
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::String.into()),
metadata: Some(Box::new(Metadata {
description: Some(
"A non-empty reasoning effort value advertised by the model.".to_string(),
),
..Default::default()
})),
string: Some(Box::new(StringValidation {
min_length: Some(1),
..Default::default()
})),
..Default::default()
})
);
}
#[test]
fn get_model_instructions_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::<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`"
);
}
#[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::<ModelInfo>(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::<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
);
}
#[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::<ModelInfo>(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::<ModelInfo>(value).expect("deserialize model info");
assert_eq!(model.tool_mode, Some(ToolMode::CodeModeOnly));
}
#[test]
fn model_info_treats_unknown_tool_mode_as_omitted() {
let mut value =
serde_json::to_value(test_model(/*spec*/ None)).expect("serialize test model");
let object = value
.as_object_mut()
.expect("model info should be an object");
object.insert(
"tool_mode".to_string(),
serde_json::Value::String("future_tool_mode".to_string()),
);
let model = serde_json::from_value::<ModelInfo>(value).expect("deserialize model info");
assert_eq!(model.tool_mode, None);
let serialized = serde_json::to_value(model).expect("serialize model info");
let object = serialized
.as_object()
.expect("model info should be an object");
assert!(!object.contains_key("tool_mode"));
}
#[test]
fn model_info_treats_unknown_multi_agent_version_as_omitted() {
let mut value =
serde_json::to_value(test_model(/*spec*/ None)).expect("serialize test model");
let object = value
.as_object_mut()
.expect("model info should be an object");
object.insert(
"multi_agent_version".to_string(),
serde_json::Value::String("future_multi_agent_version".to_string()),
);
let model = serde_json::from_value::<ModelInfo>(value).expect("deserialize model info");
assert_eq!(model.multi_agent_version, None);
}
#[test]
fn resolved_context_window_prefers_context_window() {
let model = ModelInfo {
context_window: Some(273_000),
max_context_window: Some(400_000),
..test_model(/*spec*/ None)
};
assert_eq!(model.resolved_context_window(), Some(273_000));
}
#[test]
fn resolved_context_window_falls_back_to_max_context_window() {
let model = ModelInfo {
context_window: None,
max_context_window: Some(400_000),
..test_model(/*spec*/ None)
};
assert_eq!(model.resolved_context_window(), Some(400_000));
assert_eq!(model.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);
}
}