From 1db6f3dba37a0e27586cb17ea30303416e32502a Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim <219906144+aibrahim-oai@users.noreply.github.com> Date: Fri, 20 Mar 2026 16:12:07 +0000 Subject: [PATCH] Extract codex-models from core Move model manager and provider ownership into a new workspace crate and route workspace consumers through codex-models. Co-authored-by: Codex --- codex-rs/Cargo.lock | 37 +++++ codex-rs/Cargo.toml | 2 + codex-rs/app-server-client/Cargo.toml | 1 + codex-rs/app-server-client/src/lib.rs | 2 +- codex-rs/app-server/Cargo.toml | 1 + .../app-server/src/codex_message_processor.rs | 2 +- codex-rs/app-server/src/message_processor.rs | 2 +- codex-rs/app-server/src/models.rs | 2 +- .../app-server/tests/common/models_cache.rs | 3 +- codex-rs/core/BUILD.bazel | 16 +-- codex-rs/core/Cargo.toml | 1 + codex-rs/core/src/api_bridge.rs | 13 +- codex-rs/core/src/auth_env_telemetry.rs | 5 +- codex-rs/core/src/client.rs | 9 +- codex-rs/core/src/client_tests.rs | 7 +- codex-rs/core/src/codex.rs | 34 ++--- codex-rs/core/src/codex_delegate.rs | 2 +- codex-rs/core/src/codex_tests.rs | 35 +++-- codex-rs/core/src/compact.rs | 2 +- codex-rs/core/src/config/config_tests.rs | 11 +- codex-rs/core/src/config/mod.rs | 14 +- codex-rs/core/src/guardian/review.rs | 3 +- codex-rs/core/src/guardian/review_session.rs | 2 +- codex-rs/core/src/lib.rs | 12 +- codex-rs/core/src/memories/phase1.rs | 3 +- codex-rs/core/src/memories/prompts_tests.rs | 2 +- codex-rs/core/src/model_info_overrides.rs | 15 ++ codex-rs/core/src/models_manager/mod.rs | 15 -- .../core/src/original_image_detail_tests.rs | 24 +++- codex-rs/core/src/realtime_conversation.rs | 18 ++- codex-rs/core/src/state/service.rs | 2 +- codex-rs/core/src/tasks/mod.rs | 2 +- codex-rs/core/src/test_support.rs | 20 +-- codex-rs/core/src/thread_manager.rs | 14 +- codex-rs/core/src/thread_manager_tests.rs | 4 +- .../core/src/tools/handlers/multi_agents.rs | 5 +- .../src/tools/handlers/multi_agents_tests.rs | 2 +- codex-rs/core/src/tools/spec.rs | 2 +- codex-rs/core/src/tools/spec_tests.rs | 91 ++++++------ .../templates/collaboration_mode/execute.md | 45 ------ .../collaboration_mode/pair_programming.md | 7 - codex-rs/core/tests/common/BUILD.bazel | 2 +- codex-rs/core/tests/common/Cargo.toml | 1 + codex-rs/core/tests/common/lib.rs | 13 ++ codex-rs/core/tests/common/test_codex.rs | 8 +- codex-rs/core/tests/responses_headers.rs | 4 +- codex-rs/core/tests/suite/client.rs | 14 +- .../core/tests/suite/client_websockets.rs | 4 +- codex-rs/core/tests/suite/compact.rs | 7 +- .../core/tests/suite/model_info_overrides.rs | 13 +- codex-rs/core/tests/suite/model_switching.rs | 13 +- codex-rs/core/tests/suite/models_cache_ttl.rs | 7 +- codex-rs/core/tests/suite/personality.rs | 4 +- codex-rs/core/tests/suite/prompt_caching.rs | 3 +- codex-rs/core/tests/suite/remote_models.rs | 35 +++-- codex-rs/core/tests/suite/rmcp_client.rs | 2 +- codex-rs/core/tests/suite/search_tool.rs | 3 +- .../tests/suite/spawn_agent_description.rs | 4 +- .../suite/stream_error_allows_next_turn.rs | 4 +- .../core/tests/suite/stream_no_completed.rs | 4 +- .../core/tests/suite/websocket_fallback.rs | 9 +- codex-rs/exec/Cargo.toml | 1 + codex-rs/exec/src/lib.rs | 4 +- codex-rs/lmstudio/Cargo.toml | 1 + codex-rs/lmstudio/src/client.rs | 2 +- codex-rs/mcp-server/Cargo.toml | 1 + codex-rs/mcp-server/src/message_processor.rs | 2 +- codex-rs/models/BUILD.bazel | 20 +++ codex-rs/models/Cargo.toml | 41 ++++++ codex-rs/{core => models}/models.json | 0 codex-rs/models/src/api_bridge.rs | 84 +++++++++++ codex-rs/models/src/auth_env_telemetry.rs | 74 ++++++++++ codex-rs/models/src/error.rs | 17 +++ codex-rs/models/src/feedback_tags.rs | 106 ++++++++++++++ codex-rs/models/src/lib.rs | 47 ++++++ .../src/model_provider_info.rs | 40 +++--- .../src/model_provider_info_tests.rs | 0 .../src/models_manager/cache.rs | 0 .../collaboration_mode_presets.rs | 2 +- .../collaboration_mode_presets_tests.rs | 0 .../src/models_manager/manager.rs | 68 +++++---- .../src/models_manager/manager_tests.rs | 93 ++++++++---- .../src/models_manager/model_info.rs | 44 ++++-- .../src/models_manager/model_info_tests.rs | 25 ++-- .../src/models_manager/model_presets.rs | 0 codex-rs/models/src/response_debug_context.rs | 135 ++++++++++++++++++ .../templates/collaboration_mode/default.md | 0 .../templates/collaboration_mode/plan.md | 0 codex-rs/ollama/Cargo.toml | 1 + codex-rs/ollama/src/client.rs | 9 +- codex-rs/ollama/src/lib.rs | 2 +- codex-rs/tui/BUILD.bazel | 2 +- codex-rs/tui/Cargo.toml | 1 + codex-rs/tui/src/app.rs | 8 +- codex-rs/tui/src/chatwidget.rs | 2 +- codex-rs/tui/src/chatwidget/tests.rs | 4 +- codex-rs/tui/src/collaboration_modes.rs | 2 +- codex-rs/tui/src/oss_selection.rs | 8 +- codex-rs/tui/src/status/card.rs | 2 +- .../tui/tests/suite/model_availability_nux.rs | 2 +- codex-rs/tui_app_server/BUILD.bazel | 7 +- codex-rs/tui_app_server/Cargo.toml | 1 + codex-rs/tui_app_server/src/app.rs | 6 +- .../tui_app_server/src/chatwidget/tests.rs | 2 +- codex-rs/tui_app_server/src/model_catalog.rs | 95 +----------- codex-rs/tui_app_server/src/oss_selection.rs | 8 +- codex-rs/tui_app_server/src/status/card.rs | 2 +- .../tests/suite/model_availability_nux.rs | 2 +- codex-rs/utils/oss/Cargo.toml | 1 + codex-rs/utils/oss/src/lib.rs | 4 +- codex-rs/utils/sandbox-summary/Cargo.toml | 1 + .../sandbox-summary/src/config_summary.rs | 2 +- 112 files changed, 1088 insertions(+), 536 deletions(-) create mode 100644 codex-rs/core/src/model_info_overrides.rs delete mode 100644 codex-rs/core/src/models_manager/mod.rs delete mode 100644 codex-rs/core/templates/collaboration_mode/execute.md delete mode 100644 codex-rs/core/templates/collaboration_mode/pair_programming.md create mode 100644 codex-rs/models/BUILD.bazel create mode 100644 codex-rs/models/Cargo.toml rename codex-rs/{core => models}/models.json (100%) create mode 100644 codex-rs/models/src/api_bridge.rs create mode 100644 codex-rs/models/src/auth_env_telemetry.rs create mode 100644 codex-rs/models/src/error.rs create mode 100644 codex-rs/models/src/feedback_tags.rs create mode 100644 codex-rs/models/src/lib.rs rename codex-rs/{core => models}/src/model_provider_info.rs (92%) rename codex-rs/{core => models}/src/model_provider_info_tests.rs (100%) rename codex-rs/{core => models}/src/models_manager/cache.rs (100%) rename codex-rs/{core => models}/src/models_manager/collaboration_mode_presets.rs (98%) rename codex-rs/{core => models}/src/models_manager/collaboration_mode_presets_tests.rs (100%) rename codex-rs/{core => models}/src/models_manager/manager.rs (93%) rename codex-rs/{core => models}/src/models_manager/manager_tests.rs (92%) rename codex-rs/{core => models}/src/models_manager/model_info.rs (76%) rename codex-rs/{core => models}/src/models_manager/model_info_tests.rs (52%) rename codex-rs/{core => models}/src/models_manager/model_presets.rs (100%) create mode 100644 codex-rs/models/src/response_debug_context.rs rename codex-rs/{core => models}/templates/collaboration_mode/default.md (100%) rename codex-rs/{core => models}/templates/collaboration_mode/plan.md (100%) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 13e6eaf597..2ddcd8b3bb 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1433,6 +1433,7 @@ dependencies = [ "codex-feedback", "codex-file-search", "codex-login", + "codex-models", "codex-otel", "codex-protocol", "codex-rmcp-client", @@ -1478,6 +1479,7 @@ dependencies = [ "codex-core", "codex-features", "codex-feedback", + "codex-models", "codex-protocol", "futures", "pretty_assertions", @@ -1854,6 +1856,7 @@ dependencies = [ "codex-git", "codex-hooks", "codex-login", + "codex-models", "codex-network-proxy", "codex-otel", "codex-protocol", @@ -1964,6 +1967,7 @@ dependencies = [ "codex-cloud-requirements", "codex-core", "codex-feedback", + "codex-models", "codex-otel", "codex-protocol", "codex-utils-absolute-path", @@ -2175,6 +2179,7 @@ name = "codex-lmstudio" version = "0.0.0" dependencies = [ "codex-core", + "codex-models", "reqwest", "serde_json", "tokio", @@ -2229,6 +2234,7 @@ dependencies = [ "codex-arg0", "codex-core", "codex-features", + "codex-models", "codex-protocol", "codex-shell-command", "codex-utils-cli", @@ -2249,6 +2255,31 @@ dependencies = [ "wiremock", ] +[[package]] +name = "codex-models" +version = "0.0.0" +dependencies = [ + "base64 0.22.1", + "chrono", + "codex-api", + "codex-login", + "codex-otel", + "codex-protocol", + "http 1.4.0", + "maplit", + "pretty_assertions", + "schemars 0.8.22", + "serde", + "serde_json", + "tempfile", + "thiserror 2.0.18", + "tokio", + "toml 0.9.11+spec-1.1.0", + "tracing", + "tracing-subscriber", + "wiremock", +] + [[package]] name = "codex-network-proxy" version = "0.0.0" @@ -2288,6 +2319,7 @@ dependencies = [ "async-stream", "bytes", "codex-core", + "codex-models", "futures", "pretty_assertions", "reqwest", @@ -2579,6 +2611,7 @@ dependencies = [ "codex-feedback", "codex-file-search", "codex-login", + "codex-models", "codex-otel", "codex-protocol", "codex-shell-command", @@ -2672,6 +2705,7 @@ dependencies = [ "codex-feedback", "codex-file-search", "codex-login", + "codex-models", "codex-otel", "codex-protocol", "codex-shell-command", @@ -2835,6 +2869,7 @@ version = "0.0.0" dependencies = [ "codex-core", "codex-lmstudio", + "codex-models", "codex-ollama", ] @@ -2877,6 +2912,7 @@ name = "codex-utils-sandbox-summary" version = "0.0.0" dependencies = [ "codex-core", + "codex-models", "codex-protocol", "codex-utils-absolute-path", "pretty_assertions", @@ -3122,6 +3158,7 @@ dependencies = [ "codex-arg0", "codex-core", "codex-features", + "codex-models", "codex-protocol", "codex-utils-absolute-path", "codex-utils-cargo-bin", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 331174a802..193cc84dc2 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -34,6 +34,7 @@ members = [ "linux-sandbox", "lmstudio", "login", + "models", "mcp-server", "network-proxy", "ollama", @@ -119,6 +120,7 @@ codex-keyring-store = { path = "keyring-store" } codex-linux-sandbox = { path = "linux-sandbox" } codex-lmstudio = { path = "lmstudio" } codex-login = { path = "login" } +codex-models = { path = "models" } codex-mcp-server = { path = "mcp-server" } codex-network-proxy = { path = "network-proxy" } codex-ollama = { path = "ollama" } diff --git a/codex-rs/app-server-client/Cargo.toml b/codex-rs/app-server-client/Cargo.toml index 5a3a1aa73f..a245d3c3d3 100644 --- a/codex-rs/app-server-client/Cargo.toml +++ b/codex-rs/app-server-client/Cargo.toml @@ -18,6 +18,7 @@ codex-arg0 = { workspace = true } codex-core = { workspace = true } codex-features = { workspace = true } codex-feedback = { workspace = true } +codex-models = { workspace = true } codex-protocol = { workspace = true } futures = { workspace = true } serde = { workspace = true } diff --git a/codex-rs/app-server-client/src/lib.rs b/codex-rs/app-server-client/src/lib.rs index acf9c77a10..0d9ea6f97a 100644 --- a/codex-rs/app-server-client/src/lib.rs +++ b/codex-rs/app-server-client/src/lib.rs @@ -46,9 +46,9 @@ use codex_core::ThreadManager; use codex_core::config::Config; use codex_core::config_loader::CloudRequirementsLoader; use codex_core::config_loader::LoaderOverrides; -use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_features::Feature; use codex_feedback::CodexFeedback; +use codex_models::CollaborationModesConfig; use codex_protocol::protocol::SessionSource; use serde::de::DeserializeOwned; use tokio::sync::mpsc; diff --git a/codex-rs/app-server/Cargo.toml b/codex-rs/app-server/Cargo.toml index c6be85984d..921a4c5fe9 100644 --- a/codex-rs/app-server/Cargo.toml +++ b/codex-rs/app-server/Cargo.toml @@ -42,6 +42,7 @@ codex-backend-client = { workspace = true } codex-file-search = { workspace = true } codex-chatgpt = { workspace = true } codex-login = { workspace = true } +codex-models = { workspace = true } codex-protocol = { workspace = true } codex-app-server-protocol = { workspace = true } codex-feedback = { workspace = true } diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 06e2cd3ec3..01391d247c 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -216,7 +216,6 @@ use codex_core::mcp::auth::discover_supported_scopes; use codex_core::mcp::auth::resolve_oauth_scopes; use codex_core::mcp::collect_mcp_snapshot; use codex_core::mcp::group_tools_by_server; -use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_core::parse_cursor; use codex_core::plugins::MarketplaceError; use codex_core::plugins::MarketplacePluginSource; @@ -245,6 +244,7 @@ use codex_login::ServerOptions as LoginServerOptions; use codex_login::ShutdownHandle; use codex_login::auth::login_with_chatgpt_auth_tokens; use codex_login::run_login_server; +use codex_models::CollaborationModesConfig; use codex_protocol::ThreadId; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::ForcedLoginMethod; diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index d70e8f47a1..eacb95b64a 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -58,13 +58,13 @@ use codex_core::default_client::USER_AGENT_SUFFIX; use codex_core::default_client::get_codex_user_agent; use codex_core::default_client::set_default_client_residency_requirement; use codex_core::default_client::set_default_originator; -use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_features::Feature; use codex_feedback::CodexFeedback; use codex_login::auth::ExternalAuthRefreshContext; use codex_login::auth::ExternalAuthRefreshReason; use codex_login::auth::ExternalAuthRefresher; use codex_login::auth::ExternalAuthTokens; +use codex_models::CollaborationModesConfig; use codex_protocol::ThreadId; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::W3cTraceContext; diff --git a/codex-rs/app-server/src/models.rs b/codex-rs/app-server/src/models.rs index 6dbe77455f..c313e92da7 100644 --- a/codex-rs/app-server/src/models.rs +++ b/codex-rs/app-server/src/models.rs @@ -4,7 +4,7 @@ use codex_app_server_protocol::Model; use codex_app_server_protocol::ModelUpgradeInfo; use codex_app_server_protocol::ReasoningEffortOption; use codex_core::ThreadManager; -use codex_core::models_manager::manager::RefreshStrategy; +use codex_models::RefreshStrategy; use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::ReasoningEffortPreset; diff --git a/codex-rs/app-server/tests/common/models_cache.rs b/codex-rs/app-server/tests/common/models_cache.rs index 427f6cc1f2..810c82173b 100644 --- a/codex-rs/app-server/tests/common/models_cache.rs +++ b/codex-rs/app-server/tests/common/models_cache.rs @@ -1,6 +1,7 @@ use chrono::DateTime; use chrono::Utc; use codex_core::test_support::all_model_presets; +use codex_models::client_version_to_whole; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ModelInfo; @@ -84,7 +85,7 @@ pub fn write_models_cache_with_models( let cache_path = codex_home.join("models_cache.json"); // DateTime serializes to RFC3339 format by default with serde let fetched_at: DateTime = Utc::now(); - let client_version = codex_core::models_manager::client_version_to_whole(); + let client_version = client_version_to_whole(); let cache = json!({ "fetched_at": fetched_at, "etag": null, diff --git a/codex-rs/core/BUILD.bazel b/codex-rs/core/BUILD.bazel index e8773b42fd..75f21c7101 100644 --- a/codex-rs/core/BUILD.bazel +++ b/codex-rs/core/BUILD.bazel @@ -1,19 +1,8 @@ load("//:defs.bzl", "codex_rust_crate") -exports_files( - [ - "templates/collaboration_mode/default.md", - "templates/collaboration_mode/plan.md", - ], - visibility = ["//visibility:public"], -) - filegroup( - name = "model_availability_nux_fixtures", - srcs = [ - "models.json", - "tests/cli_responses_fixture.sse", - ], + name = "cli_responses_fixture_sse", + srcs = ["tests/cli_responses_fixture.sse"], visibility = ["//visibility:public"], ) @@ -39,7 +28,6 @@ codex_rust_crate( }, integration_compile_data_extra = [ "//codex-rs/apply-patch:apply_patch_tool_instructions.md", - "models.json", "prompt.md", ], test_data_extra = [ diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index d648655b24..9ead24e9a0 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -36,6 +36,7 @@ codex-config = { workspace = true } codex-exec-server = { workspace = true } codex-features = { workspace = true } codex-login = { workspace = true } +codex-models = { workspace = true } codex-shell-command = { workspace = true } codex-skills = { workspace = true } codex-execpolicy = { workspace = true } diff --git a/codex-rs/core/src/api_bridge.rs b/codex-rs/core/src/api_bridge.rs index 2060b78cf7..1c936df874 100644 --- a/codex-rs/core/src/api_bridge.rs +++ b/codex-rs/core/src/api_bridge.rs @@ -6,16 +6,18 @@ use codex_api::TransportError; use codex_api::error::ApiError; use codex_api::rate_limits::parse_promo_message; use codex_api::rate_limits::parse_rate_limit_for_limit; +use codex_models::EnvKeyError; +use codex_models::ModelProviderInfo; use http::HeaderMap; use serde::Deserialize; use serde_json::Value; use crate::auth::CodexAuth; use crate::error::CodexErr; +use crate::error::EnvVarError; use crate::error::RetryLimitReachedError; use crate::error::UnexpectedResponseError; use crate::error::UsageLimitReachedError; -use crate::model_provider_info::ModelProviderInfo; use crate::token_data::PlanType; pub(crate) fn map_api_error(err: ApiError) -> CodexErr { @@ -168,7 +170,7 @@ pub(crate) fn auth_provider_from_auth( auth: Option, provider: &ModelProviderInfo, ) -> crate::error::Result { - if let Some(api_key) = provider.api_key()? { + if let Some(api_key) = provider.api_key().map_err(map_env_key_error)? { return Ok(CoreAuthProvider { token: Some(api_key), account_id: None, @@ -196,6 +198,13 @@ pub(crate) fn auth_provider_from_auth( } } +fn map_env_key_error(error: EnvKeyError) -> CodexErr { + CodexErr::EnvVar(EnvVarError { + var: error.var, + instructions: error.instructions, + }) +} + #[derive(Debug, Deserialize)] struct UsageErrorResponse { error: UsageErrorBody, diff --git a/codex-rs/core/src/auth_env_telemetry.rs b/codex-rs/core/src/auth_env_telemetry.rs index 85cd23fe06..2534db73b9 100644 --- a/codex-rs/core/src/auth_env_telemetry.rs +++ b/codex-rs/core/src/auth_env_telemetry.rs @@ -3,7 +3,7 @@ use codex_otel::AuthEnvTelemetryMetadata; use crate::auth::CODEX_API_KEY_ENV_VAR; use crate::auth::OPENAI_API_KEY_ENV_VAR; use crate::auth::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR; -use crate::model_provider_info::ModelProviderInfo; +use codex_models::ModelProviderInfo; #[derive(Debug, Clone, Default, PartialEq, Eq)] pub(crate) struct AuthEnvTelemetry { @@ -54,6 +54,7 @@ fn env_var_present(name: &str) -> bool { #[cfg(test)] mod tests { use super::*; + use codex_models::WireApi; use pretty_assertions::assert_eq; #[test] @@ -64,7 +65,7 @@ mod tests { env_key: Some("sk-should-not-leak".to_string()), env_key_instructions: None, experimental_bearer_token: None, - wire_api: crate::model_provider_info::WireApi::Responses, + wire_api: WireApi::Responses, query_params: None, http_headers: None, env_http_headers: None, diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index e38ff3a562..021e7024a8 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -101,8 +101,6 @@ use crate::default_client::build_reqwest_client; use crate::error::CodexErr; use crate::error::Result; use crate::flags::CODEX_RS_SSE_FIXTURE; -use crate::model_provider_info::ModelProviderInfo; -use crate::model_provider_info::WireApi; use crate::response_debug_context::extract_response_debug_context; use crate::response_debug_context::extract_response_debug_context_from_api_error; use crate::response_debug_context::telemetry_api_error_message; @@ -111,6 +109,8 @@ use crate::tools::spec::create_tools_json_for_responses_api; use crate::util::FeedbackRequestTags; use crate::util::emit_feedback_auth_recovery_tags; use crate::util::emit_feedback_request_tags_with_auth_env; +use codex_models::ModelProviderInfo; +use codex_models::WireApi; pub const OPENAI_BETA_HEADER: &str = "OpenAI-Beta"; pub const X_CODEX_TURN_STATE_HEADER: &str = "x-codex-turn-state"; @@ -122,8 +122,7 @@ const RESPONSES_ENDPOINT: &str = "/responses"; const RESPONSES_COMPACT_ENDPOINT: &str = "/responses/compact"; const MEMORIES_SUMMARIZE_ENDPOINT: &str = "/memories/trace_summarize"; #[cfg(test)] -pub(crate) const WEBSOCKET_CONNECT_TIMEOUT: Duration = - Duration::from_millis(crate::model_provider_info::DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS); +pub(crate) const WEBSOCKET_CONNECT_TIMEOUT: Duration = Duration::from_millis(15_000); /// Session-scoped state shared by all [`ModelClient`] clones. /// @@ -531,7 +530,7 @@ impl ModelClient { let api_provider = self .state .provider - .to_api_provider(auth.as_ref().map(CodexAuth::auth_mode))?; + .to_api_provider(auth.as_ref().map(CodexAuth::auth_mode)); let api_auth = auth_provider_from_auth(auth.clone(), &self.state.provider)?; Ok(CurrentClientSetup { auth, diff --git a/codex-rs/core/src/client_tests.rs b/codex-rs/core/src/client_tests.rs index 2c07b4fd1d..030c69f87b 100644 --- a/codex-rs/core/src/client_tests.rs +++ b/codex-rs/core/src/client_tests.rs @@ -2,6 +2,8 @@ use super::AuthRequestTelemetryContext; use super::ModelClient; use super::PendingUnauthorizedRetry; use super::UnauthorizedRecoveryExecution; +use codex_models::WireApi; +use codex_models::create_oss_provider_with_base_url; use codex_otel::SessionTelemetry; use codex_protocol::ThreadId; use codex_protocol::openai_models::ModelInfo; @@ -11,10 +13,7 @@ use pretty_assertions::assert_eq; use serde_json::json; fn test_model_client(session_source: SessionSource) -> ModelClient { - let provider = crate::model_provider_info::create_oss_provider_with_base_url( - "https://example.com/v1", - crate::model_provider_info::WireApi::Responses, - ); + let provider = create_oss_provider_with_base_url("https://example.com/v1", WireApi::Responses); ModelClient::new( None, ThreadId::new(), diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 12270bb6dd..5dbbc2945e 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -27,10 +27,7 @@ use crate::compact_remote::run_inline_remote_auto_compact_task; use crate::config::ManagedFeatures; use crate::connectors; use crate::exec_policy::ExecPolicyManager; -#[cfg(test)] -use crate::models_manager::collaboration_mode_presets::CollaborationModesConfig; -use crate::models_manager::manager::ModelsManager; -use crate::models_manager::manager::RefreshStrategy; +use crate::model_info_overrides::model_info_config_overrides; use crate::parse_command::parse_command; use crate::parse_turn_item; use crate::realtime_conversation::RealtimeConversationManager; @@ -65,6 +62,11 @@ use codex_hooks::HookPayload; use codex_hooks::HookResult; use codex_hooks::Hooks; use codex_hooks::HooksConfig; +#[cfg(test)] +use codex_models::CollaborationModesConfig; +use codex_models::ModelProviderInfo; +use codex_models::ModelsManager; +use codex_models::RefreshStrategy; use codex_network_proxy::NetworkProxy; use codex_network_proxy::NetworkProxyAuditMetadata; use codex_network_proxy::normalize_host; @@ -154,7 +156,6 @@ use tracing::trace_span; use tracing::warn; use uuid::Uuid; -use crate::ModelProviderInfo; use crate::client::ModelClient; use crate::client::ModelClientSession; use crate::client_common::Prompt; @@ -502,15 +503,10 @@ impl Codex { let config = Arc::new(config); let refresh_strategy = match session_source { - SessionSource::SubAgent(_) => crate::models_manager::manager::RefreshStrategy::Offline, - _ => crate::models_manager::manager::RefreshStrategy::OnlineIfUncached, + SessionSource::SubAgent(_) => RefreshStrategy::Offline, + _ => RefreshStrategy::OnlineIfUncached, }; - if config.model.is_none() - || !matches!( - refresh_strategy, - crate::models_manager::manager::RefreshStrategy::Offline - ) - { + if config.model.is_none() || !matches!(refresh_strategy, RefreshStrategy::Offline) { let _ = models_manager.list_models(refresh_strategy).await; } let model = models_manager @@ -521,7 +517,9 @@ impl Codex { // 1. config.base_instructions override // 2. conversation history => session_meta.base_instructions // 3. base_instructions for current model - let model_info = models_manager.get_model_info(model.as_str(), &config).await; + let model_info = models_manager + .get_model_info(model.as_str(), &model_info_config_overrides(&config)) + .await; let base_instructions = config .base_instructions .clone() @@ -848,7 +846,9 @@ impl TurnContext { pub(crate) async fn with_model(&self, model: String, models_manager: &ModelsManager) -> Self { let mut config = (*self.config).clone(); config.model = Some(model.clone()); - let model_info = models_manager.get_model_info(model.as_str(), &config).await; + let model_info = models_manager + .get_model_info(model.as_str(), &model_info_config_overrides(&config)) + .await; let truncation_policy = model_info.truncation_policy.into(); let supported_reasoning_levels = model_info .supported_reasoning_levels @@ -2400,7 +2400,7 @@ impl Session { .models_manager .get_model_info( session_configuration.collaboration_mode.model(), - &per_turn_config, + &model_info_config_overrides(&per_turn_config), ) .await; let skills_outcome = Arc::new( @@ -5172,7 +5172,7 @@ async fn spawn_review_thread( let review_model_info = sess .services .models_manager - .get_model_info(&model, &config) + .get_model_info(&model, &model_info_config_overrides(&config)) .await; // For reviews, disable web_search and view_image regardless of global settings. let mut review_features = sess.features.clone(); diff --git a/codex-rs/core/src/codex_delegate.rs b/codex-rs/core/src/codex_delegate.rs index e560cd9c7f..16799f1310 100644 --- a/codex-rs/core/src/codex_delegate.rs +++ b/codex-rs/core/src/codex_delegate.rs @@ -48,7 +48,7 @@ use crate::mcp_tool_call::MCP_TOOL_APPROVAL_DECLINE_SYNTHETIC; use crate::mcp_tool_call::build_guardian_mcp_tool_review_request; use crate::mcp_tool_call::is_mcp_tool_approval_question_id; use crate::mcp_tool_call::lookup_mcp_tool_metadata; -use crate::models_manager::manager::ModelsManager; +use codex_models::ModelsManager; use codex_protocol::protocol::InitialHistory; #[cfg(test)] diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index a5412eff29..5ae5c27d57 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -11,8 +11,9 @@ use crate::exec::ExecCapturePolicy; use crate::exec::ExecToolCallOutput; use crate::function_tool::FunctionCallError; use crate::mcp_connection_manager::ToolInfo; -use crate::models_manager::model_info; +use crate::model_info_overrides::model_info_config_overrides; use crate::shell::default_user_shell; +use crate::test_support::construct_model_info_offline; use crate::tools::format_exec_output_str; use codex_features::Features; @@ -62,6 +63,9 @@ use codex_app_server_protocol::AppInfo; use codex_execpolicy::Decision; use codex_execpolicy::NetworkRuleProtocol; use codex_execpolicy::Policy; +use codex_models::ModelProviderInfo; +use codex_models::ModelsManager; +use codex_models::with_config_overrides; use codex_network_proxy::NetworkProxyConfig; use codex_otel::TelemetryAuthMode; use codex_protocol::models::BaseInstructions; @@ -231,9 +235,7 @@ fn test_model_client_session() -> crate::client::ModelClientSession { None, ThreadId::try_from("00000000-0000-4000-8000-000000000001") .expect("test thread id should be valid"), - crate::model_provider_info::ModelProviderInfo::create_openai_provider( - /* base_url */ None, - ), + ModelProviderInfo::create_openai_provider(/* base_url */ None), codex_protocol::protocol::SessionSource::Exec, None, false, @@ -504,7 +506,7 @@ async fn get_base_instructions_no_user_content() { let prompt_with_apply_patch_instructions = include_str!("../prompt_with_apply_patch_instructions.md"); let models_response: ModelsResponse = - serde_json::from_str(include_str!("../models.json")).expect("valid models.json"); + serde_json::from_str(include_str!("../../models/models.json")).expect("valid models.json"); let model_info_for_slug = |slug: &str, config: &Config| { let model = models_response .models @@ -512,7 +514,7 @@ async fn get_base_instructions_no_user_content() { .find(|candidate| candidate.slug == slug) .cloned() .unwrap_or_else(|| panic!("model slug {slug} is missing from models.json")); - model_info::with_config_overrides(model, config) + with_config_overrides(model, &model_info_config_overrides(config)) }; let test_cases = vec![ InstructionsTestCase { @@ -1622,7 +1624,7 @@ async fn set_rate_limits_retains_previous_credits() { let config = build_test_config(codex_home.path()).await; let config = Arc::new(config); let model = ModelsManager::get_model_offline_for_tests(config.model.as_deref()); - let model_info = ModelsManager::construct_model_info_offline_for_tests(model.as_str(), &config); + let model_info = construct_model_info_offline(model.as_str(), config.as_ref()); let reasoning_effort = config.model_reasoning_effort; let collaboration_mode = CollaborationMode { mode: ModeKind::Default, @@ -1720,7 +1722,7 @@ async fn set_rate_limits_updates_plan_type_when_present() { let config = build_test_config(codex_home.path()).await; let config = Arc::new(config); let model = ModelsManager::get_model_offline_for_tests(config.model.as_deref()); - let model_info = ModelsManager::construct_model_info_offline_for_tests(model.as_str(), &config); + let model_info = construct_model_info_offline(model.as_str(), config.as_ref()); let reasoning_effort = config.model_reasoning_effort; let collaboration_mode = CollaborationMode { mode: ModeKind::Default, @@ -1870,7 +1872,10 @@ async fn turn_context_with_model_updates_model_fields() { let expected_model_info = session .services .models_manager - .get_model_info("gpt-5.1", updated.config.as_ref()) + .get_model_info( + "gpt-5.1", + &model_info_config_overrides(updated.config.as_ref()), + ) .await; assert_eq!(updated.config.model.as_deref(), Some("gpt-5.1")); @@ -2063,7 +2068,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati let config = build_test_config(codex_home.path()).await; let config = Arc::new(config); let model = ModelsManager::get_model_offline_for_tests(config.model.as_deref()); - let model_info = ModelsManager::construct_model_info_offline_for_tests(model.as_str(), &config); + let model_info = construct_model_info_offline(model.as_str(), config.as_ref()); let reasoning_effort = config.model_reasoning_effort; let collaboration_mode = CollaborationMode { mode: ModeKind::Default, @@ -2296,7 +2301,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() { CollaborationModesConfig::default(), )); let model = ModelsManager::get_model_offline_for_tests(config.model.as_deref()); - let model_info = ModelsManager::construct_model_info_offline_for_tests(model.as_str(), &config); + let model_info = construct_model_info_offline(model.as_str(), config.as_ref()); let collaboration_mode = CollaborationMode { mode: ModeKind::Default, settings: Settings { @@ -2390,7 +2395,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { let exec_policy = Arc::new(ExecPolicyManager::default()); let (agent_status_tx, _agent_status_rx) = watch::channel(AgentStatus::PendingInit); let model = ModelsManager::get_model_offline_for_tests(config.model.as_deref()); - let model_info = ModelsManager::construct_model_info_offline_for_tests(model.as_str(), &config); + let model_info = construct_model_info_offline(model.as_str(), config.as_ref()); let reasoning_effort = config.model_reasoning_effort; let collaboration_mode = CollaborationMode { mode: ModeKind::Default, @@ -2432,7 +2437,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { user_shell_override: None, }; let per_turn_config = Session::build_per_turn_config(&session_configuration); - let model_info = ModelsManager::construct_model_info_offline_for_tests( + let model_info = construct_model_info_offline( session_configuration.collaboration_mode.model(), &per_turn_config, ); @@ -3189,7 +3194,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( let exec_policy = Arc::new(ExecPolicyManager::default()); let (agent_status_tx, _agent_status_rx) = watch::channel(AgentStatus::PendingInit); let model = ModelsManager::get_model_offline_for_tests(config.model.as_deref()); - let model_info = ModelsManager::construct_model_info_offline_for_tests(model.as_str(), &config); + let model_info = construct_model_info_offline(model.as_str(), config.as_ref()); let reasoning_effort = config.model_reasoning_effort; let collaboration_mode = CollaborationMode { mode: ModeKind::Default, @@ -3231,7 +3236,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( user_shell_override: None, }; let per_turn_config = Session::build_per_turn_config(&session_configuration); - let model_info = ModelsManager::construct_model_info_offline_for_tests( + let model_info = construct_model_info_offline( session_configuration.collaboration_mode.model(), &per_turn_config, ); diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index 7686c5b65c..5df5f748a8 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use crate::ModelProviderInfo; use crate::Prompt; use crate::client::ModelClientSession; use crate::client_common::ResponseEvent; @@ -19,6 +18,7 @@ use crate::truncate::TruncationPolicy; use crate::truncate::approx_token_count; use crate::truncate::truncate_text; use crate::util::backoff; +use codex_models::ModelProviderInfo; use codex_protocol::items::ContextCompactionItem; use codex_protocol::items::TurnItem; use codex_protocol::models::ContentItem; diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 1c78759ec4..5498fe6190 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -17,6 +17,7 @@ use assert_matches::assert_matches; use codex_config::CONFIG_TOML_FILE; use codex_features::Feature; use codex_features::FeaturesToml; +use codex_models::WireApi; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; @@ -1721,10 +1722,7 @@ fn responses_websocket_features_do_not_change_wire_api() -> std::io::Result<()> codex_home.path().to_path_buf(), )?; - assert_eq!( - config.model_provider.wire_api, - crate::model_provider_info::WireApi::Responses - ); + assert_eq!(config.model_provider.wire_api, WireApi::Responses); } Ok(()) @@ -4074,7 +4072,8 @@ fn model_catalog_json_loads_from_path() -> std::io::Result<()> { let codex_home = TempDir::new()?; let catalog_path = codex_home.path().join("catalog.json"); let mut catalog: ModelsResponse = - serde_json::from_str(include_str!("../../models.json")).expect("valid models.json"); + serde_json::from_str(include_str!("../../../models/models.json")) + .expect("valid models.json"); catalog.models = catalog.models.into_iter().take(1).collect(); std::fs::write( &catalog_path, @@ -4188,7 +4187,7 @@ model_verbosity = "high" name: "OpenAI custom".to_string(), base_url: Some("https://api.openai.com/v1".to_string()), env_key: Some("OPENAI_API_KEY".to_string()), - wire_api: crate::WireApi::Responses, + wire_api: WireApi::Responses, env_key_instructions: None, experimental_bearer_token: None, query_params: None, diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 48bde3f177..cb11311f29 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -41,13 +41,6 @@ use crate::config_loader::Sourced; use crate::config_loader::load_config_layers_state; use crate::git_info::resolve_root_git_project_for_trust; use crate::memories::memory_root; -use crate::model_provider_info::LEGACY_OLLAMA_CHAT_PROVIDER_ID; -use crate::model_provider_info::LMSTUDIO_OSS_PROVIDER_ID; -use crate::model_provider_info::ModelProviderInfo; -use crate::model_provider_info::OLLAMA_CHAT_PROVIDER_REMOVED_ERROR; -use crate::model_provider_info::OLLAMA_OSS_PROVIDER_ID; -use crate::model_provider_info::OPENAI_PROVIDER_ID; -use crate::model_provider_info::built_in_model_providers; use crate::path_utils::normalize_for_native_workdir; use crate::project_doc::DEFAULT_PROJECT_DOC_FILENAME; use crate::project_doc::LOCAL_PROJECT_DOC_FILENAME; @@ -66,6 +59,13 @@ use codex_features::FeatureConfigSource; use codex_features::FeatureOverrides; use codex_features::Features; use codex_features::FeaturesToml; +use codex_models::LEGACY_OLLAMA_CHAT_PROVIDER_ID; +use codex_models::LMSTUDIO_OSS_PROVIDER_ID; +use codex_models::ModelProviderInfo; +use codex_models::OLLAMA_CHAT_PROVIDER_REMOVED_ERROR; +use codex_models::OLLAMA_OSS_PROVIDER_ID; +use codex_models::OPENAI_PROVIDER_ID; +use codex_models::built_in_model_providers; use codex_protocol::config_types::AltScreenMode; use codex_protocol::config_types::ForcedLoginMethod; use codex_protocol::config_types::Personality; diff --git a/codex-rs/core/src/guardian/review.rs b/codex-rs/core/src/guardian/review.rs index 3a491f6efb..bc85d01b92 100644 --- a/codex-rs/core/src/guardian/review.rs +++ b/codex-rs/core/src/guardian/review.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use codex_models::RefreshStrategy; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; @@ -274,7 +275,7 @@ pub(super) async fn run_guardian_review_session( let available_models = session .services .models_manager - .list_models(crate::models_manager::manager::RefreshStrategy::Offline) + .list_models(RefreshStrategy::Offline) .await; let preferred_reasoning_effort = |supports_low: bool, fallback| { if supports_low { diff --git a/codex-rs/core/src/guardian/review_session.rs b/codex-rs/core/src/guardian/review_session.rs index 34f0b6298e..20b5a9399a 100644 --- a/codex-rs/core/src/guardian/review_session.rs +++ b/codex-rs/core/src/guardian/review_session.rs @@ -34,10 +34,10 @@ use crate::config::ManagedFeatures; use crate::config::NetworkProxySpec; use crate::config::Permissions; use crate::config::types::McpServerConfig; -use crate::model_provider_info::ModelProviderInfo; use crate::protocol::SandboxPolicy; use crate::rollout::recorder::RolloutRecorder; use codex_features::Feature; +use codex_models::ModelProviderInfo; use super::GUARDIAN_REVIEW_TIMEOUT; use super::GUARDIAN_REVIEWER_NAME; diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 29436a0d7f..0deebcba59 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -49,7 +49,7 @@ pub mod landlock; pub mod mcp; mod mcp_connection_manager; mod mcp_tool_approval_templates; -pub mod models_manager; +mod model_info_overrides; mod network_policy_decision; pub mod network_proxy_loader; mod original_image_detail; @@ -63,7 +63,6 @@ mod memories; pub mod mention_syntax; mod mentions; pub mod message_history; -mod model_provider_info; pub mod path_utils; pub mod personality_migration; pub mod plugins; @@ -80,15 +79,6 @@ mod truncate; mod unified_exec; pub mod windows_sandbox; pub use client::X_RESPONSESAPI_INCLUDE_TIMING_METRICS_HEADER; -pub use model_provider_info::DEFAULT_LMSTUDIO_PORT; -pub use model_provider_info::DEFAULT_OLLAMA_PORT; -pub use model_provider_info::LMSTUDIO_OSS_PROVIDER_ID; -pub use model_provider_info::ModelProviderInfo; -pub use model_provider_info::OLLAMA_OSS_PROVIDER_ID; -pub use model_provider_info::OPENAI_PROVIDER_ID; -pub use model_provider_info::WireApi; -pub use model_provider_info::built_in_model_providers; -pub use model_provider_info::create_oss_provider_with_base_url; mod event_mapping; mod response_debug_context; pub mod review_format; diff --git a/codex-rs/core/src/memories/phase1.rs b/codex-rs/core/src/memories/phase1.rs index 921bc9953c..b54bdeaddc 100644 --- a/codex-rs/core/src/memories/phase1.rs +++ b/codex-rs/core/src/memories/phase1.rs @@ -10,6 +10,7 @@ use crate::memories::metrics; use crate::memories::phase_one; use crate::memories::phase_one::PRUNE_BATCH_SIZE; use crate::memories::prompts::build_stage_one_input_message; +use crate::model_info_overrides::model_info_config_overrides; use crate::rollout::INTERACTIVE_SESSION_SOURCES; use crate::rollout::policy::should_persist_response_item_for_memories; use codex_api::ResponseEvent; @@ -228,7 +229,7 @@ async fn build_request_context(session: &Arc, config: &Config) -> Reque let model = session .services .models_manager - .get_model_info(&model_name, config) + .get_model_info(&model_name, &model_info_config_overrides(config)) .await; let turn_context = session.new_default_turn().await; RequestContext::from_turn_context( diff --git a/codex-rs/core/src/memories/prompts_tests.rs b/codex-rs/core/src/memories/prompts_tests.rs index acbe5785a2..1ce8c9b4b3 100644 --- a/codex-rs/core/src/memories/prompts_tests.rs +++ b/codex-rs/core/src/memories/prompts_tests.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models_manager::model_info::model_info_from_slug; +use codex_models::model_info_from_slug; #[test] fn build_stage_one_input_message_truncates_rollout_using_model_context_window() { diff --git a/codex-rs/core/src/model_info_overrides.rs b/codex-rs/core/src/model_info_overrides.rs new file mode 100644 index 0000000000..d575fe88bb --- /dev/null +++ b/codex-rs/core/src/model_info_overrides.rs @@ -0,0 +1,15 @@ +use codex_features::Feature; +use codex_models::ModelInfoConfigOverrides; + +use crate::config::Config; + +pub(crate) fn model_info_config_overrides(config: &Config) -> ModelInfoConfigOverrides { + ModelInfoConfigOverrides { + model_supports_reasoning_summaries: config.model_supports_reasoning_summaries, + model_context_window: config.model_context_window, + model_auto_compact_token_limit: config.model_auto_compact_token_limit, + tool_output_token_limit: config.tool_output_token_limit, + base_instructions: config.base_instructions.clone(), + personality_enabled: config.features.enabled(Feature::Personality), + } +} diff --git a/codex-rs/core/src/models_manager/mod.rs b/codex-rs/core/src/models_manager/mod.rs deleted file mode 100644 index 53b33bc4e5..0000000000 --- a/codex-rs/core/src/models_manager/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -pub mod cache; -pub mod collaboration_mode_presets; -pub mod manager; -pub mod model_info; -pub mod model_presets; - -/// Convert the client version string to a whole version string (e.g. "1.2.3-alpha.4" -> "1.2.3"). -pub fn client_version_to_whole() -> String { - format!( - "{}.{}.{}", - env!("CARGO_PKG_VERSION_MAJOR"), - env!("CARGO_PKG_VERSION_MINOR"), - env!("CARGO_PKG_VERSION_PATCH") - ) -} diff --git a/codex-rs/core/src/original_image_detail_tests.rs b/codex-rs/core/src/original_image_detail_tests.rs index e4a3c09880..0004bac29b 100644 --- a/codex-rs/core/src/original_image_detail_tests.rs +++ b/codex-rs/core/src/original_image_detail_tests.rs @@ -1,15 +1,19 @@ use super::*; use crate::config::test_config; -use crate::models_manager::manager::ModelsManager; +use crate::model_info_overrides::model_info_config_overrides; use codex_features::Features; +use codex_models::ModelsManager; use pretty_assertions::assert_eq; #[test] fn image_detail_original_feature_enables_explicit_original_without_force() { let config = test_config(); - let mut model_info = - ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let mut model_info = ModelsManager::construct_model_info_offline_for_tests( + "gpt-5-codex", + config.model_catalog.as_ref(), + &model_info_config_overrides(&config), + ); model_info.supports_image_detail_original = true; let mut features = Features::with_defaults(); features.enable(Feature::ImageDetailOriginal); @@ -28,8 +32,11 @@ fn image_detail_original_feature_enables_explicit_original_without_force() { #[test] fn explicit_original_is_dropped_without_feature_or_model_support() { let config = test_config(); - let mut model_info = - ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let mut model_info = ModelsManager::construct_model_info_offline_for_tests( + "gpt-5-codex", + config.model_catalog.as_ref(), + &model_info_config_overrides(&config), + ); model_info.supports_image_detail_original = true; let features = Features::with_defaults(); @@ -50,8 +57,11 @@ fn explicit_original_is_dropped_without_feature_or_model_support() { #[test] fn unsupported_non_original_detail_is_dropped() { let config = test_config(); - let mut model_info = - ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let mut model_info = ModelsManager::construct_model_info_offline_for_tests( + "gpt-5-codex", + config.model_catalog.as_ref(), + &model_info_config_overrides(&config), + ); model_info.supports_image_detail_original = true; let mut features = Features::with_defaults(); features.enable(Feature::ImageDetailOriginal); diff --git a/codex-rs/core/src/realtime_conversation.rs b/codex-rs/core/src/realtime_conversation.rs index 1ddd72d0fd..d14dcd92d8 100644 --- a/codex-rs/core/src/realtime_conversation.rs +++ b/codex-rs/core/src/realtime_conversation.rs @@ -22,6 +22,8 @@ use codex_api::RealtimeSessionMode; use codex_api::RealtimeWebsocketClient; use codex_api::endpoint::realtime_websocket::RealtimeWebsocketEvents; use codex_api::endpoint::realtime_websocket::RealtimeWebsocketWriter; +use codex_models::EnvKeyError; +use codex_models::ModelProviderInfo; use codex_protocol::protocol::CodexErrorInfo; use codex_protocol::protocol::ConversationAudioParams; use codex_protocol::protocol::ConversationStartParams; @@ -454,7 +456,7 @@ async fn prepare_realtime_start( let provider = sess.provider().await; let auth = sess.services.auth_manager.auth().await; let realtime_api_key = realtime_api_key(auth.as_ref(), &provider)?; - let mut api_provider = provider.to_api_provider(Some(crate::auth::AuthMode::ApiKey))?; + let mut api_provider = provider.to_api_provider(Some(crate::auth::AuthMode::ApiKey)); let config = sess.get_config().await; if let Some(realtime_ws_base_url) = &config.experimental_realtime_ws_base_url { api_provider.base_url = realtime_ws_base_url.clone(); @@ -625,11 +627,8 @@ fn realtime_text_from_handoff_request(handoff: &RealtimeHandoffRequested) -> Opt .or((!handoff.input_transcript.is_empty()).then_some(handoff.input_transcript.clone())) } -fn realtime_api_key( - auth: Option<&CodexAuth>, - provider: &crate::ModelProviderInfo, -) -> CodexResult { - if let Some(api_key) = provider.api_key()? { +fn realtime_api_key(auth: Option<&CodexAuth>, provider: &ModelProviderInfo) -> CodexResult { + if let Some(api_key) = provider.api_key().map_err(map_env_key_error)? { return Ok(api_key); } @@ -654,6 +653,13 @@ fn realtime_api_key( )) } +fn map_env_key_error(error: EnvKeyError) -> CodexErr { + CodexErr::EnvVar(crate::error::EnvVarError { + var: error.var, + instructions: error.instructions, + }) +} + fn realtime_request_headers( session_id: Option<&str>, api_key: &str, diff --git a/codex-rs/core/src/state/service.rs b/codex-rs/core/src/state/service.rs index ceab67f1c7..328aec9758 100644 --- a/codex-rs/core/src/state/service.rs +++ b/codex-rs/core/src/state/service.rs @@ -11,7 +11,6 @@ use crate::exec_policy::ExecPolicyManager; use crate::file_watcher::FileWatcher; use crate::mcp::McpManager; use crate::mcp_connection_manager::McpConnectionManager; -use crate::models_manager::manager::ModelsManager; use crate::plugins::PluginsManager; use crate::skills::SkillsManager; use crate::state_db::StateDbHandle; @@ -22,6 +21,7 @@ use crate::tools::sandboxing::ApprovalStore; use crate::unified_exec::UnifiedExecProcessManager; use codex_exec_server::Environment; use codex_hooks::Hooks; +use codex_models::ModelsManager; use codex_otel::SessionTelemetry; use codex_utils_absolute_path::AbsolutePathBuf; use std::path::PathBuf; diff --git a/codex-rs/core/src/tasks/mod.rs b/codex-rs/core/src/tasks/mod.rs index b8e1d73b71..05db0f5d73 100644 --- a/codex-rs/core/src/tasks/mod.rs +++ b/codex-rs/core/src/tasks/mod.rs @@ -27,7 +27,6 @@ use crate::hook_runtime::PendingInputHookDisposition; use crate::hook_runtime::inspect_pending_input; use crate::hook_runtime::record_additional_contexts; use crate::hook_runtime::record_pending_input; -use crate::models_manager::manager::ModelsManager; use crate::protocol::EventMsg; use crate::protocol::TokenUsage; use crate::protocol::TurnAbortReason; @@ -48,6 +47,7 @@ use codex_protocol::protocol::RolloutItem; use codex_protocol::user_input::UserInput; use codex_features::Feature; +use codex_models::ModelsManager; pub(crate) use compact::CompactTask; pub(crate) use ghost_snapshot::GhostSnapshotTask; pub(crate) use regular::RegularTask; diff --git a/codex-rs/core/src/test_support.rs b/codex-rs/core/src/test_support.rs index c2aad83df4..c95e55c8af 100644 --- a/codex-rs/core/src/test_support.rs +++ b/codex-rs/core/src/test_support.rs @@ -7,6 +7,10 @@ use std::path::PathBuf; use std::sync::Arc; +use codex_models::CollaborationModesConfig; +use codex_models::ModelProviderInfo; +use codex_models::ModelsManager; +use codex_models::builtin_collaboration_mode_presets as builtin_models_collaboration_mode_presets; use codex_protocol::config_types::CollaborationModeMask; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelPreset; @@ -15,16 +19,14 @@ use once_cell::sync::Lazy; use crate::AuthManager; use crate::CodexAuth; -use crate::ModelProviderInfo; use crate::ThreadManager; use crate::config::Config; -use crate::models_manager::collaboration_mode_presets; -use crate::models_manager::manager::ModelsManager; +use crate::model_info_overrides::model_info_config_overrides; use crate::thread_manager; use crate::unified_exec; static TEST_MODEL_PRESETS: Lazy> = Lazy::new(|| { - let file_contents = include_str!("../models.json"); + let file_contents = include_str!("../../models/models.json"); let mut response: ModelsResponse = serde_json::from_str(file_contents) .unwrap_or_else(|err| panic!("bundled models.json should parse: {err}")); response.models.sort_by(|a, b| a.priority.cmp(&b.priority)); @@ -104,7 +106,11 @@ pub fn get_model_offline(model: Option<&str>) -> String { } pub fn construct_model_info_offline(model: &str, config: &Config) -> ModelInfo { - ModelsManager::construct_model_info_offline_for_tests(model, config) + ModelsManager::construct_model_info_offline_for_tests( + model, + config.model_catalog.as_ref(), + &model_info_config_overrides(config), + ) } pub fn all_model_presets() -> &'static Vec { @@ -112,7 +118,5 @@ pub fn all_model_presets() -> &'static Vec { } pub fn builtin_collaboration_mode_presets() -> Vec { - collaboration_mode_presets::builtin_collaboration_mode_presets( - collaboration_mode_presets::CollaborationModesConfig::default(), - ) + builtin_models_collaboration_mode_presets(CollaborationModesConfig::default()) } diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index a63cf2cb94..0f8b601a57 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -1,7 +1,5 @@ use crate::AuthManager; use crate::CodexAuth; -use crate::ModelProviderInfo; -use crate::OPENAI_PROVIDER_ID; use crate::agent::AgentControl; use crate::codex::Codex; use crate::codex::CodexSpawnArgs; @@ -14,8 +12,6 @@ use crate::error::Result as CodexResult; use crate::file_watcher::FileWatcher; use crate::file_watcher::FileWatcherEvent; use crate::mcp::McpManager; -use crate::models_manager::collaboration_mode_presets::CollaborationModesConfig; -use crate::models_manager::manager::ModelsManager; use crate::plugins::PluginsManager; use crate::protocol::Event; use crate::protocol::EventMsg; @@ -24,6 +20,11 @@ use crate::rollout::RolloutRecorder; use crate::rollout::truncation; use crate::shell_snapshot::ShellSnapshot; use crate::skills::SkillsManager; +use codex_models::CollaborationModesConfig; +use codex_models::ModelProviderInfo; +use codex_models::ModelsManager; +use codex_models::OPENAI_PROVIDER_ID; +use codex_models::RefreshStrategy; use codex_protocol::ThreadId; use codex_protocol::config_types::CollaborationModeMask; use codex_protocol::openai_models::ModelPreset; @@ -304,10 +305,7 @@ impl ThreadManager { self.state.models_manager.clone() } - pub async fn list_models( - &self, - refresh_strategy: crate::models_manager::manager::RefreshStrategy, - ) -> Vec { + pub async fn list_models(&self, refresh_strategy: RefreshStrategy) -> Vec { self.state .models_manager .list_models(refresh_strategy) diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index e69e88fe73..7eb39da626 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -1,9 +1,9 @@ use super::*; use crate::codex::make_session_and_context; use crate::config::test_config; -use crate::models_manager::collaboration_mode_presets::CollaborationModesConfig; -use crate::models_manager::manager::RefreshStrategy; use assert_matches::assert_matches; +use codex_models::CollaborationModesConfig; +use codex_models::RefreshStrategy; use codex_protocol::models::ContentItem; use codex_protocol::models::ReasoningItemReasoningSummary; use codex_protocol::models::ResponseItem; diff --git a/codex-rs/core/src/tools/handlers/multi_agents.rs b/codex-rs/core/src/tools/handlers/multi_agents.rs index 8fa990a3b9..0415889077 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents.rs @@ -12,7 +12,7 @@ use crate::codex::TurnContext; use crate::config::Config; use crate::error::CodexErr; use crate::function_tool::FunctionCallError; -use crate::models_manager::manager::RefreshStrategy; +use crate::model_info_overrides::model_info_config_overrides; use crate::tools::context::FunctionToolOutput; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolOutput; @@ -22,6 +22,7 @@ use crate::tools::registry::ToolHandler; use crate::tools::registry::ToolKind; use async_trait::async_trait; use codex_features::Feature; +use codex_models::RefreshStrategy; use codex_protocol::ThreadId; use codex_protocol::models::BaseInstructions; use codex_protocol::models::ResponseInputItem; @@ -340,7 +341,7 @@ async fn apply_requested_spawn_agent_model_overrides( let selected_model_info = session .services .models_manager - .get_model_info(&selected_model_name, config) + .get_model_info(&selected_model_name, &model_info_config_overrides(config)) .await; config.model = Some(selected_model_name.clone()); diff --git a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs index abd491efdd..55eac9b8bf 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -2,7 +2,6 @@ use super::*; use crate::AuthManager; use crate::CodexAuth; use crate::ThreadManager; -use crate::built_in_model_providers; use crate::codex::make_session_and_context; use crate::config::DEFAULT_AGENT_MAX_DEPTH; use crate::config::types::ShellEnvironmentPolicy; @@ -17,6 +16,7 @@ use crate::protocol::SubAgentSource; use crate::tools::context::ToolOutput; use crate::turn_diff_tracker::TurnDiffTracker; use codex_features::Feature; +use codex_models::built_in_model_providers; use codex_protocol::ThreadId; use codex_protocol::models::ContentItem; use codex_protocol::models::FunctionCallOutputBody; diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 662e97d107..13ee3955ae 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -5,7 +5,6 @@ use crate::client_common::tools::ToolSpec; use crate::config::AgentRoleConfig; use crate::mcp::CODEX_APPS_MCP_SERVER_NAME; use crate::mcp_connection_manager::ToolInfo; -use crate::models_manager::collaboration_mode_presets::CollaborationModesConfig; use crate::original_image_detail::can_request_original_image_detail; use crate::shell::Shell; use crate::shell::ShellType; @@ -35,6 +34,7 @@ use crate::tools::registry::ToolRegistryBuilder; use crate::tools::registry::tool_handler_key; use codex_features::Feature; use codex_features::Features; +use codex_models::CollaborationModesConfig; use codex_protocol::config_types::WebSearchConfig; use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WindowsSandboxLevel; diff --git a/codex-rs/core/src/tools/spec_tests.rs b/codex-rs/core/src/tools/spec_tests.rs index 3142dd46a3..b5a3a03021 100644 --- a/codex-rs/core/src/tools/spec_tests.rs +++ b/codex-rs/core/src/tools/spec_tests.rs @@ -1,13 +1,14 @@ use crate::client_common::tools::FreeformTool; use crate::config::test_config; -use crate::models_manager::manager::ModelsManager; -use crate::models_manager::model_info::with_config_overrides; +use crate::model_info_overrides::model_info_config_overrides; use crate::shell::Shell; use crate::shell::ShellType; use crate::tools::ToolRouter; use crate::tools::registry::ConfiguredToolSpec; use crate::tools::router::ToolRouterParams; use codex_app_server_protocol::AppInfo; +use codex_models::ModelsManager; +use codex_models::with_config_overrides; use codex_protocol::openai_models::InputModality; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelsResponse; @@ -54,10 +55,17 @@ fn windows_shell_safety_description() -> String { format!("\n\n{}", super::windows_destructive_filesystem_guidance()) } +fn offline_model_info(config: &crate::config::Config, model: &str) -> ModelInfo { + ModelsManager::construct_model_info_offline_for_tests( + model, + config.model_catalog.as_ref(), + &model_info_config_overrides(config), + ) +} + fn search_capable_model_info() -> ModelInfo { let config = test_config(); - let mut model_info = - ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let mut model_info = offline_model_info(&config, "gpt-5-codex"); model_info.supports_search_tool = true; model_info } @@ -361,13 +369,14 @@ fn strip_descriptions_tool(spec: &mut ToolSpec) { fn model_info_from_models_json(slug: &str) -> ModelInfo { let config = test_config(); let response: ModelsResponse = - serde_json::from_str(include_str!("../../models.json")).expect("valid models.json"); + serde_json::from_str(include_str!("../../../models/models.json")) + .expect("valid models.json"); let model = response .models .into_iter() .find(|candidate| candidate.slug == slug) .unwrap_or_else(|| panic!("model slug {slug} is missing from models.json")); - with_config_overrides(model, &config) + with_config_overrides(model, &model_info_config_overrides(&config)) } #[test] @@ -499,7 +508,7 @@ fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() { #[test] fn test_build_specs_collab_tools_enabled() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = offline_model_info(&config, "gpt-5-codex"); let mut features = Features::with_defaults(); features.enable(Feature::Collab); let available_models = Vec::new(); @@ -523,7 +532,7 @@ fn test_build_specs_collab_tools_enabled() { #[test] fn test_build_specs_enable_fanout_enables_agent_jobs_and_collab_tools() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = offline_model_info(&config, "gpt-5-codex"); let mut features = Features::with_defaults(); features.enable(Feature::SpawnCsv); features.normalize_dependencies(); @@ -553,8 +562,7 @@ fn test_build_specs_enable_fanout_enables_agent_jobs_and_collab_tools() { #[test] fn view_image_tool_omits_detail_without_original_detail_feature() { let config = test_config(); - let mut model_info = - ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let mut model_info = offline_model_info(&config, "gpt-5-codex"); model_info.supports_image_detail_original = true; let features = Features::with_defaults(); let available_models = Vec::new(); @@ -581,8 +589,7 @@ fn view_image_tool_omits_detail_without_original_detail_feature() { #[test] fn view_image_tool_includes_detail_with_original_detail_feature() { let config = test_config(); - let mut model_info = - ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let mut model_info = offline_model_info(&config, "gpt-5-codex"); model_info.supports_image_detail_original = true; let mut features = Features::with_defaults(); features.enable(Feature::ImageDetailOriginal); @@ -620,7 +627,7 @@ fn test_build_specs_artifact_tool_enabled() { let mut config = test_config(); let runtime_root = tempfile::TempDir::new().expect("create temp codex home"); config.codex_home = runtime_root.path().to_path_buf(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = offline_model_info(&config, "gpt-5-codex"); let mut features = Features::with_defaults(); features.enable(Feature::Artifact); let available_models = Vec::new(); @@ -640,7 +647,7 @@ fn test_build_specs_artifact_tool_enabled() { #[test] fn test_build_specs_agent_job_worker_tools_enabled() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = offline_model_info(&config, "gpt-5-codex"); let mut features = Features::with_defaults(); features.enable(Feature::SpawnCsv); features.normalize_dependencies(); @@ -676,7 +683,7 @@ fn test_build_specs_agent_job_worker_tools_enabled() { #[test] fn request_user_input_description_reflects_default_mode_feature_flag() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = offline_model_info(&config, "gpt-5-codex"); let mut features = Features::with_defaults(); let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { @@ -719,7 +726,7 @@ fn request_user_input_description_reflects_default_mode_feature_flag() { #[test] fn request_permissions_requires_feature_flag() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = offline_model_info(&config, "gpt-5-codex"); let features = Features::with_defaults(); let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { @@ -757,7 +764,7 @@ fn request_permissions_requires_feature_flag() { #[test] fn request_permissions_tool_is_independent_from_additional_permissions() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = offline_model_info(&config, "gpt-5-codex"); let mut features = Features::with_defaults(); features.enable(Feature::ExecPermissionApprovals); let available_models = Vec::new(); @@ -778,7 +785,7 @@ fn request_permissions_tool_is_independent_from_additional_permissions() { #[test] fn get_memory_requires_feature_flag() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = offline_model_info(&config, "gpt-5-codex"); let mut features = Features::with_defaults(); features.disable(Feature::MemoryTool); let available_models = Vec::new(); @@ -801,7 +808,7 @@ fn get_memory_requires_feature_flag() { #[test] fn js_repl_requires_feature_flag() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = offline_model_info(&config, "gpt-5-codex"); let features = Features::with_defaults(); let available_models = Vec::new(); @@ -829,7 +836,7 @@ fn js_repl_requires_feature_flag() { #[test] fn js_repl_enabled_adds_tools() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = offline_model_info(&config, "gpt-5-codex"); let mut features = Features::with_defaults(); features.enable(Feature::JsRepl); @@ -850,8 +857,7 @@ fn js_repl_enabled_adds_tools() { #[test] fn image_generation_tools_require_feature_and_supported_model() { let config = test_config(); - let mut supported_model_info = - ModelsManager::construct_model_info_offline_for_tests("gpt-5.2", &config); + let mut supported_model_info = offline_model_info(&config, "gpt-5.2"); supported_model_info.slug = "custom/gpt-5.2-variant".to_string(); let mut unsupported_model_info = supported_model_info.clone(); unsupported_model_info.input_modalities = vec![InputModality::Text]; @@ -984,7 +990,7 @@ fn assert_default_model_tools( #[test] fn web_search_mode_cached_sets_external_web_access_false() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = offline_model_info(&config, "gpt-5-codex"); let features = Features::with_defaults(); let available_models = Vec::new(); @@ -1015,7 +1021,7 @@ fn web_search_mode_cached_sets_external_web_access_false() { #[test] fn web_search_mode_live_sets_external_web_access_true() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = offline_model_info(&config, "gpt-5-codex"); let features = Features::with_defaults(); let available_models = Vec::new(); @@ -1046,7 +1052,7 @@ fn web_search_mode_live_sets_external_web_access_true() { #[test] fn web_search_config_is_forwarded_to_tool_spec() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = offline_model_info(&config, "gpt-5-codex"); let features = Features::with_defaults(); let web_search_config = WebSearchConfig { filters: Some(codex_protocol::config_types::WebSearchFilters { @@ -1095,8 +1101,7 @@ fn web_search_config_is_forwarded_to_tool_spec() { #[test] fn web_search_tool_type_text_and_image_sets_search_content_types() { let config = test_config(); - let mut model_info = - ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let mut model_info = offline_model_info(&config, "gpt-5-codex"); model_info.web_search_tool_type = WebSearchToolType::TextAndImage; let features = Features::with_defaults(); @@ -1133,7 +1138,7 @@ fn web_search_tool_type_text_and_image_sets_search_content_types() { #[test] fn mcp_resource_tools_are_hidden_without_mcp_servers() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = offline_model_info(&config, "gpt-5-codex"); let features = Features::with_defaults(); let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { @@ -1159,7 +1164,7 @@ fn mcp_resource_tools_are_hidden_without_mcp_servers() { #[test] fn mcp_resource_tools_are_included_when_mcp_servers_are_present() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = offline_model_info(&config, "gpt-5-codex"); let features = Features::with_defaults(); let available_models = Vec::new(); let tools_config = ToolsConfig::new(&ToolsConfigParams { @@ -1398,7 +1403,7 @@ fn test_gpt_5_1_codex_max_unified_exec_web_search() { #[test] fn test_build_specs_default_shell_present() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("o3", &config); + let model_info = offline_model_info(&config, "o3"); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); let available_models = Vec::new(); @@ -1424,7 +1429,7 @@ fn test_build_specs_default_shell_present() { #[test] fn shell_zsh_fork_prefers_shell_command_over_unified_exec() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("o3", &config); + let model_info = offline_model_info(&config, "o3"); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); features.enable(Feature::ShellZshFork); @@ -1488,7 +1493,7 @@ fn shell_zsh_fork_prefers_shell_command_over_unified_exec() { #[ignore] fn test_parallel_support_flags() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = offline_model_info(&config, "gpt-5-codex"); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); let available_models = Vec::new(); @@ -1554,7 +1559,7 @@ fn test_test_model_info_includes_sync_tool() { #[test] fn test_build_specs_mcp_tools_converted() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("o3", &config); + let model_info = offline_model_info(&config, "o3"); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); let available_models = Vec::new(); @@ -1647,7 +1652,7 @@ fn test_build_specs_mcp_tools_converted() { #[test] fn test_build_specs_mcp_tools_sorted_by_name() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("o3", &config); + let model_info = offline_model_info(&config, "o3"); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); let available_models = Vec::new(); @@ -2124,7 +2129,7 @@ fn tool_suggest_description_lists_discoverable_tools() { #[test] fn test_mcp_tool_property_missing_type_defaults_to_string() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = offline_model_info(&config, "gpt-5-codex"); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); let available_models = Vec::new(); @@ -2184,7 +2189,7 @@ fn test_mcp_tool_property_missing_type_defaults_to_string() { #[test] fn test_mcp_tool_integer_normalized_to_number() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = offline_model_info(&config, "gpt-5-codex"); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); let available_models = Vec::new(); @@ -2240,7 +2245,7 @@ fn test_mcp_tool_integer_normalized_to_number() { #[test] fn test_mcp_tool_array_without_items_gets_default_string_items() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = offline_model_info(&config, "gpt-5-codex"); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); features.enable(Feature::ApplyPatchFreeform); @@ -2300,7 +2305,7 @@ fn test_mcp_tool_array_without_items_gets_default_string_items() { #[test] fn test_mcp_tool_anyof_defaults_to_string() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = offline_model_info(&config, "gpt-5-codex"); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); let available_models = Vec::new(); @@ -2525,7 +2530,7 @@ Examples of valid command strings: #[test] fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = offline_model_info(&config, "gpt-5-codex"); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); let available_models = Vec::new(); @@ -2635,7 +2640,7 @@ fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() { #[test] fn code_mode_augments_builtin_tool_descriptions_with_typed_sample() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = offline_model_info(&config, "gpt-5-codex"); let mut features = Features::with_defaults(); features.enable(Feature::CodeMode); features.enable(Feature::UnifiedExec); @@ -2666,7 +2671,7 @@ fn code_mode_augments_builtin_tool_descriptions_with_typed_sample() { #[test] fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = offline_model_info(&config, "gpt-5-codex"); let mut features = Features::with_defaults(); features.enable(Feature::CodeMode); features.enable(Feature::UnifiedExec); @@ -2732,7 +2737,7 @@ fn code_mode_only_restricts_model_tools_to_exec_tools() { #[test] fn code_mode_only_exec_description_includes_full_nested_tool_details() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = offline_model_info(&config, "gpt-5-codex"); let mut features = Features::with_defaults(); features.enable(Feature::CodeMode); features.enable(Feature::CodeModeOnly); @@ -2765,7 +2770,7 @@ fn code_mode_only_exec_description_includes_full_nested_tool_details() { #[test] fn code_mode_exec_description_omits_nested_tool_details_when_not_code_mode_only() { let config = test_config(); - let model_info = ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config); + let model_info = offline_model_info(&config, "gpt-5-codex"); let mut features = Features::with_defaults(); features.enable(Feature::CodeMode); let available_models = Vec::new(); diff --git a/codex-rs/core/templates/collaboration_mode/execute.md b/codex-rs/core/templates/collaboration_mode/execute.md deleted file mode 100644 index c57878242d..0000000000 --- a/codex-rs/core/templates/collaboration_mode/execute.md +++ /dev/null @@ -1,45 +0,0 @@ -# Collaboration Style: Execute -You execute on a well-specified task independently and report progress. - -You do not collaborate on decisions in this mode. You execute end-to-end. -You make reasonable assumptions when the user hasn't specified something, and you proceed without asking questions. - -## Assumptions-first execution -When information is missing, do not ask the user questions. -Instead: -- Make a sensible assumption. -- Clearly state the assumption in the final message (briefly). -- Continue executing. - -Group assumptions logically, for example architecture/frameworks/implementation, features/behavior, design/themes/feel. -If the user does not react to a proposed suggestion, consider it accepted. - -## Execution principles -*Think out loud.* Share reasoning when it helps the user evaluate tradeoffs. Keep explanations short and grounded in consequences. Avoid design lectures or exhaustive option lists. - -*Use reasonable assumptions.* When the user hasn't specified something, suggest a sensible choice instead of asking an open-ended question. Group your assumptions logically, for example architecture/frameworks/implementation, features/behavior, design/themes/feel. Clearly label suggestions as provisional. Share reasoning when it helps the user evaluate tradeoffs. Keep explanations short and grounded in consequences. They should be easy to accept or override. If the user does not react to a proposed suggestion, consider it accepted. - -Example: "There are a few viable ways to structure this. A plugin model gives flexibility but adds complexity; a simpler core with extension points is easier to reason about. Given what you've said about your team's size, I'd lean towards the latter." -Example: "If this is a shared internal library, I'll assume API stability matters more than rapid iteration." - -*Think ahead.* What else might the user need? How will the user test and understand what you did? Think about ways to support them and propose things they might need BEFORE you build. Offer at least one suggestion you came up with by thinking ahead. -Example: "This feature changes as time passes but you probably want to test it without waiting for a full hour to pass. I'll include a debug mode where you can move through states without just waiting." - -*Be mindful of time.* The user is right here with you. Any time you spend reading files or searching for information is time that the user is waiting for you. Do make use of these tools if helpful, but minimize the time the user is waiting for you. As a rule of thumb, spend only a few seconds on most turns and no more than 60 seconds when doing research. If you are missing information and would normally ask, make a reasonable assumption and continue. -Example: "I checked the readme and searched for the feature you mentioned, but didn't find it immediately. I'll proceed with the most likely implementation and verify behavior with a quick test." - -## Long-horizon execution -Treat the task as a sequence of concrete steps that add up to a complete delivery. -- Break the work into milestones that move the task forward in a visible way. -- Execute step by step, verifying along the way rather than doing everything at the end. -- If the task is large, keep a running checklist of what is done, what is next, and what is blocked. -- Avoid blocking on uncertainty: choose a reasonable default and continue. - -## Reporting progress -In this phase you show progress on your task and appraise the user of your progress using plan tool. -- Provide updates that directly map to the work you are doing (what changed, what you verified, what remains). -- If something fails, report what failed, what you tried, and what you will do next. -- When you finish, summarize what you delivered and how the user can validate it. - -## Executing -Once you start working, you should execute independently. Your job is to deliver the task and report progress. diff --git a/codex-rs/core/templates/collaboration_mode/pair_programming.md b/codex-rs/core/templates/collaboration_mode/pair_programming.md deleted file mode 100644 index 1297129b1d..0000000000 --- a/codex-rs/core/templates/collaboration_mode/pair_programming.md +++ /dev/null @@ -1,7 +0,0 @@ -# Collaboration Style: Pair Programming - -## Build together as you go -You treat collaboration as pairing by default. The user is right with you in the terminal, so avoid taking steps that are too large or take a lot of time (like running long tests), unless asked for it. You check for alignment and comfort before moving forward, explain reasoning step by step, and dynamically adjust depth based on the user's signals. There is no need to ask multiple rounds of questions—build as you go. When there are multiple viable paths, you present clear options with friendly framing, ground them in examples and intuition, and explicitly invite the user into the decision so the choice feels empowering rather than burdensome. When you do more complex work you use the planning tool liberally to keep the user updated on what you are doing. - -## Debugging -If you are debugging something with the user, assume you are a team. You can ask them what they see and ask them to provide you with information you don't have access to, for example you can ask them to check error messages in developer tools or provide you with screenshots. diff --git a/codex-rs/core/tests/common/BUILD.bazel b/codex-rs/core/tests/common/BUILD.bazel index aec0c17817..d61244c0a2 100644 --- a/codex-rs/core/tests/common/BUILD.bazel +++ b/codex-rs/core/tests/common/BUILD.bazel @@ -5,6 +5,6 @@ codex_rust_crate( crate_name = "core_test_support", crate_srcs = glob(["*.rs"]), lib_data_extra = [ - "//codex-rs/core:model_availability_nux_fixtures", + "//codex-rs/models:model_availability_nux_fixtures", ], ) diff --git a/codex-rs/core/tests/common/Cargo.toml b/codex-rs/core/tests/common/Cargo.toml index 7377e40f53..afe0cde5b2 100644 --- a/codex-rs/core/tests/common/Cargo.toml +++ b/codex-rs/core/tests/common/Cargo.toml @@ -14,6 +14,7 @@ base64 = { workspace = true } codex-arg0 = { workspace = true } codex-core = { workspace = true } codex-features = { workspace = true } +codex-models = { workspace = true } codex-protocol = { workspace = true } codex-utils-absolute-path = { workspace = true } codex-utils-cargo-bin = { workspace = true } diff --git a/codex-rs/core/tests/common/lib.rs b/codex-rs/core/tests/common/lib.rs index 6209ded404..46312e7e38 100644 --- a/codex-rs/core/tests/common/lib.rs +++ b/codex-rs/core/tests/common/lib.rs @@ -3,6 +3,8 @@ use anyhow::Context as _; use anyhow::ensure; use codex_arg0::Arg0PathEntryGuard; +use codex_features::Feature; +use codex_models::ModelInfoConfigOverrides; use codex_utils_cargo_bin::CargoBinError; use ctor::ctor; use std::sync::OnceLock; @@ -112,6 +114,17 @@ pub fn test_tmp_path_buf() -> PathBuf { test_tmp_path().into_path_buf() } +pub fn model_info_config_overrides(config: &Config) -> ModelInfoConfigOverrides { + ModelInfoConfigOverrides { + model_supports_reasoning_summaries: config.model_supports_reasoning_summaries, + model_context_window: config.model_context_window, + model_auto_compact_token_limit: config.model_auto_compact_token_limit, + tool_output_token_limit: config.tool_output_token_limit, + base_instructions: config.base_instructions.clone(), + personality_enabled: config.features.enabled(Feature::Personality), + } +} + /// Fetch a DotSlash resource and return the resolved executable/file path. pub fn fetch_dotslash_file( dotslash_file: &std::path::Path, diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index 116a30d28d..7269c53b69 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -7,14 +7,14 @@ use anyhow::Context; use anyhow::Result; use codex_core::CodexAuth; use codex_core::CodexThread; -use codex_core::ModelProviderInfo; use codex_core::ThreadManager; -use codex_core::built_in_model_providers; use codex_core::config::Config; -use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_core::shell::Shell; use codex_core::shell::get_shell_by_model_provided_path; use codex_features::Feature; +use codex_models::CollaborationModesConfig; +use codex_models::ModelProviderInfo; +use codex_models::built_in_model_providers; use codex_protocol::config_types::ServiceTier; use codex_protocol::openai_models::ModelsResponse; use codex_protocol::protocol::AskForApproval; @@ -316,7 +316,7 @@ fn ensure_test_model_catalog(config: &mut Config) -> Result<()> { return Ok(()); } - let bundled_models_path = codex_utils_cargo_bin::find_resource!("../../models.json") + let bundled_models_path = codex_utils_cargo_bin::find_resource!("../../../models/models.json") .context("bundled models.json")?; let bundled_models_contents = std::fs::read_to_string(&bundled_models_path).with_context(|| { diff --git a/codex-rs/core/tests/responses_headers.rs b/codex-rs/core/tests/responses_headers.rs index 823057797c..f2d9773283 100644 --- a/codex-rs/core/tests/responses_headers.rs +++ b/codex-rs/core/tests/responses_headers.rs @@ -3,10 +3,10 @@ use std::sync::Arc; use codex_core::CodexAuth; use codex_core::ModelClient; -use codex_core::ModelProviderInfo; use codex_core::Prompt; use codex_core::ResponseEvent; -use codex_core::WireApi; +use codex_models::ModelProviderInfo; +use codex_models::WireApi; use codex_otel::SessionTelemetry; use codex_otel::TelemetryAuthMode; use codex_protocol::ThreadId; diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 3ea30c5967..ac0c552c4c 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -1,17 +1,17 @@ use codex_core::CodexAuth; use codex_core::ModelClient; -use codex_core::ModelProviderInfo; use codex_core::NewThread; use codex_core::Prompt; use codex_core::ResponseEvent; use codex_core::ThreadManager; -use codex_core::WireApi; use codex_core::auth::AuthCredentialsStoreMode; -use codex_core::built_in_model_providers; use codex_core::default_client::originator; use codex_core::error::CodexErr; -use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_features::Feature; +use codex_models::CollaborationModesConfig; +use codex_models::ModelProviderInfo; +use codex_models::WireApi; +use codex_models::built_in_model_providers; use codex_otel::SessionTelemetry; use codex_otel::TelemetryAuthMode; use codex_protocol::ThreadId; @@ -1397,7 +1397,8 @@ async fn user_turn_explicit_reasoning_summary_overrides_model_catalog_default() .await; let mut model_catalog: ModelsResponse = - serde_json::from_str(include_str!("../../models.json")).expect("valid models.json"); + serde_json::from_str(include_str!("../../../models/models.json")) + .expect("valid models.json"); let model = model_catalog .models .iter_mut() @@ -1509,7 +1510,8 @@ async fn reasoning_summary_none_overrides_model_catalog_default() -> anyhow::Res .await; let mut model_catalog: ModelsResponse = - serde_json::from_str(include_str!("../../models.json")).expect("valid models.json"); + serde_json::from_str(include_str!("../../../models/models.json")) + .expect("valid models.json"); let model = model_catalog .models .iter_mut() diff --git a/codex-rs/core/tests/suite/client_websockets.rs b/codex-rs/core/tests/suite/client_websockets.rs index b568c6aee2..e3c2c6faca 100755 --- a/codex-rs/core/tests/suite/client_websockets.rs +++ b/codex-rs/core/tests/suite/client_websockets.rs @@ -4,12 +4,12 @@ use codex_api::WS_REQUEST_HEADER_TRACESTATE_CLIENT_METADATA_KEY; use codex_core::CodexAuth; use codex_core::ModelClient; use codex_core::ModelClientSession; -use codex_core::ModelProviderInfo; use codex_core::Prompt; use codex_core::ResponseEvent; -use codex_core::WireApi; use codex_core::X_RESPONSESAPI_INCLUDE_TIMING_METRICS_HEADER; use codex_features::Feature; +use codex_models::ModelProviderInfo; +use codex_models::WireApi; use codex_otel::SessionTelemetry; use codex_otel::TelemetryAuthMode; use codex_otel::current_span_w3c_trace_context; diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index 2f4365a959..1b88b04e9a 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -1,11 +1,11 @@ #![allow(clippy::expect_used)] use codex_core::CodexAuth; -use codex_core::ModelProviderInfo; -use codex_core::built_in_model_providers; use codex_core::compact::SUMMARIZATION_PROMPT; use codex_core::compact::SUMMARY_PREFIX; use codex_core::config::Config; use codex_features::Feature; +use codex_models::ModelProviderInfo; +use codex_models::built_in_model_providers; use codex_protocol::items::TurnItem; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelsResponse; @@ -103,7 +103,8 @@ fn non_openai_model_provider(server: &MockServer) -> ModelProviderInfo { fn model_info_with_context_window(slug: &str, context_window: i64) -> ModelInfo { let models_response: ModelsResponse = - serde_json::from_str(include_str!("../../models.json")).expect("valid models.json"); + serde_json::from_str(include_str!("../../../models/models.json")) + .expect("valid models.json"); let mut model_info = models_response .models .into_iter() diff --git a/codex-rs/core/tests/suite/model_info_overrides.rs b/codex-rs/core/tests/suite/model_info_overrides.rs index ee4afc36bf..f5109bdbff 100644 --- a/codex-rs/core/tests/suite/model_info_overrides.rs +++ b/codex-rs/core/tests/suite/model_info_overrides.rs @@ -1,8 +1,9 @@ use codex_core::CodexAuth; -use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; -use codex_core::models_manager::manager::ModelsManager; +use codex_models::CollaborationModesConfig; +use codex_models::ModelsManager; use codex_protocol::openai_models::TruncationPolicyConfig; use core_test_support::load_default_config_for_test; +use core_test_support::model_info_config_overrides; use pretty_assertions::assert_eq; use tempfile::TempDir; @@ -20,7 +21,9 @@ async fn offline_model_info_without_tool_output_override() { CollaborationModesConfig::default(), ); - let model_info = manager.get_model_info("gpt-5.1", &config).await; + let model_info = manager + .get_model_info("gpt-5.1", &model_info_config_overrides(&config)) + .await; assert_eq!( model_info.truncation_policy, @@ -43,7 +46,9 @@ async fn offline_model_info_with_tool_output_override() { CollaborationModesConfig::default(), ); - let model_info = manager.get_model_info("gpt-5.1-codex", &config).await; + let model_info = manager + .get_model_info("gpt-5.1-codex", &model_info_config_overrides(&config)) + .await; assert_eq!( model_info.truncation_policy, diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index 9902f0ee6c..1aa6a07635 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -1,8 +1,8 @@ use anyhow::Result; use codex_core::CodexAuth; use codex_core::config::types::Personality; -use codex_core::models_manager::manager::RefreshStrategy; use codex_features::Feature; +use codex_models::RefreshStrategy; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::ServiceTier; use codex_protocol::openai_models::ConfigShellToolType; @@ -19,6 +19,7 @@ use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; +use core_test_support::model_info_config_overrides; use core_test_support::responses::ev_completed_with_tokens; use core_test_support::responses::ev_image_generation_call; use core_test_support::responses::ev_response_created; @@ -949,11 +950,17 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result< "expected {smaller_model_slug} to be available in remote model list" ); let large_model_info = models_manager - .get_model_info(large_model_slug, &test.config) + .get_model_info( + large_model_slug, + &model_info_config_overrides(test.config.as_ref()), + ) .await; assert_eq!(large_model_info.context_window, Some(large_context_window)); let smaller_model_info = models_manager - .get_model_info(smaller_model_slug, &test.config) + .get_model_info( + smaller_model_slug, + &model_info_config_overrides(test.config.as_ref()), + ) .await; assert_eq!( smaller_model_info.context_window, diff --git a/codex-rs/core/tests/suite/models_cache_ttl.rs b/codex-rs/core/tests/suite/models_cache_ttl.rs index 7cb7573347..7b93bbb194 100644 --- a/codex-rs/core/tests/suite/models_cache_ttl.rs +++ b/codex-rs/core/tests/suite/models_cache_ttl.rs @@ -6,7 +6,8 @@ use chrono::DateTime; use chrono::TimeZone; use chrono::Utc; use codex_core::CodexAuth; -use codex_core::models_manager::manager::RefreshStrategy; +use codex_models::RefreshStrategy; +use codex_models::client_version_to_whole; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ModelInfo; @@ -152,7 +153,7 @@ async fn uses_cache_when_version_matches() -> Result<()> { let cache = ModelsCache { fetched_at: Utc::now(), etag: None, - client_version: Some(codex_core::models_manager::client_version_to_whole()), + client_version: Some(client_version_to_whole()), models: vec![cached_model], }; let cache_path = home.join(CACHE_FILE); @@ -243,7 +244,7 @@ async fn refreshes_when_cache_version_differs() -> Result<()> { let mut builder = test_codex().with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()); builder = builder .with_pre_build_hook(move |home| { - let client_version = codex_core::models_manager::client_version_to_whole(); + let client_version = client_version_to_whole(); let cache = ModelsCache { fetched_at: Utc::now(), etag: None, diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index 9a495e7af4..a8f4a2b7a8 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -1,7 +1,7 @@ use codex_core::config::types::Personality; -use codex_core::models_manager::manager::ModelsManager; -use codex_core::models_manager::manager::RefreshStrategy; use codex_features::Feature; +use codex_models::ModelsManager; +use codex_models::RefreshStrategy; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ModelInfo; diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 14caaf8f0b..8b581b95fe 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -17,6 +17,7 @@ use codex_protocol::protocol::Op; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use codex_utils_absolute_path::AbsolutePathBuf; +use core_test_support::model_info_config_overrides; use core_test_support::responses::ev_completed; use core_test_support::responses::ev_response_created; use core_test_support::responses::mount_sse_once; @@ -139,7 +140,7 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> { .model .as_deref() .expect("test config should have a model"), - &config, + &model_info_config_overrides(config.as_ref()), ) .await .base_instructions; diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index 860d83fe9a..416da84db6 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -5,10 +5,10 @@ use std::sync::Arc; use anyhow::Result; use codex_core::CodexAuth; -use codex_core::ModelProviderInfo; -use codex_core::built_in_model_providers; -use codex_core::models_manager::manager::ModelsManager; -use codex_core::models_manager::manager::RefreshStrategy; +use codex_models::ModelProviderInfo; +use codex_models::ModelsManager; +use codex_models::RefreshStrategy; +use codex_models::built_in_model_providers; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ModelInfo; @@ -26,6 +26,7 @@ use codex_protocol::protocol::Op; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use core_test_support::load_default_config_for_test; +use core_test_support::model_info_config_overrides; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; use core_test_support::responses::ev_function_call; @@ -105,7 +106,9 @@ async fn remote_models_get_model_info_uses_longest_matching_prefix() -> Result<( manager.list_models(RefreshStrategy::OnlineIfUncached).await; - let model_info = manager.get_model_info("gpt-5.3-codex-test", &config).await; + let model_info = manager + .get_model_info("gpt-5.3-codex-test", &model_info_config_overrides(&config)) + .await; assert_eq!(model_info.slug, "gpt-5.3-codex-test"); assert_eq!(model_info.base_instructions, specific.base_instructions); @@ -346,7 +349,10 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { assert_eq!(requests[0].url.path(), "/v1/models"); let model_info = models_manager - .get_model_info(REMOTE_MODEL_SLUG, &config) + .get_model_info( + REMOTE_MODEL_SLUG, + &model_info_config_overrides(config.as_ref()), + ) .await; assert_eq!(model_info.shell_type, ConfigShellToolType::UnifiedExec); @@ -452,7 +458,9 @@ async fn remote_models_truncation_policy_without_override_preserves_remote() -> let models_manager = test.thread_manager.get_models_manager(); wait_for_model_available(&models_manager, slug).await; - let model_info = models_manager.get_model_info(slug, &test.config).await; + let model_info = models_manager + .get_model_info(slug, &model_info_config_overrides(test.config.as_ref())) + .await; assert_eq!( model_info.truncation_policy, TruncationPolicyConfig::bytes(12_000) @@ -497,7 +505,9 @@ async fn remote_models_truncation_policy_with_tool_output_override() -> Result<( let models_manager = test.thread_manager.get_models_manager(); wait_for_model_available(&models_manager, slug).await; - let model_info = models_manager.get_model_info(slug, &test.config).await; + let model_info = models_manager + .get_model_info(slug, &model_info_config_overrides(test.config.as_ref())) + .await; assert_eq!( model_info.truncation_policy, TruncationPolicyConfig::bytes(200) @@ -624,7 +634,9 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; - let base_model_info = models_manager.get_model_info("gpt-5.1", &config).await; + let base_model_info = models_manager + .get_model_info("gpt-5.1", &model_info_config_overrides(config.as_ref())) + .await; let body = response_mock.single_request().body_json(); let instructions = body["instructions"].as_str().unwrap(); assert_eq!(instructions, base_model_info.base_instructions); @@ -954,8 +966,9 @@ async fn wait_for_model_available(manager: &Arc, slug: &str) -> M } fn bundled_model_slug() -> String { - let response: ModelsResponse = serde_json::from_str(include_str!("../../models.json")) - .expect("bundled models.json should deserialize"); + let response: ModelsResponse = + serde_json::from_str(include_str!("../../../models/models.json")) + .expect("bundled models.json should deserialize"); response .models .first() diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index 772674f79a..46b94f9dc6 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -12,7 +12,7 @@ use std::time::UNIX_EPOCH; use codex_core::CodexAuth; use codex_core::config::types::McpServerConfig; use codex_core::config::types::McpServerTransportConfig; -use codex_core::models_manager::manager::RefreshStrategy; +use codex_models::RefreshStrategy; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::openai_models::ConfigShellToolType; diff --git a/codex-rs/core/tests/suite/search_tool.rs b/codex-rs/core/tests/suite/search_tool.rs index 0eee2f1d3d..97fae623c4 100644 --- a/codex-rs/core/tests/suite/search_tool.rs +++ b/codex-rs/core/tests/suite/search_tool.rs @@ -95,7 +95,8 @@ fn configure_apps(config: &mut Config, apps_base_url: &str) { config.model = Some("gpt-5-codex".to_string()); let mut model_catalog: ModelsResponse = - serde_json::from_str(include_str!("../../models.json")).expect("valid models.json"); + serde_json::from_str(include_str!("../../../models/models.json")) + .expect("valid models.json"); let model = model_catalog .models .iter_mut() diff --git a/codex-rs/core/tests/suite/spawn_agent_description.rs b/codex-rs/core/tests/suite/spawn_agent_description.rs index dfc5a2e546..be8ef49c9a 100644 --- a/codex-rs/core/tests/suite/spawn_agent_description.rs +++ b/codex-rs/core/tests/suite/spawn_agent_description.rs @@ -3,9 +3,9 @@ use anyhow::Result; use codex_core::CodexAuth; -use codex_core::models_manager::manager::ModelsManager; -use codex_core::models_manager::manager::RefreshStrategy; use codex_features::Feature; +use codex_models::ModelsManager; +use codex_models::RefreshStrategy; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ModelInfo; diff --git a/codex-rs/core/tests/suite/stream_error_allows_next_turn.rs b/codex-rs/core/tests/suite/stream_error_allows_next_turn.rs index 23ffc4afb0..5348b6b6b4 100644 --- a/codex-rs/core/tests/suite/stream_error_allows_next_turn.rs +++ b/codex-rs/core/tests/suite/stream_error_allows_next_turn.rs @@ -1,5 +1,5 @@ -use codex_core::ModelProviderInfo; -use codex_core::WireApi; +use codex_models::ModelProviderInfo; +use codex_models::WireApi; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; use codex_protocol::user_input::UserInput; diff --git a/codex-rs/core/tests/suite/stream_no_completed.rs b/codex-rs/core/tests/suite/stream_no_completed.rs index 5d1b214811..9efd29f558 100644 --- a/codex-rs/core/tests/suite/stream_no_completed.rs +++ b/codex-rs/core/tests/suite/stream_no_completed.rs @@ -1,8 +1,8 @@ //! Verifies that the agent retries when the SSE stream terminates before //! delivering a `response.completed` event. -use codex_core::ModelProviderInfo; -use codex_core::WireApi; +use codex_models::ModelProviderInfo; +use codex_models::WireApi; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; use codex_protocol::user_input::UserInput; diff --git a/codex-rs/core/tests/suite/websocket_fallback.rs b/codex-rs/core/tests/suite/websocket_fallback.rs index 0090093c77..cc6b301670 100644 --- a/codex-rs/core/tests/suite/websocket_fallback.rs +++ b/codex-rs/core/tests/suite/websocket_fallback.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use codex_models::WireApi; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; @@ -43,7 +44,7 @@ async fn websocket_fallback_switches_to_http_on_upgrade_required_connect() -> Re let base_url = format!("{}/v1", server.uri()); move |config| { config.model_provider.base_url = Some(base_url); - config.model_provider.wire_api = codex_core::WireApi::Responses; + config.model_provider.wire_api = WireApi::Responses; config.model_provider.supports_websockets = true; // If we don't treat 426 specially, the sampling loop would retry the WebSocket // handshake before switching to the HTTP transport. @@ -89,7 +90,7 @@ async fn websocket_fallback_switches_to_http_after_retries_exhausted() -> Result let base_url = format!("{}/v1", server.uri()); move |config| { config.model_provider.base_url = Some(base_url); - config.model_provider.wire_api = codex_core::WireApi::Responses; + config.model_provider.wire_api = WireApi::Responses; config.model_provider.supports_websockets = true; config.model_provider.stream_max_retries = Some(2); config.model_provider.request_max_retries = Some(0); @@ -134,7 +135,7 @@ async fn websocket_fallback_hides_first_websocket_retry_stream_error() -> Result let base_url = format!("{}/v1", server.uri()); move |config| { config.model_provider.base_url = Some(base_url); - config.model_provider.wire_api = codex_core::WireApi::Responses; + config.model_provider.wire_api = WireApi::Responses; config.model_provider.supports_websockets = true; config.model_provider.stream_max_retries = Some(2); config.model_provider.request_max_retries = Some(0); @@ -209,7 +210,7 @@ async fn websocket_fallback_is_sticky_across_turns() -> Result<()> { let base_url = format!("{}/v1", server.uri()); move |config| { config.model_provider.base_url = Some(base_url); - config.model_provider.wire_api = codex_core::WireApi::Responses; + config.model_provider.wire_api = WireApi::Responses; config.model_provider.supports_websockets = true; config.model_provider.stream_max_retries = Some(2); config.model_provider.request_max_retries = Some(0); diff --git a/codex-rs/exec/Cargo.toml b/codex-rs/exec/Cargo.toml index 3785235606..dcb5372a4c 100644 --- a/codex-rs/exec/Cargo.toml +++ b/codex-rs/exec/Cargo.toml @@ -24,6 +24,7 @@ codex-app-server-protocol = { workspace = true } codex-cloud-requirements = { workspace = true } codex-core = { workspace = true } codex-feedback = { workspace = true } +codex-models = { workspace = true } codex-otel = { workspace = true } codex-protocol = { workspace = true } codex-utils-absolute-path = { workspace = true } diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index f648a63952..c06a01c71d 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -43,8 +43,6 @@ use codex_app_server_protocol::TurnStartResponse; use codex_arg0::Arg0DispatchPaths; use codex_cloud_requirements::cloud_requirements_loader; use codex_core::AuthManager; -use codex_core::LMSTUDIO_OSS_PROVIDER_ID; -use codex_core::OLLAMA_OSS_PROVIDER_ID; use codex_core::auth::AuthConfig; use codex_core::auth::enforce_login_restrictions; use codex_core::check_execpolicy_for_warnings; @@ -60,6 +58,8 @@ use codex_core::config_loader::format_config_error_with_source; use codex_core::format_exec_policy_error_with_source; use codex_core::git_info::get_git_repo_root; use codex_feedback::CodexFeedback; +use codex_models::LMSTUDIO_OSS_PROVIDER_ID; +use codex_models::OLLAMA_OSS_PROVIDER_ID; use codex_otel::set_parent_from_context; use codex_otel::traceparent_context_from_env; use codex_protocol::account::PlanType as AccountPlanType; diff --git a/codex-rs/lmstudio/Cargo.toml b/codex-rs/lmstudio/Cargo.toml index 5f4849638a..dedbed8a39 100644 --- a/codex-rs/lmstudio/Cargo.toml +++ b/codex-rs/lmstudio/Cargo.toml @@ -11,6 +11,7 @@ path = "src/lib.rs" [dependencies] codex-core = { path = "../core" } +codex-models = { workspace = true } reqwest = { version = "0.12", features = ["json", "stream"] } serde_json = "1" tokio = { version = "1", features = ["rt"] } diff --git a/codex-rs/lmstudio/src/client.rs b/codex-rs/lmstudio/src/client.rs index 1d0c04c1ac..8729ea7ac6 100644 --- a/codex-rs/lmstudio/src/client.rs +++ b/codex-rs/lmstudio/src/client.rs @@ -1,5 +1,5 @@ -use codex_core::LMSTUDIO_OSS_PROVIDER_ID; use codex_core::config::Config; +use codex_models::LMSTUDIO_OSS_PROVIDER_ID; use std::io; use std::path::Path; diff --git a/codex-rs/mcp-server/Cargo.toml b/codex-rs/mcp-server/Cargo.toml index 4c05f27c10..e29c41908d 100644 --- a/codex-rs/mcp-server/Cargo.toml +++ b/codex-rs/mcp-server/Cargo.toml @@ -20,6 +20,7 @@ anyhow = { workspace = true } codex-arg0 = { workspace = true } codex-core = { workspace = true } codex-features = { workspace = true } +codex-models = { workspace = true } codex-protocol = { workspace = true } codex-shell-command = { workspace = true } codex-utils-cli = { workspace = true } diff --git a/codex-rs/mcp-server/src/message_processor.rs b/codex-rs/mcp-server/src/message_processor.rs index e5397e4cac..25ed7c325f 100644 --- a/codex-rs/mcp-server/src/message_processor.rs +++ b/codex-rs/mcp-server/src/message_processor.rs @@ -6,8 +6,8 @@ use codex_core::ThreadManager; use codex_core::config::Config; use codex_core::default_client::USER_AGENT_SUFFIX; use codex_core::default_client::get_codex_user_agent; -use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_features::Feature; +use codex_models::CollaborationModesConfig; use codex_protocol::ThreadId; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::Submission; diff --git a/codex-rs/models/BUILD.bazel b/codex-rs/models/BUILD.bazel new file mode 100644 index 0000000000..2b479ab77c --- /dev/null +++ b/codex-rs/models/BUILD.bazel @@ -0,0 +1,20 @@ +load("//:defs.bzl", "codex_rust_crate") + +filegroup( + name = "model_availability_nux_fixtures", + srcs = [ + "models.json", + "//codex-rs/core:cli_responses_fixture_sse", + ], + visibility = ["//visibility:public"], +) + +codex_rust_crate( + name = "models", + crate_name = "codex_models", + compile_data = [ + "models.json", + "templates/collaboration_mode/default.md", + "templates/collaboration_mode/plan.md", + ], +) diff --git a/codex-rs/models/Cargo.toml b/codex-rs/models/Cargo.toml new file mode 100644 index 0000000000..e5ac927b9a --- /dev/null +++ b/codex-rs/models/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "codex-models" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lib] +doctest = false +name = "codex_models" +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +base64 = { workspace = true } +chrono = { workspace = true, features = ["serde"] } +codex-api = { workspace = true } +codex-login = { workspace = true } +codex-otel = { workspace = true } +codex-protocol = { workspace = true } +http = { workspace = true } +schemars = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["fs", "sync", "time"] } +tracing = { workspace = true } + +[dev-dependencies] +base64 = { workspace = true } +chrono = { workspace = true } +http = { workspace = true } +maplit = { workspace = true } +pretty_assertions = { workspace = true } +serde_json = { workspace = true } +tempfile = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time"] } +toml = { workspace = true } +tracing-subscriber = { workspace = true } +wiremock = { workspace = true } diff --git a/codex-rs/core/models.json b/codex-rs/models/models.json similarity index 100% rename from codex-rs/core/models.json rename to codex-rs/models/models.json diff --git a/codex-rs/models/src/api_bridge.rs b/codex-rs/models/src/api_bridge.rs new file mode 100644 index 0000000000..31e4560f5f --- /dev/null +++ b/codex-rs/models/src/api_bridge.rs @@ -0,0 +1,84 @@ +use codex_api::AuthProvider as ApiAuthProvider; +use codex_api::TransportError; +use codex_api::error::ApiError; +use codex_login::CodexAuth; + +use crate::error::ModelsError; +use crate::error::Result; +use crate::model_provider_info::ModelProviderInfo; + +pub(crate) fn map_api_error(err: ApiError) -> ModelsError { + match err { + ApiError::ContextWindowExceeded => ModelsError::Api("context window exceeded".to_string()), + ApiError::QuotaExceeded => ModelsError::Api("quota exceeded".to_string()), + ApiError::UsageNotIncluded => ModelsError::Api("usage not included".to_string()), + ApiError::Retryable { message, .. } => ModelsError::Api(message), + ApiError::Stream(message) => ModelsError::Api(message), + ApiError::ServerOverloaded => ModelsError::Api("server overloaded".to_string()), + ApiError::Api { status, .. } => ModelsError::Api(format!("api error {}", status.as_u16())), + ApiError::InvalidRequest { message } => ModelsError::Api(message), + ApiError::Transport(transport) => ModelsError::Api(match transport { + TransportError::Http { status, .. } => format!("http {}", status.as_u16()), + TransportError::RetryLimit => "retry limit reached".to_string(), + TransportError::Timeout => "timeout".to_string(), + TransportError::Network(message) | TransportError::Build(message) => message, + }), + ApiError::RateLimit(message) => ModelsError::Api(message), + } +} + +pub(crate) fn auth_provider_from_auth( + auth: Option, + provider: &ModelProviderInfo, +) -> Result { + if let Some(api_key) = provider.api_key()? { + return Ok(ModelsAuthProvider { + token: Some(api_key), + account_id: None, + }); + } + + if let Some(token) = provider.experimental_bearer_token.clone() { + return Ok(ModelsAuthProvider { + token: Some(token), + account_id: None, + }); + } + + if let Some(auth) = auth { + return Ok(ModelsAuthProvider { + token: Some(auth.get_token()?), + account_id: auth.get_account_id(), + }); + } + + Ok(ModelsAuthProvider::default()) +} + +#[derive(Clone, Default)] +pub(crate) struct ModelsAuthProvider { + token: Option, + account_id: Option, +} + +impl ModelsAuthProvider { + pub(crate) fn auth_header_attached(&self) -> bool { + self.token + .as_ref() + .is_some_and(|token| http::HeaderValue::from_str(&format!("Bearer {token}")).is_ok()) + } + + pub(crate) fn auth_header_name(&self) -> Option<&'static str> { + self.auth_header_attached().then_some("authorization") + } +} + +impl ApiAuthProvider for ModelsAuthProvider { + fn bearer_token(&self) -> Option { + self.token.clone() + } + + fn account_id(&self) -> Option { + self.account_id.clone() + } +} diff --git a/codex-rs/models/src/auth_env_telemetry.rs b/codex-rs/models/src/auth_env_telemetry.rs new file mode 100644 index 0000000000..c2e9de7f5f --- /dev/null +++ b/codex-rs/models/src/auth_env_telemetry.rs @@ -0,0 +1,74 @@ +use codex_login::CODEX_API_KEY_ENV_VAR; +use codex_login::OPENAI_API_KEY_ENV_VAR; +use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR; + +use crate::model_provider_info::ModelProviderInfo; + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub(crate) struct AuthEnvTelemetry { + pub(crate) openai_api_key_env_present: bool, + pub(crate) codex_api_key_env_present: bool, + pub(crate) codex_api_key_env_enabled: bool, + pub(crate) provider_env_key_name: Option, + pub(crate) provider_env_key_present: Option, + pub(crate) refresh_token_url_override_present: bool, +} + +pub(crate) fn collect_auth_env_telemetry( + provider: &ModelProviderInfo, + codex_api_key_env_enabled: bool, +) -> AuthEnvTelemetry { + AuthEnvTelemetry { + openai_api_key_env_present: env_var_present(OPENAI_API_KEY_ENV_VAR), + codex_api_key_env_present: env_var_present(CODEX_API_KEY_ENV_VAR), + codex_api_key_env_enabled, + provider_env_key_name: provider.env_key.as_ref().map(|_| "configured".to_string()), + provider_env_key_present: provider.env_key.as_deref().map(env_var_present), + refresh_token_url_override_present: env_var_present(REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR), + } +} + +fn env_var_present(name: &str) -> bool { + match std::env::var(name) { + Ok(value) => !value.trim().is_empty(), + Err(std::env::VarError::NotUnicode(_)) => true, + Err(std::env::VarError::NotPresent) => false, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ModelProviderInfo; + use crate::WireApi; + use pretty_assertions::assert_eq; + + #[test] + fn collect_auth_env_telemetry_buckets_provider_env_key_name() { + let provider = ModelProviderInfo { + name: "Custom".to_string(), + base_url: None, + env_key: Some("sk-should-not-leak".to_string()), + env_key_instructions: None, + experimental_bearer_token: None, + wire_api: WireApi::Responses, + query_params: None, + http_headers: None, + env_http_headers: None, + request_max_retries: None, + stream_max_retries: None, + stream_idle_timeout_ms: None, + websocket_connect_timeout_ms: None, + requires_openai_auth: false, + supports_websockets: false, + }; + + let telemetry = collect_auth_env_telemetry(&provider, false); + + assert_eq!( + telemetry.provider_env_key_name, + Some("configured".to_string()) + ); + assert_eq!(telemetry.provider_env_key_present, Some(false)); + } +} diff --git a/codex-rs/models/src/error.rs b/codex-rs/models/src/error.rs new file mode 100644 index 0000000000..ad1b588134 --- /dev/null +++ b/codex-rs/models/src/error.rs @@ -0,0 +1,17 @@ +use thiserror::Error; + +use crate::model_provider_info::EnvKeyError; + +pub(crate) type Result = std::result::Result; + +#[derive(Debug, Error)] +pub(crate) enum ModelsError { + #[error(transparent)] + EnvVar(#[from] EnvKeyError), + #[error("failed to read auth token: {0}")] + Auth(#[from] std::io::Error), + #[error("timed out while refreshing remote models")] + Timeout, + #[error("failed to refresh remote models: {0}")] + Api(String), +} diff --git a/codex-rs/models/src/feedback_tags.rs b/codex-rs/models/src/feedback_tags.rs new file mode 100644 index 0000000000..2a1f41f83a --- /dev/null +++ b/codex-rs/models/src/feedback_tags.rs @@ -0,0 +1,106 @@ +use crate::auth_env_telemetry::AuthEnvTelemetry; + +pub(crate) struct FeedbackRequestTags<'a> { + pub(crate) endpoint: &'a str, + pub(crate) auth_header_attached: bool, + pub(crate) auth_header_name: Option<&'a str>, + pub(crate) auth_mode: Option<&'a str>, + pub(crate) auth_retry_after_unauthorized: Option, + pub(crate) auth_recovery_mode: Option<&'a str>, + pub(crate) auth_recovery_phase: Option<&'a str>, + pub(crate) auth_connection_reused: Option, + pub(crate) auth_request_id: Option<&'a str>, + pub(crate) auth_cf_ray: Option<&'a str>, + pub(crate) auth_error: Option<&'a str>, + pub(crate) auth_error_code: Option<&'a str>, + pub(crate) auth_recovery_followup_success: Option, + pub(crate) auth_recovery_followup_status: Option, +} + +struct FeedbackRequestSnapshot<'a> { + endpoint: &'a str, + auth_header_attached: bool, + auth_header_name: &'a str, + auth_mode: &'a str, + auth_retry_after_unauthorized: String, + auth_recovery_mode: &'a str, + auth_recovery_phase: &'a str, + auth_connection_reused: String, + auth_request_id: &'a str, + auth_cf_ray: &'a str, + auth_error: &'a str, + auth_error_code: &'a str, + auth_recovery_followup_success: String, + auth_recovery_followup_status: String, +} + +impl<'a> FeedbackRequestSnapshot<'a> { + fn from_tags(tags: &'a FeedbackRequestTags<'a>) -> Self { + Self { + endpoint: tags.endpoint, + auth_header_attached: tags.auth_header_attached, + auth_header_name: tags.auth_header_name.unwrap_or(""), + auth_mode: tags.auth_mode.unwrap_or(""), + auth_retry_after_unauthorized: tags + .auth_retry_after_unauthorized + .map_or_else(String::new, |value| value.to_string()), + auth_recovery_mode: tags.auth_recovery_mode.unwrap_or(""), + auth_recovery_phase: tags.auth_recovery_phase.unwrap_or(""), + auth_connection_reused: tags + .auth_connection_reused + .map_or_else(String::new, |value| value.to_string()), + auth_request_id: tags.auth_request_id.unwrap_or(""), + auth_cf_ray: tags.auth_cf_ray.unwrap_or(""), + auth_error: tags.auth_error.unwrap_or(""), + auth_error_code: tags.auth_error_code.unwrap_or(""), + auth_recovery_followup_success: tags + .auth_recovery_followup_success + .map_or_else(String::new, |value| value.to_string()), + auth_recovery_followup_status: tags + .auth_recovery_followup_status + .map_or_else(String::new, |value| value.to_string()), + } + } +} + +pub(crate) fn emit_feedback_request_tags_with_auth_env( + tags: &FeedbackRequestTags<'_>, + auth_env: &AuthEnvTelemetry, +) { + let snapshot = FeedbackRequestSnapshot::from_tags(tags); + tracing::info!( + target: "feedback_tags", + endpoint = tracing::field::debug(snapshot.endpoint), + auth_header_attached = tracing::field::debug(snapshot.auth_header_attached), + auth_header_name = tracing::field::debug(snapshot.auth_header_name), + auth_mode = tracing::field::debug(snapshot.auth_mode), + auth_retry_after_unauthorized = + tracing::field::debug(snapshot.auth_retry_after_unauthorized), + auth_recovery_mode = tracing::field::debug(snapshot.auth_recovery_mode), + auth_recovery_phase = tracing::field::debug(snapshot.auth_recovery_phase), + auth_connection_reused = tracing::field::debug(snapshot.auth_connection_reused), + auth_request_id = tracing::field::debug(snapshot.auth_request_id), + auth_cf_ray = tracing::field::debug(snapshot.auth_cf_ray), + auth_error = tracing::field::debug(snapshot.auth_error), + auth_error_code = tracing::field::debug(snapshot.auth_error_code), + auth_recovery_followup_success = + tracing::field::debug(snapshot.auth_recovery_followup_success), + auth_recovery_followup_status = + tracing::field::debug(snapshot.auth_recovery_followup_status), + auth_env_openai_api_key_present = + tracing::field::debug(auth_env.openai_api_key_env_present), + auth_env_codex_api_key_present = + tracing::field::debug(auth_env.codex_api_key_env_present), + auth_env_codex_api_key_enabled = + tracing::field::debug(auth_env.codex_api_key_env_enabled), + auth_env_provider_key_name = + tracing::field::debug(auth_env.provider_env_key_name.as_deref().unwrap_or("")), + auth_env_provider_key_present = tracing::field::debug( + &auth_env + .provider_env_key_present + .map_or_else(String::new, |value| value.to_string()) + ), + auth_env_refresh_token_url_override_present = + tracing::field::debug(auth_env.refresh_token_url_override_present), + ); +} diff --git a/codex-rs/models/src/lib.rs b/codex-rs/models/src/lib.rs new file mode 100644 index 0000000000..3acc1d76c6 --- /dev/null +++ b/codex-rs/models/src/lib.rs @@ -0,0 +1,47 @@ +mod api_bridge; +mod auth_env_telemetry; +mod error; +mod feedback_tags; +mod response_debug_context; + +#[path = "models_manager/cache.rs"] +pub mod cache; +#[path = "models_manager/collaboration_mode_presets.rs"] +pub mod collaboration_mode_presets; +#[path = "models_manager/manager.rs"] +pub mod manager; +#[path = "models_manager/model_info.rs"] +pub mod model_info; +#[path = "models_manager/model_presets.rs"] +pub mod model_presets; +pub mod model_provider_info; + +pub use collaboration_mode_presets::CollaborationModesConfig; +pub use collaboration_mode_presets::builtin_collaboration_mode_presets; +pub use manager::ModelsManager; +pub use manager::RefreshStrategy; +pub use model_info::ModelInfoConfigOverrides; +pub use model_info::model_info_from_slug; +pub use model_info::with_config_overrides; +pub use model_provider_info::DEFAULT_LMSTUDIO_PORT; +pub use model_provider_info::DEFAULT_OLLAMA_PORT; +pub use model_provider_info::EnvKeyError; +pub use model_provider_info::LEGACY_OLLAMA_CHAT_PROVIDER_ID; +pub use model_provider_info::LMSTUDIO_OSS_PROVIDER_ID; +pub use model_provider_info::ModelProviderInfo; +pub use model_provider_info::OLLAMA_CHAT_PROVIDER_REMOVED_ERROR; +pub use model_provider_info::OLLAMA_OSS_PROVIDER_ID; +pub use model_provider_info::OPENAI_PROVIDER_ID; +pub use model_provider_info::WireApi; +pub use model_provider_info::built_in_model_providers; +pub use model_provider_info::create_oss_provider_with_base_url; + +/// Convert the client version string to a whole version string (e.g. "1.2.3-alpha.4" -> "1.2.3"). +pub fn client_version_to_whole() -> String { + format!( + "{}.{}.{}", + env!("CARGO_PKG_VERSION_MAJOR"), + env!("CARGO_PKG_VERSION_MINOR"), + env!("CARGO_PKG_VERSION_PATCH") + ) +} diff --git a/codex-rs/core/src/model_provider_info.rs b/codex-rs/models/src/model_provider_info.rs similarity index 92% rename from codex-rs/core/src/model_provider_info.rs rename to codex-rs/models/src/model_provider_info.rs index 737a47780d..e8de74b2c6 100644 --- a/codex-rs/core/src/model_provider_info.rs +++ b/codex-rs/models/src/model_provider_info.rs @@ -5,10 +5,9 @@ //! 2. User-defined entries inside `~/.codex/config.toml` under the `model_providers` //! key. These override or extend the defaults at runtime. -use crate::auth::AuthMode; -use crate::error::EnvVarError; use codex_api::Provider as ApiProvider; use codex_api::provider::RetryConfig as ApiRetryConfig; +use codex_login::AuthMode; use http::HeaderMap; use http::header::HeaderName; use http::header::HeaderValue; @@ -18,6 +17,7 @@ use serde::Serialize; use std::collections::HashMap; use std::fmt; use std::time::Duration; +use thiserror::Error; const DEFAULT_STREAM_IDLE_TIMEOUT_MS: u64 = 300_000; const DEFAULT_STREAM_MAX_RETRIES: u64 = 5; @@ -31,8 +31,15 @@ const MAX_REQUEST_MAX_RETRIES: u64 = 100; const OPENAI_PROVIDER_NAME: &str = "OpenAI"; pub const OPENAI_PROVIDER_ID: &str = "openai"; const CHAT_WIRE_API_REMOVED_ERROR: &str = "`wire_api = \"chat\"` is no longer supported.\nHow to fix: set `wire_api = \"responses\"` in your provider config.\nMore info: https://github.com/openai/codex/discussions/7782"; -pub(crate) const LEGACY_OLLAMA_CHAT_PROVIDER_ID: &str = "ollama-chat"; -pub(crate) const OLLAMA_CHAT_PROVIDER_REMOVED_ERROR: &str = "`ollama-chat` is no longer supported.\nHow to fix: replace `ollama-chat` with `ollama` in `model_provider`, `oss_provider`, or `--local-provider`.\nMore info: https://github.com/openai/codex/discussions/7782"; +pub const LEGACY_OLLAMA_CHAT_PROVIDER_ID: &str = "ollama-chat"; +pub const OLLAMA_CHAT_PROVIDER_REMOVED_ERROR: &str = "`ollama-chat` is no longer supported.\nHow to fix: replace `ollama-chat` with `ollama` in `model_provider`, `oss_provider`, or `--local-provider`.\nMore info: https://github.com/openai/codex/discussions/7782"; + +#[derive(Debug, Clone, Error, PartialEq, Eq)] +#[error("environment variable {var} is required but missing or empty")] +pub struct EnvKeyError { + pub var: String, + pub instructions: Option, +} /// Wire protocol that the provider speaks. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, JsonSchema)] @@ -130,7 +137,7 @@ pub struct ModelProviderInfo { } impl ModelProviderInfo { - fn build_header_map(&self) -> crate::error::Result { + fn build_header_map(&self) -> HeaderMap { let capacity = self.http_headers.as_ref().map_or(0, HashMap::len) + self.env_http_headers.as_ref().map_or(0, HashMap::len); let mut headers = HeaderMap::with_capacity(capacity); @@ -154,13 +161,10 @@ impl ModelProviderInfo { } } - Ok(headers) + headers } - pub(crate) fn to_api_provider( - &self, - auth_mode: Option, - ) -> crate::error::Result { + pub fn to_api_provider(&self, auth_mode: Option) -> ApiProvider { let default_base_url = if matches!(auth_mode, Some(AuthMode::Chatgpt)) { "https://chatgpt.com/backend-api/codex" } else { @@ -171,7 +175,7 @@ impl ModelProviderInfo { .clone() .unwrap_or_else(|| default_base_url.to_string()); - let headers = self.build_header_map()?; + let headers = self.build_header_map(); let retry = ApiRetryConfig { max_attempts: self.request_max_retries(), base_delay: Duration::from_millis(200), @@ -180,30 +184,28 @@ impl ModelProviderInfo { retry_transport: true, }; - Ok(ApiProvider { + ApiProvider { name: self.name.clone(), base_url, query_params: self.query_params.clone(), headers, retry, stream_idle_timeout: self.stream_idle_timeout(), - }) + } } /// If `env_key` is Some, returns the API key for this provider if present /// (and non-empty) in the environment. If `env_key` is required but /// cannot be found, returns an error. - pub fn api_key(&self) -> crate::error::Result> { + pub fn api_key(&self) -> Result, EnvKeyError> { match &self.env_key { Some(env_key) => { let api_key = std::env::var(env_key) .ok() .filter(|v| !v.trim().is_empty()) - .ok_or_else(|| { - crate::error::CodexErr::EnvVar(EnvVarError { - var: env_key.clone(), - instructions: self.env_key_instructions.clone(), - }) + .ok_or_else(|| EnvKeyError { + var: env_key.clone(), + instructions: self.env_key_instructions.clone(), })?; Ok(Some(api_key)) } diff --git a/codex-rs/core/src/model_provider_info_tests.rs b/codex-rs/models/src/model_provider_info_tests.rs similarity index 100% rename from codex-rs/core/src/model_provider_info_tests.rs rename to codex-rs/models/src/model_provider_info_tests.rs diff --git a/codex-rs/core/src/models_manager/cache.rs b/codex-rs/models/src/models_manager/cache.rs similarity index 100% rename from codex-rs/core/src/models_manager/cache.rs rename to codex-rs/models/src/models_manager/cache.rs diff --git a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs b/codex-rs/models/src/models_manager/collaboration_mode_presets.rs similarity index 98% rename from codex-rs/core/src/models_manager/collaboration_mode_presets.rs rename to codex-rs/models/src/models_manager/collaboration_mode_presets.rs index dceab9f3bd..6718e086ef 100644 --- a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs +++ b/codex-rs/models/src/models_manager/collaboration_mode_presets.rs @@ -21,7 +21,7 @@ pub struct CollaborationModesConfig { pub default_mode_request_user_input: bool, } -pub(crate) fn builtin_collaboration_mode_presets( +pub fn builtin_collaboration_mode_presets( collaboration_modes_config: CollaborationModesConfig, ) -> Vec { vec![plan_preset(), default_preset(collaboration_modes_config)] diff --git a/codex-rs/core/src/models_manager/collaboration_mode_presets_tests.rs b/codex-rs/models/src/models_manager/collaboration_mode_presets_tests.rs similarity index 100% rename from codex-rs/core/src/models_manager/collaboration_mode_presets_tests.rs rename to codex-rs/models/src/models_manager/collaboration_mode_presets_tests.rs diff --git a/codex-rs/core/src/models_manager/manager.rs b/codex-rs/models/src/models_manager/manager.rs similarity index 93% rename from codex-rs/core/src/models_manager/manager.rs rename to codex-rs/models/src/models_manager/manager.rs index 29a1a85767..e5bcb48207 100644 --- a/codex-rs/core/src/models_manager/manager.rs +++ b/codex-rs/models/src/models_manager/manager.rs @@ -1,27 +1,27 @@ use super::cache::ModelsCacheManager; use crate::api_bridge::auth_provider_from_auth; use crate::api_bridge::map_api_error; -use crate::auth::AuthManager; -use crate::auth::AuthMode; -use crate::auth::CodexAuth; use crate::auth_env_telemetry::AuthEnvTelemetry; use crate::auth_env_telemetry::collect_auth_env_telemetry; -use crate::config::Config; -use crate::default_client::build_reqwest_client; -use crate::error::CodexErr; -use crate::error::Result as CoreResult; +use crate::collaboration_mode_presets::CollaborationModesConfig; +use crate::collaboration_mode_presets::builtin_collaboration_mode_presets; +use crate::error::ModelsError; +use crate::error::Result as ModelsResult; +use crate::feedback_tags::FeedbackRequestTags; +use crate::feedback_tags::emit_feedback_request_tags_with_auth_env; +use crate::model_info; +use crate::model_info::ModelInfoConfigOverrides; use crate::model_provider_info::ModelProviderInfo; -use crate::models_manager::collaboration_mode_presets::CollaborationModesConfig; -use crate::models_manager::collaboration_mode_presets::builtin_collaboration_mode_presets; -use crate::models_manager::model_info; use crate::response_debug_context::extract_response_debug_context; use crate::response_debug_context::telemetry_transport_error_message; -use crate::util::FeedbackRequestTags; -use crate::util::emit_feedback_request_tags_with_auth_env; use codex_api::ModelsClient; use codex_api::RequestTelemetry; use codex_api::ReqwestTransport; use codex_api::TransportError; +use codex_login::AuthManager; +use codex_login::AuthMode; +use codex_login::CodexAuth; +use codex_login::default_client::build_reqwest_client; use codex_otel::TelemetryAuthMode; use codex_protocol::config_types::CollaborationModeMask; use codex_protocol::openai_models::ModelInfo; @@ -310,10 +310,14 @@ impl ModelsManager { // todo(aibrahim): look if we can tighten it to pub(crate) /// Look up model metadata, applying remote overrides and config adjustments. - #[instrument(level = "info", skip(self, config), fields(model = model))] - pub async fn get_model_info(&self, model: &str, config: &Config) -> ModelInfo { + #[instrument(level = "info", skip(self, overrides), fields(model = model))] + pub async fn get_model_info( + &self, + model: &str, + overrides: &ModelInfoConfigOverrides, + ) -> ModelInfo { let remote_models = self.get_remote_models().await; - Self::construct_model_info_from_candidates(model, &remote_models, config) + Self::construct_model_info_from_candidates(model, &remote_models, overrides) } fn find_model_by_longest_prefix(model: &str, candidates: &[ModelInfo]) -> Option { @@ -355,7 +359,7 @@ impl ModelsManager { fn construct_model_info_from_candidates( model: &str, candidates: &[ModelInfo], - config: &Config, + overrides: &ModelInfoConfigOverrides, ) -> ModelInfo { // First use the normal longest-prefix match. If that misses, allow a narrowly scoped // retry for namespaced slugs like `custom/gpt-5.3-codex`. @@ -370,13 +374,13 @@ impl ModelsManager { } else { model_info::model_info_from_slug(model) }; - model_info::with_config_overrides(model_info, config) + model_info::with_config_overrides(model_info, overrides) } /// Refresh models if the provided ETag differs from the cached ETag. /// /// Uses `Online` strategy to fetch latest models when ETags differ. - pub(crate) async fn refresh_if_new_etag(&self, etag: String) { + pub async fn refresh_if_new_etag(&self, etag: String) { let current_etag = self.get_etag().await; if current_etag.clone().is_some() && current_etag.as_deref() == Some(etag.as_str()) { if let Err(err) = self.cache_manager.renew_cache_ttl().await { @@ -390,7 +394,10 @@ impl ModelsManager { } /// Refresh available models according to the specified strategy. - async fn refresh_available_models(&self, refresh_strategy: RefreshStrategy) -> CoreResult<()> { + async fn refresh_available_models( + &self, + refresh_strategy: RefreshStrategy, + ) -> ModelsResult<()> { // don't override the custom model catalog if one was provided by the user if matches!(self.catalog_mode, CatalogMode::Custom) { return Ok(()); @@ -428,12 +435,12 @@ impl ModelsManager { } } - async fn fetch_and_update_models(&self) -> CoreResult<()> { + async fn fetch_and_update_models(&self) -> ModelsResult<()> { let _timer = codex_otel::start_global_timer("codex.remote_models.fetch_update.duration_ms", &[]); let auth = self.auth_manager.auth().await; let auth_mode = auth.as_ref().map(CodexAuth::auth_mode); - let api_provider = self.provider.to_api_provider(auth_mode)?; + let api_provider = self.provider.to_api_provider(auth_mode); let api_auth = auth_provider_from_auth(auth.clone(), &self.provider)?; let auth_env = collect_auth_env_telemetry( &self.provider, @@ -449,13 +456,13 @@ impl ModelsManager { let client = ModelsClient::new(transport, api_provider, api_auth) .with_telemetry(Some(request_telemetry)); - let client_version = crate::models_manager::client_version_to_whole(); + let client_version = crate::client_version_to_whole(); let (models, etag) = timeout( MODELS_REFRESH_TIMEOUT, client.list_models(&client_version, HeaderMap::new()), ) .await - .map_err(|_| CodexErr::Timeout)? + .map_err(|_| ModelsError::Timeout)? .map_err(map_api_error)?; self.apply_remote_models(models.clone()).await; @@ -496,7 +503,7 @@ impl ModelsManager { async fn try_load_cache(&self) -> bool { let _timer = codex_otel::start_global_timer("codex.remote_models.load_cache.duration_ms", &[]); - let client_version = crate::models_manager::client_version_to_whole(); + let client_version = crate::client_version_to_whole(); info!(client_version, "models cache: evaluating cache eligibility"); let cache = match self.cache_manager.load_fresh(&client_version).await { Some(cache) => cache, @@ -538,7 +545,7 @@ impl ModelsManager { } /// Construct a manager with a specific provider for testing. - pub(crate) fn with_provider_for_tests( + pub fn with_provider_for_tests( codex_home: PathBuf, auth_manager: Arc, provider: ModelProviderInfo, @@ -553,7 +560,7 @@ impl ModelsManager { } /// Get model identifier without consulting remote state or cache. - pub(crate) fn get_model_offline_for_tests(model: Option<&str>) -> String { + pub fn get_model_offline_for_tests(model: Option<&str>) -> String { if let Some(model) = model { return model.to_string(); } @@ -569,16 +576,17 @@ impl ModelsManager { } /// Build `ModelInfo` without consulting remote state or cache. - pub(crate) fn construct_model_info_offline_for_tests( + pub fn construct_model_info_offline_for_tests( model: &str, - config: &Config, + model_catalog: Option<&ModelsResponse>, + overrides: &ModelInfoConfigOverrides, ) -> ModelInfo { - let candidates: &[ModelInfo] = if let Some(model_catalog) = config.model_catalog.as_ref() { + let candidates: &[ModelInfo] = if let Some(model_catalog) = model_catalog { &model_catalog.models } else { &[] }; - Self::construct_model_info_from_candidates(model, candidates, config) + Self::construct_model_info_from_candidates(model, candidates, overrides) } } diff --git a/codex-rs/core/src/models_manager/manager_tests.rs b/codex-rs/models/src/models_manager/manager_tests.rs similarity index 92% rename from codex-rs/core/src/models_manager/manager_tests.rs rename to codex-rs/models/src/models_manager/manager_tests.rs index 07cf4dc39d..cfa68c8872 100644 --- a/codex-rs/core/src/models_manager/manager_tests.rs +++ b/codex-rs/models/src/models_manager/manager_tests.rs @@ -1,13 +1,11 @@ use super::*; -use crate::CodexAuth; -use crate::auth::AuthCredentialsStoreMode; -use crate::config::ConfigBuilder; -use crate::model_provider_info::WireApi; use base64::Engine as _; use chrono::Utc; use codex_api::TransportError; +use codex_login::AuthCredentialsStoreMode; +use codex_login::AuthManager; +use codex_login::CodexAuth; use codex_protocol::openai_models::ModelsResponse; -use core_test_support::responses::mount_models_once; use http::HeaderMap; use http::StatusCode; use pretty_assertions::assert_eq; @@ -24,7 +22,14 @@ use tracing_subscriber::layer::Context; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::registry::LookupSpan; use tracing_subscriber::util::SubscriberInitExt; +use wiremock::Match; +use wiremock::Mock; use wiremock::MockServer; +use wiremock::ResponseTemplate; +use wiremock::matchers::method; +use wiremock::matchers::path_regex; + +use crate::WireApi; fn remote_model(slug: &str, display: &str, priority: i32) -> ModelInfo { remote_model_with_visibility(slug, display, priority, "list") @@ -92,6 +97,46 @@ fn provider_for(base_url: String) -> ModelProviderInfo { } } +#[derive(Debug, Clone)] +struct ModelsMock { + requests: Arc>>, +} + +impl ModelsMock { + fn new() -> Self { + Self { + requests: Arc::new(Mutex::new(Vec::new())), + } + } + + fn requests(&self) -> Vec { + self.requests.lock().unwrap().clone() + } +} + +impl Match for ModelsMock { + fn matches(&self, request: &wiremock::Request) -> bool { + self.requests.lock().unwrap().push(request.clone()); + true + } +} + +async fn mount_models_once(server: &MockServer, body: ModelsResponse) -> ModelsMock { + let models_mock = ModelsMock::new(); + Mock::given(method("GET")) + .and(path_regex(".*/models$")) + .and(models_mock.clone()) + .respond_with( + ResponseTemplate::new(200) + .insert_header("content-type", "application/json") + .set_body_json(body), + ) + .up_to_n_times(1) + .mount(server) + .await; + models_mock +} + #[derive(Default)] struct TagCollectorVisitor { tags: BTreeMap, @@ -136,11 +181,7 @@ where #[tokio::test] async fn get_model_info_tracks_fallback_usage() { let codex_home = tempdir().expect("temp dir"); - let config = ConfigBuilder::default() - .codex_home(codex_home.path().to_path_buf()) - .build() - .await - .expect("load default test config"); + let overrides = ModelInfoConfigOverrides::default(); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); let manager = ModelsManager::new( codex_home.path().to_path_buf(), @@ -156,12 +197,14 @@ async fn get_model_info_tracks_fallback_usage() { .slug .clone(); - let known = manager.get_model_info(known_slug.as_str(), &config).await; + let known = manager + .get_model_info(known_slug.as_str(), &overrides) + .await; assert!(!known.used_fallback_model_metadata); assert_eq!(known.slug, known_slug); let unknown = manager - .get_model_info("model-that-does-not-exist", &config) + .get_model_info("model-that-does-not-exist", &overrides) .await; assert!(unknown.used_fallback_model_metadata); assert_eq!(unknown.slug, "model-that-does-not-exist"); @@ -170,11 +213,7 @@ async fn get_model_info_tracks_fallback_usage() { #[tokio::test] async fn get_model_info_uses_custom_catalog() { let codex_home = tempdir().expect("temp dir"); - let config = ConfigBuilder::default() - .codex_home(codex_home.path().to_path_buf()) - .build() - .await - .expect("load default test config"); + let overrides = ModelInfoConfigOverrides::default(); let mut overlay = remote_model("gpt-overlay", "Overlay", 0); overlay.supports_image_detail_original = true; @@ -189,7 +228,7 @@ async fn get_model_info_uses_custom_catalog() { ); let model_info = manager - .get_model_info("gpt-overlay-experiment", &config) + .get_model_info("gpt-overlay-experiment", &overrides) .await; assert_eq!(model_info.slug, "gpt-overlay-experiment"); @@ -203,11 +242,7 @@ async fn get_model_info_uses_custom_catalog() { #[tokio::test] async fn get_model_info_matches_namespaced_suffix() { let codex_home = tempdir().expect("temp dir"); - let config = ConfigBuilder::default() - .codex_home(codex_home.path().to_path_buf()) - .build() - .await - .expect("load default test config"); + let overrides = ModelInfoConfigOverrides::default(); let mut remote = remote_model("gpt-image", "Image", 0); remote.supports_image_detail_original = true; let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); @@ -221,7 +256,7 @@ async fn get_model_info_matches_namespaced_suffix() { ); let namespaced_model = "custom/gpt-image".to_string(); - let model_info = manager.get_model_info(&namespaced_model, &config).await; + let model_info = manager.get_model_info(&namespaced_model, &overrides).await; assert_eq!(model_info.slug, namespaced_model); assert!(model_info.supports_image_detail_original); @@ -231,11 +266,7 @@ async fn get_model_info_matches_namespaced_suffix() { #[tokio::test] async fn get_model_info_rejects_multi_segment_namespace_suffix_matching() { let codex_home = tempdir().expect("temp dir"); - let config = ConfigBuilder::default() - .codex_home(codex_home.path().to_path_buf()) - .build() - .await - .expect("load default test config"); + let overrides = ModelInfoConfigOverrides::default(); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); let manager = ModelsManager::new( codex_home.path().to_path_buf(), @@ -252,7 +283,7 @@ async fn get_model_info_rejects_multi_segment_namespace_suffix_matching() { .clone(); let namespaced_model = format!("ns1/ns2/{known_slug}"); - let model_info = manager.get_model_info(&namespaced_model, &config).await; + let model_info = manager.get_model_info(&namespaced_model, &overrides).await; assert_eq!(model_info.slug, namespaced_model); assert!(model_info.used_fallback_model_metadata); @@ -444,7 +475,7 @@ async fn refresh_available_models_refetches_when_version_mismatch() { manager .cache_manager .mutate_cache_for_test(|cache| { - let client_version = crate::models_manager::client_version_to_whole(); + let client_version = crate::client_version_to_whole(); cache.client_version = Some(format!("{client_version}-mismatch")); }) .await diff --git a/codex-rs/core/src/models_manager/model_info.rs b/codex-rs/models/src/models_manager/model_info.rs similarity index 76% rename from codex-rs/core/src/models_manager/model_info.rs rename to codex-rs/models/src/models_manager/model_info.rs index 7d3c6e9d10..6b19b2005e 100644 --- a/codex-rs/core/src/models_manager/model_info.rs +++ b/codex-rs/models/src/models_manager/model_info.rs @@ -1,4 +1,5 @@ use codex_protocol::config_types::ReasoningSummary; +use codex_protocol::models::BASE_INSTRUCTIONS_DEFAULT; use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelInstructionsVariables; @@ -8,32 +9,41 @@ use codex_protocol::openai_models::TruncationMode; use codex_protocol::openai_models::TruncationPolicyConfig; use codex_protocol::openai_models::WebSearchToolType; use codex_protocol::openai_models::default_input_modalities; - -use crate::config::Config; -use crate::truncate::approx_bytes_for_tokens; -use codex_features::Feature; use tracing::warn; -pub const BASE_INSTRUCTIONS: &str = include_str!("../../prompt.md"); +const APPROX_BYTES_PER_TOKEN: usize = 4; const DEFAULT_PERSONALITY_HEADER: &str = "You are Codex, a coding agent based on GPT-5. You and the user share the same workspace and collaborate to achieve the user's goals."; const LOCAL_FRIENDLY_TEMPLATE: &str = "You optimize for team morale and being a supportive teammate as much as code quality."; const LOCAL_PRAGMATIC_TEMPLATE: &str = "You are a deeply pragmatic, effective software engineer."; const PERSONALITY_PLACEHOLDER: &str = "{{ personality }}"; -pub(crate) fn with_config_overrides(mut model: ModelInfo, config: &Config) -> ModelInfo { - if let Some(supports_reasoning_summaries) = config.model_supports_reasoning_summaries +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct ModelInfoConfigOverrides { + pub model_supports_reasoning_summaries: Option, + pub model_context_window: Option, + pub model_auto_compact_token_limit: Option, + pub tool_output_token_limit: Option, + pub base_instructions: Option, + pub personality_enabled: bool, +} + +pub fn with_config_overrides( + mut model: ModelInfo, + overrides: &ModelInfoConfigOverrides, +) -> ModelInfo { + if let Some(supports_reasoning_summaries) = overrides.model_supports_reasoning_summaries && supports_reasoning_summaries { model.supports_reasoning_summaries = true; } - if let Some(context_window) = config.model_context_window { + if let Some(context_window) = overrides.model_context_window { model.context_window = Some(context_window); } - if let Some(auto_compact_token_limit) = config.model_auto_compact_token_limit { + if let Some(auto_compact_token_limit) = overrides.model_auto_compact_token_limit { model.auto_compact_token_limit = Some(auto_compact_token_limit); } - if let Some(token_limit) = config.tool_output_token_limit { + if let Some(token_limit) = overrides.tool_output_token_limit { model.truncation_policy = match model.truncation_policy.mode { TruncationMode::Bytes => { let byte_limit = @@ -47,10 +57,10 @@ pub(crate) fn with_config_overrides(mut model: ModelInfo, config: &Config) -> Mo }; } - if let Some(base_instructions) = &config.base_instructions { + if let Some(base_instructions) = &overrides.base_instructions { model.base_instructions = base_instructions.clone(); model.model_messages = None; - } else if !config.features.enabled(Feature::Personality) { + } else if !overrides.personality_enabled { model.model_messages = None; } @@ -58,7 +68,7 @@ pub(crate) fn with_config_overrides(mut model: ModelInfo, config: &Config) -> Mo } /// Build a minimal fallback model descriptor for missing/unknown slugs. -pub(crate) fn model_info_from_slug(slug: &str) -> ModelInfo { +pub fn model_info_from_slug(slug: &str) -> ModelInfo { warn!("Unknown model {slug} is used. This will use fallback model metadata."); ModelInfo { slug: slug.to_string(), @@ -72,7 +82,7 @@ pub(crate) fn model_info_from_slug(slug: &str) -> ModelInfo { priority: 99, availability_nux: None, upgrade: None, - base_instructions: BASE_INSTRUCTIONS.to_string(), + base_instructions: BASE_INSTRUCTIONS_DEFAULT.to_string(), model_messages: local_personality_messages_for_slug(slug), supports_reasoning_summaries: false, default_reasoning_summary: ReasoningSummary::Auto, @@ -97,7 +107,7 @@ fn local_personality_messages_for_slug(slug: &str) -> Option { match slug { "gpt-5.2-codex" | "exp-codex-personality" => Some(ModelMessages { instructions_template: Some(format!( - "{DEFAULT_PERSONALITY_HEADER}\n\n{PERSONALITY_PLACEHOLDER}\n\n{BASE_INSTRUCTIONS}" + "{DEFAULT_PERSONALITY_HEADER}\n\n{PERSONALITY_PLACEHOLDER}\n\n{BASE_INSTRUCTIONS_DEFAULT}" )), instructions_variables: Some(ModelInstructionsVariables { personality_default: Some(String::new()), @@ -109,6 +119,10 @@ fn local_personality_messages_for_slug(slug: &str) -> Option { } } +fn approx_bytes_for_tokens(tokens: usize) -> usize { + tokens.saturating_mul(APPROX_BYTES_PER_TOKEN) +} + #[cfg(test)] #[path = "model_info_tests.rs"] mod tests; diff --git a/codex-rs/core/src/models_manager/model_info_tests.rs b/codex-rs/models/src/models_manager/model_info_tests.rs similarity index 52% rename from codex-rs/core/src/models_manager/model_info_tests.rs rename to codex-rs/models/src/models_manager/model_info_tests.rs index 27bac8d70b..5aa91ba1b0 100644 --- a/codex-rs/core/src/models_manager/model_info_tests.rs +++ b/codex-rs/models/src/models_manager/model_info_tests.rs @@ -1,14 +1,15 @@ use super::*; -use crate::config::test_config; use pretty_assertions::assert_eq; #[test] fn reasoning_summaries_override_true_enables_support() { let model = model_info_from_slug("unknown-model"); - let mut config = test_config(); - config.model_supports_reasoning_summaries = Some(true); + let overrides = ModelInfoConfigOverrides { + model_supports_reasoning_summaries: Some(true), + ..Default::default() + }; - let updated = with_config_overrides(model.clone(), &config); + let updated = with_config_overrides(model.clone(), &overrides); let mut expected = model; expected.supports_reasoning_summaries = true; @@ -19,10 +20,12 @@ fn reasoning_summaries_override_true_enables_support() { fn reasoning_summaries_override_false_does_not_disable_support() { let mut model = model_info_from_slug("unknown-model"); model.supports_reasoning_summaries = true; - let mut config = test_config(); - config.model_supports_reasoning_summaries = Some(false); + let overrides = ModelInfoConfigOverrides { + model_supports_reasoning_summaries: Some(false), + ..Default::default() + }; - let updated = with_config_overrides(model.clone(), &config); + let updated = with_config_overrides(model.clone(), &overrides); assert_eq!(updated, model); } @@ -30,10 +33,12 @@ fn reasoning_summaries_override_false_does_not_disable_support() { #[test] fn reasoning_summaries_override_false_is_noop_when_model_is_false() { let model = model_info_from_slug("unknown-model"); - let mut config = test_config(); - config.model_supports_reasoning_summaries = Some(false); + let overrides = ModelInfoConfigOverrides { + model_supports_reasoning_summaries: Some(false), + ..Default::default() + }; - let updated = with_config_overrides(model.clone(), &config); + let updated = with_config_overrides(model.clone(), &overrides); assert_eq!(updated, model); } diff --git a/codex-rs/core/src/models_manager/model_presets.rs b/codex-rs/models/src/models_manager/model_presets.rs similarity index 100% rename from codex-rs/core/src/models_manager/model_presets.rs rename to codex-rs/models/src/models_manager/model_presets.rs diff --git a/codex-rs/models/src/response_debug_context.rs b/codex-rs/models/src/response_debug_context.rs new file mode 100644 index 0000000000..df6ffecf18 --- /dev/null +++ b/codex-rs/models/src/response_debug_context.rs @@ -0,0 +1,135 @@ +use base64::Engine; +use codex_api::TransportError; + +const REQUEST_ID_HEADER: &str = "x-request-id"; +const OAI_REQUEST_ID_HEADER: &str = "x-oai-request-id"; +const CF_RAY_HEADER: &str = "cf-ray"; +const AUTH_ERROR_HEADER: &str = "x-openai-authorization-error"; +const X_ERROR_JSON_HEADER: &str = "x-error-json"; + +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub(crate) struct ResponseDebugContext { + pub(crate) request_id: Option, + pub(crate) cf_ray: Option, + pub(crate) auth_error: Option, + pub(crate) auth_error_code: Option, +} + +pub(crate) fn extract_response_debug_context(transport: &TransportError) -> ResponseDebugContext { + let mut context = ResponseDebugContext::default(); + + let TransportError::Http { + headers, body: _, .. + } = transport + else { + return context; + }; + + let extract_header = |name: &str| { + headers + .as_ref() + .and_then(|headers| headers.get(name)) + .and_then(|value| value.to_str().ok()) + .map(str::to_string) + }; + + context.request_id = + extract_header(REQUEST_ID_HEADER).or_else(|| extract_header(OAI_REQUEST_ID_HEADER)); + context.cf_ray = extract_header(CF_RAY_HEADER); + context.auth_error = extract_header(AUTH_ERROR_HEADER); + context.auth_error_code = extract_header(X_ERROR_JSON_HEADER).and_then(|encoded| { + let decoded = base64::engine::general_purpose::STANDARD + .decode(encoded) + .ok()?; + let parsed = serde_json::from_slice::(&decoded).ok()?; + parsed + .get("error") + .and_then(|error| error.get("code")) + .and_then(serde_json::Value::as_str) + .map(str::to_string) + }); + + context +} + +pub(crate) fn telemetry_transport_error_message(error: &TransportError) -> String { + match error { + TransportError::Http { status, .. } => format!("http {}", status.as_u16()), + TransportError::RetryLimit => "retry limit reached".to_string(), + TransportError::Timeout => "timeout".to_string(), + TransportError::Network(err) => err.to_string(), + TransportError::Build(err) => err.to_string(), + } +} + +#[cfg(test)] +mod tests { + use codex_api::TransportError; + use http::HeaderMap; + use http::HeaderValue; + use http::StatusCode; + use pretty_assertions::assert_eq; + + use super::ResponseDebugContext; + use super::extract_response_debug_context; + use super::telemetry_transport_error_message; + + #[test] + fn extract_response_debug_context_decodes_identity_headers() { + let mut headers = HeaderMap::new(); + headers.insert("x-oai-request-id", HeaderValue::from_static("req-auth")); + headers.insert("cf-ray", HeaderValue::from_static("ray-auth")); + headers.insert( + "x-openai-authorization-error", + HeaderValue::from_static("missing_authorization_header"), + ); + headers.insert( + "x-error-json", + HeaderValue::from_static("eyJlcnJvciI6eyJjb2RlIjoidG9rZW5fZXhwaXJlZCJ9fQ=="), + ); + + let context = extract_response_debug_context(&TransportError::Http { + status: StatusCode::UNAUTHORIZED, + url: Some("https://chatgpt.com/backend-api/codex/models".to_string()), + headers: Some(headers), + body: Some(r#"{"error":{"message":"plain text error"},"status":401}"#.to_string()), + }); + + assert_eq!( + context, + ResponseDebugContext { + request_id: Some("req-auth".to_string()), + cf_ray: Some("ray-auth".to_string()), + auth_error: Some("missing_authorization_header".to_string()), + auth_error_code: Some("token_expired".to_string()), + } + ); + } + + #[test] + fn telemetry_error_messages_omit_http_bodies() { + let transport = TransportError::Http { + status: StatusCode::UNAUTHORIZED, + url: Some("https://chatgpt.com/backend-api/codex/responses".to_string()), + headers: None, + body: Some(r#"{"error":{"message":"secret token leaked"}}"#.to_string()), + }; + + assert_eq!(telemetry_transport_error_message(&transport), "http 401"); + } + + #[test] + fn telemetry_error_messages_preserve_non_http_details() { + let network = TransportError::Network("dns lookup failed".to_string()); + let build = TransportError::Build("invalid header value".to_string()); + + assert_eq!( + telemetry_transport_error_message(&network), + "dns lookup failed" + ); + assert_eq!( + telemetry_transport_error_message(&build), + "invalid header value" + ); + } +} diff --git a/codex-rs/core/templates/collaboration_mode/default.md b/codex-rs/models/templates/collaboration_mode/default.md similarity index 100% rename from codex-rs/core/templates/collaboration_mode/default.md rename to codex-rs/models/templates/collaboration_mode/default.md diff --git a/codex-rs/core/templates/collaboration_mode/plan.md b/codex-rs/models/templates/collaboration_mode/plan.md similarity index 100% rename from codex-rs/core/templates/collaboration_mode/plan.md rename to codex-rs/models/templates/collaboration_mode/plan.md diff --git a/codex-rs/ollama/Cargo.toml b/codex-rs/ollama/Cargo.toml index 56e8d6e5d8..ca2950222b 100644 --- a/codex-rs/ollama/Cargo.toml +++ b/codex-rs/ollama/Cargo.toml @@ -15,6 +15,7 @@ workspace = true async-stream = { workspace = true } bytes = { workspace = true } codex-core = { workspace = true } +codex-models = { workspace = true } futures = { workspace = true } reqwest = { workspace = true, features = ["json", "stream"] } semver = { workspace = true } diff --git a/codex-rs/ollama/src/client.rs b/codex-rs/ollama/src/client.rs index a0ab2d04fb..2cb0d026ae 100644 --- a/codex-rs/ollama/src/client.rs +++ b/codex-rs/ollama/src/client.rs @@ -11,9 +11,11 @@ use crate::pull::PullEvent; use crate::pull::PullProgressReporter; use crate::url::base_url_to_host_root; use crate::url::is_openai_compatible_base_url; -use codex_core::ModelProviderInfo; -use codex_core::OLLAMA_OSS_PROVIDER_ID; use codex_core::config::Config; +use codex_models::ModelProviderInfo; +use codex_models::OLLAMA_OSS_PROVIDER_ID; +use codex_models::WireApi; +use codex_models::create_oss_provider_with_base_url; const OLLAMA_CONNECTION_ERROR: &str = "No running Ollama server detected. Start it with: `ollama serve` (after installing). Install instructions: https://github.com/ollama/ollama?tab=readme-ov-file#ollama"; @@ -47,8 +49,7 @@ impl OllamaClient { #[cfg(test)] async fn try_from_provider_with_base_url(base_url: &str) -> io::Result { - let provider = - codex_core::create_oss_provider_with_base_url(base_url, codex_core::WireApi::Responses); + let provider = create_oss_provider_with_base_url(base_url, WireApi::Responses); Self::try_from_provider(&provider).await } diff --git a/codex-rs/ollama/src/lib.rs b/codex-rs/ollama/src/lib.rs index 02f3754580..9cde2ce0f5 100644 --- a/codex-rs/ollama/src/lib.rs +++ b/codex-rs/ollama/src/lib.rs @@ -4,8 +4,8 @@ mod pull; mod url; pub use client::OllamaClient; -use codex_core::ModelProviderInfo; use codex_core::config::Config; +use codex_models::ModelProviderInfo; pub use pull::CliProgressReporter; pub use pull::PullEvent; pub use pull::PullProgressReporter; diff --git a/codex-rs/tui/BUILD.bazel b/codex-rs/tui/BUILD.bazel index 58fbc31fb5..0b77a68dce 100644 --- a/codex-rs/tui/BUILD.bazel +++ b/codex-rs/tui/BUILD.bazel @@ -12,7 +12,7 @@ codex_rust_crate( ], allow_empty = True, ), - test_data_extra = glob(["src/**/snapshots/**"]) + ["//codex-rs/core:model_availability_nux_fixtures"], + test_data_extra = glob(["src/**/snapshots/**"]) + ["//codex-rs/models:model_availability_nux_fixtures"], integration_compile_data_extra = ["src/test_backend.rs"], extra_binaries = [ "//codex-rs/cli:codex", diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index 8013b1325e..2ad6b54423 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -41,6 +41,7 @@ codex-features = { workspace = true } codex-feedback = { workspace = true } codex-file-search = { workspace = true } codex-login = { workspace = true } +codex-models = { workspace = true } codex-otel = { workspace = true } codex-protocol = { workspace = true } codex-shell-command = { workspace = true } diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 4aa51df21c..9bedca157a 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -64,13 +64,13 @@ use codex_core::config::types::ModelAvailabilityNuxConfig; use codex_core::config_loader::CloudRequirementsLoader; use codex_core::config_loader::ConfigLayerStackOrdering; use codex_core::config_loader::LoaderOverrides; -use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; -use codex_core::models_manager::manager::RefreshStrategy; -use codex_core::models_manager::model_presets::HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG; -use codex_core::models_manager::model_presets::HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG; #[cfg(target_os = "windows")] use codex_core::windows_sandbox::WindowsSandboxLevelExt; use codex_features::Feature; +use codex_models::CollaborationModesConfig; +use codex_models::RefreshStrategy; +use codex_models::model_presets::HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG; +use codex_models::model_presets::HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG; use codex_otel::SessionTelemetry; use codex_otel::TelemetryAuthMode; use codex_protocol::ThreadId; diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index e2480c4ec0..7195c2b82f 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -72,7 +72,6 @@ use codex_core::git_info::current_branch_name; use codex_core::git_info::get_git_repo_root; use codex_core::git_info::local_git_branches; use codex_core::mcp::McpManager; -use codex_core::models_manager::manager::ModelsManager; use codex_core::plugins::PluginsManager; use codex_core::project_doc::DEFAULT_PROJECT_DOC_FILENAME; use codex_core::skills::model::SkillMetadata; @@ -80,6 +79,7 @@ use codex_core::skills::model::SkillMetadata; use codex_core::windows_sandbox::WindowsSandboxLevelExt; use codex_features::FEATURES; use codex_features::Feature; +use codex_models::ModelsManager; use codex_otel::RuntimeMetricsSummary; use codex_otel::SessionTelemetry; use codex_protocol::ThreadId; diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index a614d1361d..c1d7972d9c 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -33,11 +33,11 @@ use codex_core::config_loader::ConfigLayerStack; use codex_core::config_loader::ConfigRequirements; use codex_core::config_loader::ConfigRequirementsToml; use codex_core::config_loader::RequirementSource; -use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; -use codex_core::models_manager::manager::ModelsManager; use codex_core::skills::model::SkillMetadata; use codex_features::FEATURES; use codex_features::Feature; +use codex_models::CollaborationModesConfig; +use codex_models::ModelsManager; use codex_otel::RuntimeMetricsSummary; use codex_otel::SessionTelemetry; use codex_protocol::ThreadId; diff --git a/codex-rs/tui/src/collaboration_modes.rs b/codex-rs/tui/src/collaboration_modes.rs index 1e5676ccde..27c436d73c 100644 --- a/codex-rs/tui/src/collaboration_modes.rs +++ b/codex-rs/tui/src/collaboration_modes.rs @@ -1,4 +1,4 @@ -use codex_core::models_manager::manager::ModelsManager; +use codex_models::ModelsManager; use codex_protocol::config_types::CollaborationModeMask; use codex_protocol::config_types::ModeKind; diff --git a/codex-rs/tui/src/oss_selection.rs b/codex-rs/tui/src/oss_selection.rs index c73b4e332a..dcff772b70 100644 --- a/codex-rs/tui/src/oss_selection.rs +++ b/codex-rs/tui/src/oss_selection.rs @@ -1,11 +1,11 @@ use std::io; use std::sync::LazyLock; -use codex_core::DEFAULT_LMSTUDIO_PORT; -use codex_core::DEFAULT_OLLAMA_PORT; -use codex_core::LMSTUDIO_OSS_PROVIDER_ID; -use codex_core::OLLAMA_OSS_PROVIDER_ID; use codex_core::config::set_default_oss_provider; +use codex_models::DEFAULT_LMSTUDIO_PORT; +use codex_models::DEFAULT_OLLAMA_PORT; +use codex_models::LMSTUDIO_OSS_PROVIDER_ID; +use codex_models::OLLAMA_OSS_PROVIDER_ID; use crossterm::event::Event; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; diff --git a/codex-rs/tui/src/status/card.rs b/codex-rs/tui/src/status/card.rs index a7546230bf..b979e6a2e0 100644 --- a/codex-rs/tui/src/status/card.rs +++ b/codex-rs/tui/src/status/card.rs @@ -5,8 +5,8 @@ use crate::history_cell::with_border_with_inner_width; use crate::version::CODEX_CLI_VERSION; use chrono::DateTime; use chrono::Local; -use codex_core::WireApi; use codex_core::config::Config; +use codex_models::WireApi; use codex_protocol::ThreadId; use codex_protocol::account::PlanType; use codex_protocol::openai_models::ReasoningEffort; diff --git a/codex-rs/tui/tests/suite/model_availability_nux.rs b/codex-rs/tui/tests/suite/model_availability_nux.rs index 512db97974..1f7d5467c3 100644 --- a/codex-rs/tui/tests/suite/model_availability_nux.rs +++ b/codex-rs/tui/tests/suite/model_availability_nux.rs @@ -19,7 +19,7 @@ async fn resume_startup_does_not_consume_model_availability_nux_count() -> Resul let repo_root = codex_utils_cargo_bin::repo_root()?; let codex_home = tempdir()?; - let source_catalog_path = codex_utils_cargo_bin::find_resource!("../core/models.json")?; + let source_catalog_path = codex_utils_cargo_bin::find_resource!("../models/models.json")?; let source_catalog = std::fs::read_to_string(&source_catalog_path)?; let mut source_catalog: JsonValue = serde_json::from_str(&source_catalog)?; let models = source_catalog diff --git a/codex-rs/tui_app_server/BUILD.bazel b/codex-rs/tui_app_server/BUILD.bazel index 5093e4dd1f..dc2df22788 100644 --- a/codex-rs/tui_app_server/BUILD.bazel +++ b/codex-rs/tui_app_server/BUILD.bazel @@ -11,11 +11,8 @@ codex_rust_crate( "Cargo.toml", ], allow_empty = True, - ) + [ - "//codex-rs/core:templates/collaboration_mode/default.md", - "//codex-rs/core:templates/collaboration_mode/plan.md", - ], - test_data_extra = glob(["src/**/snapshots/**"]) + ["//codex-rs/core:model_availability_nux_fixtures"], + ), + test_data_extra = glob(["src/**/snapshots/**"]) + ["//codex-rs/models:model_availability_nux_fixtures"], integration_compile_data_extra = ["src/test_backend.rs"], extra_binaries = [ "//codex-rs/cli:codex", diff --git a/codex-rs/tui_app_server/Cargo.toml b/codex-rs/tui_app_server/Cargo.toml index 8866042051..2f8929af2a 100644 --- a/codex-rs/tui_app_server/Cargo.toml +++ b/codex-rs/tui_app_server/Cargo.toml @@ -45,6 +45,7 @@ codex-features = { workspace = true } codex-feedback = { workspace = true } codex-file-search = { workspace = true } codex-login = { workspace = true } +codex-models = { workspace = true } codex-otel = { workspace = true } codex-protocol = { workspace = true } codex-shell-command = { workspace = true } diff --git a/codex-rs/tui_app_server/src/app.rs b/codex-rs/tui_app_server/src/app.rs index 171df69270..2d2fba51af 100644 --- a/codex-rs/tui_app_server/src/app.rs +++ b/codex-rs/tui_app_server/src/app.rs @@ -73,12 +73,12 @@ use codex_core::config::types::ApprovalsReviewer; use codex_core::config::types::ModelAvailabilityNuxConfig; use codex_core::config_loader::ConfigLayerStackOrdering; use codex_core::message_history; -use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; -use codex_core::models_manager::model_presets::HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG; -use codex_core::models_manager::model_presets::HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG; #[cfg(target_os = "windows")] use codex_core::windows_sandbox::WindowsSandboxLevelExt; use codex_features::Feature; +use codex_models::CollaborationModesConfig; +use codex_models::model_presets::HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG; +use codex_models::model_presets::HIDE_GPT5_1_MIGRATION_PROMPT_CONFIG; use codex_otel::SessionTelemetry; use codex_protocol::ThreadId; use codex_protocol::approvals::ExecApprovalRequestEvent; diff --git a/codex-rs/tui_app_server/src/chatwidget/tests.rs b/codex-rs/tui_app_server/src/chatwidget/tests.rs index 639b57da09..86b72b6afe 100644 --- a/codex-rs/tui_app_server/src/chatwidget/tests.rs +++ b/codex-rs/tui_app_server/src/chatwidget/tests.rs @@ -57,10 +57,10 @@ use codex_core::config_loader::ConfigLayerStack; use codex_core::config_loader::ConfigRequirements; use codex_core::config_loader::ConfigRequirementsToml; use codex_core::config_loader::RequirementSource; -use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_core::skills::model::SkillMetadata; use codex_features::FEATURES; use codex_features::Feature; +use codex_models::CollaborationModesConfig; use codex_otel::RuntimeMetricsSummary; use codex_otel::SessionTelemetry; use codex_protocol::ThreadId; diff --git a/codex-rs/tui_app_server/src/model_catalog.rs b/codex-rs/tui_app_server/src/model_catalog.rs index 569085bcaa..6c577e1c8c 100644 --- a/codex-rs/tui_app_server/src/model_catalog.rs +++ b/codex-rs/tui_app_server/src/model_catalog.rs @@ -1,19 +1,9 @@ -use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; +use codex_models::CollaborationModesConfig; +use codex_models::builtin_collaboration_mode_presets; use codex_protocol::config_types::CollaborationModeMask; -use codex_protocol::config_types::ModeKind; -use codex_protocol::config_types::TUI_VISIBLE_COLLABORATION_MODES; use codex_protocol::openai_models::ModelPreset; -use codex_protocol::openai_models::ReasoningEffort; use std::convert::Infallible; -const COLLABORATION_MODE_PLAN: &str = - include_str!("../../core/templates/collaboration_mode/plan.md"); -const COLLABORATION_MODE_DEFAULT: &str = - include_str!("../../core/templates/collaboration_mode/default.md"); -const KNOWN_MODE_NAMES_PLACEHOLDER: &str = "{{KNOWN_MODE_NAMES}}"; -const REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER: &str = "{{REQUEST_USER_INPUT_AVAILABILITY}}"; -const ASKING_QUESTIONS_GUIDANCE_PLACEHOLDER: &str = "{{ASKING_QUESTIONS_GUIDANCE}}"; - #[derive(Debug, Clone)] pub(crate) struct ModelCatalog { models: Vec, @@ -39,84 +29,3 @@ impl ModelCatalog { builtin_collaboration_mode_presets(self.collaboration_modes_config) } } - -fn builtin_collaboration_mode_presets( - collaboration_modes_config: CollaborationModesConfig, -) -> Vec { - vec![plan_preset(), default_preset(collaboration_modes_config)] -} - -fn plan_preset() -> CollaborationModeMask { - CollaborationModeMask { - name: ModeKind::Plan.display_name().to_string(), - mode: Some(ModeKind::Plan), - model: None, - reasoning_effort: Some(Some(ReasoningEffort::Medium)), - developer_instructions: Some(Some(COLLABORATION_MODE_PLAN.to_string())), - } -} - -fn default_preset(collaboration_modes_config: CollaborationModesConfig) -> CollaborationModeMask { - CollaborationModeMask { - name: ModeKind::Default.display_name().to_string(), - mode: Some(ModeKind::Default), - model: None, - reasoning_effort: None, - developer_instructions: Some(Some(default_mode_instructions(collaboration_modes_config))), - } -} - -fn default_mode_instructions(collaboration_modes_config: CollaborationModesConfig) -> String { - let known_mode_names = format_mode_names(&TUI_VISIBLE_COLLABORATION_MODES); - let request_user_input_availability = request_user_input_availability_message( - ModeKind::Default, - collaboration_modes_config.default_mode_request_user_input, - ); - let asking_questions_guidance = asking_questions_guidance_message( - collaboration_modes_config.default_mode_request_user_input, - ); - COLLABORATION_MODE_DEFAULT - .replace(KNOWN_MODE_NAMES_PLACEHOLDER, &known_mode_names) - .replace( - REQUEST_USER_INPUT_AVAILABILITY_PLACEHOLDER, - &request_user_input_availability, - ) - .replace( - ASKING_QUESTIONS_GUIDANCE_PLACEHOLDER, - &asking_questions_guidance, - ) -} - -fn format_mode_names(modes: &[ModeKind]) -> String { - let mode_names: Vec<&str> = modes.iter().map(|mode| mode.display_name()).collect(); - match mode_names.as_slice() { - [] => "none".to_string(), - [mode_name] => (*mode_name).to_string(), - [first, second] => format!("{first} and {second}"), - [..] => mode_names.join(", "), - } -} - -fn request_user_input_availability_message( - mode: ModeKind, - default_mode_request_user_input: bool, -) -> String { - let mode_name = mode.display_name(); - if mode.allows_request_user_input() - || (default_mode_request_user_input && mode == ModeKind::Default) - { - format!("The `request_user_input` tool is available in {mode_name} mode.") - } else { - format!( - "The `request_user_input` tool is unavailable in {mode_name} mode. If you call it while in {mode_name} mode, it will return an error." - ) - } -} - -fn asking_questions_guidance_message(default_mode_request_user_input: bool) -> String { - if default_mode_request_user_input { - "In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, prefer using the `request_user_input` tool rather than writing a multiple choice question as a textual assistant message. Never write a multiple choice question as a textual assistant message.".to_string() - } else { - "In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, ask the user directly with a concise plain-text question. Never write a multiple choice question as a textual assistant message.".to_string() - } -} diff --git a/codex-rs/tui_app_server/src/oss_selection.rs b/codex-rs/tui_app_server/src/oss_selection.rs index c73b4e332a..dcff772b70 100644 --- a/codex-rs/tui_app_server/src/oss_selection.rs +++ b/codex-rs/tui_app_server/src/oss_selection.rs @@ -1,11 +1,11 @@ use std::io; use std::sync::LazyLock; -use codex_core::DEFAULT_LMSTUDIO_PORT; -use codex_core::DEFAULT_OLLAMA_PORT; -use codex_core::LMSTUDIO_OSS_PROVIDER_ID; -use codex_core::OLLAMA_OSS_PROVIDER_ID; use codex_core::config::set_default_oss_provider; +use codex_models::DEFAULT_LMSTUDIO_PORT; +use codex_models::DEFAULT_OLLAMA_PORT; +use codex_models::LMSTUDIO_OSS_PROVIDER_ID; +use codex_models::OLLAMA_OSS_PROVIDER_ID; use crossterm::event::Event; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; diff --git a/codex-rs/tui_app_server/src/status/card.rs b/codex-rs/tui_app_server/src/status/card.rs index 4b5a6474bd..3842d3b53d 100644 --- a/codex-rs/tui_app_server/src/status/card.rs +++ b/codex-rs/tui_app_server/src/status/card.rs @@ -5,8 +5,8 @@ use crate::history_cell::with_border_with_inner_width; use crate::version::CODEX_CLI_VERSION; use chrono::DateTime; use chrono::Local; -use codex_core::WireApi; use codex_core::config::Config; +use codex_models::WireApi; use codex_protocol::ThreadId; use codex_protocol::account::PlanType; use codex_protocol::openai_models::ReasoningEffort; diff --git a/codex-rs/tui_app_server/tests/suite/model_availability_nux.rs b/codex-rs/tui_app_server/tests/suite/model_availability_nux.rs index 512db97974..1f7d5467c3 100644 --- a/codex-rs/tui_app_server/tests/suite/model_availability_nux.rs +++ b/codex-rs/tui_app_server/tests/suite/model_availability_nux.rs @@ -19,7 +19,7 @@ async fn resume_startup_does_not_consume_model_availability_nux_count() -> Resul let repo_root = codex_utils_cargo_bin::repo_root()?; let codex_home = tempdir()?; - let source_catalog_path = codex_utils_cargo_bin::find_resource!("../core/models.json")?; + let source_catalog_path = codex_utils_cargo_bin::find_resource!("../models/models.json")?; let source_catalog = std::fs::read_to_string(&source_catalog_path)?; let mut source_catalog: JsonValue = serde_json::from_str(&source_catalog)?; let models = source_catalog diff --git a/codex-rs/utils/oss/Cargo.toml b/codex-rs/utils/oss/Cargo.toml index 93623615e6..4781e68adc 100644 --- a/codex-rs/utils/oss/Cargo.toml +++ b/codex-rs/utils/oss/Cargo.toml @@ -10,4 +10,5 @@ workspace = true [dependencies] codex-core = { workspace = true } codex-lmstudio = { workspace = true } +codex-models = { workspace = true } codex-ollama = { workspace = true } diff --git a/codex-rs/utils/oss/src/lib.rs b/codex-rs/utils/oss/src/lib.rs index a44a6a7d32..9d12977d3e 100644 --- a/codex-rs/utils/oss/src/lib.rs +++ b/codex-rs/utils/oss/src/lib.rs @@ -1,8 +1,8 @@ //! OSS provider utilities shared between TUI and exec. -use codex_core::LMSTUDIO_OSS_PROVIDER_ID; -use codex_core::OLLAMA_OSS_PROVIDER_ID; use codex_core::config::Config; +use codex_models::LMSTUDIO_OSS_PROVIDER_ID; +use codex_models::OLLAMA_OSS_PROVIDER_ID; /// Returns the default model for a given OSS provider. pub fn get_default_model_for_oss_provider(provider_id: &str) -> Option<&'static str> { diff --git a/codex-rs/utils/sandbox-summary/Cargo.toml b/codex-rs/utils/sandbox-summary/Cargo.toml index 202dfaf22b..895c347058 100644 --- a/codex-rs/utils/sandbox-summary/Cargo.toml +++ b/codex-rs/utils/sandbox-summary/Cargo.toml @@ -9,6 +9,7 @@ workspace = true [dependencies] codex-core = { workspace = true } +codex-models = { workspace = true } codex-protocol = { workspace = true } [dev-dependencies] diff --git a/codex-rs/utils/sandbox-summary/src/config_summary.rs b/codex-rs/utils/sandbox-summary/src/config_summary.rs index 0f6b192979..b008c86624 100644 --- a/codex-rs/utils/sandbox-summary/src/config_summary.rs +++ b/codex-rs/utils/sandbox-summary/src/config_summary.rs @@ -1,5 +1,5 @@ -use codex_core::WireApi; use codex_core::config::Config; +use codex_models::WireApi; use crate::sandbox_summary::summarize_sandbox_policy;