From 6fff9955f192f6bc47a8458475239f6d14ccc9aa Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Thu, 2 Apr 2026 23:00:02 -0700 Subject: [PATCH] extract models manager and related ownership from core (#16508) ## Summary - split `models-manager` out of `core` and add `ModelsManagerConfig` plus `Config::to_models_manager_config()` so model metadata paths stop depending on `core::Config` - move login-owned/auth-owned code out of `core` into `codex-login`, move model provider config into `codex-model-provider-info`, move API bridge mapping into `codex-api`, move protocol-owned types/impls into `codex-protocol`, and move response debug helpers into a dedicated `response-debug-context` crate - move feedback tag emission into `codex-feedback`, relocate tests to the crates that now own the code, and keep broad temporary re-exports so this PR avoids a giant import-only rewrite ## Major moves and decisions - created `codex-models-manager` as the owner for model cache/catalog/config/model info logic, including the new `ModelsManagerConfig` struct - created `codex-model-provider-info` as the owner for provider config parsing/defaults and kept temporary `codex-login`/`codex-core` re-exports for old import paths - moved `api_bridge` error mapping + `CoreAuthProvider` into `codex-api`, while `codex-login::api_bridge` temporarily re-exports those symbols and keeps the `auth_provider_from_auth` wrapper - moved `auth_env_telemetry` and `provider_auth` ownership to `codex-login` - moved `CodexErr` ownership to `codex-protocol::error`, plus `StreamOutput`, `bytes_to_string_smart`, and network policy helpers to protocol-owned modules - created `codex-response-debug-context` for `extract_response_debug_context`, `telemetry_transport_error_message`, and related response-debug plumbing instead of leaving that behavior in `core` - moved `FeedbackRequestTags`, `emit_feedback_request_tags`, and `emit_feedback_request_tags_with_auth_env` to `codex-feedback` - deferred removal of temporary re-exports and the mechanical import rewrites to a stacked follow-up PR so this PR stays reviewable ## Test moves - moved auth refresh coverage from `core/tests/suite/auth_refresh.rs` to `login/tests/suite/auth_refresh.rs` - moved text encoding coverage from `core/tests/suite/text_encoding_fix.rs` to `protocol/src/exec_output_tests.rs` - moved model info override coverage from `core/tests/suite/model_info_overrides.rs` to `models-manager/src/model_info_overrides_tests.rs` --------- Co-authored-by: Codex --- codex-rs/Cargo.lock | 86 ++++- codex-rs/Cargo.toml | 8 + .../app-server/src/codex_message_processor.rs | 2 +- codex-rs/codex-api/Cargo.toml | 2 + .../{core => codex-api}/src/api_bridge.rs | 70 +--- .../src/api_bridge_tests.rs | 0 codex-rs/codex-api/src/lib.rs | 1 + .../collaboration-mode-templates/BUILD.bazel | 12 + .../collaboration-mode-templates/Cargo.toml | 13 + .../collaboration-mode-templates/src/lib.rs | 4 + .../templates}/default.md | 0 .../templates}/execute.md | 0 .../templates}/pair_programming.md | 0 .../templates}/plan.md | 0 codex-rs/core/BUILD.bazel | 11 - codex-rs/core/Cargo.toml | 9 +- codex-rs/core/src/agent/agent_resolver.rs | 2 +- codex-rs/core/src/client.rs | 10 +- codex-rs/core/src/client_tests.rs | 6 +- codex-rs/core/src/codex.rs | 12 +- codex-rs/core/src/codex_tests.rs | 48 ++- codex-rs/core/src/config/config_tests.rs | 4 +- codex-rs/core/src/config/mod.rs | 13 + codex-rs/core/src/exec.rs | 69 +--- codex-rs/core/src/lib.rs | 45 ++- codex-rs/core/src/memories/phase1.rs | 4 +- codex-rs/core/src/models_manager/mod.rs | 15 - codex-rs/core/src/network_policy_decision.rs | 21 +- .../core/src/network_policy_decision_tests.rs | 1 + codex-rs/core/src/sandboxing/mod.rs | 4 +- codex-rs/core/src/test_support.rs | 10 +- codex-rs/core/src/text_encoding_tests.rs | 340 ------------------ .../src/tools/handlers/multi_agents_common.rs | 2 +- .../tools/runtimes/shell/unix_escalation.rs | 7 +- codex-rs/core/src/tools/spec_tests.rs | 28 +- codex-rs/core/src/user_shell_command.rs | 2 +- codex-rs/core/src/util.rs | 120 +------ codex-rs/core/src/util_tests.rs | 5 +- codex-rs/core/tests/all.rs | 2 + codex-rs/core/tests/common/Cargo.toml | 1 + codex-rs/core/tests/common/test_codex.rs | 13 +- codex-rs/core/tests/suite/client.rs | 11 +- codex-rs/core/tests/suite/compact.rs | 4 +- codex-rs/core/tests/suite/exec.rs | 3 +- codex-rs/core/tests/suite/mod.rs | 3 - codex-rs/core/tests/suite/model_switching.rs | 4 +- codex-rs/core/tests/suite/prompt_caching.rs | 2 +- codex-rs/core/tests/suite/remote_models.rs | 22 +- codex-rs/core/tests/suite/search_tool.rs | 5 +- codex-rs/core/tests/suite/tool_suggest.rs | 5 +- .../exec-server/src/server/transport_tests.rs | 10 +- codex-rs/feedback/Cargo.toml | 1 + codex-rs/feedback/src/lib.rs | 123 +++++++ codex-rs/login/Cargo.toml | 3 + codex-rs/login/src/api_bridge.rs | 38 ++ codex-rs/login/src/auth/auth_tests.rs | 4 +- codex-rs/login/src/auth/error.rs | 27 +- codex-rs/login/src/auth/manager.rs | 8 +- .../{core => login}/src/auth_env_telemetry.rs | 30 +- codex-rs/login/src/lib.rs | 20 ++ codex-rs/{core => login}/src/provider_auth.rs | 8 +- codex-rs/login/src/token_data.rs | 90 +---- codex-rs/login/src/token_data_tests.rs | 1 + .../tests/suite/auth_refresh.rs | 2 +- codex-rs/login/tests/suite/mod.rs | 1 + codex-rs/model-provider-info/BUILD.bazel | 6 + codex-rs/model-provider-info/Cargo.toml | 28 ++ .../src/lib.rs} | 39 +- .../src/model_provider_info_tests.rs | 0 codex-rs/models-manager/BUILD.bazel | 10 + codex-rs/models-manager/Cargo.toml | 41 +++ codex-rs/{core => models-manager}/models.json | 0 codex-rs/{core => models-manager}/prompt.md | 0 .../src}/cache.rs | 0 .../src}/collaboration_mode_presets.rs | 5 +- .../src}/collaboration_mode_presets_tests.rs | 0 codex-rs/models-manager/src/config.rs | 12 + codex-rs/models-manager/src/lib.rs | 30 ++ .../src}/manager.rs | 59 ++- .../src}/manager_tests.rs | 43 +-- .../src}/model_info.rs | 11 +- .../src/model_info_overrides_tests.rs} | 26 +- .../src}/model_info_tests.rs | 8 +- .../src}/model_presets.rs | 0 codex-rs/protocol/Cargo.toml | 13 + codex-rs/protocol/src/auth.rs | 116 ++++++ codex-rs/{core => protocol}/src/error.rs | 197 +++++----- .../{core => protocol}/src/error_tests.rs | 7 +- .../src/exec_output.rs} | 52 ++- .../src/exec_output_tests.rs} | 2 +- codex-rs/protocol/src/lib.rs | 4 + codex-rs/protocol/src/network_policy.rs | 22 ++ codex-rs/response-debug-context/BUILD.bazel | 6 + codex-rs/response-debug-context/Cargo.toml | 22 ++ .../src/lib.rs} | 20 +- codex-rs/sandboxing/src/lib.rs | 16 + codex-rs/tui/BUILD.bazel | 4 +- codex-rs/tui/Cargo.toml | 1 + .../tui/tests/suite/model_availability_nux.rs | 5 +- 99 files changed, 1095 insertions(+), 1137 deletions(-) rename codex-rs/{core => codex-api}/src/api_bridge.rs (81%) rename codex-rs/{core => codex-api}/src/api_bridge_tests.rs (100%) create mode 100644 codex-rs/collaboration-mode-templates/BUILD.bazel create mode 100644 codex-rs/collaboration-mode-templates/Cargo.toml create mode 100644 codex-rs/collaboration-mode-templates/src/lib.rs rename codex-rs/{core/templates/collaboration_mode => collaboration-mode-templates/templates}/default.md (100%) rename codex-rs/{core/templates/collaboration_mode => collaboration-mode-templates/templates}/execute.md (100%) rename codex-rs/{core/templates/collaboration_mode => collaboration-mode-templates/templates}/pair_programming.md (100%) rename codex-rs/{core/templates/collaboration_mode => collaboration-mode-templates/templates}/plan.md (100%) delete mode 100644 codex-rs/core/src/models_manager/mod.rs delete mode 100644 codex-rs/core/src/text_encoding_tests.rs create mode 100644 codex-rs/login/src/api_bridge.rs rename codex-rs/{core => login}/src/auth_env_telemetry.rs (75%) rename codex-rs/{core => login}/src/provider_auth.rs (83%) rename codex-rs/{core => login}/tests/suite/auth_refresh.rs (99%) create mode 100644 codex-rs/model-provider-info/BUILD.bazel create mode 100644 codex-rs/model-provider-info/Cargo.toml rename codex-rs/{core/src/model_provider_info.rs => model-provider-info/src/lib.rs} (93%) rename codex-rs/{core => model-provider-info}/src/model_provider_info_tests.rs (100%) create mode 100644 codex-rs/models-manager/BUILD.bazel create mode 100644 codex-rs/models-manager/Cargo.toml rename codex-rs/{core => models-manager}/models.json (100%) rename codex-rs/{core => models-manager}/prompt.md (100%) rename codex-rs/{core/src/models_manager => models-manager/src}/cache.rs (100%) rename codex-rs/{core/src/models_manager => models-manager/src}/collaboration_mode_presets.rs (96%) rename codex-rs/{core/src/models_manager => models-manager/src}/collaboration_mode_presets_tests.rs (100%) create mode 100644 codex-rs/models-manager/src/config.rs create mode 100644 codex-rs/models-manager/src/lib.rs rename codex-rs/{core/src/models_manager => models-manager/src}/manager.rs (92%) rename codex-rs/{core/src/models_manager => models-manager/src}/manager_tests.rs (95%) rename codex-rs/{core/src/models_manager => models-manager/src}/model_info.rs (93%) rename codex-rs/{core/tests/suite/model_info_overrides.rs => models-manager/src/model_info_overrides_tests.rs} (64%) rename codex-rs/{core/src/models_manager => models-manager/src}/model_info_tests.rs (84%) rename codex-rs/{core/src/models_manager => models-manager/src}/model_presets.rs (100%) create mode 100644 codex-rs/protocol/src/auth.rs rename codex-rs/{core => protocol}/src/error.rs (95%) rename codex-rs/{core => protocol}/src/error_tests.rs (99%) rename codex-rs/{core/src/text_encoding.rs => protocol/src/exec_output.rs} (80%) rename codex-rs/{core/tests/suite/text_encoding_fix.rs => protocol/src/exec_output_tests.rs} (98%) create mode 100644 codex-rs/protocol/src/network_policy.rs create mode 100644 codex-rs/response-debug-context/BUILD.bazel create mode 100644 codex-rs/response-debug-context/Cargo.toml rename codex-rs/{core/src/response_debug_context.rs => response-debug-context/src/lib.rs} (91%) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index c59b8d9c4..744d0b8db 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1367,7 +1367,9 @@ dependencies = [ "anyhow", "assert_matches", "async-trait", + "base64 0.22.1", "bytes", + "chrono", "codex-client", "codex-protocol", "codex-utils-rustls-provider", @@ -1787,6 +1789,10 @@ dependencies = [ "v8", ] +[[package]] +name = "codex-collaboration-mode-templates" +version = "0.0.0" + [[package]] name = "codex-config" version = "0.0.0" @@ -1837,7 +1843,6 @@ dependencies = [ "async-trait", "base64 0.22.1", "bm25", - "chardetng", "chrono", "clap", "codex-analytics", @@ -1853,15 +1858,18 @@ dependencies = [ "codex-exec-server", "codex-execpolicy", "codex-features", + "codex-feedback", "codex-git-utils", "codex-hooks", "codex-instructions", "codex-login", "codex-mcp", + "codex-models-manager", "codex-network-proxy", "codex-otel", "codex-plugin", "codex-protocol", + "codex-response-debug-context", "codex-rmcp-client", "codex-rollout", "codex-sandboxing", @@ -1891,7 +1899,6 @@ dependencies = [ "ctor 0.6.3", "dirs", "dunce", - "encoding_rs", "env-flags", "eventsource-stream", "futures", @@ -1900,7 +1907,6 @@ dependencies = [ "image", "indexmap 2.13.0", "insta", - "landlock", "libc", "maplit", "notify", @@ -1915,7 +1921,6 @@ dependencies = [ "reqwest", "rmcp", "schemars 0.8.22", - "seccompiler", "serde", "serde_json", "serial_test", @@ -2119,6 +2124,7 @@ name = "codex-feedback" version = "0.0.0" dependencies = [ "anyhow", + "codex-login", "codex-protocol", "pretty_assertions", "sentry", @@ -2240,10 +2246,13 @@ dependencies = [ "async-trait", "base64 0.22.1", "chrono", + "codex-api", "codex-app-server-protocol", "codex-client", "codex-config", "codex-keyring-store", + "codex-model-provider-info", + "codex-otel", "codex-protocol", "codex-terminal-detection", "codex-utils-template", @@ -2331,6 +2340,51 @@ dependencies = [ "wiremock", ] +[[package]] +name = "codex-model-provider-info" +version = "0.0.0" +dependencies = [ + "codex-api", + "codex-app-server-protocol", + "codex-protocol", + "codex-utils-absolute-path", + "http 1.4.0", + "maplit", + "pretty_assertions", + "schemars 0.8.22", + "serde", + "tempfile", + "toml 0.9.11+spec-1.1.0", +] + +[[package]] +name = "codex-models-manager" +version = "0.0.0" +dependencies = [ + "base64 0.22.1", + "chrono", + "codex-api", + "codex-collaboration-mode-templates", + "codex-feedback", + "codex-login", + "codex-otel", + "codex-protocol", + "codex-response-debug-context", + "codex-utils-absolute-path", + "codex-utils-output-truncation", + "codex-utils-template", + "core_test_support", + "http 1.4.0", + "pretty_assertions", + "serde", + "serde_json", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", + "wiremock", +] + [[package]] name = "codex-network-proxy" version = "0.0.0" @@ -2434,18 +2488,27 @@ name = "codex-protocol" version = "0.0.0" dependencies = [ "anyhow", + "chardetng", + "chrono", + "codex-async-utils", "codex-execpolicy", "codex-git-utils", + "codex-network-proxy", "codex-utils-absolute-path", "codex-utils-image", "codex-utils-string", "codex-utils-template", + "encoding_rs", + "http 1.4.0", "icu_decimal", "icu_locale_core", "icu_provider", + "landlock", "pretty_assertions", "quick-xml", + "reqwest", "schemars 0.8.22", + "seccompiler", "serde", "serde_json", "serde_with", @@ -2453,11 +2516,24 @@ dependencies = [ "strum_macros 0.28.0", "sys-locale", "tempfile", + "thiserror 2.0.18", + "tokio", "tracing", "ts-rs", "uuid", ] +[[package]] +name = "codex-response-debug-context" +version = "0.0.0" +dependencies = [ + "base64 0.22.1", + "codex-api", + "http 1.4.0", + "pretty_assertions", + "serde_json", +] + [[package]] name = "codex-responses-api-proxy" version = "0.0.0" @@ -2705,6 +2781,7 @@ dependencies = [ "codex-git-utils", "codex-login", "codex-mcp", + "codex-models-manager", "codex-otel", "codex-protocol", "codex-rollout", @@ -3210,6 +3287,7 @@ dependencies = [ "codex-exec-server", "codex-features", "codex-login", + "codex-models-manager", "codex-protocol", "codex-utils-absolute-path", "codex-utils-cargo-bin", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 2d80bbc69..16329276c 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -20,6 +20,7 @@ members = [ "cloud-tasks-client", "cloud-tasks-mock-client", "cli", + "collaboration-mode-templates", "connectors", "config", "shell-command", @@ -41,6 +42,8 @@ members = [ "login", "codex-mcp", "mcp-server", + "model-provider-info", + "models-manager", "network-proxy", "ollama", "process-hardening", @@ -48,6 +51,7 @@ members = [ "rollout", "rmcp-client", "responses-api-proxy", + "response-debug-context", "sandboxing", "stdio-to-uds", "otel", @@ -112,6 +116,7 @@ codex-backend-client = { path = "backend-client" } codex-chatgpt = { path = "chatgpt" } codex-cli = { path = "cli" } codex-client = { path = "codex-client" } +codex-collaboration-mode-templates = { path = "collaboration-mode-templates" } codex-cloud-requirements = { path = "cloud-requirements" } codex-cloud-tasks-client = { path = "cloud-tasks-client" } codex-cloud-tasks-mock-client = { path = "cloud-tasks-mock-client" } @@ -136,6 +141,8 @@ codex-lmstudio = { path = "lmstudio" } codex-login = { path = "login" } codex-mcp = { path = "codex-mcp" } codex-mcp-server = { path = "mcp-server" } +codex-model-provider-info = { path = "model-provider-info" } +codex-models-manager = { path = "models-manager" } codex-network-proxy = { path = "network-proxy" } codex-ollama = { path = "ollama" } codex-otel = { path = "otel" } @@ -143,6 +150,7 @@ codex-plugin = { path = "plugin" } codex-process-hardening = { path = "process-hardening" } codex-protocol = { path = "protocol" } codex-responses-api-proxy = { path = "responses-api-proxy" } +codex-response-debug-context = { path = "response-debug-context" } codex-rmcp-client = { path = "rmcp-client" } codex-rollout = { path = "rollout" } codex-sandboxing = { path = "sandboxing" } diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 7910c417c..932e94f28 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -202,6 +202,7 @@ use codex_core::config_loader::CloudRequirementsLoadErrorCode; use codex_core::config_loader::CloudRequirementsLoader; use codex_core::config_loader::LoaderOverrides; use codex_core::config_loader::load_config_layers_state; +use codex_core::default_client::set_default_client_residency_requirement; use codex_core::error::CodexErr; use codex_core::error::Result as CodexResult; use codex_core::exec::ExecCapturePolicy; @@ -244,7 +245,6 @@ use codex_login::ServerOptions as LoginServerOptions; use codex_login::ShutdownHandle; use codex_login::auth::login_with_chatgpt_auth_tokens; use codex_login::complete_device_code_login; -use codex_login::default_client::set_default_client_residency_requirement; use codex_login::login_with_api_key; use codex_login::request_device_code; use codex_login::run_login_server; diff --git a/codex-rs/codex-api/Cargo.toml b/codex-rs/codex-api/Cargo.toml index a3a5fa4d9..7bafd1532 100644 --- a/codex-rs/codex-api/Cargo.toml +++ b/codex-rs/codex-api/Cargo.toml @@ -6,7 +6,9 @@ license.workspace = true [dependencies] async-trait = { workspace = true } +base64 = { workspace = true } bytes = { workspace = true } +chrono = { workspace = true } codex-client = { workspace = true } codex-protocol = { workspace = true } codex-utils-rustls-provider = { workspace = true } diff --git a/codex-rs/core/src/api_bridge.rs b/codex-rs/codex-api/src/api_bridge.rs similarity index 81% rename from codex-rs/core/src/api_bridge.rs rename to codex-rs/codex-api/src/api_bridge.rs index 8f900b724..0ad2b1397 100644 --- a/codex-rs/core/src/api_bridge.rs +++ b/codex-rs/codex-api/src/api_bridge.rs @@ -1,24 +1,21 @@ +use crate::AuthProvider as ApiAuthProvider; +use crate::TransportError; +use crate::error::ApiError; +use crate::rate_limits::parse_promo_message; +use crate::rate_limits::parse_rate_limit_for_limit; use base64::Engine; use chrono::DateTime; use chrono::Utc; -use codex_api::AuthProvider as ApiAuthProvider; -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_login::token_data::PlanType; +use codex_protocol::auth::PlanType; +use codex_protocol::error::CodexErr; +use codex_protocol::error::RetryLimitReachedError; +use codex_protocol::error::UnexpectedResponseError; +use codex_protocol::error::UsageLimitReachedError; use http::HeaderMap; use serde::Deserialize; use serde_json::Value; -use crate::error::CodexErr; -use crate::error::RetryLimitReachedError; -use crate::error::UnexpectedResponseError; -use crate::error::UsageLimitReachedError; -use crate::model_provider_info::ModelProviderInfo; -use codex_login::CodexAuth; - -pub(crate) fn map_api_error(err: ApiError) -> CodexErr { +pub fn map_api_error(err: ApiError) -> CodexErr { match err { ApiError::ContextWindowExceeded => CodexErr::ContextWindowExceeded, ApiError::QuotaExceeded => CodexErr::QuotaExceeded, @@ -164,38 +161,6 @@ fn extract_x_error_json_code(headers: Option<&HeaderMap>) -> Option { .map(str::to_string) } -pub(crate) fn auth_provider_from_auth( - auth: Option, - provider: &ModelProviderInfo, -) -> crate::error::Result { - if let Some(api_key) = provider.api_key()? { - return Ok(CoreAuthProvider { - token: Some(api_key), - account_id: None, - }); - } - - if let Some(token) = provider.experimental_bearer_token.clone() { - return Ok(CoreAuthProvider { - token: Some(token), - account_id: None, - }); - } - - if let Some(auth) = auth { - let token = auth.get_token()?; - Ok(CoreAuthProvider { - token: Some(token), - account_id: auth.get_account_id(), - }) - } else { - Ok(CoreAuthProvider { - token: None, - account_id: None, - }) - } -} - #[derive(Debug, Deserialize)] struct UsageErrorResponse { error: UsageErrorBody, @@ -210,24 +175,23 @@ struct UsageErrorBody { } #[derive(Clone, Default)] -pub(crate) struct CoreAuthProvider { - token: Option, - account_id: Option, +pub struct CoreAuthProvider { + pub token: Option, + pub account_id: Option, } impl CoreAuthProvider { - pub(crate) fn auth_header_attached(&self) -> bool { + pub 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> { + pub fn auth_header_name(&self) -> Option<&'static str> { self.auth_header_attached().then_some("authorization") } - #[cfg(test)] - pub(crate) fn for_test(token: Option<&str>, account_id: Option<&str>) -> Self { + pub fn for_test(token: Option<&str>, account_id: Option<&str>) -> Self { Self { token: token.map(str::to_string), account_id: account_id.map(str::to_string), diff --git a/codex-rs/core/src/api_bridge_tests.rs b/codex-rs/codex-api/src/api_bridge_tests.rs similarity index 100% rename from codex-rs/core/src/api_bridge_tests.rs rename to codex-rs/codex-api/src/api_bridge_tests.rs diff --git a/codex-rs/codex-api/src/lib.rs b/codex-rs/codex-api/src/lib.rs index 865abf8a7..c7bb60705 100644 --- a/codex-rs/codex-api/src/lib.rs +++ b/codex-rs/codex-api/src/lib.rs @@ -1,3 +1,4 @@ +pub mod api_bridge; pub mod auth; pub mod common; pub mod endpoint; diff --git a/codex-rs/collaboration-mode-templates/BUILD.bazel b/codex-rs/collaboration-mode-templates/BUILD.bazel new file mode 100644 index 000000000..0fbc86ec8 --- /dev/null +++ b/codex-rs/collaboration-mode-templates/BUILD.bazel @@ -0,0 +1,12 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "collaboration-mode-templates", + crate_name = "codex_collaboration_mode_templates", + compile_data = glob(["templates/*.md"]), +) + +exports_files( + glob(["templates/*.md"]), + visibility = ["//visibility:public"], +) diff --git a/codex-rs/collaboration-mode-templates/Cargo.toml b/codex-rs/collaboration-mode-templates/Cargo.toml new file mode 100644 index 000000000..18c4e9982 --- /dev/null +++ b/codex-rs/collaboration-mode-templates/Cargo.toml @@ -0,0 +1,13 @@ +[package] +edition.workspace = true +license.workspace = true +name = "codex-collaboration-mode-templates" +version.workspace = true + +[lib] +doctest = false +name = "codex_collaboration_mode_templates" +path = "src/lib.rs" + +[lints] +workspace = true diff --git a/codex-rs/collaboration-mode-templates/src/lib.rs b/codex-rs/collaboration-mode-templates/src/lib.rs new file mode 100644 index 000000000..345990db0 --- /dev/null +++ b/codex-rs/collaboration-mode-templates/src/lib.rs @@ -0,0 +1,4 @@ +pub const PLAN: &str = include_str!("../templates/plan.md"); +pub const DEFAULT: &str = include_str!("../templates/default.md"); +pub const EXECUTE: &str = include_str!("../templates/execute.md"); +pub const PAIR_PROGRAMMING: &str = include_str!("../templates/pair_programming.md"); diff --git a/codex-rs/core/templates/collaboration_mode/default.md b/codex-rs/collaboration-mode-templates/templates/default.md similarity index 100% rename from codex-rs/core/templates/collaboration_mode/default.md rename to codex-rs/collaboration-mode-templates/templates/default.md diff --git a/codex-rs/core/templates/collaboration_mode/execute.md b/codex-rs/collaboration-mode-templates/templates/execute.md similarity index 100% rename from codex-rs/core/templates/collaboration_mode/execute.md rename to codex-rs/collaboration-mode-templates/templates/execute.md diff --git a/codex-rs/core/templates/collaboration_mode/pair_programming.md b/codex-rs/collaboration-mode-templates/templates/pair_programming.md similarity index 100% rename from codex-rs/core/templates/collaboration_mode/pair_programming.md rename to codex-rs/collaboration-mode-templates/templates/pair_programming.md diff --git a/codex-rs/core/templates/collaboration_mode/plan.md b/codex-rs/collaboration-mode-templates/templates/plan.md similarity index 100% rename from codex-rs/core/templates/collaboration_mode/plan.md rename to codex-rs/collaboration-mode-templates/templates/plan.md diff --git a/codex-rs/core/BUILD.bazel b/codex-rs/core/BUILD.bazel index 591eb1f28..86405cb8a 100644 --- a/codex-rs/core/BUILD.bazel +++ b/codex-rs/core/BUILD.bazel @@ -1,17 +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", ], visibility = ["//visibility:public"], @@ -38,8 +29,6 @@ codex_rust_crate( }, integration_compile_data_extra = [ "//codex-rs/apply-patch:apply_patch_tool_instructions.md", - "models.json", - "prompt.md", ], test_data_extra = [ "config.schema.json", diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index ad82b6c08..a3cb7aba6 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -23,7 +23,6 @@ async-channel = { workspace = true } async-trait = { workspace = true } base64 = { workspace = true } bm25 = { workspace = true } -chardetng = { workspace = true } chrono = { workspace = true, features = ["serde"] } clap = { workspace = true, features = ["derive"] } codex-analytics = { workspace = true } @@ -37,8 +36,10 @@ codex-config = { workspace = true } codex-core-skills = { workspace = true } codex-exec-server = { workspace = true } codex-features = { workspace = true } +codex-feedback = { workspace = true } codex-login = { workspace = true } codex-mcp = { workspace = true } +codex-models-manager = { workspace = true } codex-shell-command = { workspace = true } codex-execpolicy = { workspace = true } codex-git-utils = { workspace = true } @@ -48,6 +49,7 @@ codex-network-proxy = { workspace = true } codex-otel = { workspace = true } codex-plugin = { workspace = true } codex-protocol = { workspace = true } +codex-response-debug-context = { workspace = true } codex-rollout = { workspace = true } codex-rmcp-client = { workspace = true } codex-sandboxing = { workspace = true } @@ -71,7 +73,6 @@ codex-windows-sandbox = { package = "codex-windows-sandbox", path = "../windows- csv = { workspace = true } dirs = { workspace = true } dunce = { workspace = true } -encoding_rs = { workspace = true } env-flags = { workspace = true } eventsource-stream = { workspace = true } futures = { workspace = true } @@ -117,10 +118,6 @@ uuid = { workspace = true, features = ["serde", "v4", "v5"] } which = { workspace = true } zip = { workspace = true } -[target.'cfg(target_os = "linux")'.dependencies] -landlock = { workspace = true } -seccompiler = { workspace = true } - [target.'cfg(target_os = "macos")'.dependencies] core-foundation = "0.9" diff --git a/codex-rs/core/src/agent/agent_resolver.rs b/codex-rs/core/src/agent/agent_resolver.rs index 14f21c731..adb3f23ac 100644 --- a/codex-rs/core/src/agent/agent_resolver.rs +++ b/codex-rs/core/src/agent/agent_resolver.rs @@ -21,7 +21,7 @@ pub(crate) async fn resolve_agent_target( .resolve_agent_reference(session.conversation_id, &turn.session_source, target) .await .map_err(|err| match err { - crate::error::CodexErr::UnsupportedOperation(message) => { + codex_protocol::error::CodexErr::UnsupportedOperation(message) => { FunctionCallError::RespondToModel(message) } other => FunctionCallError::RespondToModel(other.to_string()), diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 407c4f2e9..80aed5112 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -30,11 +30,6 @@ use std::sync::OnceLock; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; -use crate::api_bridge::CoreAuthProvider; -use crate::api_bridge::auth_provider_from_auth; -use crate::api_bridge::map_api_error; -use crate::auth_env_telemetry::AuthEnvTelemetry; -use crate::auth_env_telemetry::collect_auth_env_telemetry; use codex_api::CompactClient as ApiCompactClient; use codex_api::CompactionInput as ApiCompactionInput; use codex_api::MemoriesClient as ApiMemoriesClient; @@ -97,6 +92,11 @@ use tracing::instrument; use tracing::trace; use tracing::warn; +use crate::api_bridge::CoreAuthProvider; +use crate::api_bridge::auth_provider_from_auth; +use crate::api_bridge::map_api_error; +use crate::auth_env_telemetry::AuthEnvTelemetry; +use crate::auth_env_telemetry::collect_auth_env_telemetry; use crate::client_common::Prompt; use crate::client_common::ResponseEvent; use crate::client_common::ResponseStream; diff --git a/codex-rs/core/src/client_tests.rs b/codex-rs/core/src/client_tests.rs index 53a110aca..23eba5b94 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 crate::api_bridge::CoreAuthProvider; +use codex_login::AuthMode; use codex_otel::SessionTelemetry; use codex_protocol::ThreadId; use codex_protocol::openai_models::ModelInfo; @@ -105,8 +107,8 @@ async fn summarize_memories_returns_empty_for_empty_input() { #[test] fn auth_request_telemetry_context_tracks_attached_auth_and_retry_phase() { let auth_context = AuthRequestTelemetryContext::new( - Some(codex_login::AuthMode::Chatgpt), - &crate::api_bridge::CoreAuthProvider::for_test(Some("access-token"), Some("workspace-123")), + Some(AuthMode::Chatgpt), + &CoreAuthProvider::for_test(Some("access-token"), Some("workspace-123")), PendingUnauthorizedRetry::from_recovery(UnauthorizedRecoveryExecution { mode: "managed", phase: "refresh_token", diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 73f5c6ed7..0528a9bf2 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -570,7 +570,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(), &config.to_models_manager_config()) + .await; let base_instructions = config .base_instructions .clone() @@ -903,7 +905,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(), &config.to_models_manager_config()) + .await; let truncation_policy = model_info.truncation_policy.into(); let supported_reasoning_levels = model_info .supported_reasoning_levels @@ -2466,7 +2470,7 @@ impl Session { .models_manager .get_model_info( session_configuration.collaboration_mode.model(), - &per_turn_config, + &per_turn_config.to_models_manager_config(), ) .await; let plugin_outcome = self @@ -5504,7 +5508,7 @@ async fn spawn_review_thread( let review_model_info = sess .services .models_manager - .get_model_info(&model, &config) + .get_model_info(&model, &config.to_models_manager_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_tests.rs b/codex-rs/core/src/codex_tests.rs index c93912073..291eb6d3f 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -9,12 +9,12 @@ use crate::config_loader::NetworkDomainPermissionsToml; use crate::config_loader::RequirementSource; use crate::config_loader::Sourced; use crate::exec::ExecCapturePolicy; -use crate::exec::ExecToolCallOutput; use crate::function_tool::FunctionCallError; use crate::models_manager::model_info; use crate::shell::default_user_shell; use crate::tools::format_exec_output_str; +use crate::exec::ExecToolCallOutput; use codex_features::Features; use codex_login::CodexAuth; use codex_mcp::mcp_connection_manager::ToolInfo; @@ -63,7 +63,6 @@ use codex_protocol::models::ContentItem; use codex_protocol::models::DeveloperInstructions; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; -use codex_protocol::openai_models::ModelsResponse; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::CompactedItem; use codex_protocol::protocol::ConversationAudioParams; @@ -532,8 +531,8 @@ async fn start_managed_network_proxy_ignores_invalid_execpolicy_network_rules() 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"); + let models_response = codex_models_manager::bundled_models_response() + .unwrap_or_else(|err| panic!("bundled models.json should parse: {err}")); let model_info_for_slug = |slug: &str, config: &Config| { let model = models_response .models @@ -541,7 +540,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) + model_info::with_config_overrides(model, &config.to_models_manager_config()) }; let test_cases = vec![ InstructionsTestCase { @@ -1789,7 +1788,10 @@ 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 = ModelsManager::construct_model_info_offline_for_tests( + model.as_str(), + &config.to_models_manager_config(), + ); let reasoning_effort = config.model_reasoning_effort; let collaboration_mode = CollaborationMode { mode: ModeKind::Default, @@ -1887,7 +1889,10 @@ 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 = ModelsManager::construct_model_info_offline_for_tests( + model.as_str(), + &config.to_models_manager_config(), + ); let reasoning_effort = config.model_reasoning_effort; let collaboration_mode = CollaborationMode { mode: ModeKind::Default, @@ -2037,7 +2042,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", + &updated.config.as_ref().to_models_manager_config(), + ) .await; assert_eq!(updated.config.model.as_deref(), Some("gpt-5.1")); @@ -2228,7 +2236,10 @@ 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 = ModelsManager::construct_model_info_offline_for_tests( + model.as_str(), + &config.to_models_manager_config(), + ); let reasoning_effort = config.model_reasoning_effort; let collaboration_mode = CollaborationMode { mode: ModeKind::Default, @@ -2492,7 +2503,10 @@ 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 = ModelsManager::construct_model_info_offline_for_tests( + model.as_str(), + &config.to_models_manager_config(), + ); let collaboration_mode = CollaborationMode { mode: ModeKind::Default, settings: Settings { @@ -2588,7 +2602,10 @@ 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 = ModelsManager::construct_model_info_offline_for_tests( + model.as_str(), + &config.to_models_manager_config(), + ); let reasoning_effort = config.model_reasoning_effort; let collaboration_mode = CollaborationMode { mode: ModeKind::Default, @@ -2632,7 +2649,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { let per_turn_config = Session::build_per_turn_config(&session_configuration); let model_info = ModelsManager::construct_model_info_offline_for_tests( session_configuration.collaboration_mode.model(), - &per_turn_config, + &per_turn_config.to_models_manager_config(), ); let session_telemetry = session_telemetry( conversation_id, @@ -3424,7 +3441,10 @@ 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 = ModelsManager::construct_model_info_offline_for_tests( + model.as_str(), + &config.to_models_manager_config(), + ); let reasoning_effort = config.model_reasoning_effort; let collaboration_mode = CollaborationMode { mode: ModeKind::Default, @@ -3468,7 +3488,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( let per_turn_config = Session::build_per_turn_config(&session_configuration); let model_info = ModelsManager::construct_model_info_offline_for_tests( session_configuration.collaboration_mode.model(), - &per_turn_config, + &per_turn_config.to_models_manager_config(), ); let session_telemetry = session_telemetry( conversation_id, diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 02f958256..09d601df5 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -4242,8 +4242,8 @@ fn load_config_rejects_unsafe_agent_role_nickname_candidates() -> std::io::Resul 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"); + let mut catalog = codex_models_manager::bundled_models_response() + .unwrap_or_else(|err| panic!("bundled models.json should parse: {err}")); catalog.models = catalog.models.into_iter().take(1).collect(); std::fs::write( &catalog_path, diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index f22258dfb..d85ba15f1 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -65,6 +65,7 @@ use codex_features::FeaturesToml; use codex_git_utils::resolve_root_git_project_for_trust; use codex_login::AuthCredentialsStoreMode; use codex_mcp::mcp::McpConfig; +use codex_models_manager::ModelsManagerConfig; use codex_protocol::config_types::AltScreenMode; use codex_protocol::config_types::ForcedLoginMethod; use codex_protocol::config_types::Personality; @@ -683,6 +684,18 @@ impl ConfigBuilder { } impl Config { + pub fn to_models_manager_config(&self) -> ModelsManagerConfig { + ModelsManagerConfig { + model_context_window: self.model_context_window, + model_auto_compact_token_limit: self.model_auto_compact_token_limit, + tool_output_token_limit: self.tool_output_token_limit, + base_instructions: self.base_instructions.clone(), + personality_enabled: self.features.enabled(Feature::Personality), + model_supports_reasoning_summaries: self.model_supports_reasoning_summaries, + model_catalog: self.model_catalog.clone(), + } + } + pub fn to_mcp_config(&self, plugins_manager: &crate::plugins::PluginsManager) -> McpConfig { let loaded_plugins = plugins_manager.plugins_for_config(self); let mut configured_mcp_servers = self.mcp_servers.get().clone(); diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index bfefc2510..28be1b1e5 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -26,9 +26,10 @@ use crate::sandboxing::SandboxPermissions; use crate::spawn::SpawnChildRequest; use crate::spawn::StdioPolicy; use crate::spawn::spawn_child_async; -use crate::text_encoding::bytes_to_string_smart; use codex_network_proxy::NetworkProxy; use codex_protocol::config_types::WindowsSandboxLevel; +pub use codex_protocol::exec_output::ExecToolCallOutput; +pub use codex_protocol::exec_output::StreamOutput; use codex_protocol::permissions::FileSystemSandboxKind; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; @@ -632,25 +633,6 @@ fn finalize_exec_result( } } -pub(crate) mod errors { - use super::CodexErr; - use codex_sandboxing::SandboxTransformError; - - impl From for CodexErr { - fn from(err: SandboxTransformError) -> Self { - match err { - SandboxTransformError::MissingLinuxSandboxExecutable => { - CodexErr::LandlockSandboxExecutableNotProvided - } - #[cfg(not(target_os = "macos"))] - SandboxTransformError::SeatbeltUnavailable => CodexErr::UnsupportedOperation( - "seatbelt sandbox is only available on macOS".to_string(), - ), - } - } - } -} - /// We don't have a fully deterministic way to tell if our command failed /// because of the sandbox - a command in the user's zshrc file might hit an /// error, but the command itself might fail or succeed for other reasons. @@ -713,12 +695,6 @@ pub(crate) fn is_likely_sandbox_denied( false } -#[derive(Debug, Clone)] -pub struct StreamOutput { - pub text: T, - pub truncated_after_lines: Option, -} - #[derive(Debug)] struct RawExecToolCallOutput { pub exit_status: ExitStatus, @@ -728,24 +704,6 @@ struct RawExecToolCallOutput { pub timed_out: bool, } -impl StreamOutput { - pub fn new(text: String) -> Self { - Self { - text, - truncated_after_lines: None, - } - } -} - -impl StreamOutput> { - pub fn from_utf8_lossy(&self) -> StreamOutput { - StreamOutput { - text: bytes_to_string_smart(&self.text), - truncated_after_lines: self.truncated_after_lines, - } - } -} - #[inline] fn append_capped(dst: &mut Vec, src: &[u8], max_bytes: usize) { if dst.len() >= max_bytes { @@ -800,29 +758,6 @@ fn aggregate_output( } } -#[derive(Clone, Debug)] -pub struct ExecToolCallOutput { - pub exit_code: i32, - pub stdout: StreamOutput, - pub stderr: StreamOutput, - pub aggregated_output: StreamOutput, - pub duration: Duration, - pub timed_out: bool, -} - -impl Default for ExecToolCallOutput { - fn default() -> Self { - Self { - exit_code: 0, - stdout: StreamOutput::new(String::new()), - stderr: StreamOutput::new(String::new()), - aggregated_output: StreamOutput::new(String::new()), - duration: Duration::ZERO, - timed_out: false, - } - } -} - #[allow(clippy::too_many_arguments)] async fn exec( params: ExecParams, diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 8ffc7acfb..5a401767e 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -5,11 +5,13 @@ // the TUI or the tracing stack). #![deny(clippy::print_stdout, clippy::print_stderr)] -pub mod api_bridge; +pub use codex_login::api_bridge; mod apply_patch; mod apps; mod arc_monitor; -mod auth_env_telemetry; +pub use codex_login as auth; +pub use codex_login::auth_env_telemetry; +pub use codex_login::default_client; mod client; mod client_common; pub mod codex; @@ -30,7 +32,7 @@ pub mod connectors; mod context_manager; mod contextual_user_message; mod environment_context; -pub mod error; +pub use codex_protocol::error; pub mod exec; pub mod exec_env; mod exec_policy; @@ -46,21 +48,24 @@ pub mod landlock; pub mod mcp; mod mcp_skill_dependencies; mod mcp_tool_approval_templates; -pub mod models_manager; +pub use codex_models_manager as models_manager; mod network_policy_decision; pub mod network_proxy_loader; mod original_image_detail; -pub use text_encoding::bytes_to_string_smart; +pub use codex_mcp::mcp_connection_manager; +pub use codex_mcp::mcp_connection_manager::MCP_SANDBOX_STATE_CAPABILITY; +pub use codex_mcp::mcp_connection_manager::MCP_SANDBOX_STATE_METHOD; +pub use codex_mcp::mcp_connection_manager::SandboxState; mod mcp_tool_call; mod memories; pub mod mention_syntax; pub mod message_history; -mod model_provider_info; +pub use codex_login::model_provider_info; +pub use codex_login::provider_auth; pub mod utils; pub use utils::path_utils; pub mod personality_migration; pub mod plugins; -mod provider_auth; pub(crate) mod mentions { pub(crate) use crate::plugins::build_connector_slug_counts; pub(crate) use crate::plugins::build_skill_name_counts; @@ -95,21 +100,25 @@ pub(crate) use skills::skills_load_input_from_config; mod skills_watcher; mod stream_events_utils; pub mod test_support; -mod text_encoding; +mod text_encoding { + pub use codex_protocol::exec_output::bytes_to_string_smart; +} 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; +pub use codex_login::DEFAULT_LMSTUDIO_PORT; +pub use codex_login::DEFAULT_OLLAMA_PORT; +pub use codex_login::LMSTUDIO_OSS_PROVIDER_ID; +pub use codex_login::ModelProviderInfo; +pub use codex_login::OLLAMA_OSS_PROVIDER_ID; +pub use codex_login::OPENAI_PROVIDER_ID; +pub use codex_login::WireApi; +pub use codex_login::built_in_model_providers; +pub use codex_login::create_oss_provider_with_base_url; +pub use codex_protocol::config_types::ModelProviderAuthInfo; +pub use text_encoding::bytes_to_string_smart; mod event_mapping; -mod response_debug_context; +pub use codex_response_debug_context as response_debug_context; pub mod review_format; pub mod review_prompts; mod thread_manager; diff --git a/codex-rs/core/src/memories/phase1.rs b/codex-rs/core/src/memories/phase1.rs index ff050ac5b..179620fe6 100644 --- a/codex-rs/core/src/memories/phase1.rs +++ b/codex-rs/core/src/memories/phase1.rs @@ -228,7 +228,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, &config.to_models_manager_config()) .await; let turn_context = session.new_default_turn().await; RequestContext::from_turn_context( @@ -466,7 +466,7 @@ mod job { /// Serializes filtered stage-1 memory items for prompt inclusion. pub(super) fn serialize_filtered_rollout_response_items( items: &[RolloutItem], - ) -> crate::error::Result { + ) -> codex_protocol::error::Result { let filtered = items .iter() .filter_map(|item| { 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 53b33bc4e..000000000 --- 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/network_policy_decision.rs b/codex-rs/core/src/network_policy_decision.rs index 484905cfd..7a983e8c5 100644 --- a/codex-rs/core/src/network_policy_decision.rs +++ b/codex-rs/core/src/network_policy_decision.rs @@ -1,25 +1,12 @@ use codex_execpolicy::Decision as ExecPolicyDecision; use codex_execpolicy::NetworkRuleProtocol as ExecPolicyNetworkRuleProtocol; use codex_network_proxy::BlockedRequest; -use codex_network_proxy::NetworkDecisionSource; use codex_network_proxy::NetworkPolicyDecision; use codex_protocol::approvals::NetworkApprovalContext; use codex_protocol::approvals::NetworkApprovalProtocol; use codex_protocol::approvals::NetworkPolicyAmendment; use codex_protocol::approvals::NetworkPolicyRuleAction; -use serde::Deserialize; - -#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct NetworkPolicyDecisionPayload { - pub decision: NetworkPolicyDecision, - pub source: NetworkDecisionSource, - #[serde(default)] - pub protocol: Option, - pub host: Option, - pub reason: Option, - pub port: Option, -} +use codex_protocol::network_policy::NetworkPolicyDecisionPayload; #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct ExecPolicyNetworkRuleAmendment { @@ -28,12 +15,6 @@ pub(crate) struct ExecPolicyNetworkRuleAmendment { pub justification: String, } -impl NetworkPolicyDecisionPayload { - pub(crate) fn is_ask_from_decider(&self) -> bool { - self.decision == NetworkPolicyDecision::Ask && self.source == NetworkDecisionSource::Decider - } -} - fn parse_network_policy_decision(value: &str) -> Option { match value { "deny" => Some(NetworkPolicyDecision::Deny), diff --git a/codex-rs/core/src/network_policy_decision_tests.rs b/codex-rs/core/src/network_policy_decision_tests.rs index ebb17f724..064004184 100644 --- a/codex-rs/core/src/network_policy_decision_tests.rs +++ b/codex-rs/core/src/network_policy_decision_tests.rs @@ -1,5 +1,6 @@ use super::*; use codex_network_proxy::BlockedRequest; +use codex_network_proxy::NetworkDecisionSource; use codex_protocol::approvals::NetworkPolicyAmendment; use codex_protocol::approvals::NetworkPolicyRuleAction; use pretty_assertions::assert_eq; diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index f2b151ccd..d4c12cc3e 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -140,7 +140,7 @@ impl ExecRequest { pub async fn execute_env( exec_request: ExecRequest, stdout_stream: Option, -) -> crate::error::Result { +) -> codex_protocol::error::Result { execute_exec_request(exec_request, stdout_stream, /*after_spawn*/ None).await } @@ -148,6 +148,6 @@ pub async fn execute_exec_request_with_after_spawn( exec_request: ExecRequest, stdout_stream: Option, after_spawn: Option>, -) -> crate::error::Result { +) -> codex_protocol::error::Result { execute_exec_request(exec_request, stdout_stream, after_spawn).await } diff --git a/codex-rs/core/src/test_support.rs b/codex-rs/core/src/test_support.rs index d6f99a0ae..2abb20b9f 100644 --- a/codex-rs/core/src/test_support.rs +++ b/codex-rs/core/src/test_support.rs @@ -11,7 +11,6 @@ use codex_exec_server::EnvironmentManager; use codex_protocol::config_types::CollaborationModeMask; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelPreset; -use codex_protocol::openai_models::ModelsResponse; use once_cell::sync::Lazy; use crate::ModelProviderInfo; @@ -25,8 +24,7 @@ use codex_login::AuthManager; use codex_login::CodexAuth; static TEST_MODEL_PRESETS: Lazy> = Lazy::new(|| { - let file_contents = include_str!("../models.json"); - let mut response: ModelsResponse = serde_json::from_str(file_contents) + let mut response = codex_models_manager::bundled_models_response() .unwrap_or_else(|err| panic!("bundled models.json should parse: {err}")); response.models.sort_by(|a, b| a.priority.cmp(&b.priority)); let mut presets: Vec = response.models.into_iter().map(Into::into).collect(); @@ -75,7 +73,7 @@ pub async fn start_thread_with_user_shell_override( thread_manager: &ThreadManager, config: Config, user_shell_override: crate::shell::Shell, -) -> crate::error::Result { +) -> codex_protocol::error::Result { thread_manager .start_thread_with_user_shell_override_for_tests(config, user_shell_override) .await @@ -87,7 +85,7 @@ pub async fn resume_thread_from_rollout_with_user_shell_override( rollout_path: PathBuf, auth_manager: Arc, user_shell_override: crate::shell::Shell, -) -> crate::error::Result { +) -> codex_protocol::error::Result { thread_manager .resume_thread_from_rollout_with_user_shell_override_for_tests( config, @@ -111,7 +109,7 @@ 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.to_models_manager_config()) } pub fn all_model_presets() -> &'static Vec { diff --git a/codex-rs/core/src/text_encoding_tests.rs b/codex-rs/core/src/text_encoding_tests.rs deleted file mode 100644 index 6368f38be..000000000 --- a/codex-rs/core/src/text_encoding_tests.rs +++ /dev/null @@ -1,340 +0,0 @@ -use super::*; -use encoding_rs::BIG5; -use encoding_rs::EUC_KR; -use encoding_rs::GBK; -use encoding_rs::ISO_8859_2; -use encoding_rs::ISO_8859_3; -use encoding_rs::ISO_8859_4; -use encoding_rs::ISO_8859_5; -use encoding_rs::ISO_8859_6; -use encoding_rs::ISO_8859_7; -use encoding_rs::ISO_8859_8; -use encoding_rs::ISO_8859_10; -use encoding_rs::ISO_8859_13; -use encoding_rs::SHIFT_JIS; -use encoding_rs::WINDOWS_874; -use encoding_rs::WINDOWS_1250; -use encoding_rs::WINDOWS_1251; -use encoding_rs::WINDOWS_1253; -use encoding_rs::WINDOWS_1254; -use encoding_rs::WINDOWS_1255; -use encoding_rs::WINDOWS_1256; -use encoding_rs::WINDOWS_1257; -use encoding_rs::WINDOWS_1258; -use pretty_assertions::assert_eq; - -#[test] -fn test_utf8_passthrough() { - // Fast path: when UTF-8 is valid we should avoid copies and return as-is. - let utf8_text = "Hello, мир! 世界"; - let bytes = utf8_text.as_bytes(); - assert_eq!(bytes_to_string_smart(bytes), utf8_text); -} - -#[test] -fn test_cp1251_russian_text() { - // Cyrillic text emitted by PowerShell/WSL in CP1251 should decode cleanly. - let bytes = b"\xEF\xF0\xE8\xEC\xE5\xF0"; // "пример" encoded with Windows-1251 - assert_eq!(bytes_to_string_smart(bytes), "пример"); -} - -#[test] -fn test_cp1251_privet_word() { - // Regression: CP1251 words like "Привет" must not be mis-identified as Windows-1252. - let bytes = b"\xCF\xF0\xE8\xE2\xE5\xF2"; // "Привет" encoded with Windows-1251 - assert_eq!(bytes_to_string_smart(bytes), "Привет"); -} - -#[test] -fn test_koi8_r_privet_word() { - // KOI8-R output should decode to the original Cyrillic as well. - let bytes = b"\xF0\xD2\xC9\xD7\xC5\xD4"; // "Привет" encoded with KOI8-R - assert_eq!(bytes_to_string_smart(bytes), "Привет"); -} - -#[test] -fn test_cp866_russian_text() { - // Legacy consoles (cmd.exe) commonly emit CP866 bytes for Cyrillic content. - let bytes = b"\xAF\xE0\xA8\xAC\xA5\xE0"; // "пример" encoded with CP866 - assert_eq!(bytes_to_string_smart(bytes), "пример"); -} - -#[test] -fn test_cp866_uppercase_text() { - // Ensure the IBM866 heuristic still returns IBM866 for uppercase-only words. - let bytes = b"\x8F\x90\x88"; // "ПРИ" encoded with CP866 uppercase letters - assert_eq!(bytes_to_string_smart(bytes), "ПРИ"); -} - -#[test] -fn test_cp866_uppercase_followed_by_ascii() { - // Regression test: uppercase CP866 tokens next to ASCII text should not be treated as - // CP1252. - let bytes = b"\x8F\x90\x88 test"; // "ПРИ test" encoded with CP866 uppercase letters followed by ASCII - assert_eq!(bytes_to_string_smart(bytes), "ПРИ test"); -} - -#[test] -fn test_windows_1252_quotes() { - // Smart detection should map Windows-1252 punctuation into proper Unicode. - let bytes = b"\x93\x94test"; - assert_eq!(bytes_to_string_smart(bytes), "\u{201C}\u{201D}test"); -} - -#[test] -fn test_windows_1252_multiple_quotes() { - // Longer snippets of punctuation (e.g., “foo” – “bar”) should still flip to CP1252. - let bytes = b"\x93foo\x94 \x96 \x93bar\x94"; - assert_eq!( - bytes_to_string_smart(bytes), - "\u{201C}foo\u{201D} \u{2013} \u{201C}bar\u{201D}" - ); -} - -#[test] -fn test_windows_1252_privet_gibberish_is_preserved() { - // Windows-1252 cannot encode Cyrillic; if the input literally contains "ПÑ..." we should not "fix" it. - let bytes = "Привет".as_bytes(); - assert_eq!(bytes_to_string_smart(bytes), "Привет"); -} - -#[test] -fn test_iso8859_1_latin_text() { - // ISO-8859-1 (code page 28591) is the Latin segment used by LatArCyrHeb. - // encoding_rs unifies ISO-8859-1 with Windows-1252, so reuse that constant here. - let (encoded, _, had_errors) = WINDOWS_1252.encode("Hello"); - assert!(!had_errors, "failed to encode Latin sample"); - assert_eq!(bytes_to_string_smart(encoded.as_ref()), "Hello"); -} - -#[test] -fn test_iso8859_2_central_european_text() { - // ISO-8859-2 (code page 28592) covers additional Central European glyphs. - let (encoded, _, had_errors) = ISO_8859_2.encode("Příliš žluťoučký kůň"); - assert!(!had_errors, "failed to encode ISO-8859-2 sample"); - assert_eq!( - bytes_to_string_smart(encoded.as_ref()), - "Příliš žluťoučký kůň" - ); -} - -#[test] -fn test_iso8859_3_south_europe_text() { - // ISO-8859-3 (code page 28593) adds support for Maltese/Esperanto letters. - // chardetng rarely distinguishes ISO-8859-3 from neighboring Latin code pages, so we rely on - // an ASCII-only sample to ensure round-tripping still succeeds. - let (encoded, _, had_errors) = ISO_8859_3.encode("Esperanto and Maltese"); - assert!(!had_errors, "failed to encode ISO-8859-3 sample"); - assert_eq!( - bytes_to_string_smart(encoded.as_ref()), - "Esperanto and Maltese" - ); -} - -#[test] -fn test_iso8859_4_baltic_text() { - // ISO-8859-4 (code page 28594) targets the Baltic/Nordic repertoire. - let sample = "Šis ir rakstzīmju kodēšanas tests. Dažās valodās, kurās tiek \ - izmantotas latīņu valodas burti, lēmuma pieņemšanai mums ir nepieciešams \ - vairāk ieguldījuma."; - let (encoded, _, had_errors) = ISO_8859_4.encode(sample); - assert!(!had_errors, "failed to encode ISO-8859-4 sample"); - assert_eq!(bytes_to_string_smart(encoded.as_ref()), sample); -} - -#[test] -fn test_iso8859_5_cyrillic_text() { - // ISO-8859-5 (code page 28595) covers the Cyrillic portion. - let (encoded, _, had_errors) = ISO_8859_5.encode("Привет"); - assert!(!had_errors, "failed to encode Cyrillic sample"); - assert_eq!(bytes_to_string_smart(encoded.as_ref()), "Привет"); -} - -#[test] -fn test_iso8859_6_arabic_text() { - // ISO-8859-6 (code page 28596) covers the Arabic glyphs. - let (encoded, _, had_errors) = ISO_8859_6.encode("مرحبا"); - assert!(!had_errors, "failed to encode Arabic sample"); - assert_eq!(bytes_to_string_smart(encoded.as_ref()), "مرحبا"); -} - -#[test] -fn test_iso8859_7_greek_text() { - // ISO-8859-7 (code page 28597) is used for Greek locales. - let (encoded, _, had_errors) = ISO_8859_7.encode("Καλημέρα"); - assert!(!had_errors, "failed to encode ISO-8859-7 sample"); - assert_eq!(bytes_to_string_smart(encoded.as_ref()), "Καλημέρα"); -} - -#[test] -fn test_iso8859_8_hebrew_text() { - // ISO-8859-8 (code page 28598) covers the Hebrew glyphs. - let (encoded, _, had_errors) = ISO_8859_8.encode("שלום"); - assert!(!had_errors, "failed to encode Hebrew sample"); - assert_eq!(bytes_to_string_smart(encoded.as_ref()), "שלום"); -} - -#[test] -fn test_iso8859_9_turkish_text() { - // ISO-8859-9 (code page 28599) mirrors Latin-1 but inserts Turkish letters. - // encoding_rs exposes the equivalent Windows-1254 mapping. - let (encoded, _, had_errors) = WINDOWS_1254.encode("İstanbul"); - assert!(!had_errors, "failed to encode ISO-8859-9 sample"); - assert_eq!(bytes_to_string_smart(encoded.as_ref()), "İstanbul"); -} - -#[test] -fn test_iso8859_10_nordic_text() { - // ISO-8859-10 (code page 28600) adds additional Nordic letters. - let sample = "Þetta er prófun fyrir Ægir og Øystein."; - let (encoded, _, had_errors) = ISO_8859_10.encode(sample); - assert!(!had_errors, "failed to encode ISO-8859-10 sample"); - assert_eq!(bytes_to_string_smart(encoded.as_ref()), sample); -} - -#[test] -fn test_iso8859_11_thai_text() { - // ISO-8859-11 (code page 28601) mirrors TIS-620 / Windows-874 for Thai. - let sample = "ภาษาไทยสำหรับการทดสอบ ISO-8859-11"; - // encoding_rs exposes the equivalent Windows-874 encoding, so use that constant. - let (encoded, _, had_errors) = WINDOWS_874.encode(sample); - assert!(!had_errors, "failed to encode ISO-8859-11 sample"); - assert_eq!(bytes_to_string_smart(encoded.as_ref()), sample); -} - -// ISO-8859-12 was never standardized, and encodings 14–16 cannot be distinguished reliably -// without the heuristics we removed (chardetng generally reports neighboring Latin pages), so -// we intentionally omit coverage for those slots until the detector can identify them. - -#[test] -fn test_iso8859_13_baltic_text() { - // ISO-8859-13 (code page 28603) is common across Baltic languages. - let (encoded, _, had_errors) = ISO_8859_13.encode("Sveiki"); - assert!(!had_errors, "failed to encode ISO-8859-13 sample"); - assert_eq!(bytes_to_string_smart(encoded.as_ref()), "Sveiki"); -} - -#[test] -fn test_windows_1250_central_european_text() { - let (encoded, _, had_errors) = WINDOWS_1250.encode("Příliš žluťoučký kůň"); - assert!(!had_errors, "failed to encode Central European sample"); - assert_eq!( - bytes_to_string_smart(encoded.as_ref()), - "Příliš žluťoučký kůň" - ); -} - -#[test] -fn test_windows_1251_encoded_text() { - let (encoded, _, had_errors) = WINDOWS_1251.encode("Привет из Windows-1251"); - assert!(!had_errors, "failed to encode Windows-1251 sample"); - assert_eq!( - bytes_to_string_smart(encoded.as_ref()), - "Привет из Windows-1251" - ); -} - -#[test] -fn test_windows_1253_greek_text() { - let (encoded, _, had_errors) = WINDOWS_1253.encode("Γειά σου"); - assert!(!had_errors, "failed to encode Greek sample"); - assert_eq!(bytes_to_string_smart(encoded.as_ref()), "Γειά σου"); -} - -#[test] -fn test_windows_1254_turkish_text() { - let (encoded, _, had_errors) = WINDOWS_1254.encode("İstanbul"); - assert!(!had_errors, "failed to encode Turkish sample"); - assert_eq!(bytes_to_string_smart(encoded.as_ref()), "İstanbul"); -} - -#[test] -fn test_windows_1255_hebrew_text() { - let (encoded, _, had_errors) = WINDOWS_1255.encode("שלום"); - assert!(!had_errors, "failed to encode Windows-1255 Hebrew sample"); - assert_eq!(bytes_to_string_smart(encoded.as_ref()), "שלום"); -} - -#[test] -fn test_windows_1256_arabic_text() { - let (encoded, _, had_errors) = WINDOWS_1256.encode("مرحبا"); - assert!(!had_errors, "failed to encode Windows-1256 Arabic sample"); - assert_eq!(bytes_to_string_smart(encoded.as_ref()), "مرحبا"); -} - -#[test] -fn test_windows_1257_baltic_text() { - let (encoded, _, had_errors) = WINDOWS_1257.encode("Pērkons"); - assert!(!had_errors, "failed to encode Baltic sample"); - assert_eq!(bytes_to_string_smart(encoded.as_ref()), "Pērkons"); -} - -#[test] -fn test_windows_1258_vietnamese_text() { - let (encoded, _, had_errors) = WINDOWS_1258.encode("Xin chào"); - assert!(!had_errors, "failed to encode Vietnamese sample"); - assert_eq!(bytes_to_string_smart(encoded.as_ref()), "Xin chào"); -} - -#[test] -fn test_windows_874_thai_text() { - let (encoded, _, had_errors) = WINDOWS_874.encode("สวัสดีครับ นี่คือการทดสอบภาษาไทย"); - assert!(!had_errors, "failed to encode Thai sample"); - assert_eq!( - bytes_to_string_smart(encoded.as_ref()), - "สวัสดีครับ นี่คือการทดสอบภาษาไทย" - ); -} - -#[test] -fn test_windows_932_shift_jis_text() { - let (encoded, _, had_errors) = SHIFT_JIS.encode("こんにちは"); - assert!(!had_errors, "failed to encode Shift-JIS sample"); - assert_eq!(bytes_to_string_smart(encoded.as_ref()), "こんにちは"); -} - -#[test] -fn test_windows_936_gbk_text() { - let (encoded, _, had_errors) = GBK.encode("你好,世界,这是一个测试"); - assert!(!had_errors, "failed to encode GBK sample"); - assert_eq!( - bytes_to_string_smart(encoded.as_ref()), - "你好,世界,这是一个测试" - ); -} - -#[test] -fn test_windows_949_korean_text() { - let (encoded, _, had_errors) = EUC_KR.encode("안녕하세요"); - assert!(!had_errors, "failed to encode Korean sample"); - assert_eq!(bytes_to_string_smart(encoded.as_ref()), "안녕하세요"); -} - -#[test] -fn test_windows_950_big5_text() { - let (encoded, _, had_errors) = BIG5.encode("繁體"); - assert!(!had_errors, "failed to encode Big5 sample"); - assert_eq!(bytes_to_string_smart(encoded.as_ref()), "繁體"); -} - -#[test] -fn test_latin1_cafe() { - // Latin-1 bytes remain common in Western-European locales; decode them directly. - let bytes = b"caf\xE9"; // codespell:ignore caf - assert_eq!(bytes_to_string_smart(bytes), "café"); -} - -#[test] -fn test_preserves_ansi_sequences() { - // ANSI escape sequences should survive regardless of the detected encoding. - let bytes = b"\x1b[31mred\x1b[0m"; - assert_eq!(bytes_to_string_smart(bytes), "\x1b[31mred\x1b[0m"); -} - -#[test] -fn test_fallback_to_lossy() { - // Completely invalid sequences fall back to the old lossy behavior. - let invalid_bytes = [0xFF, 0xFE, 0xFD]; - let result = bytes_to_string_smart(&invalid_bytes); - assert_eq!(result, String::from_utf8_lossy(&invalid_bytes)); -} diff --git a/codex-rs/core/src/tools/handlers/multi_agents_common.rs b/codex-rs/core/src/tools/handlers/multi_agents_common.rs index dd68465a7..f32fdf431 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_common.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_common.rs @@ -292,7 +292,7 @@ pub(crate) 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, &config.to_models_manager_config()) .await; config.model = Some(selected_model_name.clone()); diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index 3c13ab442..ab8c8369a 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -4,6 +4,7 @@ use crate::error::SandboxErr; use crate::exec::ExecCapturePolicy; use crate::exec::ExecExpiration; use crate::exec::ExecToolCallOutput; +use crate::exec::StreamOutput; use crate::exec::is_likely_sandbox_denied; use crate::guardian::GuardianApprovalRequest; use crate::guardian::review_approval_request; @@ -901,9 +902,9 @@ fn map_exec_result( ) -> Result { let output = ExecToolCallOutput { exit_code: result.exit_code, - stdout: crate::exec::StreamOutput::new(result.stdout.clone()), - stderr: crate::exec::StreamOutput::new(result.stderr.clone()), - aggregated_output: crate::exec::StreamOutput::new(result.output.clone()), + stdout: StreamOutput::new(result.stdout.clone()), + stderr: StreamOutput::new(result.stderr.clone()), + aggregated_output: StreamOutput::new(result.output.clone()), duration: result.duration, timed_out: result.timed_out, }; diff --git a/codex-rs/core/src/tools/spec_tests.rs b/codex-rs/core/src/tools/spec_tests.rs index 4e73be350..1c24a4287 100644 --- a/codex-rs/core/src/tools/spec_tests.rs +++ b/codex-rs/core/src/tools/spec_tests.rs @@ -1,8 +1,8 @@ use crate::config::test_config; -use crate::models_manager::manager::ModelsManager; use crate::models_manager::model_info::with_config_overrides; use crate::shell::Shell; use crate::shell::ShellType; +use crate::test_support::construct_model_info_offline; use crate::tools::ToolRouter; use crate::tools::registry::tool_handler_key; use crate::tools::router::ToolRouterParams; @@ -14,7 +14,6 @@ use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ModelInfo; -use codex_protocol::openai_models::ModelsResponse; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_tools::ConfiguredToolSpec; @@ -73,8 +72,7 @@ fn discoverable_connector(id: &str, name: &str, description: &str) -> Discoverab 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 = construct_model_info_offline("gpt-5-codex", &config); model_info.supports_search_tool = true; model_info } @@ -161,14 +159,14 @@ fn find_tool<'a>(tools: &'a [ConfiguredToolSpec], expected_name: &str) -> &'a Co 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"); + let response = codex_models_manager::bundled_models_response() + .unwrap_or_else(|err| panic!("bundled models.json should parse: {err}")); 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, &config.to_models_manager_config()) } /// Builds the tool registry builder while collecting tool specs for later serialization. @@ -214,7 +212,7 @@ fn model_provided_unified_exec_is_blocked_for_windows_sandboxed_policies() { #[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 = construct_model_info_offline("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.disable(Feature::MemoryTool); let available_models = Vec::new(); @@ -506,7 +504,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 = construct_model_info_offline("o3", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); let available_models = Vec::new(); @@ -538,7 +536,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 = construct_model_info_offline("o3", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); features.enable(Feature::ShellZshFork); @@ -794,7 +792,7 @@ fn search_tool_registers_namespaced_app_tool_aliases() { #[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 = construct_model_info_offline("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); let available_models = Vec::new(); @@ -854,7 +852,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 = construct_model_info_offline("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); let available_models = Vec::new(); @@ -910,7 +908,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 = construct_model_info_offline("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); features.enable(Feature::ApplyPatchFreeform); @@ -970,7 +968,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 = construct_model_info_offline("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); let available_models = Vec::new(); @@ -1028,7 +1026,7 @@ fn test_mcp_tool_anyof_defaults_to_string() { #[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 = construct_model_info_offline("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); let available_models = Vec::new(); diff --git a/codex-rs/core/src/user_shell_command.rs b/codex-rs/core/src/user_shell_command.rs index 32cf78cf2..d46570e32 100644 --- a/codex-rs/core/src/user_shell_command.rs +++ b/codex-rs/core/src/user_shell_command.rs @@ -1,10 +1,10 @@ use std::time::Duration; +use crate::exec::ExecToolCallOutput; use codex_protocol::models::ResponseItem; use crate::codex::TurnContext; use crate::contextual_user_message::USER_SHELL_COMMAND_FRAGMENT; -use crate::exec::ExecToolCallOutput; use crate::tools::format_exec_output_str; fn format_duration_line(duration: Duration) -> String { diff --git a/codex-rs/core/src/util.rs b/codex-rs/core/src/util.rs index 0856b1689..a859bfd71 100644 --- a/codex-rs/core/src/util.rs +++ b/codex-rs/core/src/util.rs @@ -6,7 +6,10 @@ use codex_protocol::ThreadId; use rand::Rng; use tracing::error; -use crate::auth_env_telemetry::AuthEnvTelemetry; +pub(crate) use codex_feedback::FeedbackRequestTags; +#[cfg(test)] +pub(crate) use codex_feedback::emit_feedback_request_tags; +pub(crate) use codex_feedback::emit_feedback_request_tags_with_auth_env; use codex_shell_command::parse_command::shlex_join; const INITIAL_DELAY_MS: u64 = 200; @@ -37,40 +40,6 @@ macro_rules! feedback_tags { }; } -pub(crate) struct FeedbackRequestTags<'a> { - pub endpoint: &'a str, - pub auth_header_attached: bool, - pub auth_header_name: Option<&'a str>, - pub auth_mode: Option<&'a str>, - pub auth_retry_after_unauthorized: Option, - pub auth_recovery_mode: Option<&'a str>, - pub auth_recovery_phase: Option<&'a str>, - pub auth_connection_reused: Option, - pub auth_request_id: Option<&'a str>, - pub auth_cf_ray: Option<&'a str>, - pub auth_error: Option<&'a str>, - pub auth_error_code: Option<&'a str>, - pub auth_recovery_followup_success: Option, - pub 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, -} - struct Auth401FeedbackSnapshot<'a> { request_id: &'a str, cf_ray: &'a str, @@ -94,87 +63,6 @@ impl<'a> Auth401FeedbackSnapshot<'a> { } } -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()), - } - } -} - -#[cfg(test)] -pub(crate) fn emit_feedback_request_tags(tags: &FeedbackRequestTags<'_>) { - let snapshot = FeedbackRequestSnapshot::from_tags(tags); - feedback_tags!( - endpoint = snapshot.endpoint, - auth_header_attached = snapshot.auth_header_attached, - auth_header_name = snapshot.auth_header_name, - auth_mode = snapshot.auth_mode, - auth_retry_after_unauthorized = snapshot.auth_retry_after_unauthorized, - auth_recovery_mode = snapshot.auth_recovery_mode, - auth_recovery_phase = snapshot.auth_recovery_phase, - auth_connection_reused = snapshot.auth_connection_reused, - auth_request_id = snapshot.auth_request_id, - auth_cf_ray = snapshot.auth_cf_ray, - auth_error = snapshot.auth_error, - auth_error_code = snapshot.auth_error_code, - auth_recovery_followup_success = snapshot.auth_recovery_followup_success, - auth_recovery_followup_status = snapshot.auth_recovery_followup_status - ); -} - -pub(crate) fn emit_feedback_request_tags_with_auth_env( - tags: &FeedbackRequestTags<'_>, - auth_env: &AuthEnvTelemetry, -) { - let snapshot = FeedbackRequestSnapshot::from_tags(tags); - feedback_tags!( - endpoint = snapshot.endpoint, - auth_header_attached = snapshot.auth_header_attached, - auth_header_name = snapshot.auth_header_name, - auth_mode = snapshot.auth_mode, - auth_retry_after_unauthorized = snapshot.auth_retry_after_unauthorized, - auth_recovery_mode = snapshot.auth_recovery_mode, - auth_recovery_phase = snapshot.auth_recovery_phase, - auth_connection_reused = snapshot.auth_connection_reused, - auth_request_id = snapshot.auth_request_id, - auth_cf_ray = snapshot.auth_cf_ray, - auth_error = snapshot.auth_error, - auth_error_code = snapshot.auth_error_code, - auth_recovery_followup_success = snapshot.auth_recovery_followup_success, - auth_recovery_followup_status = snapshot.auth_recovery_followup_status, - auth_env_openai_api_key_present = auth_env.openai_api_key_env_present, - auth_env_codex_api_key_present = auth_env.codex_api_key_env_present, - auth_env_codex_api_key_enabled = auth_env.codex_api_key_env_enabled, - auth_env_provider_key_name = auth_env.provider_env_key_name.as_deref().unwrap_or(""), - auth_env_provider_key_present = auth_env - .provider_env_key_present - .map_or_else(String::new, |value| value.to_string()), - auth_env_refresh_token_url_override_present = auth_env.refresh_token_url_override_present - ); -} - pub(crate) fn emit_feedback_auth_recovery_tags( auth_recovery_mode: &str, auth_recovery_phase: &str, diff --git a/codex-rs/core/src/util_tests.rs b/codex-rs/core/src/util_tests.rs index 61bf70284..28f93b148 100644 --- a/codex-rs/core/src/util_tests.rs +++ b/codex-rs/core/src/util_tests.rs @@ -1,5 +1,8 @@ use super::*; -use crate::auth_env_telemetry::AuthEnvTelemetry; +use crate::util::FeedbackRequestTags; +use crate::util::emit_feedback_request_tags; +use crate::util::emit_feedback_request_tags_with_auth_env; +use codex_login::AuthEnvTelemetry; use std::collections::BTreeMap; use std::sync::Arc; use std::sync::Mutex; diff --git a/codex-rs/core/tests/all.rs b/codex-rs/core/tests/all.rs index a77ad987e..ada0c3e15 100644 --- a/codex-rs/core/tests/all.rs +++ b/codex-rs/core/tests/all.rs @@ -1,3 +1,5 @@ // Single integration test binary that aggregates all test modules. // The submodules live in `tests/all/`. +pub use codex_core::error; + mod suite; diff --git a/codex-rs/core/tests/common/Cargo.toml b/codex-rs/core/tests/common/Cargo.toml index 2611aa5d2..21f92dcdc 100644 --- a/codex-rs/core/tests/common/Cargo.toml +++ b/codex-rs/core/tests/common/Cargo.toml @@ -19,6 +19,7 @@ codex-core = { workspace = true } codex-exec-server = { workspace = true } codex-features = { workspace = true } codex-login = { workspace = true } +codex-models-manager = { 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/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index a347488c2..05527ee4c 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -609,17 +609,8 @@ fn ensure_test_model_catalog(config: &mut Config) -> Result<()> { return Ok(()); } - let bundled_models_path = codex_utils_cargo_bin::find_resource!("../../models.json") - .context("bundled models.json")?; - let bundled_models_contents = - std::fs::read_to_string(&bundled_models_path).with_context(|| { - format!( - "read bundled models.json from {}", - bundled_models_path.display() - ) - })?; - let bundled_models: ModelsResponse = - serde_json::from_str(&bundled_models_contents).context("parse bundled models.json")?; + let bundled_models = codex_models_manager::bundled_models_response() + .unwrap_or_else(|err| panic!("bundled models.json should parse: {err}")); let mut model = bundled_models .models .iter() diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 231ab1a5d..898e37ee1 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -1,3 +1,4 @@ +use crate::error::CodexErr; use codex_core::ModelClient; use codex_core::ModelProviderInfo; use codex_core::NewThread; @@ -6,7 +7,6 @@ use codex_core::ResponseEvent; use codex_core::ThreadManager; use codex_core::WireApi; use codex_core::built_in_model_providers; -use codex_core::error::CodexErr; use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; use codex_features::Feature; use codex_login::AuthCredentialsStoreMode; @@ -34,7 +34,6 @@ use codex_protocol::models::ReasoningItemContent; use codex_protocol::models::ReasoningItemReasoningSummary; use codex_protocol::models::ResponseItem; use codex_protocol::models::WebSearchAction; -use codex_protocol::openai_models::ModelsResponse; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; @@ -1636,8 +1635,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"); + let mut model_catalog = codex_models_manager::bundled_models_response() + .unwrap_or_else(|err| panic!("bundled models.json should parse: {err}")); let model = model_catalog .models .iter_mut() @@ -1749,8 +1748,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"); + let mut model_catalog = codex_models_manager::bundled_models_response() + .unwrap_or_else(|err| panic!("bundled models.json should parse: {err}")); let model = model_catalog .models .iter_mut() diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index 35fe9d07e..1313c7617 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -103,8 +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"); + let models_response = codex_models_manager::bundled_models_response() + .unwrap_or_else(|err| panic!("bundled models.json should parse: {err}")); let mut model_info = models_response .models .into_iter() diff --git a/codex-rs/core/tests/suite/exec.rs b/codex-rs/core/tests/suite/exec.rs index 8f334fbe2..e6dd109cc 100644 --- a/codex-rs/core/tests/suite/exec.rs +++ b/codex-rs/core/tests/suite/exec.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use std::string::ToString; +use crate::error::Result; use codex_core::exec::ExecCapturePolicy; use codex_core::exec::ExecParams; use codex_core::exec::ExecToolCallOutput; @@ -17,8 +18,6 @@ use codex_sandboxing::SandboxType; use codex_sandboxing::get_platform_sandbox; use tempfile::TempDir; -use codex_core::error::Result; - fn skip_test() -> bool { if std::env::var(CODEX_SANDBOX_ENV_VAR) == Ok("seatbelt".to_string()) { eprintln!("{CODEX_SANDBOX_ENV_VAR} is set to 'seatbelt', skipping test."); diff --git a/codex-rs/core/tests/suite/mod.rs b/codex-rs/core/tests/suite/mod.rs index 5bbf233f1..0efeac47d 100644 --- a/codex-rs/core/tests/suite/mod.rs +++ b/codex-rs/core/tests/suite/mod.rs @@ -77,7 +77,6 @@ mod agent_websocket; mod apply_patch_cli; #[cfg(not(target_os = "windows"))] mod approvals; -mod auth_refresh; mod cli_stream; mod client; mod client_websockets; @@ -101,7 +100,6 @@ mod json_result; mod live_cli; mod live_reload; mod memories; -mod model_info_overrides; mod model_overrides; mod model_switching; mod model_visible_layout; @@ -142,7 +140,6 @@ mod sqlite_state; mod stream_error_allows_next_turn; mod stream_no_completed; mod subagent_notifications; -mod text_encoding_fix; mod tool_harness; mod tool_parallelism; mod tool_suggest; diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index 4cb84e8b6..99cc74766 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -961,11 +961,11 @@ 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, &test.config.to_models_manager_config()) .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, &test.config.to_models_manager_config()) .await; assert_eq!( smaller_model_info.context_window, diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 1bcd06861..4d34e691d 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -139,7 +139,7 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> { .model .as_deref() .expect("test config should have a model"), - &config, + &config.to_models_manager_config(), ) .await .base_instructions; diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index c0776e1ea..0692f7efd 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -105,7 +105,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", &config.to_models_manager_config()) + .await; assert_eq!(model_info.slug, "gpt-5.3-codex-test"); assert_eq!(model_info.base_instructions, specific.base_instructions); @@ -348,7 +350,7 @@ 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, &config.to_models_manager_config()) .await; assert_eq!(model_info.shell_type, ConfigShellToolType::UnifiedExec); @@ -455,7 +457,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, &test.config.to_models_manager_config()) + .await; assert_eq!( model_info.truncation_policy, TruncationPolicyConfig::bytes(/*limit*/ 12_000) @@ -500,7 +504,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, &test.config.to_models_manager_config()) + .await; assert_eq!( model_info.truncation_policy, TruncationPolicyConfig::bytes(/*limit*/ 200) @@ -628,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", &config.to_models_manager_config()) + .await; let body = response_mock.single_request().body_json(); let instructions = body["instructions"].as_str().unwrap(); assert_eq!(instructions, base_model_info.base_instructions); @@ -968,8 +976,8 @@ 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 = codex_models_manager::bundled_models_response() + .unwrap_or_else(|err| panic!("bundled models.json should parse: {err}")); response .models .first() diff --git a/codex-rs/core/tests/suite/search_tool.rs b/codex-rs/core/tests/suite/search_tool.rs index a9f4c3e44..79fe4bff4 100644 --- a/codex-rs/core/tests/suite/search_tool.rs +++ b/codex-rs/core/tests/suite/search_tool.rs @@ -5,7 +5,6 @@ use anyhow::Result; use codex_core::config::Config; use codex_features::Feature; use codex_login::CodexAuth; -use codex_protocol::openai_models::ModelsResponse; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::McpInvocation; @@ -94,8 +93,8 @@ fn configure_apps_without_tool_search(config: &mut Config, apps_base_url: &str) config.chatgpt_base_url = apps_base_url.to_string(); 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"); + let mut model_catalog = codex_models_manager::bundled_models_response() + .unwrap_or_else(|err| panic!("bundled models.json should parse: {err}")); let model = model_catalog .models .iter_mut() diff --git a/codex-rs/core/tests/suite/tool_suggest.rs b/codex-rs/core/tests/suite/tool_suggest.rs index a65c58587..73c97838b 100644 --- a/codex-rs/core/tests/suite/tool_suggest.rs +++ b/codex-rs/core/tests/suite/tool_suggest.rs @@ -7,7 +7,6 @@ use codex_config::types::ToolSuggestDiscoverableType; use codex_core::config::Config; use codex_features::Feature; use codex_login::CodexAuth; -use codex_protocol::openai_models::ModelsResponse; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::SandboxPolicy; use core_test_support::apps_test_server::AppsTestServer; @@ -78,8 +77,8 @@ fn configure_apps_without_search_tool(config: &mut Config, apps_base_url: &str) id: DISCOVERABLE_GMAIL_ID.to_string(), }]; - let mut model_catalog: ModelsResponse = - serde_json::from_str(include_str!("../../models.json")).expect("valid models.json"); + let mut model_catalog = codex_models_manager::bundled_models_response() + .unwrap_or_else(|err| panic!("bundled models.json should parse: {err}")); let model = model_catalog .models .iter_mut() diff --git a/codex-rs/exec-server/src/server/transport_tests.rs b/codex-rs/exec-server/src/server/transport_tests.rs index b81e82727..bec91c936 100644 --- a/codex-rs/exec-server/src/server/transport_tests.rs +++ b/codex-rs/exec-server/src/server/transport_tests.rs @@ -1,3 +1,5 @@ +use std::net::SocketAddr; + use pretty_assertions::assert_eq; use super::DEFAULT_LISTEN_URL; @@ -9,7 +11,9 @@ fn parse_listen_url_accepts_default_websocket_url() { parse_listen_url(DEFAULT_LISTEN_URL).expect("default listen URL should parse"); assert_eq!( bind_address, - "127.0.0.1:0".parse().expect("valid socket address") + "127.0.0.1:0" + .parse::() + .expect("valid socket address") ); } @@ -19,7 +23,9 @@ fn parse_listen_url_accepts_websocket_url() { parse_listen_url("ws://127.0.0.1:1234").expect("websocket listen URL should parse"); assert_eq!( bind_address, - "127.0.0.1:1234".parse().expect("valid socket address") + "127.0.0.1:1234" + .parse::() + .expect("valid socket address") ); } diff --git a/codex-rs/feedback/Cargo.toml b/codex-rs/feedback/Cargo.toml index dd8795c8c..94af597f6 100644 --- a/codex-rs/feedback/Cargo.toml +++ b/codex-rs/feedback/Cargo.toml @@ -9,6 +9,7 @@ workspace = true [dependencies] anyhow = { workspace = true } +codex-login = { workspace = true } codex-protocol = { workspace = true } sentry = { version = "0.46" } tracing = { workspace = true } diff --git a/codex-rs/feedback/src/lib.rs b/codex-rs/feedback/src/lib.rs index 9b2cf33d8..705fed6ba 100644 --- a/codex-rs/feedback/src/lib.rs +++ b/codex-rs/feedback/src/lib.rs @@ -11,6 +11,7 @@ use std::time::Duration; use anyhow::Result; use anyhow::anyhow; +use codex_login::AuthEnvTelemetry; use codex_protocol::ThreadId; use codex_protocol::protocol::SessionSource; use feedback_diagnostics::FEEDBACK_DIAGNOSTICS_ATTACHMENT_FILENAME; @@ -32,6 +33,128 @@ const UPLOAD_TIMEOUT_SECS: u64 = 10; const FEEDBACK_TAGS_TARGET: &str = "feedback_tags"; const MAX_FEEDBACK_TAGS: usize = 64; +/// Structured request/auth fields that should be attached to feedback uploads. +pub struct FeedbackRequestTags<'a> { + pub endpoint: &'a str, + pub auth_header_attached: bool, + pub auth_header_name: Option<&'a str>, + pub auth_mode: Option<&'a str>, + pub auth_retry_after_unauthorized: Option, + pub auth_recovery_mode: Option<&'a str>, + pub auth_recovery_phase: Option<&'a str>, + pub auth_connection_reused: Option, + pub auth_request_id: Option<&'a str>, + pub auth_cf_ray: Option<&'a str>, + pub auth_error: Option<&'a str>, + pub auth_error_code: Option<&'a str>, + pub auth_recovery_followup_success: Option, + pub 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 fn emit_feedback_request_tags(tags: &FeedbackRequestTags<'_>) { + let snapshot = FeedbackRequestSnapshot::from_tags(tags); + tracing::info!( + target: FEEDBACK_TAGS_TARGET, + 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), + ); +} + +pub fn emit_feedback_request_tags_with_auth_env( + tags: &FeedbackRequestTags<'_>, + auth_env: &AuthEnvTelemetry, +) { + let snapshot = FeedbackRequestSnapshot::from_tags(tags); + tracing::info!( + target: FEEDBACK_TAGS_TARGET, + 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), + // Custom provider `env_key` is arbitrary config text, so emit only a safe bucket. + 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 + ), + ); +} + #[derive(Clone)] pub struct CodexFeedback { inner: Arc, diff --git a/codex-rs/login/Cargo.toml b/codex-rs/login/Cargo.toml index 92ed39a6c..459d76c38 100644 --- a/codex-rs/login/Cargo.toml +++ b/codex-rs/login/Cargo.toml @@ -12,9 +12,12 @@ async-trait = { workspace = true } base64 = { workspace = true } chrono = { workspace = true, features = ["serde"] } codex-app-server-protocol = { workspace = true } +codex-api = { workspace = true } codex-client = { workspace = true } codex-config = { workspace = true } codex-keyring-store = { workspace = true } +codex-model-provider-info = { workspace = true } +codex-otel = { workspace = true } codex-protocol = { workspace = true } codex-terminal-detection = { workspace = true } codex-utils-template = { workspace = true } diff --git a/codex-rs/login/src/api_bridge.rs b/codex-rs/login/src/api_bridge.rs new file mode 100644 index 000000000..ebb451e16 --- /dev/null +++ b/codex-rs/login/src/api_bridge.rs @@ -0,0 +1,38 @@ +use codex_model_provider_info::ModelProviderInfo; + +pub use codex_api::api_bridge::CoreAuthProvider; +pub use codex_api::api_bridge::map_api_error; + +use crate::CodexAuth; + +pub fn auth_provider_from_auth( + auth: Option, + provider: &ModelProviderInfo, +) -> codex_protocol::error::Result { + if let Some(api_key) = provider.api_key()? { + return Ok(CoreAuthProvider { + token: Some(api_key), + account_id: None, + }); + } + + if let Some(token) = provider.experimental_bearer_token.clone() { + return Ok(CoreAuthProvider { + token: Some(token), + account_id: None, + }); + } + + if let Some(auth) = auth { + let token = auth.get_token()?; + Ok(CoreAuthProvider { + token: Some(token), + account_id: auth.get_account_id(), + }) + } else { + Ok(CoreAuthProvider { + token: None, + account_id: None, + }) + } +} diff --git a/codex-rs/login/src/auth/auth_tests.rs b/codex-rs/login/src/auth/auth_tests.rs index fb7da63e7..2388e5c15 100644 --- a/codex-rs/login/src/auth/auth_tests.rs +++ b/codex-rs/login/src/auth/auth_tests.rs @@ -2,9 +2,9 @@ use super::*; use crate::auth::storage::FileAuthStorage; use crate::auth::storage::get_auth_file; use crate::token_data::IdTokenInfo; -use crate::token_data::KnownPlan as InternalKnownPlan; -use crate::token_data::PlanType as InternalPlanType; use codex_protocol::account::PlanType as AccountPlanType; +use codex_protocol::auth::KnownPlan as InternalKnownPlan; +use codex_protocol::auth::PlanType as InternalPlanType; use base64::Engine; use codex_protocol::config_types::ForcedLoginMethod; diff --git a/codex-rs/login/src/auth/error.rs b/codex-rs/login/src/auth/error.rs index fcbd4c709..ec8a3790f 100644 --- a/codex-rs/login/src/auth/error.rs +++ b/codex-rs/login/src/auth/error.rs @@ -1,25 +1,2 @@ -use thiserror::Error; - -#[derive(Debug, Clone, PartialEq, Eq, Error)] -#[error("{message}")] -pub struct RefreshTokenFailedError { - pub reason: RefreshTokenFailedReason, - pub message: String, -} - -impl RefreshTokenFailedError { - pub fn new(reason: RefreshTokenFailedReason, message: impl Into) -> Self { - Self { - reason, - message: message.into(), - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RefreshTokenFailedReason { - Expired, - Exhausted, - Revoked, - Other, -} +pub use codex_protocol::auth::RefreshTokenFailedError; +pub use codex_protocol::auth::RefreshTokenFailedReason; diff --git a/codex-rs/login/src/auth/manager.rs b/codex-rs/login/src/auth/manager.rs index 632d92b3d..286966558 100644 --- a/codex-rs/login/src/auth/manager.rs +++ b/codex-rs/login/src/auth/manager.rs @@ -19,21 +19,21 @@ use codex_protocol::config_types::ForcedLoginMethod; use codex_protocol::config_types::ModelProviderAuthInfo; use super::external_bearer::BearerTokenRefresher; -use crate::auth::error::RefreshTokenFailedError; -use crate::auth::error::RefreshTokenFailedReason; pub use crate::auth::storage::AuthCredentialsStoreMode; pub use crate::auth::storage::AuthDotJson; use crate::auth::storage::AuthStorageBackend; use crate::auth::storage::create_auth_storage; use crate::auth::util::try_parse_error_message; use crate::default_client::create_client; -use crate::token_data::KnownPlan as InternalKnownPlan; -use crate::token_data::PlanType as InternalPlanType; use crate::token_data::TokenData; use crate::token_data::parse_chatgpt_jwt_claims; use crate::token_data::parse_jwt_expiration; use codex_client::CodexHttpClient; use codex_protocol::account::PlanType as AccountPlanType; +use codex_protocol::auth::KnownPlan as InternalKnownPlan; +use codex_protocol::auth::PlanType as InternalPlanType; +use codex_protocol::auth::RefreshTokenFailedError; +use codex_protocol::auth::RefreshTokenFailedReason; use serde_json::Value; use thiserror::Error; diff --git a/codex-rs/core/src/auth_env_telemetry.rs b/codex-rs/login/src/auth_env_telemetry.rs similarity index 75% rename from codex-rs/core/src/auth_env_telemetry.rs rename to codex-rs/login/src/auth_env_telemetry.rs index b87dd42cc..a17bceb02 100644 --- a/codex-rs/core/src/auth_env_telemetry.rs +++ b/codex-rs/login/src/auth_env_telemetry.rs @@ -1,22 +1,22 @@ use codex_otel::AuthEnvTelemetryMetadata; -use crate::model_provider_info::ModelProviderInfo; -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::CODEX_API_KEY_ENV_VAR; +use crate::ModelProviderInfo; +use crate::OPENAI_API_KEY_ENV_VAR; +use crate::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR; #[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 struct AuthEnvTelemetry { + pub openai_api_key_env_present: bool, + pub codex_api_key_env_present: bool, + pub codex_api_key_env_enabled: bool, + pub provider_env_key_name: Option, + pub provider_env_key_present: Option, + pub refresh_token_url_override_present: bool, } impl AuthEnvTelemetry { - pub(crate) fn to_otel_metadata(&self) -> AuthEnvTelemetryMetadata { + pub fn to_otel_metadata(&self) -> AuthEnvTelemetryMetadata { AuthEnvTelemetryMetadata { openai_api_key_env_present: self.openai_api_key_env_present, codex_api_key_env_present: self.codex_api_key_env_present, @@ -28,7 +28,7 @@ impl AuthEnvTelemetry { } } -pub(crate) fn collect_auth_env_telemetry( +pub fn collect_auth_env_telemetry( provider: &ModelProviderInfo, codex_api_key_env_enabled: bool, ) -> AuthEnvTelemetry { @@ -36,7 +36,6 @@ pub(crate) fn collect_auth_env_telemetry( 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, - // Custom provider `env_key` is arbitrary config text, so emit only a safe bucket. 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), @@ -54,6 +53,7 @@ fn env_var_present(name: &str) -> bool { #[cfg(test)] mod tests { use super::*; + use crate::WireApi; use pretty_assertions::assert_eq; #[test] @@ -65,7 +65,7 @@ mod tests { env_key_instructions: None, experimental_bearer_token: None, auth: 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/login/src/lib.rs b/codex-rs/login/src/lib.rs index 1ea1c0cb4..d74cc0c7f 100644 --- a/codex-rs/login/src/lib.rs +++ b/codex-rs/login/src/lib.rs @@ -1,4 +1,7 @@ +pub mod api_bridge; pub mod auth; +pub mod auth_env_telemetry; +pub mod provider_auth; pub mod token_data; mod device_code_auth; @@ -15,6 +18,9 @@ pub use server::ServerOptions; pub use server::ShutdownHandle; pub use server::run_login_server; +pub use api_bridge::CoreAuthProvider; +pub use api_bridge::auth_provider_from_auth; +pub use api_bridge::map_api_error; pub use auth::AuthConfig; pub use auth::AuthCredentialsStoreMode; pub use auth::AuthDotJson; @@ -38,5 +44,19 @@ pub use auth::login_with_api_key; pub use auth::logout; pub use auth::read_openai_api_key_from_env; pub use auth::save_auth; +pub use auth_env_telemetry::AuthEnvTelemetry; +pub use auth_env_telemetry::collect_auth_env_telemetry; pub use codex_app_server_protocol::AuthMode; +pub use codex_model_provider_info as model_provider_info; +pub use codex_model_provider_info::DEFAULT_LMSTUDIO_PORT; +pub use codex_model_provider_info::DEFAULT_OLLAMA_PORT; +pub use codex_model_provider_info::LMSTUDIO_OSS_PROVIDER_ID; +pub use codex_model_provider_info::ModelProviderInfo; +pub use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID; +pub use codex_model_provider_info::OPENAI_PROVIDER_ID; +pub use codex_model_provider_info::WireApi; +pub use codex_model_provider_info::built_in_model_providers; +pub use codex_model_provider_info::create_oss_provider_with_base_url; +pub use provider_auth::auth_manager_for_provider; +pub use provider_auth::required_auth_manager_for_provider; pub use token_data::TokenData; diff --git a/codex-rs/core/src/provider_auth.rs b/codex-rs/login/src/provider_auth.rs similarity index 83% rename from codex-rs/core/src/provider_auth.rs rename to codex-rs/login/src/provider_auth.rs index 09f8b273c..5a6719760 100644 --- a/codex-rs/core/src/provider_auth.rs +++ b/codex-rs/login/src/provider_auth.rs @@ -1,12 +1,12 @@ use std::sync::Arc; -use crate::model_provider_info::ModelProviderInfo; -use codex_login::AuthManager; +use crate::AuthManager; +use crate::ModelProviderInfo; /// Returns the provider-scoped auth manager when this provider uses command-backed auth. /// /// Providers without custom auth continue using the caller-supplied base manager. -pub(crate) fn auth_manager_for_provider( +pub fn auth_manager_for_provider( auth_manager: Option>, provider: &ModelProviderInfo, ) -> Option> { @@ -20,7 +20,7 @@ pub(crate) fn auth_manager_for_provider( /// /// Providers with command-backed auth get a bearer-only manager; otherwise the caller's manager /// is reused unchanged. -pub(crate) fn required_auth_manager_for_provider( +pub fn required_auth_manager_for_provider( auth_manager: Arc, provider: &ModelProviderInfo, ) -> Arc { diff --git a/codex-rs/login/src/token_data.rs b/codex-rs/login/src/token_data.rs index 6d05187d8..f70696d5a 100644 --- a/codex-rs/login/src/token_data.rs +++ b/codex-rs/login/src/token_data.rs @@ -1,6 +1,7 @@ use base64::Engine; use chrono::DateTime; use chrono::Utc; +use codex_protocol::auth::PlanType; use serde::Deserialize; use serde::Serialize; use serde::de::DeserializeOwned; @@ -61,95 +62,6 @@ impl IdTokenInfo { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(untagged)] -pub enum PlanType { - Known(KnownPlan), - Unknown(String), -} - -impl PlanType { - pub fn from_raw_value(raw: &str) -> Self { - match raw.to_ascii_lowercase().as_str() { - "free" => Self::Known(KnownPlan::Free), - "go" => Self::Known(KnownPlan::Go), - "plus" => Self::Known(KnownPlan::Plus), - "pro" => Self::Known(KnownPlan::Pro), - "team" => Self::Known(KnownPlan::Team), - "self_serve_business_usage_based" => { - Self::Known(KnownPlan::SelfServeBusinessUsageBased) - } - "business" => Self::Known(KnownPlan::Business), - "enterprise_cbp_usage_based" => Self::Known(KnownPlan::EnterpriseCbpUsageBased), - "enterprise" | "hc" => Self::Known(KnownPlan::Enterprise), - "education" | "edu" => Self::Known(KnownPlan::Edu), - _ => Self::Unknown(raw.to_string()), - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum KnownPlan { - Free, - Go, - Plus, - Pro, - Team, - #[serde(rename = "self_serve_business_usage_based")] - SelfServeBusinessUsageBased, - Business, - #[serde(rename = "enterprise_cbp_usage_based")] - EnterpriseCbpUsageBased, - #[serde(alias = "hc")] - Enterprise, - Edu, -} - -impl KnownPlan { - pub fn display_name(self) -> &'static str { - match self { - Self::Free => "Free", - Self::Go => "Go", - Self::Plus => "Plus", - Self::Pro => "Pro", - Self::Team => "Team", - Self::SelfServeBusinessUsageBased => "Self Serve Business Usage Based", - Self::Business => "Business", - Self::EnterpriseCbpUsageBased => "Enterprise CBP Usage Based", - Self::Enterprise => "Enterprise", - Self::Edu => "Edu", - } - } - - pub fn raw_value(self) -> &'static str { - match self { - Self::Free => "free", - Self::Go => "go", - Self::Plus => "plus", - Self::Pro => "pro", - Self::Team => "team", - Self::SelfServeBusinessUsageBased => "self_serve_business_usage_based", - Self::Business => "business", - Self::EnterpriseCbpUsageBased => "enterprise_cbp_usage_based", - Self::Enterprise => "enterprise", - Self::Edu => "edu", - } - } - - pub fn is_workspace_account(self) -> bool { - matches!( - self, - Self::Team - | Self::SelfServeBusinessUsageBased - | Self::Business - | Self::EnterpriseCbpUsageBased - | Self::Enterprise - | Self::Edu - ) - } -} - #[derive(Deserialize)] struct IdClaims { #[serde(default)] diff --git a/codex-rs/login/src/token_data_tests.rs b/codex-rs/login/src/token_data_tests.rs index f15528b48..be160ecd6 100644 --- a/codex-rs/login/src/token_data_tests.rs +++ b/codex-rs/login/src/token_data_tests.rs @@ -1,6 +1,7 @@ use super::*; use chrono::TimeZone; use chrono::Utc; +use codex_protocol::auth::KnownPlan; use pretty_assertions::assert_eq; use serde::Serialize; diff --git a/codex-rs/core/tests/suite/auth_refresh.rs b/codex-rs/login/tests/suite/auth_refresh.rs similarity index 99% rename from codex-rs/core/tests/suite/auth_refresh.rs rename to codex-rs/login/tests/suite/auth_refresh.rs index b4249ea96..94ba8220e 100644 --- a/codex-rs/core/tests/suite/auth_refresh.rs +++ b/codex-rs/login/tests/suite/auth_refresh.rs @@ -4,7 +4,6 @@ use base64::Engine; use chrono::Duration; use chrono::Utc; use codex_app_server_protocol::AuthMode; -use codex_core::error::RefreshTokenFailedReason; use codex_login::AuthCredentialsStoreMode; use codex_login::AuthDotJson; use codex_login::AuthManager; @@ -14,6 +13,7 @@ use codex_login::load_auth_dot_json; use codex_login::save_auth; use codex_login::token_data::IdTokenInfo; use codex_login::token_data::TokenData; +use codex_protocol::auth::RefreshTokenFailedReason; use core_test_support::skip_if_no_network; use pretty_assertions::assert_eq; use serde::Serialize; diff --git a/codex-rs/login/tests/suite/mod.rs b/codex-rs/login/tests/suite/mod.rs index b84b264be..3d1eddd1a 100644 --- a/codex-rs/login/tests/suite/mod.rs +++ b/codex-rs/login/tests/suite/mod.rs @@ -1,3 +1,4 @@ // Aggregates all former standalone integration tests as modules. +mod auth_refresh; mod device_code_login; mod login_server_e2e; diff --git a/codex-rs/model-provider-info/BUILD.bazel b/codex-rs/model-provider-info/BUILD.bazel new file mode 100644 index 000000000..4d5b61127 --- /dev/null +++ b/codex-rs/model-provider-info/BUILD.bazel @@ -0,0 +1,6 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "model-provider-info", + crate_name = "codex_model_provider_info", +) diff --git a/codex-rs/model-provider-info/Cargo.toml b/codex-rs/model-provider-info/Cargo.toml new file mode 100644 index 000000000..23c1b3415 --- /dev/null +++ b/codex-rs/model-provider-info/Cargo.toml @@ -0,0 +1,28 @@ +[package] +edition.workspace = true +license.workspace = true +name = "codex-model-provider-info" +version.workspace = true + +[lib] +doctest = false +name = "codex_model_provider_info" +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +codex-api = { workspace = true } +codex-app-server-protocol = { workspace = true } +codex-protocol = { workspace = true } +http = { workspace = true } +schemars = { workspace = true } +serde = { workspace = true, features = ["derive"] } + +[dev-dependencies] +codex-utils-absolute-path = { workspace = true } +maplit = { workspace = true } +pretty_assertions = { workspace = true } +tempfile = { workspace = true } +toml = { workspace = true } diff --git a/codex-rs/core/src/model_provider_info.rs b/codex-rs/model-provider-info/src/lib.rs similarity index 93% rename from codex-rs/core/src/model_provider_info.rs rename to codex-rs/model-provider-info/src/lib.rs index 231fe73b9..968bcc8ed 100644 --- a/codex-rs/core/src/model_provider_info.rs +++ b/codex-rs/model-provider-info/src/lib.rs @@ -5,11 +5,13 @@ //! 2. User-defined entries inside `~/.codex/config.toml` under the `model_providers` //! key. These override or extend the defaults at runtime. -use crate::error::EnvVarError; use codex_api::Provider as ApiProvider; use codex_api::provider::RetryConfig as ApiRetryConfig; -use codex_login::AuthMode; +use codex_app_server_protocol::AuthMode; use codex_protocol::config_types::ModelProviderAuthInfo; +use codex_protocol::error::CodexErr; +use codex_protocol::error::EnvVarError; +use codex_protocol::error::Result as CodexResult; use http::HeaderMap; use http::header::HeaderName; use http::header::HeaderValue; @@ -23,7 +25,7 @@ use std::time::Duration; const DEFAULT_STREAM_IDLE_TIMEOUT_MS: u64 = 300_000; const DEFAULT_STREAM_MAX_RETRIES: u64 = 5; const DEFAULT_REQUEST_MAX_RETRIES: u64 = 4; -pub(crate) const DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS: u64 = 15_000; +pub const DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS: u64 = 15_000; /// Hard cap for user-configured `stream_max_retries`. const MAX_STREAM_MAX_RETRIES: u64 = 100; /// Hard cap for user-configured `request_max_retries`. @@ -32,8 +34,8 @@ 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"; /// Wire protocol that the provider speaks. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, JsonSchema)] @@ -81,60 +83,48 @@ pub struct ModelProviderInfo { /// Optional instructions to help the user get a valid value for the /// variable and set it. pub env_key_instructions: Option, - /// Value to use with `Authorization: Bearer ` header. Use of this /// config is discouraged in favor of `env_key` for security reasons, but /// this may be necessary when using this programmatically. pub experimental_bearer_token: Option, - /// Command-backed bearer-token configuration for this provider. pub auth: Option, - /// Which wire protocol this provider expects. #[serde(default)] pub wire_api: WireApi, - /// Optional query parameters to append to the base URL. pub query_params: Option>, - /// Additional HTTP headers to include in requests to this provider where /// the (key, value) pairs are the header name and value. pub http_headers: Option>, - /// Optional HTTP headers to include in requests to this provider where the /// (key, value) pairs are the header name and _environment variable_ whose /// value should be used. If the environment variable is not set, or the /// value is empty, the header will not be included in the request. pub env_http_headers: Option>, - /// Maximum number of times to retry a failed HTTP request to this provider. pub request_max_retries: Option, - /// Number of times to retry reconnecting a dropped streaming response before failing. pub stream_max_retries: Option, - /// Idle timeout (in milliseconds) to wait for activity on a streaming response before treating /// the connection as lost. pub stream_idle_timeout_ms: Option, - /// Maximum time (in milliseconds) to wait for a websocket connection attempt before treating /// it as failed. pub websocket_connect_timeout_ms: Option, - /// Does this provider require an OpenAI API Key or ChatGPT login token? If true, /// user is presented with login screen on first run, and login preference and token/key /// are stored in auth.json. If false (which is the default), login screen is skipped, /// and API key (if needed) comes from the "env_key" environment variable. #[serde(default)] pub requires_openai_auth: bool, - /// Whether this provider supports the Responses API WebSocket transport. #[serde(default)] pub supports_websockets: bool, } impl ModelProviderInfo { - pub(crate) fn validate(&self) -> std::result::Result<(), String> { + pub fn validate(&self) -> std::result::Result<(), String> { let Some(auth) = self.auth.as_ref() else { return Ok(()); }; @@ -164,7 +154,7 @@ impl ModelProviderInfo { } } - fn build_header_map(&self) -> crate::error::Result { + fn build_header_map(&self) -> CodexResult { 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); @@ -191,10 +181,7 @@ impl ModelProviderInfo { Ok(headers) } - pub(crate) fn to_api_provider( - &self, - auth_mode: Option, - ) -> crate::error::Result { + pub fn to_api_provider(&self, auth_mode: Option) -> CodexResult { let default_base_url = if matches!(auth_mode, Some(AuthMode::Chatgpt)) { "https://chatgpt.com/backend-api/codex" } else { @@ -227,14 +214,14 @@ impl ModelProviderInfo { /// 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) -> CodexResult> { 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 { + CodexErr::EnvVar(EnvVarError { var: env_key.clone(), instructions: self.env_key_instructions.clone(), }) @@ -313,7 +300,7 @@ impl ModelProviderInfo { self.name == OPENAI_PROVIDER_NAME } - pub(crate) fn has_command_auth(&self) -> bool { + pub fn has_command_auth(&self) -> bool { self.auth.is_some() } } diff --git a/codex-rs/core/src/model_provider_info_tests.rs b/codex-rs/model-provider-info/src/model_provider_info_tests.rs similarity index 100% rename from codex-rs/core/src/model_provider_info_tests.rs rename to codex-rs/model-provider-info/src/model_provider_info_tests.rs diff --git a/codex-rs/models-manager/BUILD.bazel b/codex-rs/models-manager/BUILD.bazel new file mode 100644 index 000000000..17be8aeda --- /dev/null +++ b/codex-rs/models-manager/BUILD.bazel @@ -0,0 +1,10 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "models-manager", + crate_name = "codex_models_manager", + compile_data = [ + "models.json", + "prompt.md", + ], +) diff --git a/codex-rs/models-manager/Cargo.toml b/codex-rs/models-manager/Cargo.toml new file mode 100644 index 000000000..93a9886c6 --- /dev/null +++ b/codex-rs/models-manager/Cargo.toml @@ -0,0 +1,41 @@ +[package] +edition.workspace = true +license.workspace = true +name = "codex-models-manager" +version.workspace = true + +[lib] +doctest = false +name = "codex_models_manager" +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +chrono = { workspace = true, features = ["serde"] } +codex-api = { workspace = true } +codex-collaboration-mode-templates = { workspace = true } +codex-feedback = { workspace = true } +codex-login = { workspace = true } +codex-otel = { workspace = true } +codex-protocol = { workspace = true } +codex-response-debug-context = { workspace = true } +codex-utils-output-truncation = { workspace = true } +codex-utils-template = { workspace = true } +http = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +tokio = { workspace = true, features = ["fs", "sync", "time"] } +tracing = { workspace = true, features = ["log"] } + +[dev-dependencies] +base64 = { workspace = true } +codex-utils-absolute-path = { workspace = true } +core_test_support = { workspace = true } +pretty_assertions = { workspace = true } +serde_json = { workspace = true } +tempfile = { workspace = true } +tracing = { workspace = true, features = ["log"] } +tracing-subscriber = { workspace = true } +wiremock = { workspace = true } diff --git a/codex-rs/core/models.json b/codex-rs/models-manager/models.json similarity index 100% rename from codex-rs/core/models.json rename to codex-rs/models-manager/models.json diff --git a/codex-rs/core/prompt.md b/codex-rs/models-manager/prompt.md similarity index 100% rename from codex-rs/core/prompt.md rename to codex-rs/models-manager/prompt.md diff --git a/codex-rs/core/src/models_manager/cache.rs b/codex-rs/models-manager/src/cache.rs similarity index 100% rename from codex-rs/core/src/models_manager/cache.rs rename to codex-rs/models-manager/src/cache.rs diff --git a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs b/codex-rs/models-manager/src/collaboration_mode_presets.rs similarity index 96% rename from codex-rs/core/src/models_manager/collaboration_mode_presets.rs rename to codex-rs/models-manager/src/collaboration_mode_presets.rs index afb002bce..f67ab91db 100644 --- a/codex-rs/core/src/models_manager/collaboration_mode_presets.rs +++ b/codex-rs/models-manager/src/collaboration_mode_presets.rs @@ -1,3 +1,5 @@ +use codex_collaboration_mode_templates::DEFAULT as COLLABORATION_MODE_DEFAULT; +use codex_collaboration_mode_templates::PLAN as COLLABORATION_MODE_PLAN; use codex_protocol::config_types::CollaborationModeMask; use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::TUI_VISIBLE_COLLABORATION_MODES; @@ -5,9 +7,6 @@ use codex_protocol::openai_models::ReasoningEffort; use codex_utils_template::Template; use std::sync::LazyLock; -const COLLABORATION_MODE_PLAN: &str = include_str!("../../templates/collaboration_mode/plan.md"); -const COLLABORATION_MODE_DEFAULT: &str = - include_str!("../../templates/collaboration_mode/default.md"); const KNOWN_MODE_NAMES_TEMPLATE_KEY: &str = "KNOWN_MODE_NAMES"; const REQUEST_USER_INPUT_AVAILABILITY_TEMPLATE_KEY: &str = "REQUEST_USER_INPUT_AVAILABILITY"; const ASKING_QUESTIONS_GUIDANCE_TEMPLATE_KEY: &str = "ASKING_QUESTIONS_GUIDANCE"; diff --git a/codex-rs/core/src/models_manager/collaboration_mode_presets_tests.rs b/codex-rs/models-manager/src/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-manager/src/collaboration_mode_presets_tests.rs diff --git a/codex-rs/models-manager/src/config.rs b/codex-rs/models-manager/src/config.rs new file mode 100644 index 000000000..b64add40f --- /dev/null +++ b/codex-rs/models-manager/src/config.rs @@ -0,0 +1,12 @@ +use codex_protocol::openai_models::ModelsResponse; + +#[derive(Debug, Clone, Default)] +pub struct ModelsManagerConfig { + 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 model_supports_reasoning_summaries: Option, + pub model_catalog: Option, +} diff --git a/codex-rs/models-manager/src/lib.rs b/codex-rs/models-manager/src/lib.rs new file mode 100644 index 000000000..14db37161 --- /dev/null +++ b/codex-rs/models-manager/src/lib.rs @@ -0,0 +1,30 @@ +pub mod cache; +pub mod collaboration_mode_presets; +pub mod config; +pub mod manager; +pub mod model_info; +pub mod model_presets; + +pub use codex_login::AuthCredentialsStoreMode; +pub use codex_login::AuthManager; +pub use codex_login::AuthMode; +pub use codex_login::CodexAuth; +pub use codex_login::ModelProviderInfo; +pub use codex_login::WireApi; +pub use config::ModelsManagerConfig; + +/// Load the bundled model catalog shipped with `codex-models-manager`. +pub fn bundled_models_response() +-> std::result::Result { + serde_json::from_str(include_str!("../models.json")) +} + +/// 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/models_manager/manager.rs b/codex-rs/models-manager/src/manager.rs similarity index 92% rename from codex-rs/core/src/models_manager/manager.rs rename to codex-rs/models-manager/src/manager.rs index 8f52bab37..9429515a6 100644 --- a/codex-rs/core/src/models_manager/manager.rs +++ b/codex-rs/models-manager/src/manager.rs @@ -1,33 +1,33 @@ use super::cache::ModelsCacheManager; -use crate::api_bridge::auth_provider_from_auth; -use crate::api_bridge::map_api_error; -use crate::auth_env_telemetry::AuthEnvTelemetry; -use crate::auth_env_telemetry::collect_auth_env_telemetry; -use crate::config::Config; -use crate::error::CodexErr; -use crate::error::Result as CoreResult; -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::provider_auth::required_auth_manager_for_provider; -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 crate::collaboration_mode_presets::CollaborationModesConfig; +use crate::collaboration_mode_presets::builtin_collaboration_mode_presets; +use crate::config::ModelsManagerConfig; +use crate::model_info; use codex_api::ModelsClient; use codex_api::RequestTelemetry; use codex_api::ReqwestTransport; use codex_api::TransportError; +use codex_feedback::FeedbackRequestTags; +use codex_feedback::emit_feedback_request_tags_with_auth_env; +use codex_login::AuthEnvTelemetry; use codex_login::AuthManager; use codex_login::AuthMode; use codex_login::CodexAuth; +use codex_login::ModelProviderInfo; +use codex_login::auth_provider_from_auth; +use codex_login::collect_auth_env_telemetry; use codex_login::default_client::build_reqwest_client; +use codex_login::map_api_error; +use codex_login::required_auth_manager_for_provider; use codex_otel::TelemetryAuthMode; use codex_protocol::config_types::CollaborationModeMask; +use codex_protocol::error::CodexErr; +use codex_protocol::error::Result as CoreResult; use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::ModelsResponse; +use codex_response_debug_context::extract_response_debug_context; +use codex_response_debug_context::telemetry_transport_error_message; use http::HeaderMap; use std::fmt; use std::path::PathBuf; @@ -223,10 +223,7 @@ impl ModelsManager { }; let remote_models = model_catalog .map(|catalog| catalog.models) - .unwrap_or_else(|| { - Self::load_remote_models_from_file() - .unwrap_or_else(|err| panic!("failed to load bundled models.json: {err}")) - }); + .unwrap_or_else(|| Self::load_remote_models_from_file().unwrap_or_default()); Self { remote_models: RwLock::new(remote_models), catalog_mode, @@ -313,7 +310,7 @@ 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 { + pub async fn get_model_info(&self, model: &str, config: &ModelsManagerConfig) -> ModelInfo { let remote_models = self.get_remote_models().await; Self::construct_model_info_from_candidates(model, &remote_models, config) } @@ -357,7 +354,7 @@ impl ModelsManager { fn construct_model_info_from_candidates( model: &str, candidates: &[ModelInfo], - config: &Config, + config: &ModelsManagerConfig, ) -> 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`. @@ -378,7 +375,7 @@ impl ModelsManager { /// 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 { @@ -453,7 +450,7 @@ 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()), @@ -491,16 +488,14 @@ impl ModelsManager { } fn load_remote_models_from_file() -> Result, std::io::Error> { - let file_contents = include_str!("../../models.json"); - let response: ModelsResponse = serde_json::from_str(file_contents)?; - Ok(response.models) + Ok(crate::bundled_models_response()?.models) } /// Attempt to satisfy the refresh from the cache when it matches the provider and TTL. 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, @@ -542,7 +537,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, @@ -557,7 +552,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(); } @@ -573,9 +568,9 @@ 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, + config: &ModelsManagerConfig, ) -> ModelInfo { let candidates: &[ModelInfo] = if let Some(model_catalog) = config.model_catalog.as_ref() { &model_catalog.models diff --git a/codex-rs/core/src/models_manager/manager_tests.rs b/codex-rs/models-manager/src/manager_tests.rs similarity index 95% rename from codex-rs/core/src/models_manager/manager_tests.rs rename to codex-rs/models-manager/src/manager_tests.rs index 48c01f198..7b6dfe182 100644 --- a/codex-rs/core/src/models_manager/manager_tests.rs +++ b/codex-rs/models-manager/src/manager_tests.rs @@ -1,14 +1,15 @@ use super::*; -use crate::config::ConfigBuilder; -use crate::model_provider_info::WireApi; +use crate::ModelsManagerConfig; 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_login::WireApi; use codex_protocol::config_types::ModelProviderAuthInfo; use codex_protocol::openai_models::ModelsResponse; +use codex_utils_absolute_path::AbsolutePathBuf; use core_test_support::responses::mount_models_once; use http::HeaderMap; use http::StatusCode; @@ -35,6 +36,9 @@ use wiremock::matchers::header_regex; use wiremock::matchers::method; use wiremock::matchers::path; +#[path = "model_info_overrides_tests.rs"] +mod model_info_overrides_tests; + fn remote_model(slug: &str, display: &str, priority: i32) -> ModelInfo { remote_model_with_visibility(slug, display, priority, "list") } @@ -189,7 +193,7 @@ move /y tokens.next tokens.txt >nul args: self.args.clone(), timeout_ms: NonZeroU64::new(timeout_ms).unwrap(), refresh_interval_ms: 60_000, - cwd: match codex_utils_absolute_path::AbsolutePathBuf::try_from(self.tempdir.path()) { + cwd: match AbsolutePathBuf::try_from(self.tempdir.path()) { Ok(cwd) => cwd, Err(err) => panic!("tempdir should be absolute: {err}"), }, @@ -241,11 +245,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 config = ModelsManagerConfig::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(), @@ -275,11 +275,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 config = ModelsManagerConfig::default(); let mut overlay = remote_model("gpt-overlay", "Overlay", /*priority*/ 0); overlay.supports_image_detail_original = true; @@ -308,11 +304,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 config = ModelsManagerConfig::default(); let mut remote = remote_model("gpt-image", "Image", /*priority*/ 0); remote.supports_image_detail_original = true; let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); @@ -336,11 +328,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 config = ModelsManagerConfig::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(), @@ -593,7 +581,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 @@ -754,7 +742,7 @@ fn models_request_telemetry_emits_auth_env_feedback_tags_on_failure() { auth_mode: Some(TelemetryAuthMode::Chatgpt.to_string()), auth_header_attached: true, auth_header_name: Some("authorization"), - auth_env: crate::auth_env_telemetry::AuthEnvTelemetry { + auth_env: codex_login::AuthEnvTelemetry { openai_api_key_env_present: false, codex_api_key_env_present: false, codex_api_key_env_enabled: false, @@ -868,9 +856,8 @@ fn build_available_models_picks_default_after_hiding_hidden_models() { #[test] fn bundled_models_json_roundtrips() { - let file_contents = include_str!("../../models.json"); - let response: ModelsResponse = - serde_json::from_str(file_contents).expect("bundled models.json should deserialize"); + let response = crate::bundled_models_response() + .unwrap_or_else(|err| panic!("bundled models.json should parse: {err}")); let serialized = serde_json::to_string(&response).expect("bundled models.json should serialize"); diff --git a/codex-rs/core/src/models_manager/model_info.rs b/codex-rs/models-manager/src/model_info.rs similarity index 93% rename from codex-rs/core/src/models_manager/model_info.rs rename to codex-rs/models-manager/src/model_info.rs index 055a6459d..b176af720 100644 --- a/codex-rs/core/src/models_manager/model_info.rs +++ b/codex-rs/models-manager/src/model_info.rs @@ -9,19 +9,18 @@ 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 codex_features::Feature; +use crate::config::ModelsManagerConfig; use codex_utils_output_truncation::approx_bytes_for_tokens; use tracing::warn; -pub const BASE_INSTRUCTIONS: &str = include_str!("../../prompt.md"); +pub const BASE_INSTRUCTIONS: &str = include_str!("../prompt.md"); 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 { +pub fn with_config_overrides(mut model: ModelInfo, config: &ModelsManagerConfig) -> ModelInfo { if let Some(supports_reasoning_summaries) = config.model_supports_reasoning_summaries && supports_reasoning_summaries { @@ -50,7 +49,7 @@ pub(crate) fn with_config_overrides(mut model: ModelInfo, config: &Config) -> Mo if let Some(base_instructions) = &config.base_instructions { model.base_instructions = base_instructions.clone(); model.model_messages = None; - } else if !config.features.enabled(Feature::Personality) { + } else if !config.personality_enabled { model.model_messages = None; } @@ -58,7 +57,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(), diff --git a/codex-rs/core/tests/suite/model_info_overrides.rs b/codex-rs/models-manager/src/model_info_overrides_tests.rs similarity index 64% rename from codex-rs/core/tests/suite/model_info_overrides.rs rename to codex-rs/models-manager/src/model_info_overrides_tests.rs index 52656affe..1e5b5dcf4 100644 --- a/codex-rs/core/tests/suite/model_info_overrides.rs +++ b/codex-rs/models-manager/src/model_info_overrides_tests.rs @@ -1,20 +1,21 @@ -use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig; -use codex_core::models_manager::manager::ModelsManager; +use codex_login::AuthManager; use codex_login::CodexAuth; + +use crate::ModelsManagerConfig; +use crate::collaboration_mode_presets::CollaborationModesConfig; +use crate::manager::ModelsManager; use codex_protocol::openai_models::TruncationPolicyConfig; -use core_test_support::load_default_config_for_test; use pretty_assertions::assert_eq; use tempfile::TempDir; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn offline_model_info_without_tool_output_override() { let codex_home = TempDir::new().expect("create temp dir"); - let config = load_default_config_for_test(&codex_home).await; - let auth_manager = codex_core::test_support::auth_manager_from_auth( - CodexAuth::create_dummy_chatgpt_auth_for_testing(), - ); + let config = ModelsManagerConfig::default(); + let auth_manager = + AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); let manager = ModelsManager::new( - config.codex_home.clone(), + codex_home.path().to_path_buf(), auth_manager, /*model_catalog*/ None, CollaborationModesConfig::default(), @@ -31,13 +32,12 @@ async fn offline_model_info_without_tool_output_override() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn offline_model_info_with_tool_output_override() { let codex_home = TempDir::new().expect("create temp dir"); - let mut config = load_default_config_for_test(&codex_home).await; + let mut config = ModelsManagerConfig::default(); config.tool_output_token_limit = Some(123); - let auth_manager = codex_core::test_support::auth_manager_from_auth( - CodexAuth::create_dummy_chatgpt_auth_for_testing(), - ); + let auth_manager = + AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); let manager = ModelsManager::new( - config.codex_home.clone(), + codex_home.path().to_path_buf(), auth_manager, /*model_catalog*/ None, CollaborationModesConfig::default(), diff --git a/codex-rs/core/src/models_manager/model_info_tests.rs b/codex-rs/models-manager/src/model_info_tests.rs similarity index 84% rename from codex-rs/core/src/models_manager/model_info_tests.rs rename to codex-rs/models-manager/src/model_info_tests.rs index 27bac8d70..61dac6d7c 100644 --- a/codex-rs/core/src/models_manager/model_info_tests.rs +++ b/codex-rs/models-manager/src/model_info_tests.rs @@ -1,11 +1,11 @@ use super::*; -use crate::config::test_config; +use crate::ModelsManagerConfig; 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(); + let mut config = ModelsManagerConfig::default(); config.model_supports_reasoning_summaries = Some(true); let updated = with_config_overrides(model.clone(), &config); @@ -19,7 +19,7 @@ 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(); + let mut config = ModelsManagerConfig::default(); config.model_supports_reasoning_summaries = Some(false); let updated = with_config_overrides(model.clone(), &config); @@ -30,7 +30,7 @@ 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(); + let mut config = ModelsManagerConfig::default(); config.model_supports_reasoning_summaries = Some(false); let updated = with_config_overrides(model.clone(), &config); diff --git a/codex-rs/core/src/models_manager/model_presets.rs b/codex-rs/models-manager/src/model_presets.rs similarity index 100% rename from codex-rs/core/src/models_manager/model_presets.rs rename to codex-rs/models-manager/src/model_presets.rs diff --git a/codex-rs/protocol/Cargo.toml b/codex-rs/protocol/Cargo.toml index 58f3e0606..c4249a13b 100644 --- a/codex-rs/protocol/Cargo.toml +++ b/codex-rs/protocol/Cargo.toml @@ -12,16 +12,22 @@ path = "src/lib.rs" workspace = true [dependencies] +chardetng = { workspace = true } +chrono = { workspace = true, features = ["serde"] } +codex-async-utils = { workspace = true } codex-execpolicy = { workspace = true } codex-git-utils = { workspace = true } +codex-network-proxy = { workspace = true } codex-utils-absolute-path = { workspace = true } codex-utils-image = { workspace = true } codex-utils-string = { workspace = true } codex-utils-template = { workspace = true } +encoding_rs = { workspace = true } icu_decimal = { workspace = true } icu_locale_core = { workspace = true } icu_provider = { workspace = true, features = ["sync"] } quick-xml = { workspace = true, features = ["serialize"] } +reqwest = { workspace = true } schemars = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } @@ -29,6 +35,8 @@ serde_with = { workspace = true, features = ["macros", "base64"] } strum = { workspace = true } strum_macros = { workspace = true } sys-locale = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } tracing = { workspace = true } ts-rs = { workspace = true, features = [ "uuid-impl", @@ -37,8 +45,13 @@ ts-rs = { workspace = true, features = [ ] } uuid = { workspace = true, features = ["serde", "v7", "v4"] } +[target.'cfg(target_os = "linux")'.dependencies] +landlock = { workspace = true } +seccompiler = { workspace = true } + [dev-dependencies] anyhow = { workspace = true } +http = { workspace = true } pretty_assertions = { workspace = true } tempfile = { workspace = true } diff --git a/codex-rs/protocol/src/auth.rs b/codex-rs/protocol/src/auth.rs new file mode 100644 index 000000000..11455ec90 --- /dev/null +++ b/codex-rs/protocol/src/auth.rs @@ -0,0 +1,116 @@ +use serde::Deserialize; +use serde::Serialize; +use thiserror::Error; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum PlanType { + Known(KnownPlan), + Unknown(String), +} + +impl PlanType { + pub fn from_raw_value(raw: &str) -> Self { + match raw.to_ascii_lowercase().as_str() { + "free" => Self::Known(KnownPlan::Free), + "go" => Self::Known(KnownPlan::Go), + "plus" => Self::Known(KnownPlan::Plus), + "pro" => Self::Known(KnownPlan::Pro), + "team" => Self::Known(KnownPlan::Team), + "self_serve_business_usage_based" => { + Self::Known(KnownPlan::SelfServeBusinessUsageBased) + } + "business" => Self::Known(KnownPlan::Business), + "enterprise_cbp_usage_based" => Self::Known(KnownPlan::EnterpriseCbpUsageBased), + "enterprise" | "hc" => Self::Known(KnownPlan::Enterprise), + "education" | "edu" => Self::Known(KnownPlan::Edu), + _ => Self::Unknown(raw.to_string()), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum KnownPlan { + Free, + Go, + Plus, + Pro, + Team, + #[serde(rename = "self_serve_business_usage_based")] + SelfServeBusinessUsageBased, + Business, + #[serde(rename = "enterprise_cbp_usage_based")] + EnterpriseCbpUsageBased, + #[serde(alias = "hc")] + Enterprise, + Edu, +} + +impl KnownPlan { + pub fn display_name(self) -> &'static str { + match self { + Self::Free => "Free", + Self::Go => "Go", + Self::Plus => "Plus", + Self::Pro => "Pro", + Self::Team => "Team", + Self::SelfServeBusinessUsageBased => "Self Serve Business Usage Based", + Self::Business => "Business", + Self::EnterpriseCbpUsageBased => "Enterprise CBP Usage Based", + Self::Enterprise => "Enterprise", + Self::Edu => "Edu", + } + } + + pub fn raw_value(self) -> &'static str { + match self { + Self::Free => "free", + Self::Go => "go", + Self::Plus => "plus", + Self::Pro => "pro", + Self::Team => "team", + Self::SelfServeBusinessUsageBased => "self_serve_business_usage_based", + Self::Business => "business", + Self::EnterpriseCbpUsageBased => "enterprise_cbp_usage_based", + Self::Enterprise => "enterprise", + Self::Edu => "edu", + } + } + + pub fn is_workspace_account(self) -> bool { + matches!( + self, + Self::Team + | Self::SelfServeBusinessUsageBased + | Self::Business + | Self::EnterpriseCbpUsageBased + | Self::Enterprise + | Self::Edu + ) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Error)] +#[error("{message}")] +pub struct RefreshTokenFailedError { + pub reason: RefreshTokenFailedReason, + pub message: String, +} + +impl RefreshTokenFailedError { + pub fn new(reason: RefreshTokenFailedReason, message: impl Into) -> Self { + Self { + reason, + message: message.into(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RefreshTokenFailedReason { + Expired, + Exhausted, + Revoked, + Other, +} diff --git a/codex-rs/core/src/error.rs b/codex-rs/protocol/src/error.rs similarity index 95% rename from codex-rs/core/src/error.rs rename to codex-rs/protocol/src/error.rs index f8058915b..3ffdeb48e 100644 --- a/codex-rs/core/src/error.rs +++ b/codex-rs/protocol/src/error.rs @@ -1,20 +1,21 @@ -use crate::exec::ExecToolCallOutput; -use crate::network_policy_decision::NetworkPolicyDecisionPayload; +use crate::ThreadId; +use crate::auth::KnownPlan; +use crate::auth::PlanType; +pub use crate::auth::RefreshTokenFailedError; +pub use crate::auth::RefreshTokenFailedReason; +use crate::exec_output::ExecToolCallOutput; +use crate::network_policy::NetworkPolicyDecisionPayload; +use crate::protocol::CodexErrorInfo; +use crate::protocol::ErrorEvent; +use crate::protocol::RateLimitSnapshot; +use crate::protocol::TruncationPolicy; use chrono::DateTime; use chrono::Datelike; use chrono::Local; use chrono::Utc; use codex_async_utils::CancelErr; -pub use codex_login::auth::RefreshTokenFailedError; -pub use codex_login::auth::RefreshTokenFailedReason; -use codex_login::token_data::KnownPlan; -use codex_login::token_data::PlanType; -use codex_protocol::ThreadId; -use codex_protocol::protocol::CodexErrorInfo; -use codex_protocol::protocol::ErrorEvent; -use codex_protocol::protocol::RateLimitSnapshot; -use codex_utils_output_truncation::TruncationPolicy; -use codex_utils_output_truncation::truncate_text; +use codex_utils_string::truncate_middle_chars; +use codex_utils_string::truncate_middle_with_token_budget; use reqwest::StatusCode; use serde_json; use std::io; @@ -25,7 +26,7 @@ use tokio::task::JoinError; pub type Result = std::result::Result; /// Limit UI error messages to a reasonable size while keeping useful context. -const ERROR_MESSAGE_UI_MAX_BYTES: usize = 2 * 1024; // 2 KiB +const ERROR_MESSAGE_UI_MAX_BYTES: usize = 2 * 1024; #[derive(Error, Debug)] pub enum SandboxErr { @@ -75,114 +76,84 @@ pub enum CodexErr { /// Optionally includes the requested delay before retrying the turn. #[error("stream disconnected before completion: {0}")] Stream(String, Option), - #[error( "Codex ran out of room in the model's context window. Start a new thread or clear earlier history before retrying." )] ContextWindowExceeded, - #[error("no thread with id: {0}")] ThreadNotFound(ThreadId), - #[error("agent thread limit reached (max {max_threads})")] AgentLimitReached { max_threads: usize }, - #[error("session configured event was not the first event in the stream")] SessionConfiguredNotFirstEvent, - /// Returned by run_command_stream when the spawned child process timed out (10s). #[error("timeout waiting for child process to exit")] Timeout, - /// Returned by run_command_stream when the child could not be spawned (its stdout/stderr pipes /// could not be captured). Analogous to the previous `CodexError::Spawn` variant. #[error("spawn failed: child stdout/stderr not captured")] Spawn, - - /// Returned by run_command_stream when the user pressed Ctrl‑C (SIGINT). Session uses this to + /// Returned by run_command_stream when the user pressed Ctrl-C (SIGINT). Session uses this to /// surface a polite FunctionCallOutput back to the model instead of crashing the CLI. #[error("interrupted (Ctrl-C). Something went wrong? Hit `/feedback` to report the issue.")] Interrupted, - /// Unexpected HTTP status code. #[error("{0}")] UnexpectedStatus(UnexpectedResponseError), - /// Invalid request. #[error("{0}")] InvalidRequest(String), - /// Invalid image. #[error("Image poisoning")] InvalidImageRequest(), - #[error("{0}")] UsageLimitReached(UsageLimitReachedError), - #[error("Selected model is at capacity. Please try a different model.")] ServerOverloaded, - #[error("{0}")] ResponseStreamFailed(ResponseStreamFailed), - #[error("{0}")] ConnectionFailed(ConnectionFailedError), - #[error("Quota exceeded. Check your plan and billing details.")] QuotaExceeded, - #[error( "To use Codex with your ChatGPT plan, upgrade to Plus: https://chatgpt.com/explore/plus." )] UsageNotIncluded, - #[error("We're currently experiencing high demand, which may cause temporary errors.")] InternalServerError, - /// Retry limit exceeded. #[error("{0}")] RetryLimit(RetryLimitReachedError), - /// Agent loop died unexpectedly #[error("internal error; agent loop died unexpectedly")] InternalAgentDied, - /// Sandbox error #[error("sandbox error: {0}")] Sandbox(#[from] SandboxErr), - #[error("codex-linux-sandbox was required but not provided")] LandlockSandboxExecutableNotProvided, - #[error("unsupported operation: {0}")] UnsupportedOperation(String), - #[error("{0}")] RefreshTokenFailed(RefreshTokenFailedError), - #[error("Fatal error: {0}")] Fatal(String), - // ----------------------------------------------------------------- // Automatic conversions for common external error types // ----------------------------------------------------------------- #[error(transparent)] Io(#[from] io::Error), - #[error(transparent)] Json(#[from] serde_json::Error), - #[cfg(target_os = "linux")] #[error(transparent)] LandlockRuleset(#[from] landlock::RulesetError), - #[cfg(target_os = "linux")] #[error(transparent)] LandlockPathFd(#[from] landlock::PathFdError), - #[error(transparent)] TokioJoin(#[from] JoinError), - #[error("{0}")] EnvVar(EnvVarError), } @@ -230,6 +201,65 @@ impl CodexErr { CodexErr::LandlockRuleset(_) | CodexErr::LandlockPathFd(_) => false, } } + + /// Minimal shim so that existing `e.downcast_ref::()` checks continue to compile + /// after replacing `anyhow::Error` in the return signature. This mirrors the behavior of + /// `anyhow::Error::downcast_ref` but works directly on our concrete enum. + pub fn downcast_ref(&self) -> Option<&T> { + (self as &dyn std::any::Any).downcast_ref::() + } + + /// Translate core error to client-facing protocol error. + pub fn to_codex_protocol_error(&self) -> CodexErrorInfo { + match self { + CodexErr::ContextWindowExceeded => CodexErrorInfo::ContextWindowExceeded, + CodexErr::UsageLimitReached(_) + | CodexErr::QuotaExceeded + | CodexErr::UsageNotIncluded => CodexErrorInfo::UsageLimitExceeded, + CodexErr::ServerOverloaded => CodexErrorInfo::ServerOverloaded, + CodexErr::RetryLimit(_) => CodexErrorInfo::ResponseTooManyFailedAttempts { + http_status_code: self.http_status_code_value(), + }, + CodexErr::ConnectionFailed(_) => CodexErrorInfo::HttpConnectionFailed { + http_status_code: self.http_status_code_value(), + }, + CodexErr::ResponseStreamFailed(_) => CodexErrorInfo::ResponseStreamConnectionFailed { + http_status_code: self.http_status_code_value(), + }, + CodexErr::RefreshTokenFailed(_) => CodexErrorInfo::Unauthorized, + CodexErr::SessionConfiguredNotFirstEvent + | CodexErr::InternalServerError + | CodexErr::InternalAgentDied => CodexErrorInfo::InternalServerError, + CodexErr::UnsupportedOperation(_) + | CodexErr::ThreadNotFound(_) + | CodexErr::AgentLimitReached { .. } => CodexErrorInfo::BadRequest, + CodexErr::Sandbox(_) => CodexErrorInfo::SandboxError, + _ => CodexErrorInfo::Other, + } + } + + pub fn to_error_event(&self, message_prefix: Option) -> ErrorEvent { + let error_message = self.to_string(); + let message: String = match message_prefix { + Some(prefix) => format!("{prefix}: {error_message}"), + None => error_message, + }; + ErrorEvent { + message, + codex_error_info: Some(self.to_codex_protocol_error()), + } + } + + pub fn http_status_code_value(&self) -> Option { + let http_status_code = match self { + CodexErr::RetryLimit(err) => Some(err.status), + CodexErr::UnexpectedStatus(err) => Some(err.status), + CodexErr::ConnectionFailed(err) => err.source.status(), + CodexErr::ResponseStreamFailed(err) => err.source.status(), + _ => None, + }; + http_status_code.as_ref().map(StatusCode::as_u16) + } } #[derive(Debug)] @@ -381,6 +411,13 @@ fn truncate_with_ellipsis(text: &str, max_bytes: usize) -> String { truncated } +fn truncate_text(content: &str, policy: TruncationPolicy) -> String { + match policy { + TruncationPolicy::Bytes(bytes) => truncate_middle_chars(content, bytes), + TruncationPolicy::Tokens(tokens) => truncate_middle_with_token_budget(content, tokens).0, + } +} + #[derive(Debug)] pub struct RetryLimitReachedError { pub status: StatusCode, @@ -403,10 +440,10 @@ impl std::fmt::Display for RetryLimitReachedError { #[derive(Debug)] pub struct UsageLimitReachedError { - pub(crate) plan_type: Option, - pub(crate) resets_at: Option>, - pub(crate) rate_limits: Option>, - pub(crate) promo_message: Option, + pub plan_type: Option, + pub resets_at: Option>, + pub rate_limits: Option>, + pub promo_message: Option, } impl std::fmt::Display for UsageLimitReachedError { @@ -538,7 +575,6 @@ fn now_for_retry() -> DateTime { pub struct EnvVarError { /// Name of the environment variable that is missing. pub var: String, - /// Optional instructions to help the user get a valid value for the /// variable and set it. pub instructions: Option, @@ -554,67 +590,6 @@ impl std::fmt::Display for EnvVarError { } } -impl CodexErr { - /// Minimal shim so that existing `e.downcast_ref::()` checks continue to compile - /// after replacing `anyhow::Error` in the return signature. This mirrors the behavior of - /// `anyhow::Error::downcast_ref` but works directly on our concrete enum. - pub fn downcast_ref(&self) -> Option<&T> { - (self as &dyn std::any::Any).downcast_ref::() - } - - /// Translate core error to client-facing protocol error. - pub fn to_codex_protocol_error(&self) -> CodexErrorInfo { - match self { - CodexErr::ContextWindowExceeded => CodexErrorInfo::ContextWindowExceeded, - CodexErr::UsageLimitReached(_) - | CodexErr::QuotaExceeded - | CodexErr::UsageNotIncluded => CodexErrorInfo::UsageLimitExceeded, - CodexErr::ServerOverloaded => CodexErrorInfo::ServerOverloaded, - CodexErr::RetryLimit(_) => CodexErrorInfo::ResponseTooManyFailedAttempts { - http_status_code: self.http_status_code_value(), - }, - CodexErr::ConnectionFailed(_) => CodexErrorInfo::HttpConnectionFailed { - http_status_code: self.http_status_code_value(), - }, - CodexErr::ResponseStreamFailed(_) => CodexErrorInfo::ResponseStreamConnectionFailed { - http_status_code: self.http_status_code_value(), - }, - CodexErr::RefreshTokenFailed(_) => CodexErrorInfo::Unauthorized, - CodexErr::SessionConfiguredNotFirstEvent - | CodexErr::InternalServerError - | CodexErr::InternalAgentDied => CodexErrorInfo::InternalServerError, - CodexErr::UnsupportedOperation(_) - | CodexErr::ThreadNotFound(_) - | CodexErr::AgentLimitReached { .. } => CodexErrorInfo::BadRequest, - CodexErr::Sandbox(_) => CodexErrorInfo::SandboxError, - _ => CodexErrorInfo::Other, - } - } - - pub fn to_error_event(&self, message_prefix: Option) -> ErrorEvent { - let error_message = self.to_string(); - let message: String = match message_prefix { - Some(prefix) => format!("{prefix}: {error_message}"), - None => error_message, - }; - ErrorEvent { - message, - codex_error_info: Some(self.to_codex_protocol_error()), - } - } - - pub fn http_status_code_value(&self) -> Option { - let http_status_code = match self { - CodexErr::RetryLimit(err) => Some(err.status), - CodexErr::UnexpectedStatus(err) => Some(err.status), - CodexErr::ConnectionFailed(err) => err.source.status(), - CodexErr::ResponseStreamFailed(err) => err.source.status(), - _ => None, - }; - http_status_code.as_ref().map(StatusCode::as_u16) - } -} - pub fn get_error_message_ui(e: &CodexErr) -> String { let message = match e { CodexErr::Sandbox(SandboxErr::Denied { output, .. }) => { @@ -635,7 +610,7 @@ pub fn get_error_message_ui(e: &CodexErr) -> String { } } } - // Timeouts are not sandbox errors from a UX perspective; present them plainly + // Timeouts are not sandbox errors from a UX perspective; present them plainly. CodexErr::Sandbox(SandboxErr::Timeout { output }) => { format!( "error: command timed out after {} ms", diff --git a/codex-rs/core/src/error_tests.rs b/codex-rs/protocol/src/error_tests.rs similarity index 99% rename from codex-rs/core/src/error_tests.rs rename to codex-rs/protocol/src/error_tests.rs index 623831b2c..0b1d18972 100644 --- a/codex-rs/core/src/error_tests.rs +++ b/codex-rs/protocol/src/error_tests.rs @@ -1,10 +1,11 @@ use super::*; -use crate::exec::StreamOutput; +use crate::exec_output::StreamOutput; +use crate::protocol::RateLimitWindow; use chrono::DateTime; use chrono::Duration as ChronoDuration; use chrono::TimeZone; use chrono::Utc; -use codex_protocol::protocol::RateLimitWindow; +use http::Response as HttpResponse; use pretty_assertions::assert_eq; use reqwest::Response; use reqwest::ResponseBuilderExt; @@ -123,7 +124,7 @@ fn sandbox_denied_reports_stdout_when_no_stderr() { #[test] fn to_error_event_handles_response_stream_failed() { - let response = http::Response::builder() + let response = HttpResponse::builder() .status(StatusCode::TOO_MANY_REQUESTS) .url(Url::parse("http://example.com").unwrap()) .body("") diff --git a/codex-rs/core/src/text_encoding.rs b/codex-rs/protocol/src/exec_output.rs similarity index 80% rename from codex-rs/core/src/text_encoding.rs rename to codex-rs/protocol/src/exec_output.rs index b70d8af54..63675fe1d 100644 --- a/codex-rs/core/src/text_encoding.rs +++ b/codex-rs/protocol/src/exec_output.rs @@ -10,6 +10,54 @@ use chardetng::EncodingDetector; use encoding_rs::Encoding; use encoding_rs::IBM866; use encoding_rs::WINDOWS_1252; +use std::time::Duration; + +#[derive(Debug, Clone)] +pub struct StreamOutput { + pub text: T, + pub truncated_after_lines: Option, +} + +impl StreamOutput { + pub fn new(text: String) -> Self { + Self { + text, + truncated_after_lines: None, + } + } +} + +impl StreamOutput> { + pub fn from_utf8_lossy(&self) -> StreamOutput { + StreamOutput { + text: bytes_to_string_smart(&self.text), + truncated_after_lines: self.truncated_after_lines, + } + } +} + +#[derive(Clone, Debug)] +pub struct ExecToolCallOutput { + pub exit_code: i32, + pub stdout: StreamOutput, + pub stderr: StreamOutput, + pub aggregated_output: StreamOutput, + pub duration: Duration, + pub timed_out: bool, +} + +impl Default for ExecToolCallOutput { + fn default() -> Self { + Self { + exit_code: 0, + stdout: StreamOutput::new(String::new()), + stderr: StreamOutput::new(String::new()), + aggregated_output: StreamOutput::new(String::new()), + duration: Duration::ZERO, + timed_out: false, + } + } +} /// Attempts to convert arbitrary bytes to UTF-8 with best-effort encoding detection. pub fn bytes_to_string_smart(bytes: &[u8]) -> String { @@ -86,7 +134,7 @@ fn decode_bytes(bytes: &[u8], encoding: &'static Encoding) -> String { /// `“test”` into unreadable Cyrillic. To avoid that, we treat inputs comprising a handful of bytes /// from the problematic range plus ASCII letters as CP1252 punctuation. We deliberately do *not* /// cap how many of those punctuation bytes we accept: VS Code frequently prints several quoted -/// phrases (e.g., `"foo" – "bar"`), and truncating the count would once again mis-decode those as +/// phrases (e.g., `"foo" - "bar"`), and truncating the count would once again mis-decode those as /// Cyrillic. If we discover additional encodings with overlapping byte ranges, prefer adding /// encoding-specific byte allowlists like `WINDOWS_1252_PUNCT` and tests that exercise real-world /// shell snippets. @@ -117,5 +165,5 @@ fn is_windows_1252_punct(byte: u8) -> bool { } #[cfg(test)] -#[path = "text_encoding_tests.rs"] +#[path = "exec_output_tests.rs"] mod tests; diff --git a/codex-rs/core/tests/suite/text_encoding_fix.rs b/codex-rs/protocol/src/exec_output_tests.rs similarity index 98% rename from codex-rs/core/tests/suite/text_encoding_fix.rs rename to codex-rs/protocol/src/exec_output_tests.rs index ecebb1e42..8871daf83 100644 --- a/codex-rs/core/tests/suite/text_encoding_fix.rs +++ b/codex-rs/protocol/src/exec_output_tests.rs @@ -3,7 +3,7 @@ //! These tests simulate VSCode's shell preview on Windows/WSL where the output //! may be encoded with a legacy code page before it reaches Codex. -use codex_core::exec::StreamOutput; +use super::StreamOutput; use pretty_assertions::assert_eq; #[test] diff --git a/codex-rs/protocol/src/lib.rs b/codex-rs/protocol/src/lib.rs index 97e204d0a..de580de6f 100644 --- a/codex-rs/protocol/src/lib.rs +++ b/codex-rs/protocol/src/lib.rs @@ -1,16 +1,20 @@ pub mod account; mod agent_path; +pub mod auth; mod thread_id; pub use agent_path::AgentPath; pub use thread_id::ThreadId; pub mod approvals; pub mod config_types; pub mod dynamic_tools; +pub mod error; +pub mod exec_output; pub mod items; pub mod mcp; pub mod memory_citation; pub mod message_history; pub mod models; +pub mod network_policy; pub mod num_format; pub mod openai_models; pub mod parse_command; diff --git a/codex-rs/protocol/src/network_policy.rs b/codex-rs/protocol/src/network_policy.rs new file mode 100644 index 000000000..47d8f8712 --- /dev/null +++ b/codex-rs/protocol/src/network_policy.rs @@ -0,0 +1,22 @@ +use crate::approvals::NetworkApprovalProtocol; +use codex_network_proxy::NetworkDecisionSource; +use codex_network_proxy::NetworkPolicyDecision; +use serde::Deserialize; + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct NetworkPolicyDecisionPayload { + pub decision: NetworkPolicyDecision, + pub source: NetworkDecisionSource, + #[serde(default)] + pub protocol: Option, + pub host: Option, + pub reason: Option, + pub port: Option, +} + +impl NetworkPolicyDecisionPayload { + pub fn is_ask_from_decider(&self) -> bool { + self.decision == NetworkPolicyDecision::Ask && self.source == NetworkDecisionSource::Decider + } +} diff --git a/codex-rs/response-debug-context/BUILD.bazel b/codex-rs/response-debug-context/BUILD.bazel new file mode 100644 index 000000000..2b7c3e446 --- /dev/null +++ b/codex-rs/response-debug-context/BUILD.bazel @@ -0,0 +1,6 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "response-debug-context", + crate_name = "codex_response_debug_context", +) diff --git a/codex-rs/response-debug-context/Cargo.toml b/codex-rs/response-debug-context/Cargo.toml new file mode 100644 index 000000000..fcdb6c963 --- /dev/null +++ b/codex-rs/response-debug-context/Cargo.toml @@ -0,0 +1,22 @@ +[package] +edition.workspace = true +license.workspace = true +name = "codex-response-debug-context" +version.workspace = true + +[lib] +doctest = false +name = "codex_response_debug_context" +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +base64 = { workspace = true } +codex-api = { workspace = true } +http = { workspace = true } +serde_json = { workspace = true } + +[dev-dependencies] +pretty_assertions = { workspace = true } diff --git a/codex-rs/core/src/response_debug_context.rs b/codex-rs/response-debug-context/src/lib.rs similarity index 91% rename from codex-rs/core/src/response_debug_context.rs rename to codex-rs/response-debug-context/src/lib.rs index bc7eab172..ed5a6bdc6 100644 --- a/codex-rs/core/src/response_debug_context.rs +++ b/codex-rs/response-debug-context/src/lib.rs @@ -9,14 +9,14 @@ 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 struct ResponseDebugContext { + pub request_id: Option, + pub cf_ray: Option, + pub auth_error: Option, + pub auth_error_code: Option, } -pub(crate) fn extract_response_debug_context(transport: &TransportError) -> ResponseDebugContext { +pub fn extract_response_debug_context(transport: &TransportError) -> ResponseDebugContext { let mut context = ResponseDebugContext::default(); let TransportError::Http { @@ -53,16 +53,14 @@ pub(crate) fn extract_response_debug_context(transport: &TransportError) -> Resp context } -pub(crate) fn extract_response_debug_context_from_api_error( - error: &ApiError, -) -> ResponseDebugContext { +pub fn extract_response_debug_context_from_api_error(error: &ApiError) -> ResponseDebugContext { match error { ApiError::Transport(transport) => extract_response_debug_context(transport), _ => ResponseDebugContext::default(), } } -pub(crate) fn telemetry_transport_error_message(error: &TransportError) -> String { +pub 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(), @@ -72,7 +70,7 @@ pub(crate) fn telemetry_transport_error_message(error: &TransportError) -> Strin } } -pub(crate) fn telemetry_api_error_message(error: &ApiError) -> String { +pub fn telemetry_api_error_message(error: &ApiError) -> String { match error { ApiError::Transport(transport) => telemetry_transport_error_message(transport), ApiError::Api { status, .. } => format!("api error {}", status.as_u16()), diff --git a/codex-rs/sandboxing/src/lib.rs b/codex-rs/sandboxing/src/lib.rs index 75bddad5e..d89e95bf9 100644 --- a/codex-rs/sandboxing/src/lib.rs +++ b/codex-rs/sandboxing/src/lib.rs @@ -19,7 +19,23 @@ pub use manager::SandboxType; pub use manager::SandboxablePreference; pub use manager::get_platform_sandbox; +use codex_protocol::error::CodexErr; + #[cfg(not(target_os = "linux"))] pub fn system_bwrap_warning() -> Option { None } + +impl From for CodexErr { + fn from(err: SandboxTransformError) -> Self { + match err { + SandboxTransformError::MissingLinuxSandboxExecutable => { + CodexErr::LandlockSandboxExecutableNotProvided + } + #[cfg(not(target_os = "macos"))] + SandboxTransformError::SeatbeltUnavailable => CodexErr::UnsupportedOperation( + "seatbelt sandbox is only available on macOS".to_string(), + ), + } + } +} diff --git a/codex-rs/tui/BUILD.bazel b/codex-rs/tui/BUILD.bazel index 164c7d1fa..baebe18e9 100644 --- a/codex-rs/tui/BUILD.bazel +++ b/codex-rs/tui/BUILD.bazel @@ -12,8 +12,8 @@ codex_rust_crate( ], allow_empty = True, ) + [ - "//codex-rs/core:templates/collaboration_mode/default.md", - "//codex-rs/core:templates/collaboration_mode/plan.md", + "//codex-rs/collaboration-mode-templates:templates/default.md", + "//codex-rs/collaboration-mode-templates:templates/plan.md", ], test_data_extra = glob(["src/**/snapshots/**"]) + ["//codex-rs/core:model_availability_nux_fixtures"], integration_compile_data_extra = ["src/test_backend.rs"], diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index 814a7a783..951187c8a 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -133,6 +133,7 @@ arboard = { workspace = true } codex-cli = { workspace = true } codex-core = { workspace = true } codex-mcp = { workspace = true } +codex-models-manager = { workspace = true } codex-utils-cargo-bin = { workspace = true } codex-utils-pty = { workspace = true } assert_matches = { workspace = true } diff --git a/codex-rs/tui/tests/suite/model_availability_nux.rs b/codex-rs/tui/tests/suite/model_availability_nux.rs index bcbb3f0e2..04e66a157 100644 --- a/codex-rs/tui/tests/suite/model_availability_nux.rs +++ b/codex-rs/tui/tests/suite/model_availability_nux.rs @@ -19,9 +19,8 @@ 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 = std::fs::read_to_string(&source_catalog_path)?; - let mut source_catalog: JsonValue = serde_json::from_str(&source_catalog)?; + let mut source_catalog: JsonValue = + serde_json::to_value(codex_models_manager::bundled_models_response()?)?; let models = source_catalog .get_mut("models") .and_then(JsonValue::as_array_mut)