Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,8 @@ assets/inject/upstream/snow-skin/*.css text eol=lf
# Keep byte-exact macOS theme assets identical on every checkout platform.
assets/inject/upstream/*/macos/*.js text eol=lf
assets/inject/upstream/*/macos/*.css text eol=lf

# Keep every asset covered by upstream_theme_assets.rs byte-exact on Windows.
assets/inject/upstream/**/*.js text eol=lf
assets/inject/upstream/**/*.css text eol=lf
assets/inject/upstream/skin-packs/packs/*/theme.json text eol=lf
34 changes: 26 additions & 8 deletions crates/codex-plus-core/src/relay_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::settings::{RelayContextSelection, RelayProfile, RelayProtocol};

const RELAY_PROVIDER: &str = "custom";
const LEGACY_RELAY_PROVIDERS: &[&str] = &["CodexPlusPlus", "CodexPP"];
const CC_SWITCH_MODEL_CATALOG_FILENAME: &str = "cc-switch-model-catalog.json";
const CHAT_UPSTREAM_BASE_URL_KEY: &str = "codex_plus_chat_base_url";
const PROVIDER_SPECIFIC_COMMON_ROOT_KEYS: &[&str] = &[
"model",
Expand Down Expand Up @@ -1557,22 +1558,28 @@ fn apply_model_catalog_to_config(
sanitize_catalog_filename(&profile.id)
);
let custom_responses = custom_responses_provider(config_text);
let mut config_text = config_text.to_string();
// 用户已手写 model_catalog_json 指针时保留,不覆盖(保 preserves_user_model_catalog_json 测试)
// 仅当现有指针指向本 profile 自己生成的 catalog 时才重新生成。
if let Some(existing) = root_key_string(config_text, "model_catalog_json") {
// cc-switch 的固定文件名属于已知的其他管理器投影,不视为用户手写 catalog;
// 切换到 Codex++ profile 时应接管,否则旧 catalog 会继续覆盖本 profile 的模型元数据。
if let Some(existing) = root_key_string(&config_text, "model_catalog_json") {
if existing != catalog_relative {
if custom_responses
if is_cc_switch_model_catalog(&existing) {
config_text = remove_root_key(&config_text, "model_catalog_json");
} else if custom_responses
&& copy_standard_responses_catalog(home, &existing, &catalog_relative)?
{
let mut doc = parse_toml_document(config_text)?;
let mut doc = parse_toml_document(&config_text)?;
doc["model_catalog_json"] = toml_edit::value(catalog_relative);
return Ok(normalize_optional_toml(doc));
} else {
return Ok(config_text);
}
return Ok(config_text.to_string());
}
}
if let Some(external_catalog) = live_external_model_catalog(home) {
let mut doc = parse_toml_document(config_text)?;
let mut doc = parse_toml_document(&config_text)?;
if custom_responses
&& copy_standard_responses_catalog(home, &external_catalog, &catalog_relative)?
{
Expand All @@ -1598,7 +1605,7 @@ fn apply_model_catalog_to_config(
entry.suffix_window.is_some()
|| crate::model_suffix::requires_bundled_metadata_catalog(&entry.slug)
}) {
return Ok(config_text.to_string());
return Ok(config_text);
}
let fallback = parse_optional_positive_u64(&profile.context_window, "上下文大小")?;
let catalog_path = home.join(&catalog_relative);
Expand All @@ -1614,7 +1621,7 @@ fn apply_model_catalog_to_config(
custom_responses.then_some(false),
);
std::fs::write(&catalog_path, catalog_json)?;
let mut doc = parse_toml_document(config_text)?;
let mut doc = parse_toml_document(&config_text)?;
doc["model_catalog_json"] = toml_edit::value(catalog_relative);
Ok(normalize_optional_toml(doc))
}
Expand Down Expand Up @@ -1683,7 +1690,18 @@ fn live_external_model_catalog(home: &Path) -> Option<String> {
let live_text = read_optional_text(&home.join("config.toml")).ok()?;
let live = parse_toml_document(&live_text).ok()?;
let path = live.get("model_catalog_json")?.as_str()?.trim();
(!path.is_empty() && !is_codex_plus_managed_model_catalog(home, path)).then(|| path.to_string())
(!path.is_empty()
&& !is_codex_plus_managed_model_catalog(home, path)
&& !is_cc_switch_model_catalog(path))
.then(|| path.to_string())
}

fn is_cc_switch_model_catalog(path: &str) -> bool {
path.trim()
.replace('\\', "/")
.rsplit('/')
.next()
.is_some_and(|name| name.eq_ignore_ascii_case(CC_SWITCH_MODEL_CATALOG_FILENAME))
}

fn is_codex_plus_managed_model_catalog(home: &Path, path: &str) -> bool {
Expand Down
107 changes: 107 additions & 0 deletions crates/codex-plus-core/tests/relay_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1364,6 +1364,113 @@ experimental_bearer_token = "sk-new"
assert!(!temp.path().join("model-catalogs").exists());
}

#[test]
fn apply_relay_profile_replaces_cc_switch_catalog_from_profile_config() {
let temp = tempfile::tempdir().unwrap();
let profile = RelayProfile {
id: "relay-a".to_string(),
model: "deepseek-v4-pro".to_string(),
relay_mode: RelayMode::PureApi,
config_contents: r#"model = "deepseek-v4-pro"
model_catalog_json = "cc-switch-model-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_list: "deepseek-v4-pro".to_string(),
model_windows: r#"{"deepseek-v4-pro":"1000000"}"#.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();
assert!(config.contains(r#"model_catalog_json = "model-catalogs/relay-a.json""#));
assert!(!config.contains("cc-switch-model-catalog.json"));

let catalog: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(temp.path().join("model-catalogs/relay-a.json")).unwrap(),
)
.unwrap();
assert_eq!(catalog["models"][0]["slug"], "deepseek-v4-pro");
assert_eq!(catalog["models"][0]["context_window"], 1_000_000);
}

#[test]
fn apply_relay_profile_replaces_cc_switch_catalog_from_live_config() {
let temp = tempfile::tempdir().unwrap();
std::fs::write(
temp.path().join("config.toml"),
"model_catalog_json = \"C:\\\\Users\\\\test\\\\.codex\\\\cc-switch-model-catalog.json\"\n",
)
.unwrap();
let profile = RelayProfile {
id: "relay-a".to_string(),
model: "deepseek-v4-pro".to_string(),
relay_mode: RelayMode::PureApi,
config_contents: r#"model = "deepseek-v4-pro"
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_list: "deepseek-v4-pro".to_string(),
model_windows: r#"{"deepseek-v4-pro":"500000"}"#.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();
assert!(config.contains(r#"model_catalog_json = "model-catalogs/relay-a.json""#));
assert!(!config.contains("cc-switch-model-catalog.json"));
}

#[test]
fn apply_relay_profile_replaces_cc_switch_catalog_without_custom_model_windows() {
let temp = tempfile::tempdir().unwrap();
let profile = RelayProfile {
id: "relay-a".to_string(),
model: "qwen3-coder".to_string(),
relay_mode: RelayMode::PureApi,
config_contents: r#"model = "qwen3-coder"
model_catalog_json = "cc-switch-model-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_list: "qwen3-coder".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();
assert!(!config.contains("model_catalog_json"));
assert!(!config.contains("cc-switch-model-catalog.json"));
assert!(!temp.path().join("model-catalogs").exists());
}

#[test]
fn apply_relay_profile_does_not_carry_previous_managed_model_catalog() {
let temp = tempfile::tempdir().unwrap();
Expand Down
Loading