mirror of
https://github.com/openai/codex.git
synced 2026-10-30 19:18:49 +00:00
Preserve model metadata origin through runtime resolution (#58361)
## What changed Return `CatalogModel` from model lookups, keeping metadata together with its `Bundled`, `Provider`, `ExplicitConfig`, or `Synthetic` origin through config overrides and runtime snapshots. Replace `used_fallback_model_metadata` checks with explicit `Synthetic` origin checks. Share the underlying `Arc<ModelInfo>` with extension stores and use copy-on-write for metadata changes. Keep origin tracking outside serialized model metadata. ## Testing Extend coverage for origin retention across cache reloads, namespaced model lookups, catalog fallback, and config overrides. Verify that step snapshots and extension stores share the same model metadata allocation. GitOrigin-RevId: bb6130c94a7a795aa168ee24664a1dd453579fba
This commit is contained in:
@@ -63,7 +63,6 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo {
|
||||
effective_context_window_percent: 95,
|
||||
experimental_supported_tools: Vec::new(),
|
||||
input_modalities: default_input_modalities(),
|
||||
used_fallback_model_metadata: false,
|
||||
supports_search_tool: false,
|
||||
supports_experimental_context: false,
|
||||
use_responses_lite: false,
|
||||
|
||||
@@ -157,7 +157,6 @@ async fn models_client_hits_models_endpoint() {
|
||||
effective_context_window_percent: 95,
|
||||
experimental_supported_tools: Vec::new(),
|
||||
input_modalities: default_input_modalities(),
|
||||
used_fallback_model_metadata: false,
|
||||
supports_search_tool: false,
|
||||
supports_experimental_context: false,
|
||||
use_responses_lite: false,
|
||||
|
||||
@@ -13,6 +13,7 @@ use codex_features::Feature;
|
||||
use codex_models_manager::manager::RefreshStrategy;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use codex_protocol::models::BaseInstructions;
|
||||
use codex_protocol::openai_models::ModelOrigin;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::openai_models::ReasoningEffortPreset;
|
||||
@@ -242,7 +243,7 @@ async fn apply_requested_spawn_agent_model_overrides(
|
||||
)?;
|
||||
config.model_reasoning_effort = Some(reasoning_effort);
|
||||
} else {
|
||||
config.model_reasoning_effort = selected_model_info.default_reasoning_level;
|
||||
config.model_reasoning_effort = selected_model_info.default_reasoning_level.clone();
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
@@ -312,7 +313,7 @@ async fn apply_spawn_agent_role(
|
||||
.models_manager
|
||||
.get_model_info(&model, &config.to_models_manager_config())
|
||||
.await;
|
||||
if model_info.used_fallback_model_metadata {
|
||||
if model_info.origin == ModelOrigin::Synthetic {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use crate::context::UserGoalUpdate;
|
||||
use codex_protocol::ResponseItemId;
|
||||
use codex_protocol::openai_models::CatalogModel;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -28,7 +29,6 @@ use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::Result as CodexResult;
|
||||
use codex_protocol::models::BaseInstructions;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::protocol::TokenUsage;
|
||||
use codex_rollout_trace::CompactionTraceContext;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
@@ -53,7 +53,7 @@ pub(super) enum RemoteCompactV2AttemptInput<'a> {
|
||||
|
||||
pub(crate) struct BackgroundCompactionCandidate {
|
||||
pub(crate) prefix: CompactionPrefix,
|
||||
pub(super) model_info: Arc<ModelInfo>,
|
||||
pub(super) model_info: Arc<CatalogModel>,
|
||||
pub(super) cutoff_world_state: Arc<WorldState>,
|
||||
pub(super) attempt: RemoteCompactV2Attempt,
|
||||
}
|
||||
|
||||
@@ -34,12 +34,12 @@ pub(crate) use score_authorization::ServingAssessment;
|
||||
pub(crate) mod test_host;
|
||||
|
||||
use codex_protocol::items::ModelInvocationContext;
|
||||
use codex_protocol::openai_models::CatalogModel;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::GuardianAssessmentOutcome;
|
||||
@@ -99,7 +99,7 @@ pub(crate) struct GuardianReviewContext {
|
||||
environments: TurnEnvironmentSnapshot,
|
||||
project_instructions: Option<Arc<LoadedAgentsMd>>,
|
||||
// Model and reasoning inputs are carried for the follow-up Guardian and V2 migrations.
|
||||
pub(crate) model_info: Arc<ModelInfo>,
|
||||
pub(crate) model_info: Arc<CatalogModel>,
|
||||
pub(crate) reasoning_effort: Option<ReasoningEffort>,
|
||||
pub(crate) reasoning_summary: ReasoningSummary,
|
||||
pub(crate) personality: Option<Personality>,
|
||||
|
||||
@@ -113,7 +113,7 @@ impl ReviewHost for super::super::runtime::ReviewRuntime {
|
||||
) -> Option<(String, Arc<codex_protocol::openai_models::ModelInfo>)> {
|
||||
let active = self.session.active_turn.lock().await;
|
||||
let turn = &active.as_ref()?.task.as_ref()?.turn_context;
|
||||
Some((turn.sub_id.clone(), Arc::clone(turn.model_info())))
|
||||
Some((turn.sub_id.clone(), Arc::clone(&turn.model_info().model)))
|
||||
}
|
||||
|
||||
async fn prepare(
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
use codex_guardian_reviewer::GuardianPromptItems;
|
||||
use codex_protocol::models::ImageDetail;
|
||||
use codex_protocol::models::ImageReference;
|
||||
use codex_protocol::openai_models::CatalogModel;
|
||||
use codex_protocol::openai_models::InputModality;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ModelOrigin;
|
||||
use codex_tools::normalize_output_image_detail;
|
||||
use std::borrow::Cow;
|
||||
use tracing::warn;
|
||||
@@ -23,7 +25,7 @@ use crate::image_preparation::unified_image_budget_enabled;
|
||||
pub(super) async fn build_guardian_prompt_items(
|
||||
params: &GuardianReviewSessionParams,
|
||||
reviewer: &GuardianReviewSession,
|
||||
model_info: &ModelInfo,
|
||||
model_info: &CatalogModel,
|
||||
reviewer_history: &ContextManager,
|
||||
history: GuardianTranscriptHistory<'_>,
|
||||
mode: GuardianPromptMode,
|
||||
@@ -49,7 +51,7 @@ pub(super) async fn build_guardian_prompt_items(
|
||||
/ 100
|
||||
});
|
||||
let context_window =
|
||||
context_window.filter(|limit| *limit > 0 && !model_info.used_fallback_model_metadata);
|
||||
context_window.filter(|limit| *limit > 0 && model_info.origin != ModelOrigin::Synthetic);
|
||||
let image_mode = if unified_image_budget_enabled(¶ms.spawn_config.features, model_info) {
|
||||
ImagePreparationMode::UnifiedBudget
|
||||
} else {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//! A missing default reviewer gets one shared catalog refresh per parent session.
|
||||
//! Refresh work survives cancelled waiters and precedes checkpoint compatibility decisions.
|
||||
|
||||
use codex_protocol::openai_models::CatalogModel;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_features::Feature;
|
||||
@@ -82,10 +83,7 @@ pub fn build_guardian_review_session_config(
|
||||
pub(crate) async fn resolve_review_model(
|
||||
session: &crate::session::session::Session,
|
||||
context: &super::GuardianReviewContext,
|
||||
) -> (
|
||||
codex_guardian_reviewer::ReviewModel,
|
||||
Arc<codex_protocol::openai_models::ModelInfo>,
|
||||
) {
|
||||
) -> (codex_guardian_reviewer::ReviewModel, Arc<CatalogModel>) {
|
||||
let turn = context.turn();
|
||||
// Check before resolving: a refresh that finishes during the lookup still
|
||||
// requires a reread, but later calls only need one catalog resolution.
|
||||
@@ -131,10 +129,7 @@ pub(crate) async fn resolve_review_model(
|
||||
async fn resolve_review_model_from_catalog(
|
||||
session: &crate::session::session::Session,
|
||||
context: &super::GuardianReviewContext,
|
||||
) -> (
|
||||
codex_guardian_reviewer::ReviewModel,
|
||||
Arc<codex_protocol::openai_models::ModelInfo>,
|
||||
) {
|
||||
) -> (codex_guardian_reviewer::ReviewModel, Arc<CatalogModel>) {
|
||||
let turn = context.turn();
|
||||
// Keep a successful refresh even if writing it to the disk cache failed.
|
||||
let models_manager = &session.services.models_manager;
|
||||
@@ -146,9 +141,13 @@ async fn resolve_review_model_from_catalog(
|
||||
.into_iter()
|
||||
.filter(|entry| {
|
||||
context.model_info.auto_review_model_override.is_some()
|
||||
|| entry.origin != codex_models_manager::ModelOrigin::Bundled
|
||||
|| matches!(
|
||||
entry.origin,
|
||||
codex_models_manager::ModelOrigin::Provider
|
||||
| codex_models_manager::ModelOrigin::ExplicitConfig
|
||||
)
|
||||
})
|
||||
.map(|entry| entry.model)
|
||||
.map(|entry| Arc::unwrap_or_clone(entry.model))
|
||||
.collect(),
|
||||
);
|
||||
let default_review_model_id = turn.provider.approval_review_preferred_model();
|
||||
|
||||
@@ -1999,7 +1999,7 @@ async fn guardian_request_model_for_auto_review(
|
||||
crate::guardian::test_host::install(&session, &turn.config);
|
||||
}
|
||||
GuardianTestCatalog::ParentOnly => {
|
||||
let parent_model = turn.model_info().as_ref().clone();
|
||||
let parent_model = turn.model_info().model.as_ref().clone();
|
||||
let auth_manager = Arc::clone(&session.services.auth_manager);
|
||||
let models_manager = StaticModelsManager::new(
|
||||
Some(auth_manager),
|
||||
@@ -2510,7 +2510,7 @@ async fn guardian_reuses_prompt_cache_key_and_appends_prior_reviews() -> anyhow:
|
||||
.models_manager = Arc::new(StaticModelsManager::new(
|
||||
Some(auth_manager),
|
||||
ModelsResponse {
|
||||
models: vec![reviewer_model],
|
||||
models: vec![Arc::unwrap_or_clone(reviewer_model.model)],
|
||||
},
|
||||
codex_models_manager::ModelOrigin::ExplicitConfig,
|
||||
));
|
||||
@@ -3992,7 +3992,7 @@ async fn guardian_review_session_config_clears_context_overrides_for_distinct_ef
|
||||
async fn guardian_review_session_config_preserves_context_overrides_for_same_effective_model() {
|
||||
let server = start_mock_server().await;
|
||||
let (mut session, mut turn) = guardian_test_session_and_turn(&server).await;
|
||||
let parent_model = turn.model_info().as_ref().clone();
|
||||
let parent_model = turn.model_info().model.as_ref().clone();
|
||||
let auth_manager = Arc::clone(&session.services.auth_manager);
|
||||
Arc::get_mut(&mut session)
|
||||
.expect("session should be unique")
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
use codex_features::Feature;
|
||||
use codex_features::Features;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::CatalogModel;
|
||||
use codex_protocol::openai_models::ModelOrigin;
|
||||
|
||||
pub(super) fn unsupported_code_mode_warning(
|
||||
model_info: &ModelInfo,
|
||||
model_info: &CatalogModel,
|
||||
features: &Features,
|
||||
) -> Option<String> {
|
||||
let code_mode_enabled =
|
||||
features.enabled(Feature::CodeMode) || features.enabled(Feature::CodeModeOnly);
|
||||
if !code_mode_enabled
|
||||
|| model_info.tool_mode.is_some()
|
||||
|| model_info.used_fallback_model_metadata
|
||||
|| model_info.origin == ModelOrigin::Synthetic
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -2,16 +2,17 @@ use super::unsupported_code_mode_warning;
|
||||
use codex_features::Feature;
|
||||
use codex_features::Features;
|
||||
use codex_models_manager::model_info::model_info_from_slug;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::CatalogModel;
|
||||
use codex_protocol::openai_models::ModelOrigin;
|
||||
use codex_protocol::openai_models::ToolMode;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
const MODEL_SLUG: &str = "test-model";
|
||||
|
||||
fn known_model_info() -> ModelInfo {
|
||||
ModelInfo {
|
||||
used_fallback_model_metadata: false,
|
||||
..model_info_from_slug(MODEL_SLUG)
|
||||
fn known_model_info() -> CatalogModel {
|
||||
CatalogModel {
|
||||
model: model_info_from_slug(MODEL_SLUG).into(),
|
||||
origin: ModelOrigin::Provider,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,10 +51,8 @@ fn does_not_warn_when_model_has_tool_mode_selector() {
|
||||
features.enable(Feature::CodeModeOnly);
|
||||
|
||||
for tool_mode in [ToolMode::Direct, ToolMode::CodeMode, ToolMode::CodeModeOnly] {
|
||||
let model_info = ModelInfo {
|
||||
tool_mode: Some(tool_mode),
|
||||
..known_model_info()
|
||||
};
|
||||
let mut model_info = known_model_info();
|
||||
model_info.tool_mode = Some(tool_mode);
|
||||
assert_eq!(unsupported_code_mode_warning(&model_info, &features), None);
|
||||
}
|
||||
}
|
||||
@@ -64,7 +63,13 @@ fn fallback_metadata_only_uses_existing_warning() {
|
||||
features.enable(Feature::CodeMode);
|
||||
|
||||
assert_eq!(
|
||||
unsupported_code_mode_warning(&model_info_from_slug(MODEL_SLUG), &features),
|
||||
unsupported_code_mode_warning(
|
||||
&CatalogModel {
|
||||
model: model_info_from_slug(MODEL_SLUG).into(),
|
||||
origin: ModelOrigin::Synthetic
|
||||
},
|
||||
&features
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4199,7 +4199,7 @@ impl Session {
|
||||
}
|
||||
let extension_data =
|
||||
codex_extension_api::ExtensionData::new(turn_context.sub_id.clone());
|
||||
extension_data.insert_arc(Arc::clone(&settings.model_info));
|
||||
extension_data.insert_arc(Arc::clone(&settings.model_info.model));
|
||||
// Standalone and hosted search must use the same turn-scoped permission mode.
|
||||
extension_data.insert(turn_context.config.web_search_mode.value());
|
||||
if let Some(messages) = settings
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::step_settings::ResolvedStepSettings;
|
||||
use super::*;
|
||||
use codex_protocol::openai_models::ModelOrigin;
|
||||
|
||||
/// Spawn a review thread using the given prompt.
|
||||
pub(super) async fn spawn_review_thread(
|
||||
@@ -45,7 +46,7 @@ pub(super) async fn spawn_review_thread(
|
||||
per_turn_config.features = review_features.clone();
|
||||
if let Some(current_effort) = reasoning_effort.as_ref()
|
||||
&& review_model_info.slug != parent_turn_context.model_info().slug
|
||||
&& !review_model_info.used_fallback_model_metadata
|
||||
&& review_model_info.origin != ModelOrigin::Synthetic
|
||||
&& !review_model_info
|
||||
.supported_reasoning_levels
|
||||
.iter()
|
||||
|
||||
@@ -35,6 +35,7 @@ use codex_protocol::capabilities::SelectedCapabilityRoot;
|
||||
use codex_protocol::config_types::ShellEnvironmentPolicy;
|
||||
use codex_protocol::mcp::ClientMcpExtensions;
|
||||
use codex_protocol::models::ProfileWorkspaceRoot;
|
||||
use codex_protocol::openai_models::CatalogModel;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::protocol::EnvironmentConfig;
|
||||
@@ -829,7 +830,7 @@ impl Session {
|
||||
auth_manager: Arc<AuthManager>,
|
||||
models_manager: SharedModelsManager,
|
||||
git_root_discovery: Arc<GitRootDiscovery>,
|
||||
model_info: ModelInfo,
|
||||
model_info: CatalogModel,
|
||||
exec_policy: Arc<ExecPolicyManager>,
|
||||
tx_event: Sender<Event>,
|
||||
agent_status: watch::Sender<AgentStatus>,
|
||||
@@ -1057,7 +1058,7 @@ impl Session {
|
||||
));
|
||||
// Publish the already resolved model before extensions make startup decisions.
|
||||
// Turn construction refreshes this attachment when the selected model changes.
|
||||
thread_extension_init.insert(model_info);
|
||||
thread_extension_init.insert(Arc::unwrap_or_clone(model_info.model));
|
||||
let tool_policy = thread_extension_init
|
||||
.get::<codex_extension_api::ToolPolicy>()
|
||||
.unwrap_or_else(|| {
|
||||
|
||||
@@ -14,10 +14,12 @@ use crate::environment_selection::validate_environment_ids_and_cwds;
|
||||
use crate::exec_policy::AllowPrefixRules;
|
||||
use codex_features::Feature;
|
||||
use codex_prompts::ResolvedModelMessages;
|
||||
use codex_protocol::openai_models::CatalogModel;
|
||||
use codex_protocol::openai_models::GuardianV2ModelConfig;
|
||||
use codex_protocol::openai_models::GuardianV2TranscriptModelConfig;
|
||||
use codex_protocol::openai_models::MODEL_SPECIALTY_CYBER;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ModelOrigin;
|
||||
use codex_protocol::protocol::EnvironmentConfigState;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use codex_protocol::protocol::TurnSettingsUpdate;
|
||||
@@ -86,16 +88,16 @@ fn check_legacy_turn_safety(
|
||||
/// Model-owned portion of the temporary legacy-turn safety check. Ordinary
|
||||
/// model metadata may differ so diagnostics can expose unmigrated consumers.
|
||||
fn check_legacy_model_safety(
|
||||
admitted: &ModelInfo,
|
||||
current: &ModelInfo,
|
||||
destination: &ModelInfo,
|
||||
admitted: &CatalogModel,
|
||||
current: &CatalogModel,
|
||||
destination: &CatalogModel,
|
||||
admitted_config: &Config,
|
||||
live_config: &Config,
|
||||
) -> Result<(), String> {
|
||||
if admitted.used_fallback_model_metadata || current.used_fallback_model_metadata {
|
||||
if admitted.origin == ModelOrigin::Synthetic || current.origin == ModelOrigin::Synthetic {
|
||||
return Err("the active model has only fallback metadata".to_string());
|
||||
}
|
||||
if destination.used_fallback_model_metadata {
|
||||
if destination.origin == ModelOrigin::Synthetic {
|
||||
return Err("the destination model has only fallback metadata".to_string());
|
||||
}
|
||||
let retained_models = [admitted, current];
|
||||
@@ -183,7 +185,8 @@ fn check_legacy_model_safety(
|
||||
if destination.auto_review_model_override.is_none() {
|
||||
let destination_model_messages = ResolvedModelMessages::from_model(destination);
|
||||
let destination_auto_review = destination_model_messages.auto_review();
|
||||
let retained_model_messages = retained_models.map(ResolvedModelMessages::from_model);
|
||||
let retained_model_messages =
|
||||
retained_models.map(|model| ResolvedModelMessages::from_model(model));
|
||||
let retained_auto_review = retained_model_messages
|
||||
.each_ref()
|
||||
.map(ResolvedModelMessages::auto_review);
|
||||
|
||||
@@ -38,8 +38,10 @@ use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::openai_models::AsyncClassifierMode;
|
||||
use codex_protocol::openai_models::AutoReviewMessages;
|
||||
use codex_protocol::openai_models::CatalogModel;
|
||||
use codex_protocol::openai_models::GuardianV2ModelConfig;
|
||||
use codex_protocol::openai_models::GuardianV2TranscriptModelConfig;
|
||||
use codex_protocol::openai_models::ModelOrigin;
|
||||
use codex_protocol::openai_models::ModelTokenBudgetConfig;
|
||||
use codex_protocol::openai_models::ModelsResponse;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
@@ -394,7 +396,7 @@ impl ModelsManager for GatedModelsManager {
|
||||
&'a self,
|
||||
model: &'a str,
|
||||
config: &'a ModelsManagerConfig,
|
||||
) -> ModelsManagerFuture<'a, ModelInfo> {
|
||||
) -> ModelsManagerFuture<'a, CatalogModel> {
|
||||
Box::pin(async move {
|
||||
let pause = if model == MODEL_B {
|
||||
self.first_b_lookup.lock().expect("lookup gate").take()
|
||||
@@ -685,7 +687,13 @@ async fn submitted_sparse_updates_preserve_captured_steps_and_ordering() {
|
||||
]
|
||||
);
|
||||
assert!(Arc::ptr_eq(&before.turn, &after.turn));
|
||||
assert_eq!(after.settings.model_info.as_ref(), &expected_destination);
|
||||
assert_eq!(
|
||||
after.settings.model_info.as_ref(),
|
||||
&CatalogModel {
|
||||
model: expected_destination.into(),
|
||||
origin: ModelOrigin::ExplicitConfig
|
||||
}
|
||||
);
|
||||
let initial_budget = turn
|
||||
.config
|
||||
.token_budget
|
||||
@@ -743,6 +751,16 @@ async fn submitted_sparse_updates_preserve_captured_steps_and_ordering() {
|
||||
assert_eq!(step_values(&retained), step_values(&after));
|
||||
assert_eq!(before.settings.model_info.slug, MODEL_A);
|
||||
assert_eq!(turn.model_info().slug, MODEL_A);
|
||||
for step in [&before, &during, &after, &retained] {
|
||||
let extension_model = step
|
||||
.extension_data
|
||||
.get::<ModelInfo>()
|
||||
.expect("step metadata");
|
||||
assert!(Arc::ptr_eq(
|
||||
&step.settings.model_info.model,
|
||||
&extension_model
|
||||
));
|
||||
}
|
||||
drop(submissions);
|
||||
loop_task.await.expect("submission loop teardown");
|
||||
}
|
||||
@@ -1044,7 +1062,7 @@ async fn activation_must_match_the_retained_turn_authority(change: ManagedPolicy
|
||||
});
|
||||
let model_info = Arc::make_mut(&mut settings.model_info);
|
||||
model_info.model_specialty = None;
|
||||
model_info.used_fallback_model_metadata = false;
|
||||
model_info.origin = ModelOrigin::Provider;
|
||||
});
|
||||
assert!(
|
||||
!turn
|
||||
@@ -1148,7 +1166,7 @@ async fn activation_must_match_the_retained_turn_authority(change: ManagedPolicy
|
||||
);
|
||||
}
|
||||
|
||||
fn safety_models() -> (ModelInfo, ModelInfo) {
|
||||
fn safety_models() -> (CatalogModel, CatalogModel) {
|
||||
let [mut admitted, mut destination]: [ModelInfo; 2] = activation_models()
|
||||
.try_into()
|
||||
.expect("two activation models");
|
||||
@@ -1157,12 +1175,20 @@ fn safety_models() -> (ModelInfo, ModelInfo) {
|
||||
model.node_repl_auto_review_required = false;
|
||||
model.node_repl_disabled = false;
|
||||
model.auto_review_model_override = None;
|
||||
model.used_fallback_model_metadata = false;
|
||||
let messages = model.model_messages.as_mut().expect("model messages");
|
||||
messages.auto_review = None;
|
||||
messages.guardian_v2 = None;
|
||||
}
|
||||
(admitted, destination)
|
||||
(
|
||||
CatalogModel {
|
||||
model: admitted.into(),
|
||||
origin: ModelOrigin::Provider,
|
||||
},
|
||||
CatalogModel {
|
||||
model: destination.into(),
|
||||
origin: ModelOrigin::Provider,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn parent_review_messages(model: &mut ModelInfo) -> &mut AutoReviewMessages {
|
||||
@@ -1222,7 +1248,7 @@ async fn model_safety_rejects_fallback_metadata() {
|
||||
let (_, turn) = make_session_and_context().await;
|
||||
let (admitted, destination) = safety_models();
|
||||
let mut fallback = admitted.clone();
|
||||
fallback.used_fallback_model_metadata = true;
|
||||
fallback.origin = ModelOrigin::Synthetic;
|
||||
for (old, current, next, reason) in [
|
||||
(
|
||||
&fallback,
|
||||
@@ -1291,7 +1317,7 @@ async fn parent_fallback_preserves_explicit_empty_and_bundled_defaults() {
|
||||
let mut config = turn.config.as_ref().clone();
|
||||
config.guardian_policy_config = None;
|
||||
let (admitted, mut destination) = safety_models();
|
||||
let check = |destination: &ModelInfo| {
|
||||
let check = |destination: &CatalogModel| {
|
||||
check_legacy_model_safety(&admitted, &admitted, destination, &config, &config)
|
||||
};
|
||||
parent_review_messages(&mut destination).policy = Some(defaults.policy.to_string());
|
||||
|
||||
@@ -14,7 +14,7 @@ use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::config_types::SERVICE_TIER_DEFAULT_REQUEST_VALUE;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::CatalogModel;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use std::sync::Arc;
|
||||
@@ -42,7 +42,7 @@ pub(crate) struct ResolvedStepSettings {
|
||||
/// Inputs for later sparse patches. Unset defaults and unsupported requested
|
||||
/// tiers must not be reconstructed from the effective values below.
|
||||
selected: Arc<StepSettings>,
|
||||
pub(crate) model_info: Arc<ModelInfo>,
|
||||
pub(crate) model_info: Arc<CatalogModel>,
|
||||
/// Effective request summary: the configured value or the pinned model's default.
|
||||
pub(crate) reasoning_summary: ReasoningSummary,
|
||||
/// Tier sent on requests, filtered by feature and model support.
|
||||
@@ -56,7 +56,7 @@ impl ResolvedStepSettings {
|
||||
/// Derives request values from selected settings and pinned metadata.
|
||||
pub(super) fn new(
|
||||
selected: Arc<StepSettings>,
|
||||
model_info: Arc<ModelInfo>,
|
||||
model_info: Arc<CatalogModel>,
|
||||
features: &Features,
|
||||
) -> Self {
|
||||
let reasoning_summary = selected
|
||||
@@ -241,7 +241,7 @@ impl StepSettings {
|
||||
&self,
|
||||
models_manager: &dyn ModelsManager,
|
||||
overrides: &ModelInfoOverrides,
|
||||
) -> ModelInfo {
|
||||
) -> CatalogModel {
|
||||
let config = overrides.models_manager_config(self.personality);
|
||||
models_manager
|
||||
.get_model_info(self.collaboration_mode.model(), &config)
|
||||
|
||||
@@ -26,8 +26,9 @@ use codex_protocol::SessionId;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::ShellEnvironmentPolicy;
|
||||
use codex_protocol::models::AdditionalPermissionProfile;
|
||||
use codex_protocol::openai_models::CatalogModel;
|
||||
use codex_protocol::openai_models::MODEL_SPECIALTY_CYBER;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ModelOrigin;
|
||||
use codex_protocol::permissions::RawFileSystemSandboxPolicy;
|
||||
use codex_protocol::protocol::EnvironmentConfig;
|
||||
use codex_protocol::protocol::EnvironmentConfigState;
|
||||
@@ -538,13 +539,13 @@ impl TurnContext {
|
||||
}
|
||||
|
||||
/// Captures current model metadata without preparing a step.
|
||||
pub(crate) fn capture_current_model_info(&self) -> Arc<ModelInfo> {
|
||||
pub(crate) fn capture_current_model_info(&self) -> Arc<CatalogModel> {
|
||||
Arc::clone(&self.next_step_settings.load().model_info)
|
||||
}
|
||||
|
||||
/// Legacy: returns the frozen initial-turn model metadata.
|
||||
/// Step-scoped consumers should use their captured `StepContext::settings`.
|
||||
pub(crate) fn model_info(&self) -> &Arc<ModelInfo> {
|
||||
pub(crate) fn model_info(&self) -> &Arc<CatalogModel> {
|
||||
&self.initial_settings.model_info
|
||||
}
|
||||
|
||||
@@ -1295,7 +1296,7 @@ impl Session {
|
||||
// Only execution and initial context creation publish model metadata.
|
||||
self.services
|
||||
.thread_extension_data
|
||||
.insert(model_info.clone());
|
||||
.insert_arc(Arc::clone(&model_info.model));
|
||||
self.resolve_multi_agent_version_for_model(&model_info, &per_turn_config)
|
||||
}
|
||||
TurnContextBuildMode::StartupPrewarm | TurnContextBuildMode::InjectItems => {
|
||||
@@ -1407,7 +1408,7 @@ impl Session {
|
||||
}
|
||||
|
||||
pub(crate) async fn maybe_emit_model_warnings_for_turn(&self, tc: &TurnContext) {
|
||||
if tc.model_info().used_fallback_model_metadata {
|
||||
if tc.model_info().origin == ModelOrigin::Synthetic {
|
||||
self.send_event(
|
||||
tc,
|
||||
EventMsg::Warning(WarningEvent {
|
||||
|
||||
@@ -242,7 +242,9 @@ pub fn get_model_offline(model: Option<&str>) -> String {
|
||||
}
|
||||
|
||||
pub fn construct_model_info_offline(model: &str, config: &Config) -> ModelInfo {
|
||||
construct_model_info_offline_for_tests(model, &config.to_models_manager_config())
|
||||
Arc::unwrap_or_clone(
|
||||
construct_model_info_offline_for_tests(model, &config.to_models_manager_config()).model,
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use codex_protocol::openai_models::CatalogModel;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
@@ -24,7 +25,6 @@ use crate::tools::events::ToolEventStage;
|
||||
use codex_core_plugins::PluginCommandAttribution;
|
||||
use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
use codex_protocol::exec_output::StreamOutput;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::ExecCommandOutputDeltaEvent;
|
||||
use codex_protocol::protocol::ExecCommandSource;
|
||||
@@ -332,7 +332,7 @@ pub(crate) async fn emit_exec_end_for_unified_exec(
|
||||
sandbox_type: Option<codex_protocol::sandbox::SandboxType>,
|
||||
session_ref: Arc<Session>,
|
||||
turn_ref: Arc<TurnContext>,
|
||||
model_info: Arc<ModelInfo>,
|
||||
model_info: Arc<CatalogModel>,
|
||||
call_id: String,
|
||||
command: Vec<String>,
|
||||
cwd: PathUri,
|
||||
@@ -384,7 +384,7 @@ pub(crate) async fn emit_failed_exec_end_for_unified_exec(
|
||||
sandbox_type: Option<codex_protocol::sandbox::SandboxType>,
|
||||
session_ref: Arc<Session>,
|
||||
turn_ref: Arc<TurnContext>,
|
||||
model_info: Arc<ModelInfo>,
|
||||
model_info: Arc<CatalogModel>,
|
||||
call_id: String,
|
||||
command: Vec<String>,
|
||||
cwd: PathUri,
|
||||
|
||||
@@ -645,7 +645,6 @@ fn remote_model_with_auto_review_override(slug: &str, review_model: &str) -> Mod
|
||||
visibility: ModelVisibility::List,
|
||||
supported_in_api: true,
|
||||
input_modalities: default_input_modalities(),
|
||||
used_fallback_model_metadata: false,
|
||||
supports_search_tool: false,
|
||||
supports_experimental_context: false,
|
||||
use_responses_lite: false,
|
||||
|
||||
@@ -359,14 +359,9 @@ async fn guardian_custom_gateway_selects_reviewer_by_catalog_origin(
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_config(move |config| {
|
||||
config.model = Some("gateway-main".to_string());
|
||||
config.model_catalog = match origin {
|
||||
ModelOrigin::ExplicitConfig => Some(catalog),
|
||||
ModelOrigin::Bundled | ModelOrigin::Provider => None,
|
||||
};
|
||||
config.model_provider.model_catalog_url = match origin {
|
||||
ModelOrigin::Provider => Some(catalog_url.into()),
|
||||
ModelOrigin::Bundled | ModelOrigin::ExplicitConfig => None,
|
||||
};
|
||||
config.model_catalog = (origin == ModelOrigin::ExplicitConfig).then_some(catalog);
|
||||
config.model_provider.model_catalog_url =
|
||||
(origin == ModelOrigin::Provider).then(|| catalog_url.into());
|
||||
config.model_provider.name = "Gateway".to_string();
|
||||
config.model_provider.supports_websockets = false;
|
||||
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest);
|
||||
|
||||
@@ -172,7 +172,6 @@ impl ModelsEndpointClient for TestModelsEndpoint {
|
||||
fn remote_model(slug: &str) -> ModelInfo {
|
||||
ModelInfo {
|
||||
visibility: ModelVisibility::List,
|
||||
used_fallback_model_metadata: false,
|
||||
..model_info_from_slug(slug)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,6 @@ struct RemoteModelResponse {
|
||||
fn remote_model(slug: &str) -> ModelInfo {
|
||||
ModelInfo {
|
||||
visibility: ModelVisibility::List,
|
||||
used_fallback_model_metadata: false,
|
||||
..model_info_from_slug(slug)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +122,6 @@ fn test_model_info(
|
||||
visibility: ModelVisibility::List,
|
||||
supported_in_api: true,
|
||||
input_modalities,
|
||||
used_fallback_model_metadata: false,
|
||||
supports_search_tool: false,
|
||||
supports_experimental_context: false,
|
||||
use_responses_lite: false,
|
||||
@@ -1255,7 +1254,6 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result<
|
||||
visibility: ModelVisibility::List,
|
||||
supported_in_api: true,
|
||||
input_modalities: default_input_modalities(),
|
||||
used_fallback_model_metadata: false,
|
||||
supports_search_tool: false,
|
||||
supports_experimental_context: false,
|
||||
use_responses_lite: false,
|
||||
|
||||
@@ -675,7 +675,6 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo {
|
||||
effective_context_window_percent: 95,
|
||||
experimental_supported_tools: Vec::new(),
|
||||
input_modalities: default_input_modalities(),
|
||||
used_fallback_model_metadata: false,
|
||||
supports_search_tool: false,
|
||||
supports_experimental_context: false,
|
||||
use_responses_lite: false,
|
||||
|
||||
@@ -579,7 +579,6 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> {
|
||||
visibility: ModelVisibility::List,
|
||||
supported_in_api: true,
|
||||
input_modalities: default_input_modalities(),
|
||||
used_fallback_model_metadata: false,
|
||||
supports_search_tool: false,
|
||||
supports_experimental_context: false,
|
||||
use_responses_lite: false,
|
||||
@@ -854,7 +853,6 @@ async fn remote_models_apply_legacy_instructions(auth: CodexAuth) -> Result<()>
|
||||
visibility: ModelVisibility::List,
|
||||
supported_in_api: true,
|
||||
input_modalities: default_input_modalities(),
|
||||
used_fallback_model_metadata: false,
|
||||
supports_search_tool: false,
|
||||
supports_experimental_context: false,
|
||||
use_responses_lite: false,
|
||||
@@ -1443,7 +1441,6 @@ fn test_remote_model_with_policy(
|
||||
visibility,
|
||||
supported_in_api: true,
|
||||
input_modalities: default_input_modalities(),
|
||||
used_fallback_model_metadata: false,
|
||||
supports_search_tool: false,
|
||||
supports_experimental_context: false,
|
||||
use_responses_lite: false,
|
||||
@@ -1538,7 +1535,7 @@ async fn model_catalog_url_supplies_conversation_model_and_instructions() -> Res
|
||||
model.visibility = ModelVisibility::List;
|
||||
model.supported_in_api = true;
|
||||
model.support_verbosity = false;
|
||||
model.used_fallback_model_metadata = false;
|
||||
|
||||
model
|
||||
.model_messages
|
||||
.as_mut()
|
||||
@@ -1597,7 +1594,7 @@ async fn failed_catalog_refresh_preserves_tool_search_on_the_next_turn() -> Resu
|
||||
let mut model = codex_models_manager::model_info::model_info_from_slug("gateway-model");
|
||||
model.visibility = ModelVisibility::List;
|
||||
model.supports_search_tool = true;
|
||||
model.used_fallback_model_metadata = false;
|
||||
|
||||
let catalog = ModelsResponse {
|
||||
models: vec![model],
|
||||
};
|
||||
|
||||
@@ -3594,7 +3594,6 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re
|
||||
effective_context_window_percent: 95,
|
||||
experimental_supported_tools: Vec::new(),
|
||||
input_modalities: vec![InputModality::Text],
|
||||
used_fallback_model_metadata: false,
|
||||
supports_search_tool: false,
|
||||
supports_experimental_context: false,
|
||||
use_responses_lite: false,
|
||||
|
||||
@@ -90,7 +90,6 @@ fn test_model_info(
|
||||
visibility,
|
||||
supported_in_api: true,
|
||||
input_modalities: default_input_modalities(),
|
||||
used_fallback_model_metadata: false,
|
||||
supports_search_tool: false,
|
||||
supports_experimental_context: false,
|
||||
use_responses_lite: false,
|
||||
|
||||
@@ -1621,7 +1621,6 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an
|
||||
visibility: ModelVisibility::List,
|
||||
supported_in_api: true,
|
||||
input_modalities: vec![InputModality::Text],
|
||||
used_fallback_model_metadata: false,
|
||||
supports_search_tool: false,
|
||||
supports_experimental_context: false,
|
||||
use_responses_lite: false,
|
||||
|
||||
@@ -85,7 +85,7 @@ impl ModelsManager for GatedModelsManager {
|
||||
&'a self,
|
||||
model: &'a str,
|
||||
config: &'a ModelsManagerConfig,
|
||||
) -> ModelsManagerFuture<'a, ModelInfo> {
|
||||
) -> ModelsManagerFuture<'a, codex_models_manager::CatalogModel> {
|
||||
Box::pin(async move {
|
||||
if self.gate_next_lookup.swap(/*val*/ false, Ordering::SeqCst) {
|
||||
self.context_build.entered.notify_one();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//! Requests outside every active action's lag allowance stop as superseded, not failed.
|
||||
//! Expiry also cancels metadata waits before transcript preparation, releasing reservations.
|
||||
|
||||
use codex_protocol::openai_models::ModelOrigin;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
@@ -302,10 +303,10 @@ impl Classification {
|
||||
.get_models_manager()
|
||||
.get_model_info(review_model_id, &config.to_models_manager_config())
|
||||
.await;
|
||||
if review_model.used_fallback_model_metadata && review_model_override.is_none() {
|
||||
if review_model.origin == ModelOrigin::Synthetic && review_model_override.is_none() {
|
||||
parent_model.clone()
|
||||
} else {
|
||||
Some(Arc::new(review_model))
|
||||
Some(review_model.model)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -310,7 +310,7 @@ async fn installed_extension_uses_http_after_warm_socket_auth_expires() -> Resul
|
||||
.get_model_info("gpt-5.5", &config.to_models_manager_config())
|
||||
.await;
|
||||
model.node_repl_auto_review_required = true;
|
||||
thread_store.insert(model);
|
||||
thread_store.insert_arc(model.model);
|
||||
registry.thread_lifecycle_contributors()[0]
|
||||
.on_thread_start(ThreadStartInput {
|
||||
config: &config,
|
||||
@@ -801,7 +801,7 @@ async fn computer_use_only_scores_cannot_approve_other_actions() -> Result<()> {
|
||||
computer_use: Some(codex_protocol::openai_models::GuardianReviewMode::Adaptive),
|
||||
..Default::default()
|
||||
});
|
||||
thread_store.insert(changed_model);
|
||||
thread_store.insert_arc(changed_model.model);
|
||||
assert_eq!(
|
||||
cached_approval(
|
||||
&fixture.registry,
|
||||
@@ -1049,7 +1049,7 @@ async fn sample_configured_conversation_history_with_delivery(
|
||||
.get_models_manager()
|
||||
.get_model_info("gpt-5.5", &config.to_models_manager_config())
|
||||
.await;
|
||||
thread_store.insert(parent_model);
|
||||
thread_store.insert_arc(parent_model.model);
|
||||
}
|
||||
assert_eq!(
|
||||
cached_approval(
|
||||
@@ -1316,7 +1316,7 @@ async fn contributor_fails_closed_when_model_configuration_is_invalid() -> Resul
|
||||
.test
|
||||
.codex
|
||||
.thread_extension_data()
|
||||
.insert(parent_model);
|
||||
.insert_arc(parent_model.model);
|
||||
|
||||
fixture.score_tool(ToolName::plain("read_file")).await;
|
||||
fixture.assert_fails_closed("elevated_risk").await
|
||||
@@ -2472,7 +2472,7 @@ async fn contributor_skips_required_models_in_standard_scope() -> Result<()> {
|
||||
.get_model_info("gpt-5.5", &config.to_models_manager_config())
|
||||
.await;
|
||||
model_info.slug = "protected-model".to_owned();
|
||||
thread_store.insert(model_info);
|
||||
thread_store.insert_arc(model_info.model);
|
||||
// A late prewarm preview must leave the active model's review requirements intact.
|
||||
let _ = codex_core::guardian_review::prepare_review_prewarm(&test.codex).await?;
|
||||
let authorization = ScoreAuthorization::current(&test.codex, &Default::default()).await;
|
||||
@@ -3348,7 +3348,7 @@ async fn assert_parent_compaction_reuse(parent_context_for_review: bool) -> Resu
|
||||
retained_context: parent_context_for_review.then(codex_history::RetainedContext::default),
|
||||
review_context_revision,
|
||||
};
|
||||
thread_store.insert(parent_model);
|
||||
thread_store.insert_arc(parent_model.model);
|
||||
|
||||
registry.tool_lifecycle_contributors()[0]
|
||||
.on_tool_start(ToolStartInput {
|
||||
|
||||
@@ -101,7 +101,7 @@ pub(super) async fn sampler_config(
|
||||
model, /*configured_window*/ None,
|
||||
)
|
||||
});
|
||||
let luna_compaction_hash = luna_model.and_then(|model| model.comp_hash);
|
||||
let luna_compaction_hash = luna_model.and_then(|model| model.comp_hash.clone());
|
||||
LunaSamplerConfig {
|
||||
api_key_cyber_access_programs: ApiKeyCyberAccessPrograms::from_config(input.config),
|
||||
workspace_routing: input
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
//! Text and image bytes stay separate. Hosts provide existing-context estimates;
|
||||
//! the shared wire estimate reserves image tokens without counting base64 as text.
|
||||
|
||||
use codex_protocol::openai_models::CatalogModel;
|
||||
use codex_protocol::openai_models::ModelOrigin;
|
||||
use std::io::Write;
|
||||
|
||||
use codex_protocol::models::ContentItem;
|
||||
@@ -86,14 +88,11 @@ pub const DEFAULT_MAX_INPUT_TOKENS: usize = 128_000;
|
||||
|
||||
/// Applies the same configured-window cap and effective percentage as sync review.
|
||||
/// Async callers supply no parent-model override.
|
||||
pub fn effective_input_token_limit(
|
||||
model: &codex_protocol::openai_models::ModelInfo,
|
||||
configured_window: Option<i64>,
|
||||
) -> usize {
|
||||
pub fn effective_input_token_limit(model: &CatalogModel, configured_window: Option<i64>) -> usize {
|
||||
let supported = model
|
||||
.resolved_context_window()
|
||||
.unwrap_or(DEFAULT_MAX_INPUT_TOKENS as i64);
|
||||
let supported = if model.used_fallback_model_metadata {
|
||||
let supported = if model.origin == ModelOrigin::Synthetic {
|
||||
supported.min(DEFAULT_MAX_INPUT_TOKENS as i64)
|
||||
} else {
|
||||
supported
|
||||
|
||||
@@ -300,7 +300,7 @@ impl MemoryStartupContext {
|
||||
|
||||
StageOneRequestContext {
|
||||
version: self.version,
|
||||
model_info,
|
||||
model_info: Arc::unwrap_or_clone(model_info.model),
|
||||
session_telemetry: build_session_telemetry(
|
||||
&self.auth_manager,
|
||||
self.thread_id,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use codex_models_manager::ModelsManagerConfig;
|
||||
use codex_protocol::config_types::ModelProviderAuthInfo;
|
||||
use codex_protocol::openai_models::ApplyPatchToolType;
|
||||
use codex_protocol::openai_models::ModelOrigin;
|
||||
use codex_protocol::protocol::MultiAgentVersion;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -329,7 +330,7 @@ async fn unknown_vertex_models_use_static_catalog_fallback() {
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
(model.slug, model.used_fallback_model_metadata),
|
||||
("vertex-test-model".to_string(), true)
|
||||
(model.slug.as_str(), model.origin),
|
||||
("vertex-test-model", ModelOrigin::Synthetic)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -905,10 +905,7 @@ mod tests {
|
||||
))))
|
||||
.await
|
||||
.unwrap();
|
||||
let model = codex_protocol::openai_models::ModelInfo {
|
||||
used_fallback_model_metadata: false,
|
||||
..model_info_from_slug("command-auth-model")
|
||||
};
|
||||
let model = model_info_from_slug("command-auth-model");
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/models"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(ModelsResponse {
|
||||
@@ -959,7 +956,10 @@ mod tests {
|
||||
manager
|
||||
.get_model_info(&model.slug, &Default::default())
|
||||
.await,
|
||||
model_info_from_slug(&model.slug)
|
||||
codex_models_manager::CatalogModel {
|
||||
model: model_info_from_slug(&model.slug).into(),
|
||||
origin: codex_models_manager::ModelOrigin::Synthetic,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
manager
|
||||
@@ -1001,7 +1001,7 @@ mod tests {
|
||||
let mut model = model_info_from_slug("provider-model");
|
||||
model.visibility = ModelVisibility::List;
|
||||
model.supported_in_api = true;
|
||||
model.used_fallback_model_metadata = false;
|
||||
|
||||
let expected = ModelsResponse {
|
||||
models: vec![model],
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@ async fn originator_managers_share_api_key_discovery_policy() {
|
||||
)]));
|
||||
let mut model = model_info_from_slug("gpt-5.5");
|
||||
model.visibility = codex_protocol::openai_models::ModelVisibility::List;
|
||||
model.used_fallback_model_metadata = false;
|
||||
|
||||
let catalog = ModelsResponse {
|
||||
models: vec![model],
|
||||
};
|
||||
@@ -118,7 +118,7 @@ async fn originator_catalogs_are_isolated_and_reused_from_disk() {
|
||||
] {
|
||||
let mut model = model_info_from_slug("gpt-5.5");
|
||||
model.visibility = codex_protocol::openai_models::ModelVisibility::List;
|
||||
model.used_fallback_model_metadata = false;
|
||||
|
||||
model.description = Some(originator.to_string());
|
||||
let catalog = ModelsResponse {
|
||||
models: vec![model],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! API-key discovery opt-outs must gate network requests and cached catalog authority.
|
||||
|
||||
use super::*;
|
||||
use codex_protocol::openai_models::ModelOrigin;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[tokio::test]
|
||||
@@ -70,7 +71,13 @@ async fn api_key_discovery_flag_controls_fetches_and_cached_catalogs() {
|
||||
models
|
||||
);
|
||||
assert_eq!(endpoint.fetch_count(), 1);
|
||||
assert_eq!(manager.get_model_info("dynamic", &config).await, models[0]);
|
||||
assert_eq!(
|
||||
manager.get_model_info("dynamic", &config).await,
|
||||
CatalogModel {
|
||||
model: models[0].clone().into(),
|
||||
origin: ModelOrigin::Provider
|
||||
}
|
||||
);
|
||||
|
||||
// A runtime rollback must also suppress the already-loaded catalog.
|
||||
manager.set_api_key_model_discovery_enabled(/*enabled*/ false);
|
||||
@@ -82,11 +89,9 @@ async fn api_key_discovery_flag_controls_fetches_and_cached_catalogs() {
|
||||
bundled
|
||||
);
|
||||
assert_eq!(manager.try_get_remote_models().unwrap(), bundled);
|
||||
assert!(
|
||||
manager
|
||||
.get_model_info("dynamic", &config)
|
||||
.await
|
||||
.used_fallback_model_metadata
|
||||
assert_eq!(
|
||||
manager.get_model_info("dynamic", &config).await.origin,
|
||||
ModelOrigin::Synthetic
|
||||
);
|
||||
assert_eq!(endpoint.fetch_count(), 1);
|
||||
|
||||
|
||||
@@ -7,20 +7,8 @@ use codex_protocol::openai_models::ModelInfo;
|
||||
|
||||
use crate::cache::ModelsCacheEntry;
|
||||
|
||||
/// Where an effective catalog entry was obtained.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ModelOrigin {
|
||||
Bundled,
|
||||
Provider,
|
||||
ExplicitConfig,
|
||||
}
|
||||
|
||||
/// Model metadata and its source captured from the same catalog snapshot.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CatalogModel {
|
||||
pub model: ModelInfo,
|
||||
pub origin: ModelOrigin,
|
||||
}
|
||||
pub use codex_protocol::openai_models::CatalogModel;
|
||||
pub use codex_protocol::openai_models::ModelOrigin;
|
||||
|
||||
/// Catalog authority is independent of whether its model metadata is borrowed or owned.
|
||||
pub(crate) enum SelectedCatalog<'a> {
|
||||
@@ -64,19 +52,25 @@ impl CatalogState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn model_origin(&self, selected: &SelectedCatalog<'_>, slug: &str) -> ModelOrigin {
|
||||
if matches!(selected, SelectedCatalog::CurrentSnapshot(_))
|
||||
&& self.provider_model_ids.contains(slug)
|
||||
{
|
||||
ModelOrigin::Provider
|
||||
} else {
|
||||
ModelOrigin::Bundled
|
||||
}
|
||||
}
|
||||
|
||||
pub fn model_catalog(&self, selected: SelectedCatalog<'_>) -> Vec<CatalogModel> {
|
||||
let uses_snapshot_ids = matches!(selected, SelectedCatalog::CurrentSnapshot(_));
|
||||
selected
|
||||
.into_models()
|
||||
.into_owned()
|
||||
.into_iter()
|
||||
.map(|model| {
|
||||
let origin = if uses_snapshot_ids && self.provider_model_ids.contains(&model.slug) {
|
||||
ModelOrigin::Provider
|
||||
} else {
|
||||
ModelOrigin::Bundled
|
||||
};
|
||||
CatalogModel { model, origin }
|
||||
let models = match &selected {
|
||||
SelectedCatalog::CurrentSnapshot(models) | SelectedCatalog::Fallback(models) => models,
|
||||
};
|
||||
models
|
||||
.iter()
|
||||
.map(|model| CatalogModel {
|
||||
origin: self.model_origin(&selected, &model.slug),
|
||||
model: model.clone().into(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ async fn catalog_origins_survive_reload_and_auth_change() {
|
||||
.model_catalog_with_origins(RefreshStrategy::Online, DEFAULT_HTTP_CLIENT_FACTORY)
|
||||
.await;
|
||||
assert!(catalog.contains(&CatalogModel {
|
||||
model,
|
||||
model: model.into(),
|
||||
origin: ModelOrigin::Provider
|
||||
}));
|
||||
assert_eq!(
|
||||
@@ -45,6 +45,16 @@ async fn catalog_origins_survive_reload_and_auth_change() {
|
||||
catalog
|
||||
);
|
||||
assert_eq!(endpoint.fetch_count(), 1);
|
||||
assert_eq!(
|
||||
restarted
|
||||
.get_model_info(
|
||||
&format!("gateway/{slug}-variant"),
|
||||
&ModelsManagerConfig::default()
|
||||
)
|
||||
.await
|
||||
.origin,
|
||||
ModelOrigin::Provider
|
||||
);
|
||||
|
||||
// An empty subsequent response restores bundled origins instead of retaining old IDs.
|
||||
assert_eq!(
|
||||
@@ -65,6 +75,13 @@ async fn catalog_origins_survive_reload_and_auth_change() {
|
||||
bundled
|
||||
);
|
||||
assert_eq!(endpoint.fetch_count(), 2);
|
||||
assert_eq!(
|
||||
manager
|
||||
.get_model_info("codex-auto-review", &ModelsManagerConfig::default())
|
||||
.await
|
||||
.origin,
|
||||
ModelOrigin::Bundled
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,12 +100,28 @@ async fn static_catalog_records_its_supplied_origin() {
|
||||
},
|
||||
origin,
|
||||
);
|
||||
assert_eq!(
|
||||
manager
|
||||
.get_model_info(
|
||||
"namespace/reviewer-variant",
|
||||
&ModelsManagerConfig::default()
|
||||
)
|
||||
.await,
|
||||
CatalogModel {
|
||||
model: ModelInfo {
|
||||
slug: "namespace/reviewer-variant".to_string(),
|
||||
..model.clone()
|
||||
}
|
||||
.into(),
|
||||
origin
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
manager
|
||||
.model_catalog_with_origins(RefreshStrategy::Offline, DEFAULT_HTTP_CLIENT_FACTORY)
|
||||
.await,
|
||||
vec![CatalogModel {
|
||||
model: model.clone(),
|
||||
model: model.clone().into(),
|
||||
origin
|
||||
}]
|
||||
);
|
||||
|
||||
@@ -177,7 +177,7 @@ pub trait ModelsManager: fmt::Debug + Send + Sync {
|
||||
.model_catalog_with_origins(refresh_strategy, http_client_factory)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|entry| entry.model)
|
||||
.map(|entry| Arc::unwrap_or_clone(entry.model))
|
||||
.collect(),
|
||||
}
|
||||
})
|
||||
@@ -272,20 +272,12 @@ pub trait ModelsManager: fmt::Debug + Send + Sync {
|
||||
}
|
||||
|
||||
// todo(aibrahim): look if we can tighten it to pub(crate)
|
||||
/// Look up model metadata, applying remote overrides and config adjustments.
|
||||
/// Resolve model metadata and its source, preserving provenance through config adjustments.
|
||||
fn get_model_info<'a>(
|
||||
&'a self,
|
||||
model: &'a str,
|
||||
config: &'a ModelsManagerConfig,
|
||||
) -> ModelsManagerFuture<'a, ModelInfo> {
|
||||
Box::pin(
|
||||
async move {
|
||||
let remote_models = self.get_remote_models().await;
|
||||
construct_model_info_from_candidates(model, &remote_models, config)
|
||||
}
|
||||
.instrument(tracing::info_span!("get_model_info", model = model)),
|
||||
)
|
||||
}
|
||||
) -> ModelsManagerFuture<'a, CatalogModel>;
|
||||
|
||||
/// Refresh models if the provided ETag differs from the cached ETag.
|
||||
///
|
||||
@@ -535,18 +527,26 @@ impl ModelsManager for OpenAiModelsManager {
|
||||
&'a self,
|
||||
model: &'a str,
|
||||
config: &'a ModelsManagerConfig,
|
||||
) -> ModelsManagerFuture<'a, ModelInfo> {
|
||||
) -> ModelsManagerFuture<'a, CatalogModel> {
|
||||
Box::pin(async move {
|
||||
let remote = {
|
||||
let entry = self.remote_models.read().await;
|
||||
let models = self.models_for_entry(&entry.entry).into_models();
|
||||
match &self.catalog_source {
|
||||
CatalogSource::Default => find_model_from_candidates(model, &models),
|
||||
let selected = self.models_for_entry(&entry.entry);
|
||||
let models = match &selected {
|
||||
SelectedCatalog::CurrentSnapshot(models)
|
||||
| SelectedCatalog::Fallback(models) => models,
|
||||
};
|
||||
let model = match &self.catalog_source {
|
||||
CatalogSource::Default => find_model_from_candidates(model, models),
|
||||
CatalogSource::ExplicitProvider(_) => models
|
||||
.iter()
|
||||
.find(|candidate| candidate.slug == model)
|
||||
.cloned(),
|
||||
}
|
||||
};
|
||||
model.map(|model| CatalogModel {
|
||||
origin: entry.model_origin(&selected, &model.slug),
|
||||
model: model.into(),
|
||||
})
|
||||
};
|
||||
construct_model_info(model, remote, config)
|
||||
})
|
||||
@@ -866,7 +866,7 @@ impl ModelsManager for StaticModelsManager {
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|model| CatalogModel {
|
||||
model,
|
||||
model: model.into(),
|
||||
origin: self.origin,
|
||||
})
|
||||
.collect()
|
||||
@@ -877,10 +877,10 @@ impl ModelsManager for StaticModelsManager {
|
||||
&'a self,
|
||||
model: &'a str,
|
||||
config: &'a ModelsManagerConfig,
|
||||
) -> ModelsManagerFuture<'a, ModelInfo> {
|
||||
Box::pin(
|
||||
async move { construct_model_info_from_candidates(model, &self.remote_models, config) },
|
||||
)
|
||||
) -> ModelsManagerFuture<'a, CatalogModel> {
|
||||
Box::pin(async move {
|
||||
construct_model_info_from_candidates(model, &self.remote_models, self.origin, config)
|
||||
})
|
||||
}
|
||||
|
||||
fn get_default_model<'a>(
|
||||
@@ -1016,27 +1016,29 @@ fn find_model_from_candidates(model: &str, candidates: &[ModelInfo]) -> Option<M
|
||||
pub(crate) fn construct_model_info_from_candidates(
|
||||
model: &str,
|
||||
candidates: &[ModelInfo],
|
||||
origin: ModelOrigin,
|
||||
config: &ModelsManagerConfig,
|
||||
) -> ModelInfo {
|
||||
let remote = find_model_from_candidates(model, candidates);
|
||||
) -> CatalogModel {
|
||||
let remote = find_model_from_candidates(model, candidates).map(|model| CatalogModel {
|
||||
model: model.into(),
|
||||
origin,
|
||||
});
|
||||
construct_model_info(model, remote, config)
|
||||
}
|
||||
|
||||
fn construct_model_info(
|
||||
model: &str,
|
||||
remote: Option<ModelInfo>,
|
||||
remote: Option<CatalogModel>,
|
||||
config: &ModelsManagerConfig,
|
||||
) -> ModelInfo {
|
||||
let model_info = if let Some(remote) = remote {
|
||||
ModelInfo {
|
||||
slug: model.to_string(),
|
||||
used_fallback_model_metadata: false,
|
||||
..remote
|
||||
}
|
||||
} else {
|
||||
model_info::model_info_from_slug(model)
|
||||
};
|
||||
model_info::with_config_overrides(model_info, config)
|
||||
) -> CatalogModel {
|
||||
let mut resolved = remote.unwrap_or_else(|| CatalogModel {
|
||||
model: model_info::model_info_from_slug(model).into(),
|
||||
origin: ModelOrigin::Synthetic,
|
||||
});
|
||||
let mut metadata = Arc::unwrap_or_clone(resolved.model);
|
||||
metadata.slug = model.to_owned();
|
||||
resolved.model = model_info::with_config_overrides(metadata, config).into();
|
||||
resolved
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -18,6 +18,7 @@ use codex_login::TokenData;
|
||||
use codex_protocol::auth::AuthMode;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::openai_models::ModelAccessPrograms;
|
||||
use codex_protocol::openai_models::ModelOrigin;
|
||||
use codex_protocol::openai_models::ModelsResponse;
|
||||
use codex_protocol::turn_input::CyberAccessProgram;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -572,7 +573,7 @@ async fn injected_cache_write_error_does_not_fail_remote_refresh() {
|
||||
remote_models
|
||||
.into_iter()
|
||||
.map(|model| CatalogModel {
|
||||
model,
|
||||
model: model.into(),
|
||||
origin: ModelOrigin::Provider,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
@@ -768,9 +769,12 @@ async fn dynamic_manager_preserves_requested_model_when_fallback_is_allowed() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_model_info_tracks_fallback_usage() {
|
||||
async fn get_model_info_tracks_metadata_origin() {
|
||||
let codex_home = tempdir().expect("temp dir");
|
||||
let config = ModelsManagerConfig::default();
|
||||
let config = ModelsManagerConfig {
|
||||
base_instructions: Some("Custom instructions".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let manager = openai_manager_for_tests(
|
||||
codex_home.path().to_path_buf(),
|
||||
TestModelsEndpoint::new(Vec::new()),
|
||||
@@ -784,14 +788,21 @@ async fn get_model_info_tracks_fallback_usage() {
|
||||
.clone();
|
||||
|
||||
let known = manager.get_model_info(known_slug.as_str(), &config).await;
|
||||
assert!(!known.used_fallback_model_metadata);
|
||||
assert_eq!(known.origin, ModelOrigin::Bundled);
|
||||
assert_eq!(known.slug, known_slug);
|
||||
|
||||
let unknown = manager
|
||||
.get_model_info("model-that-does-not-exist", &config)
|
||||
.await;
|
||||
assert!(unknown.used_fallback_model_metadata);
|
||||
assert_eq!(unknown.origin, ModelOrigin::Synthetic);
|
||||
assert_eq!(unknown.slug, "model-that-does-not-exist");
|
||||
assert_eq!(
|
||||
unknown
|
||||
.model_messages
|
||||
.as_ref()
|
||||
.and_then(|messages| messages.instructions_template.as_deref()),
|
||||
config.base_instructions.as_deref()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -835,16 +846,14 @@ async fn get_model_info_uses_custom_catalog() {
|
||||
assert_eq!(model_info.display_name, "Overlay");
|
||||
assert_eq!(model_info.context_window, Some(272_000));
|
||||
assert!(model_info.supports_image_detail_original);
|
||||
assert!(!model_info.used_fallback_model_metadata);
|
||||
assert_eq!(model_info.origin, ModelOrigin::ExplicitConfig);
|
||||
|
||||
let config = ModelsManagerConfig {
|
||||
model_context_window: Some(128_000),
|
||||
..Default::default()
|
||||
};
|
||||
let expected = ModelInfo {
|
||||
context_window: Some(128_000),
|
||||
..model_info
|
||||
};
|
||||
let mut expected = model_info;
|
||||
expected.context_window = Some(128_000);
|
||||
assert_eq!(
|
||||
manager
|
||||
.get_model_info("gpt-overlay-experiment", &config)
|
||||
@@ -867,7 +876,7 @@ async fn get_model_info_matches_namespaced_suffix() {
|
||||
|
||||
assert_eq!(model_info.slug, namespaced_model);
|
||||
assert!(model_info.supports_image_detail_original);
|
||||
assert!(!model_info.used_fallback_model_metadata);
|
||||
assert_eq!(model_info.origin, ModelOrigin::ExplicitConfig);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -882,7 +891,7 @@ async fn get_model_info_matches_hyphenated_provider_namespace_suffix() {
|
||||
let model_info = manager.get_model_info(&namespaced_model, &config).await;
|
||||
|
||||
assert_eq!(model_info.slug, namespaced_model);
|
||||
assert!(!model_info.used_fallback_model_metadata);
|
||||
assert_eq!(model_info.origin, ModelOrigin::ExplicitConfig);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -905,7 +914,7 @@ async fn get_model_info_rejects_multi_segment_namespace_suffix_matching() {
|
||||
let model_info = manager.get_model_info(&namespaced_model, &config).await;
|
||||
|
||||
assert_eq!(model_info.slug, namespaced_model);
|
||||
assert!(model_info.used_fallback_model_metadata);
|
||||
assert_eq!(model_info.origin, ModelOrigin::Synthetic);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1037,7 +1046,7 @@ async fn get_model_info_uses_fallback_for_bundled_models_when_chatgpt_remote_is_
|
||||
.await;
|
||||
|
||||
assert_eq!(model_info.slug, bundled_slug);
|
||||
assert!(model_info.used_fallback_model_metadata);
|
||||
assert_eq!(model_info.origin, ModelOrigin::Synthetic);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1137,7 +1146,10 @@ async fn authoritative_catalog_preserves_hidden_models_and_matches_exact_ids() {
|
||||
manager
|
||||
.get_model_info(unknown, &ModelsManagerConfig::default())
|
||||
.await,
|
||||
model_info::model_info_from_slug(unknown)
|
||||
CatalogModel {
|
||||
model: model_info::model_info_from_slug(unknown).into(),
|
||||
origin: ModelOrigin::Synthetic
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +132,6 @@ pub fn model_info_from_slug(slug: &str) -> ModelInfo {
|
||||
effective_context_window_percent: 95,
|
||||
experimental_supported_tools: Vec::new(),
|
||||
input_modalities: default_input_modalities(),
|
||||
used_fallback_model_metadata: true, // this is the fallback model metadata
|
||||
supports_search_tool: false,
|
||||
supports_experimental_context: false,
|
||||
use_responses_lite: false,
|
||||
|
||||
@@ -244,7 +244,6 @@ fn unknown_model_uses_builtin_instruction_template() {
|
||||
let model = model_info_from_slug("unknown-model");
|
||||
|
||||
assert_eq!(render_model_instructions(&model), BASE_INSTRUCTIONS);
|
||||
assert!(model.used_fallback_model_metadata);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
//!
|
||||
//! Production code should not depend on this module.
|
||||
|
||||
use crate::CatalogModel;
|
||||
use crate::ModelOrigin;
|
||||
use crate::ModelsManagerConfig;
|
||||
use crate::bundled_models_response;
|
||||
use crate::manager::construct_model_info_from_candidates;
|
||||
@@ -24,15 +26,15 @@ pub fn get_model_offline_for_tests(model: Option<&str>) -> String {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Build `ModelInfo` without consulting remote state or cache.
|
||||
/// Resolve metadata and its source without consulting remote state or cache.
|
||||
pub fn construct_model_info_offline_for_tests(
|
||||
model: &str,
|
||||
config: &ModelsManagerConfig,
|
||||
) -> ModelInfo {
|
||||
) -> CatalogModel {
|
||||
let candidates: &[ModelInfo] = if let Some(model_catalog) = config.model_catalog.as_ref() {
|
||||
&model_catalog.models
|
||||
} else {
|
||||
&[]
|
||||
};
|
||||
construct_model_info_from_candidates(model, candidates, config)
|
||||
construct_model_info_from_candidates(model, candidates, ModelOrigin::ExplicitConfig, config)
|
||||
}
|
||||
|
||||
@@ -43,7 +43,6 @@ fn test_model(model_messages: Option<ModelMessages>) -> ModelInfo {
|
||||
effective_context_window_percent: 95,
|
||||
experimental_supported_tools: vec![],
|
||||
input_modalities: vec![],
|
||||
used_fallback_model_metadata: false,
|
||||
supports_search_tool: false,
|
||||
supports_experimental_context: false,
|
||||
use_responses_lite: false,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::DateTime;
|
||||
use chrono::Utc;
|
||||
@@ -478,11 +479,6 @@ pub struct ModelInfo {
|
||||
/// 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.
|
||||
@@ -519,6 +515,39 @@ pub struct ModelInfo {
|
||||
pub multi_agent_reasoning_effort: Option<ReasoningEffort>,
|
||||
}
|
||||
|
||||
/// Where model metadata was obtained during local catalog resolution.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ModelOrigin {
|
||||
Bundled,
|
||||
Provider,
|
||||
ExplicitConfig,
|
||||
/// Generic metadata synthesized when no catalog entry matches the requested model.
|
||||
Synthetic,
|
||||
}
|
||||
|
||||
/// Model metadata and its locally determined source, retained together through resolution.
|
||||
/// Metadata is shared with runtime snapshots and extension stores; mutation uses copy-on-write.
|
||||
/// This runtime wrapper is not serialized; managers assign its source from local catalog state.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CatalogModel {
|
||||
pub model: Arc<ModelInfo>,
|
||||
pub origin: ModelOrigin,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for CatalogModel {
|
||||
type Target = ModelInfo;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.model
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for CatalogModel {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
Arc::make_mut(&mut self.model)
|
||||
}
|
||||
}
|
||||
|
||||
impl ModelInfo {
|
||||
pub fn resolved_context_window(&self) -> Option<i64> {
|
||||
self.context_window.or(self.max_context_window)
|
||||
@@ -1173,7 +1202,6 @@ mod tests {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user