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 <noreply@openai.com>
This commit is contained in:
Ahmed Ibrahim
2026-04-02 23:00:02 -07:00
committed by GitHub
co-authored by Codex
parent 8cd7f20b48
commit 6fff9955f1
99 changed files with 1095 additions and 1137 deletions
+82 -4
View File
@@ -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",
+8
View File
@@ -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" }
@@ -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;
+2
View File
@@ -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 }
@@ -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<String> {
.map(str::to_string)
}
pub(crate) fn auth_provider_from_auth(
auth: Option<CodexAuth>,
provider: &ModelProviderInfo,
) -> crate::error::Result<CoreAuthProvider> {
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<String>,
account_id: Option<String>,
pub struct CoreAuthProvider {
pub token: Option<String>,
pub account_id: Option<String>,
}
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),
+1
View File
@@ -1,3 +1,4 @@
pub mod api_bridge;
pub mod auth;
pub mod common;
pub mod endpoint;
@@ -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"],
)
@@ -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
@@ -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");
-11
View File
@@ -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",
+3 -6
View File
@@ -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"
+1 -1
View File
@@ -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()),
+5 -5
View File
@@ -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;
+4 -2
View File
@@ -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",
+8 -4
View File
@@ -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();
+34 -14
View File
@@ -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,
+2 -2
View File
@@ -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,
+13
View File
@@ -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();
+2 -67
View File
@@ -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<SandboxTransformError> 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<T: Clone> {
pub text: T,
pub truncated_after_lines: Option<u32>,
}
#[derive(Debug)]
struct RawExecToolCallOutput {
pub exit_status: ExitStatus,
@@ -728,24 +704,6 @@ struct RawExecToolCallOutput {
pub timed_out: bool,
}
impl StreamOutput<String> {
pub fn new(text: String) -> Self {
Self {
text,
truncated_after_lines: None,
}
}
}
impl StreamOutput<Vec<u8>> {
pub fn from_utf8_lossy(&self) -> StreamOutput<String> {
StreamOutput {
text: bytes_to_string_smart(&self.text),
truncated_after_lines: self.truncated_after_lines,
}
}
}
#[inline]
fn append_capped(dst: &mut Vec<u8>, 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<String>,
pub stderr: StreamOutput<String>,
pub aggregated_output: StreamOutput<String>,
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,
+27 -18
View File
@@ -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;
+2 -2
View File
@@ -228,7 +228,7 @@ async fn build_request_context(session: &Arc<Session>, 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<String> {
) -> codex_protocol::error::Result<String> {
let filtered = items
.iter()
.filter_map(|item| {
-15
View File
@@ -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")
)
}
+1 -20
View File
@@ -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<NetworkApprovalProtocol>,
pub host: Option<String>,
pub reason: Option<String>,
pub port: Option<u16>,
}
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<NetworkPolicyDecision> {
match value {
"deny" => Some(NetworkPolicyDecision::Deny),
@@ -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;
+2 -2
View File
@@ -140,7 +140,7 @@ impl ExecRequest {
pub async fn execute_env(
exec_request: ExecRequest,
stdout_stream: Option<StdoutStream>,
) -> crate::error::Result<ExecToolCallOutput> {
) -> codex_protocol::error::Result<ExecToolCallOutput> {
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<StdoutStream>,
after_spawn: Option<Box<dyn FnOnce() + Send>>,
) -> crate::error::Result<ExecToolCallOutput> {
) -> codex_protocol::error::Result<ExecToolCallOutput> {
execute_exec_request(exec_request, stdout_stream, after_spawn).await
}
+4 -6
View File
@@ -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<Vec<ModelPreset>> = 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<ModelPreset> = 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<crate::NewThread> {
) -> codex_protocol::error::Result<crate::NewThread> {
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<AuthManager>,
user_shell_override: crate::shell::Shell,
) -> crate::error::Result<crate::NewThread> {
) -> codex_protocol::error::Result<crate::NewThread> {
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<ModelPreset> {
-340
View File
@@ -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 1416 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));
}
@@ -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());
@@ -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<ExecToolCallOutput, ToolError> {
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,
};
+13 -15
View File
@@ -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();
+1 -1
View File
@@ -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 {
+4 -116
View File
@@ -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<bool>,
pub auth_recovery_mode: Option<&'a str>,
pub auth_recovery_phase: Option<&'a str>,
pub auth_connection_reused: Option<bool>,
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<bool>,
pub auth_recovery_followup_status: Option<u16>,
}
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,
+4 -1
View File
@@ -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;
+2
View File
@@ -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;
+1
View File
@@ -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 }
+2 -11
View File
@@ -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()
+5 -6
View File
@@ -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()
+2 -2
View File
@@ -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()
+1 -2
View File
@@ -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.");
-3
View File
@@ -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;
+2 -2
View File
@@ -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,
+1 -1
View File
@@ -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;
+15 -7
View File
@@ -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<ModelsManager>, 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()
+2 -3
View File
@@ -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()
+2 -3
View File
@@ -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()
@@ -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::<SocketAddr>()
.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::<SocketAddr>()
.expect("valid socket address")
);
}
+1
View File
@@ -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 }
+123
View File
@@ -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<bool>,
pub auth_recovery_mode: Option<&'a str>,
pub auth_recovery_phase: Option<&'a str>,
pub auth_connection_reused: Option<bool>,
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<bool>,
pub auth_recovery_followup_status: Option<u16>,
}
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<FeedbackInner>,
+3
View File
@@ -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 }
+38
View File
@@ -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<CodexAuth>,
provider: &ModelProviderInfo,
) -> codex_protocol::error::Result<CoreAuthProvider> {
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,
})
}
}
+2 -2
View File
@@ -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;
+2 -25
View File
@@ -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<String>) -> 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;
+4 -4
View File
@@ -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;
@@ -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<String>,
pub(crate) provider_env_key_present: Option<bool>,
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<String>,
pub provider_env_key_present: Option<bool>,
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,
+20
View File
@@ -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;
@@ -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<Arc<AuthManager>>,
provider: &ModelProviderInfo,
) -> Option<Arc<AuthManager>> {
@@ -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<AuthManager>,
provider: &ModelProviderInfo,
) -> Arc<AuthManager> {
+1 -89
View File
@@ -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)]
+1
View File
@@ -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;
@@ -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;
+1
View File
@@ -1,3 +1,4 @@
// Aggregates all former standalone integration tests as modules.
mod auth_refresh;
mod device_code_login;
mod login_server_e2e;
+6
View File
@@ -0,0 +1,6 @@
load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "model-provider-info",
crate_name = "codex_model_provider_info",
)
+28
View File
@@ -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 }
@@ -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<String>,
/// Value to use with `Authorization: Bearer <token>` 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<String>,
/// Command-backed bearer-token configuration for this provider.
pub auth: Option<ModelProviderAuthInfo>,
/// 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<HashMap<String, String>>,
/// 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<HashMap<String, String>>,
/// 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<HashMap<String, String>>,
/// Maximum number of times to retry a failed HTTP request to this provider.
pub request_max_retries: Option<u64>,
/// Number of times to retry reconnecting a dropped streaming response before failing.
pub stream_max_retries: Option<u64>,
/// Idle timeout (in milliseconds) to wait for activity on a streaming response before treating
/// the connection as lost.
pub stream_idle_timeout_ms: Option<u64>,
/// Maximum time (in milliseconds) to wait for a websocket connection attempt before treating
/// it as failed.
pub websocket_connect_timeout_ms: Option<u64>,
/// 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<HeaderMap> {
fn build_header_map(&self) -> CodexResult<HeaderMap> {
let capacity = self.http_headers.as_ref().map_or(0, HashMap::len)
+ self.env_http_headers.as_ref().map_or(0, HashMap::len);
let mut headers = HeaderMap::with_capacity(capacity);
@@ -191,10 +181,7 @@ impl ModelProviderInfo {
Ok(headers)
}
pub(crate) fn to_api_provider(
&self,
auth_mode: Option<AuthMode>,
) -> crate::error::Result<ApiProvider> {
pub fn to_api_provider(&self, auth_mode: Option<AuthMode>) -> CodexResult<ApiProvider> {
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<Option<String>> {
pub fn api_key(&self) -> CodexResult<Option<String>> {
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()
}
}
+10
View File
@@ -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",
],
)
+41
View File
@@ -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 }
@@ -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";
+12
View File
@@ -0,0 +1,12 @@
use codex_protocol::openai_models::ModelsResponse;
#[derive(Debug, Clone, Default)]
pub struct ModelsManagerConfig {
pub model_context_window: Option<i64>,
pub model_auto_compact_token_limit: Option<i64>,
pub tool_output_token_limit: Option<usize>,
pub base_instructions: Option<String>,
pub personality_enabled: bool,
pub model_supports_reasoning_summaries: Option<bool>,
pub model_catalog: Option<ModelsResponse>,
}
+30
View File
@@ -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<codex_protocol::openai_models::ModelsResponse, serde_json::Error> {
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")
)
}
@@ -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<Vec<ModelInfo>, 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<AuthManager>,
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
@@ -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");
@@ -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(),
@@ -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(),
@@ -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);
+13
View File
@@ -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 }
+116
View File
@@ -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<String>) -> Self {
Self {
reason,
message: message.into(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RefreshTokenFailedReason {
Expired,
Exhausted,
Revoked,
Other,
}
@@ -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<T> = std::result::Result<T, CodexErr>;
/// 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<Duration>),
#[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 CtrlC (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::<CodexErr>()` 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<T: std::any::Any>(&self) -> Option<&T> {
(self as &dyn std::any::Any).downcast_ref::<T>()
}
/// 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<String>) -> 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<u16> {
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<PlanType>,
pub(crate) resets_at: Option<DateTime<Utc>>,
pub(crate) rate_limits: Option<Box<RateLimitSnapshot>>,
pub(crate) promo_message: Option<String>,
pub plan_type: Option<PlanType>,
pub resets_at: Option<DateTime<Utc>>,
pub rate_limits: Option<Box<RateLimitSnapshot>>,
pub promo_message: Option<String>,
}
impl std::fmt::Display for UsageLimitReachedError {
@@ -538,7 +575,6 @@ fn now_for_retry() -> DateTime<Utc> {
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<String>,
@@ -554,67 +590,6 @@ impl std::fmt::Display for EnvVarError {
}
}
impl CodexErr {
/// Minimal shim so that existing `e.downcast_ref::<CodexErr>()` 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<T: std::any::Any>(&self) -> Option<&T> {
(self as &dyn std::any::Any).downcast_ref::<T>()
}
/// 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<String>) -> 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<u16> {
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",
@@ -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("")
@@ -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<T: Clone> {
pub text: T,
pub truncated_after_lines: Option<u32>,
}
impl StreamOutput<String> {
pub fn new(text: String) -> Self {
Self {
text,
truncated_after_lines: None,
}
}
}
impl StreamOutput<Vec<u8>> {
pub fn from_utf8_lossy(&self) -> StreamOutput<String> {
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<String>,
pub stderr: StreamOutput<String>,
pub aggregated_output: StreamOutput<String>,
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;
@@ -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]
+4
View File
@@ -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;
+22
View File
@@ -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<NetworkApprovalProtocol>,
pub host: Option<String>,
pub reason: Option<String>,
pub port: Option<u16>,
}
impl NetworkPolicyDecisionPayload {
pub fn is_ask_from_decider(&self) -> bool {
self.decision == NetworkPolicyDecision::Ask && self.source == NetworkDecisionSource::Decider
}
}
@@ -0,0 +1,6 @@
load("//:defs.bzl", "codex_rust_crate")
codex_rust_crate(
name = "response-debug-context",
crate_name = "codex_response_debug_context",
)
@@ -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 }
@@ -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<String>,
pub(crate) cf_ray: Option<String>,
pub(crate) auth_error: Option<String>,
pub(crate) auth_error_code: Option<String>,
pub struct ResponseDebugContext {
pub request_id: Option<String>,
pub cf_ray: Option<String>,
pub auth_error: Option<String>,
pub auth_error_code: Option<String>,
}
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()),
+16
View File
@@ -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<String> {
None
}
impl From<SandboxTransformError> 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(),
),
}
}
}
+2 -2
View File
@@ -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"],
+1
View File
@@ -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 }
@@ -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)