From 8a9fb11001ffc92d7092df977c8f22ab54baaef7 Mon Sep 17 00:00:00 2001 From: lennney <94768569+lennney@users.noreply.github.com> Date: Sun, 28 Jun 2026 23:12:27 +0800 Subject: [PATCH 1/4] fix(guard): auto-offset guard port by USERNAME for multi-user RDP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prevents port collision when multiple Windows RDP users run Codex++ on the same machine. Resolution order: 1. CODEX_PLUS_GUARD_PORT env var — exact override for all roles 2. CODEX_PLUS_LAUNCHER_GUARD_PORT / CODEX_PLUS_MANAGER_GUARD_PORT — role-specific env override 3. CODEX_PLUS_GUARD_PORT_OFFSET env var — explicit offset from base 4. Windows: auto-offset by USERNAME hash (mod 1000) 5. Other platforms: 0 (backward-compatible default) Old constants LAUNCHER_GUARD_PORT / MANAGER_GUARD_PORT replaced with launcher_guard_port() / manager_guard_port() functions. Fixes #328 --- crates/codex-plus-core/src/ports.rs | 86 ++++++----------------------- 1 file changed, 16 insertions(+), 70 deletions(-) diff --git a/crates/codex-plus-core/src/ports.rs b/crates/codex-plus-core/src/ports.rs index 2263b21f..11516046 100644 --- a/crates/codex-plus-core/src/ports.rs +++ b/crates/codex-plus-core/src/ports.rs @@ -29,16 +29,14 @@ fn guard_port_offset() -> u16 { /// Effective launcher guard port (base + auto-offset, overridable via env var). pub fn launcher_guard_port() -> u16 { - if let Some(port) = std::env::var("CODEX_PLUS_GUARD_PORT") + if let Ok(port) = std::env::var("CODEX_PLUS_GUARD_PORT") .or_else(|_| std::env::var("CODEX_PLUS_LAUNCHER_GUARD_PORT")) - .ok() - .and_then(|v| v.parse::().ok()) + .and_then(|v| v.parse::().map_err(|_| ())) { return port; } - if let Some(offset) = std::env::var("CODEX_PLUS_GUARD_PORT_OFFSET") - .ok() - .and_then(|v| v.parse::().ok()) + if let Ok(offset) = std::env::var("CODEX_PLUS_GUARD_PORT_OFFSET") + .and_then(|v| v.parse::().map_err(|_| ())) { return LAUNCHER_GUARD_PORT_BASE + offset; } @@ -47,16 +45,14 @@ pub fn launcher_guard_port() -> u16 { /// Effective manager guard port (base + auto-offset, overridable via env var). pub fn manager_guard_port() -> u16 { - if let Some(port) = std::env::var("CODEX_PLUS_GUARD_PORT") + if let Ok(port) = std::env::var("CODEX_PLUS_GUARD_PORT") .or_else(|_| std::env::var("CODEX_PLUS_MANAGER_GUARD_PORT")) - .ok() - .and_then(|v| v.parse::().ok()) + .and_then(|v| v.parse::().map_err(|_| ())) { return port; } - if let Some(offset) = std::env::var("CODEX_PLUS_GUARD_PORT_OFFSET") - .ok() - .and_then(|v| v.parse::().ok()) + if let Ok(offset) = std::env::var("CODEX_PLUS_GUARD_PORT_OFFSET") + .and_then(|v| v.parse::().map_err(|_| ())) { return MANAGER_GUARD_PORT_BASE + offset; } @@ -72,24 +68,6 @@ pub fn select_platform_loopback_port(requested: u16) -> u16 { ) } -pub fn select_packaged_codex_debug_port(requested: u16) -> u16 { - select_packaged_codex_debug_port_with( - requested, - cfg!(windows), - can_bind_loopback_port, - find_available_loopback_port, - ) -} - -pub fn select_packaged_codex_debug_port_with( - requested: u16, - is_windows: bool, - can_bind: impl Fn(u16) -> bool, - find_available: impl Fn() -> u16, -) -> u16 { - select_platform_loopback_port_with(requested, is_windows, can_bind, find_available) -} - pub fn select_platform_loopback_port_with( requested: u16, is_windows: bool, @@ -242,9 +220,6 @@ fn normalize_lock_error(error: std::io::Error) -> std::io::Error { #[cfg(test)] mod tests { use super::*; - use std::sync::{Mutex, MutexGuard}; - - static GUARD_PORT_ENV_LOCK: Mutex<()> = Mutex::new(()); #[test] fn resilient_guard_holds_lock_and_listener_when_requested_port_is_available() { @@ -324,8 +299,6 @@ mod tests { #[test] fn launcher_guard_port_returns_base_when_no_env_override() { - let _guard = guard_port_env_lock(); - _clear_guard_port_env_vars(); let port = launcher_guard_port(); // On non-Windows: LAUNCHER_GUARD_PORT_BASE + 0 // On Windows: LAUNCHER_GUARD_PORT_BASE + USERNAME hash mod 1000 @@ -335,8 +308,6 @@ mod tests { #[test] fn manager_guard_port_returns_base_when_no_env_override() { - let _guard = guard_port_env_lock(); - _clear_guard_port_env_vars(); let port = manager_guard_port(); assert!(port >= MANAGER_GUARD_PORT_BASE); assert!(port < MANAGER_GUARD_PORT_BASE + 1000); @@ -344,58 +315,33 @@ mod tests { #[test] fn launcher_guard_port_honors_env_override() { - let _guard = guard_port_env_lock(); - _clear_guard_port_env_vars(); - unsafe { std::env::set_var("CODEX_PLUS_GUARD_PORT", "9999") }; + std::env::set_var("CODEX_PLUS_GUARD_PORT", "9999"); let port = launcher_guard_port(); - unsafe { std::env::remove_var("CODEX_PLUS_GUARD_PORT") }; + std::env::remove_var("CODEX_PLUS_GUARD_PORT"); assert_eq!(port, 9999); } #[test] fn launcher_guard_port_honors_specific_env_override() { - let _guard = guard_port_env_lock(); - _clear_guard_port_env_vars(); - unsafe { std::env::set_var("CODEX_PLUS_LAUNCHER_GUARD_PORT", "8888") }; + std::env::set_var("CODEX_PLUS_LAUNCHER_GUARD_PORT", "8888"); let port = launcher_guard_port(); - unsafe { std::env::remove_var("CODEX_PLUS_LAUNCHER_GUARD_PORT") }; + std::env::remove_var("CODEX_PLUS_LAUNCHER_GUARD_PORT"); assert_eq!(port, 8888); } #[test] fn manager_guard_port_honors_specific_env_override() { - let _guard = guard_port_env_lock(); - _clear_guard_port_env_vars(); - unsafe { std::env::set_var("CODEX_PLUS_MANAGER_GUARD_PORT", "7777") }; + std::env::set_var("CODEX_PLUS_MANAGER_GUARD_PORT", "7777"); let port = manager_guard_port(); - unsafe { std::env::remove_var("CODEX_PLUS_MANAGER_GUARD_PORT") }; + std::env::remove_var("CODEX_PLUS_MANAGER_GUARD_PORT"); assert_eq!(port, 7777); } #[test] fn launcher_guard_port_honors_offset_env() { - let _guard = guard_port_env_lock(); - _clear_guard_port_env_vars(); - unsafe { std::env::set_var("CODEX_PLUS_GUARD_PORT_OFFSET", "50") }; + std::env::set_var("CODEX_PLUS_GUARD_PORT_OFFSET", "50"); let port = launcher_guard_port(); - unsafe { std::env::remove_var("CODEX_PLUS_GUARD_PORT_OFFSET") }; + std::env::remove_var("CODEX_PLUS_GUARD_PORT_OFFSET"); assert_eq!(port, LAUNCHER_GUARD_PORT_BASE + 50); } - - fn guard_port_env_lock() -> MutexGuard<'static, ()> { - GUARD_PORT_ENV_LOCK - .lock() - .expect("guard port env lock should not be poisoned") - } -} - -/// Clear all guard-port env vars to prevent cross-test contamination -/// when cargo runs tests in parallel threads. -fn _clear_guard_port_env_vars() { - unsafe { - let _ = std::env::remove_var("CODEX_PLUS_GUARD_PORT"); - let _ = std::env::remove_var("CODEX_PLUS_LAUNCHER_GUARD_PORT"); - let _ = std::env::remove_var("CODEX_PLUS_MANAGER_GUARD_PORT"); - let _ = std::env::remove_var("CODEX_PLUS_GUARD_PORT_OFFSET"); - } } From f60bef38e28fae3105c6a9731acbde1061688570 Mon Sep 17 00:00:00 2001 From: lennney <94768569+lennney@users.noreply.github.com> Date: Mon, 29 Jun 2026 18:43:30 +0800 Subject: [PATCH 2/4] fix(config): generate custom model catalog for non-standard models with context_window When model_context_window is set for a custom model (e.g. mimo-v2.5) that isn't in the standard OpenAI catalog, Codex CLI previously ignored the value and fell back to the default 272000. Now apply_context_limits_to_config() detects when: - model_context_window is set - model_catalog_json is not already configured - A model name is present And generates ~/.codex/model_catalog_custom.json with the model's context_window, then sets model_catalog_json to point to it. This allows Codex CLI to find the model in the catalog and use the user-specified context window. Also fixes unsafe set_var/remove_var calls in ports.rs tests for Rust 2024 edition compatibility. Closes #1260 --- crates/codex-plus-core/src/ports.rs | 52 ++++--- crates/codex-plus-core/src/relay_config.rs | 72 +++++++++- crates/codex-plus-core/tests/relay_config.rs | 142 +++++++++++++++++-- 3 files changed, 240 insertions(+), 26 deletions(-) diff --git a/crates/codex-plus-core/src/ports.rs b/crates/codex-plus-core/src/ports.rs index 11516046..e1abda1b 100644 --- a/crates/codex-plus-core/src/ports.rs +++ b/crates/codex-plus-core/src/ports.rs @@ -29,14 +29,16 @@ fn guard_port_offset() -> u16 { /// Effective launcher guard port (base + auto-offset, overridable via env var). pub fn launcher_guard_port() -> u16 { - if let Ok(port) = std::env::var("CODEX_PLUS_GUARD_PORT") + if let Some(port) = std::env::var("CODEX_PLUS_GUARD_PORT") .or_else(|_| std::env::var("CODEX_PLUS_LAUNCHER_GUARD_PORT")) - .and_then(|v| v.parse::().map_err(|_| ())) + .ok() + .and_then(|v| v.parse::().ok()) { return port; } - if let Ok(offset) = std::env::var("CODEX_PLUS_GUARD_PORT_OFFSET") - .and_then(|v| v.parse::().map_err(|_| ())) + if let Some(offset) = std::env::var("CODEX_PLUS_GUARD_PORT_OFFSET") + .ok() + .and_then(|v| v.parse::().ok()) { return LAUNCHER_GUARD_PORT_BASE + offset; } @@ -45,14 +47,16 @@ pub fn launcher_guard_port() -> u16 { /// Effective manager guard port (base + auto-offset, overridable via env var). pub fn manager_guard_port() -> u16 { - if let Ok(port) = std::env::var("CODEX_PLUS_GUARD_PORT") + if let Some(port) = std::env::var("CODEX_PLUS_GUARD_PORT") .or_else(|_| std::env::var("CODEX_PLUS_MANAGER_GUARD_PORT")) - .and_then(|v| v.parse::().map_err(|_| ())) + .ok() + .and_then(|v| v.parse::().ok()) { return port; } - if let Ok(offset) = std::env::var("CODEX_PLUS_GUARD_PORT_OFFSET") - .and_then(|v| v.parse::().map_err(|_| ())) + if let Some(offset) = std::env::var("CODEX_PLUS_GUARD_PORT_OFFSET") + .ok() + .and_then(|v| v.parse::().ok()) { return MANAGER_GUARD_PORT_BASE + offset; } @@ -315,33 +319,49 @@ mod tests { #[test] fn launcher_guard_port_honors_env_override() { - std::env::set_var("CODEX_PLUS_GUARD_PORT", "9999"); + unsafe { + std::env::set_var("CODEX_PLUS_GUARD_PORT", "9999"); + } let port = launcher_guard_port(); - std::env::remove_var("CODEX_PLUS_GUARD_PORT"); + unsafe { + std::env::remove_var("CODEX_PLUS_GUARD_PORT"); + } assert_eq!(port, 9999); } #[test] fn launcher_guard_port_honors_specific_env_override() { - std::env::set_var("CODEX_PLUS_LAUNCHER_GUARD_PORT", "8888"); + unsafe { + std::env::set_var("CODEX_PLUS_LAUNCHER_GUARD_PORT", "8888"); + } let port = launcher_guard_port(); - std::env::remove_var("CODEX_PLUS_LAUNCHER_GUARD_PORT"); + unsafe { + std::env::remove_var("CODEX_PLUS_LAUNCHER_GUARD_PORT"); + } assert_eq!(port, 8888); } #[test] fn manager_guard_port_honors_specific_env_override() { - std::env::set_var("CODEX_PLUS_MANAGER_GUARD_PORT", "7777"); + unsafe { + std::env::set_var("CODEX_PLUS_MANAGER_GUARD_PORT", "7777"); + } let port = manager_guard_port(); - std::env::remove_var("CODEX_PLUS_MANAGER_GUARD_PORT"); + unsafe { + std::env::remove_var("CODEX_PLUS_MANAGER_GUARD_PORT"); + } assert_eq!(port, 7777); } #[test] fn launcher_guard_port_honors_offset_env() { - std::env::set_var("CODEX_PLUS_GUARD_PORT_OFFSET", "50"); + unsafe { + std::env::set_var("CODEX_PLUS_GUARD_PORT_OFFSET", "50"); + } let port = launcher_guard_port(); - std::env::remove_var("CODEX_PLUS_GUARD_PORT_OFFSET"); + unsafe { + std::env::remove_var("CODEX_PLUS_GUARD_PORT_OFFSET"); + } assert_eq!(port, LAUNCHER_GUARD_PORT_BASE + 50); } } diff --git a/crates/codex-plus-core/src/relay_config.rs b/crates/codex-plus-core/src/relay_config.rs index fc9e76a4..050c7aaf 100644 --- a/crates/codex-plus-core/src/relay_config.rs +++ b/crates/codex-plus-core/src/relay_config.rs @@ -342,7 +342,7 @@ pub fn apply_relay_files_to_home_with_context( let config_with_common = preserve_unmanaged_live_context_entries(home, &config_with_common, common_config_contents)?; let config_with_limits = - apply_context_limits_to_config(&config_with_common, context_window, auto_compact_limit)?; + apply_context_limits_to_config(home, &config_with_common, context_window, auto_compact_limit)?; apply_relay_files_to_home(home, &config_with_limits, auth_contents) } @@ -361,6 +361,7 @@ pub fn apply_relay_profile_files_to_home_with_context( let config_with_common = preserve_unmanaged_live_context_entries(home, &config_with_common, common_config_contents)?; let config_with_limits = apply_context_limits_to_config( + home, &config_with_common, &profile.context_window, &profile.auto_compact_limit, @@ -398,6 +399,7 @@ pub fn apply_relay_profile_to_home_with_switch_rules_and_computer_use_guard( let config_with_common = preserve_unmanaged_live_context_entries(home, &config_with_common, common_config_contents)?; let config_with_limits = apply_context_limits_to_config( + home, &config_with_common, &profile.context_window, &profile.auto_compact_limit, @@ -435,6 +437,7 @@ pub fn apply_relay_profile_config_to_home_with_context( let profile_config = complete_relay_profile_config(profile)?; let config_with_common = merge_common_config_into_config(&profile_config, &selected_common)?; let config_with_limits = apply_context_limits_to_config( + home, &config_with_common, &profile.context_window, &profile.auto_compact_limit, @@ -1359,6 +1362,7 @@ fn parse_optional_positive_u64(value: &str, label: &str) -> anyhow::Result Option { .and_then(|value| value.parse::().ok()) .filter(|value| *value > 0) .map(|value| value.to_string()) +/// Generate a custom model catalog file for non-standard models that have +/// an explicit `model_context_window` set. This allows Codex CLI to find +/// the model in its catalog lookup and use the user-specified context window. +/// +/// Skips generation if: +/// - `model_catalog_json` is already set in the config +/// - No `model` is configured +fn ensure_custom_model_catalog( + home: &Path, + doc: &mut DocumentMut, + context_window: u64, +) -> anyhow::Result<()> { + // Don't override an existing model_catalog_json setting + if doc.contains_key("model_catalog_json") { + return Ok(()); + } + + // Read model name from the TOML doc + let model = doc + .get("model") + .and_then(|item| item.as_str()) + .map(str::trim) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()); + + let model = match model { + Some(m) if !m.is_empty() => m, + _ => return Ok(()), + }; + + let catalog_filename = "model_catalog_custom.json"; + let catalog_path = home.join(catalog_filename); + + // Use a display name derived from the slug + let display_name = model + .split(['-', '_']) + .map(|part| { + let mut chars = part.chars(); + match chars.next() { + None => String::new(), + Some(c) => c.to_uppercase().to_string() + chars.as_str(), + } + }) + .collect::>() + .join(" "); + + let catalog = serde_json::json!({ + "models": [{ + "slug": model, + "display_name": display_name, + "base_instructions": "You are Codex, a coding agent that helps users write code.", + "context_window": context_window, + "max_context_window": context_window, + "effective_context_window_percent": 95 + }] + }); + + let catalog_json = serde_json::to_string_pretty(&catalog) + .with_context(|| "序列化自定义模型目录失败")?; + crate::settings::atomic_write(&catalog_path, catalog_json.as_bytes()) + .with_context(|| format!("写入自定义模型目录 {} 失败", catalog_path.display()))?; + + doc["model_catalog_json"] = toml_edit::value(catalog_path.to_string_lossy().to_string()); + + Ok(()) } fn toml_value_is_subset(target: &toml_edit::Value, source: &toml_edit::Value) -> bool { diff --git a/crates/codex-plus-core/tests/relay_config.rs b/crates/codex-plus-core/tests/relay_config.rs index c19476f4..2c0bb1bf 100644 --- a/crates/codex-plus-core/tests/relay_config.rs +++ b/crates/codex-plus-core/tests/relay_config.rs @@ -968,14 +968,14 @@ enabled = true } #[test] -fn apply_relay_profile_does_not_write_model_catalog_json_for_selected_models() { +fn apply_relay_profile_writes_model_catalog_json_for_custom_model_with_context_window() { let temp = tempfile::tempdir().unwrap(); let profile = RelayProfile { id: "relay-a".to_string(), name: "Relay A".to_string(), - model: "qwen3-coder".to_string(), + model: "mimo-v2.5".to_string(), relay_mode: RelayMode::PureApi, - config_contents: r#"model = "qwen3-coder" + config_contents: r#"model = "mimo-v2.5" model_provider = "custom" [model_providers.custom] @@ -988,19 +988,143 @@ experimental_bearer_token = "sk-new" .to_string(), auth_contents: r#"{"OPENAI_API_KEY":"sk-new"}"#.to_string(), model_insert_mode: Default::default(), - model_list: "deepseek-coder\nqwen3-coder".to_string(), - context_window: "200000".to_string(), - auto_compact_limit: "160000".to_string(), + model_list: String::new(), + context_window: "1000000".to_string(), + auto_compact_limit: "800000".to_string(), ..RelayProfile::default() }; apply_relay_profile_files_to_home_with_context(temp.path(), &profile, "").unwrap(); + // Check config has all expected settings let config = std::fs::read_to_string(temp.path().join("config.toml")).unwrap(); + assert!(config.contains(r#"model = "mimo-v2.5""#)); + assert!(config.contains("model_context_window = 1000000")); + assert!(config.contains("model_auto_compact_token_limit = 800000")); + // Custom model with context_window should generate a model_catalog_json entry + assert!(config.contains("model_catalog_json"), "Expected model_catalog_json for custom model with context_window"); + assert!(config.contains("model_catalog_custom.json")); + + // Check the custom catalog file was created + let catalog_path = temp.path().join("model_catalog_custom.json"); + assert!(catalog_path.exists(), "Custom catalog file should exist"); + let catalog: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(&catalog_path).unwrap()).unwrap(); + let models = catalog.get("models").and_then(|v| v.as_array()).unwrap(); + assert_eq!(models.len(), 1); + let model_entry = &models[0]; + assert_eq!(model_entry["slug"].as_str().unwrap(), "mimo-v2.5"); + assert_eq!(model_entry["context_window"].as_u64().unwrap(), 1000000); + assert_eq!(model_entry["max_context_window"].as_u64().unwrap(), 1000000); + assert!(model_entry["base_instructions"].as_str().unwrap().contains("coding agent")); +} + +#[test] +fn apply_relay_profile_skips_custom_catalog_when_context_window_empty() { + let temp = tempfile::tempdir().unwrap(); + let profile = RelayProfile { + id: "relay-a".to_string(), + name: "Relay A".to_string(), + model: "mimo-v2.5".to_string(), + relay_mode: RelayMode::PureApi, + config_contents: r#"model = "mimo-v2.5" +model_provider = "custom" + +[model_providers.custom] +name = "custom" +wire_api = "responses" +requires_openai_auth = true +base_url = "https://relay.example/v1" +"# + .to_string(), + auth_contents: r#"{"OPENAI_API_KEY":"sk-new"}"#.to_string(), + model_insert_mode: Default::default(), + model_list: String::new(), + context_window: String::new(), // empty = no override + auto_compact_limit: String::new(), + ..RelayProfile::default() + }; + + apply_relay_profile_files_to_home_with_context(temp.path(), &profile, "").unwrap(); + + let config = std::fs::read_to_string(temp.path().join("config.toml")).unwrap(); + assert!(config.contains(r#"model = "mimo-v2.5""#)); + // No context_window set → no model_context_window in config → no custom catalog + assert!(!config.contains("model_context_window")); assert!(!config.contains("model_catalog_json")); - assert!(config.contains("model_context_window = 200000")); - assert!(config.contains("model_auto_compact_token_limit = 160000")); - assert!(!temp.path().join("model-catalogs").exists()); + assert!(!temp.path().join("model_catalog_custom.json").exists()); +} + +#[test] +fn apply_relay_profile_skips_custom_catalog_when_model_empty() { + let temp = tempfile::tempdir().unwrap(); + let profile = RelayProfile { + id: "relay-a".to_string(), + name: "Relay A".to_string(), + model: String::new(), // empty + relay_mode: RelayMode::PureApi, + config_contents: r#"model = "" +model_provider = "custom" + +[model_providers.custom] +name = "custom" +wire_api = "responses" +requires_openai_auth = true +base_url = "https://relay.example/v1" +"# + .to_string(), + auth_contents: r#"{"OPENAI_API_KEY":"sk-new"}"#.to_string(), + model_insert_mode: Default::default(), + model_list: String::new(), + context_window: "1000000".to_string(), + auto_compact_limit: "800000".to_string(), + ..RelayProfile::default() + }; + + apply_relay_profile_files_to_home_with_context(temp.path(), &profile, "").unwrap(); + + let config = std::fs::read_to_string(temp.path().join("config.toml")).unwrap(); + // context_window is set, but model is empty → no custom catalog + assert!(config.contains("model_context_window = 1000000")); + assert!(!config.contains("model_catalog_json")); + assert!(!temp.path().join("model_catalog_custom.json").exists()); +} + +#[test] +fn apply_relay_profile_does_not_override_existing_model_catalog_json() { + let temp = tempfile::tempdir().unwrap(); + let profile = RelayProfile { + id: "relay-a".to_string(), + name: "Relay A".to_string(), + model: "mimo-v2.5".to_string(), + relay_mode: RelayMode::PureApi, + config_contents: r#"model = "mimo-v2.5" +model_catalog_json = "/custom/path/to/catalog.json" +model_provider = "custom" + +[model_providers.custom] +name = "custom" +wire_api = "responses" +requires_openai_auth = true +base_url = "https://relay.example/v1" +experimental_bearer_token = "sk-new" +"# + .to_string(), + auth_contents: r#"{"OPENAI_API_KEY":"sk-new"}"#.to_string(), + model_insert_mode: Default::default(), + model_list: String::new(), + context_window: "1000000".to_string(), + auto_compact_limit: "800000".to_string(), + ..RelayProfile::default() + }; + + apply_relay_profile_files_to_home_with_context(temp.path(), &profile, "").unwrap(); + + let config = std::fs::read_to_string(temp.path().join("config.toml")).unwrap(); + // Existing model_catalog_json should be preserved, not overwritten + assert!(config.contains(r#"model_catalog_json = "/custom/path/to/catalog.json""#)); + assert!(!config.contains("model_catalog_custom.json")); + assert!(!temp.path().join("model_catalog_custom.json").exists()); } #[test] From 95baadbbfc2480ef233b057822c9501ba5506cdc Mon Sep 17 00:00:00 2001 From: lennney <94768569+lennney@users.noreply.github.com> Date: Thu, 2 Jul 2026 23:25:49 +0800 Subject: [PATCH 3/4] fix(rebase): restore select_packaged_codex_debug_port lost in conflict resolution --- crates/codex-plus-core/src/ports.rs | 18 ++++++++++++++++++ crates/codex-plus-core/src/relay_config.rs | 1 + 2 files changed, 19 insertions(+) diff --git a/crates/codex-plus-core/src/ports.rs b/crates/codex-plus-core/src/ports.rs index e1abda1b..8dfa258a 100644 --- a/crates/codex-plus-core/src/ports.rs +++ b/crates/codex-plus-core/src/ports.rs @@ -72,6 +72,24 @@ pub fn select_platform_loopback_port(requested: u16) -> u16 { ) } +pub fn select_packaged_codex_debug_port(requested: u16) -> u16 { + select_packaged_codex_debug_port_with( + requested, + cfg!(windows), + can_bind_loopback_port, + find_available_loopback_port, + ) +} + +pub fn select_packaged_codex_debug_port_with( + requested: u16, + is_windows: bool, + can_bind: impl Fn(u16) -> bool, + find_available: impl Fn() -> u16, +) -> u16 { + select_platform_loopback_port_with(requested, is_windows, can_bind, find_available) +} + pub fn select_platform_loopback_port_with( requested: u16, is_windows: bool, diff --git a/crates/codex-plus-core/src/relay_config.rs b/crates/codex-plus-core/src/relay_config.rs index 050c7aaf..f0b3e39f 100644 --- a/crates/codex-plus-core/src/relay_config.rs +++ b/crates/codex-plus-core/src/relay_config.rs @@ -1460,6 +1460,7 @@ fn root_positive_int_string(config_text: &str, key: &str) -> Option { .and_then(|value| value.parse::().ok()) .filter(|value| *value > 0) .map(|value| value.to_string()) +} /// Generate a custom model catalog file for non-standard models that have /// an explicit `model_context_window` set. This allows Codex CLI to find /// the model in its catalog lookup and use the user-specified context window. From dca162a60c571fc9dd9ce90122addcde68e5dc0a Mon Sep 17 00:00:00 2001 From: lennney <94768569+lennney@users.noreply.github.com> Date: Fri, 3 Jul 2026 00:03:07 +0800 Subject: [PATCH 4/4] fix(config): move custom catalog generation after model_catalog_to_config The custom model catalog fallback (ensure_custom_model_catalog) was incorrectly placed inside apply_context_limits_to_config, which runs BEFORE apply_model_catalog_to_config. This caused two bugs: 1. For suffixed model_list: ensure_custom_model_catalog wrote model_catalog_custom.json first, then apply_model_catalog_to_config saw a mismatched model_catalog_json value and skipped relay catalog generation entirely. 2. For unsuffixed model_list: ensure_custom_model_catalog generated an unwanted catalog, even when no context_window was set. Fix: - Remove home param and ensure_custom_model_catalog call from apply_context_limits_to_config - Add apply_custom_catalog_fallback() called AFTER apply_model_catalog_to_config at all profile-based call sites - Update test for context_window without suffixes to expect custom fallback catalog generation --- crates/codex-plus-core/src/relay_config.rs | 26 +++++++++++++++----- crates/codex-plus-core/tests/relay_config.rs | 9 +++++-- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/crates/codex-plus-core/src/relay_config.rs b/crates/codex-plus-core/src/relay_config.rs index f0b3e39f..17eaac84 100644 --- a/crates/codex-plus-core/src/relay_config.rs +++ b/crates/codex-plus-core/src/relay_config.rs @@ -342,7 +342,7 @@ pub fn apply_relay_files_to_home_with_context( let config_with_common = preserve_unmanaged_live_context_entries(home, &config_with_common, common_config_contents)?; let config_with_limits = - apply_context_limits_to_config(home, &config_with_common, context_window, auto_compact_limit)?; + apply_context_limits_to_config(&config_with_common, context_window, auto_compact_limit)?; apply_relay_files_to_home(home, &config_with_limits, auth_contents) } @@ -361,12 +361,12 @@ pub fn apply_relay_profile_files_to_home_with_context( let config_with_common = preserve_unmanaged_live_context_entries(home, &config_with_common, common_config_contents)?; let config_with_limits = apply_context_limits_to_config( - home, &config_with_common, &profile.context_window, &profile.auto_compact_limit, )?; let config_with_catalog = apply_model_catalog_to_config(home, profile, &config_with_limits)?; + let config_with_catalog = apply_custom_catalog_fallback(home, &config_with_catalog, profile)?; apply_relay_files_to_home(home, &config_with_catalog, &profile.auth_contents) } @@ -399,12 +399,12 @@ pub fn apply_relay_profile_to_home_with_switch_rules_and_computer_use_guard( let config_with_common = preserve_unmanaged_live_context_entries(home, &config_with_common, common_config_contents)?; let config_with_limits = apply_context_limits_to_config( - home, &config_with_common, &profile.context_window, &profile.auto_compact_limit, )?; let config_with_catalog = apply_model_catalog_to_config(home, profile, &config_with_limits)?; + let config_with_catalog = apply_custom_catalog_fallback(home, &config_with_catalog, profile)?; if profile.relay_mode == crate::settings::RelayMode::PureApi { apply_relay_files_to_home_with_computer_use_guard( @@ -437,12 +437,12 @@ pub fn apply_relay_profile_config_to_home_with_context( let profile_config = complete_relay_profile_config(profile)?; let config_with_common = merge_common_config_into_config(&profile_config, &selected_common)?; let config_with_limits = apply_context_limits_to_config( - home, &config_with_common, &profile.context_window, &profile.auto_compact_limit, )?; let config_with_catalog = apply_model_catalog_to_config(home, profile, &config_with_limits)?; + let config_with_catalog = apply_custom_catalog_fallback(home, &config_with_catalog, profile)?; apply_relay_config_file_to_home(home, &config_with_catalog) } @@ -1362,7 +1362,6 @@ fn parse_optional_positive_u64(value: &str, label: &str) -> anyhow::Result Option { .filter(|value| *value > 0) .map(|value| value.to_string()) } +/// Fallback custom model catalog: only generates when `apply_model_catalog_to_config` +/// hasn't already set `model_catalog_json` (e.g. no suffixes in model_list). +fn apply_custom_catalog_fallback( + home: &Path, + config_text: &str, + profile: &RelayProfile, +) -> anyhow::Result { + let mut doc = parse_toml_document(config_text)?; + // Only generate when no catalog was set by apply_model_catalog_to_config + let context_window = parse_optional_positive_u64(&profile.context_window, "上下文大小")?; + if let Some(value) = context_window { + ensure_custom_model_catalog(home, &mut doc, value)?; + } + Ok(normalize_optional_toml(doc)) +} + /// Generate a custom model catalog file for non-standard models that have /// an explicit `model_context_window` set. This allows Codex CLI to find /// the model in its catalog lookup and use the user-specified context window. diff --git a/crates/codex-plus-core/tests/relay_config.rs b/crates/codex-plus-core/tests/relay_config.rs index 2c0bb1bf..ea4c8234 100644 --- a/crates/codex-plus-core/tests/relay_config.rs +++ b/crates/codex-plus-core/tests/relay_config.rs @@ -3026,7 +3026,7 @@ experimental_bearer_token = "sk-new" } #[test] -fn apply_relay_profile_no_catalog_when_model_list_has_no_suffix() { +fn apply_relay_profile_generates_custom_catalog_when_context_window_set() { let temp = tempfile::tempdir().unwrap(); let profile = RelayProfile { id: "relay-a".to_string(), @@ -3055,9 +3055,14 @@ experimental_bearer_token = "sk-new" apply_relay_profile_files_to_home_with_context(temp.path(), &profile, "").unwrap(); let config = std::fs::read_to_string(temp.path().join("config.toml")).unwrap(); - assert!(!config.contains("model_catalog_json")); + // context_window set → custom fallback catalog is generated + assert!(config.contains("model_catalog_json")); + assert!(config.contains("model_catalog_custom.json")); assert!(config.contains("model_context_window = 200000")); + // But no suffix-based model-catalogs directory assert!(!temp.path().join("model-catalogs").exists()); + // Custom catalog file exists + assert!(temp.path().join("model_catalog_custom.json").exists()); } #[test]