mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-10-26 01:34:51 +08:00
fix(codex): mirror official entries for GPT models in the model catalog
Once `model_catalog_json` is set, Codex only knows the models in that file. Third-party GPT rows were built from the neutral native template (a one-line "You are Codex" prompt, `model_messages` stripped) or, on ProxyChat, from a gpt-5.5 clone regardless of the model name, so GPT models lost their own harness prompt, freeform apply_patch and reasoning levels. Rows now look up Codex's bundled official list with Codex's own matching rule (longest prefix, then one simple namespace segment, case-sensitive) and mirror the matched entry verbatim; row overrides do not apply, and a narrower relay window goes in `model_context_window` in config.toml. Fields owned by the official account or backend are reset: Responses Lite off, speed tiers, upgrade and NUX cleared, always listed, and ProxyChat keeps `supports_image_detail_original` off. Anthropic rows and the DeepSeek vendor catalog are unchanged. Stack members get the same entries under their prefixed ids. The reverse parser only collapses a row to its bare model name when it regenerates identically, so older ProxyChat gpt-5.5 clones keep the user's display name and window.
This commit is contained in:
1 parent
d46478e8e0
commit
c6255cc91d
1 file changed
+445
-6
@@ -2074,6 +2074,7 @@ fn load_codex_model_catalog_template() -> Result<Value, AppError> {
|
||||
load_codex_model_catalog_template_uncached()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn codex_model_catalog_from_specs(
|
||||
specs: &[CodexCatalogModelSpec],
|
||||
template: &Value,
|
||||
@@ -2103,6 +2104,124 @@ fn codex_model_catalog_from_settings(
|
||||
codex_catalog_from_specs_for_row(&specs, config_text, profile).map(Some)
|
||||
}
|
||||
|
||||
/// Codex 自带的 OpenAI 官方模型列表(`codex debug models --bundled`),给第三方的 GPT
|
||||
/// 行按模型名找官方条目用。只缓存成功读到的列表:Codex 升级后要重启 CC Switch 才换新。
|
||||
#[cfg(not(test))]
|
||||
static CODEX_OPENAI_OFFICIAL_MODELS_CACHE: OnceCell<Vec<Value>> = OnceCell::new();
|
||||
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
/// 测试里的官方列表(`None` 时只有编译期内置的 gpt-5.5),不跑本机的 `codex`。
|
||||
static CODEX_OPENAI_OFFICIAL_MODELS_OVERRIDE: std::cell::RefCell<Option<Vec<Value>>> =
|
||||
const { std::cell::RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// 编译期内置的 gpt-5.5:本机没有可用的 `codex` 时唯一的官方条目。
|
||||
fn codex_static_official_models() -> Vec<Value> {
|
||||
load_codex_model_template_static()
|
||||
.and_then(|template| normalize_codex_native_rows(vec![template]))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// 和账号无关的 OpenAI 官方模型列表。不用 `models_cache.json`:它是哪个账号、哪个版本
|
||||
/// 写的证明不了(见 `codex_official_models`)。
|
||||
#[cfg(not(test))]
|
||||
fn codex_openai_official_models() -> Vec<Value> {
|
||||
CODEX_OPENAI_OFFICIAL_MODELS_CACHE
|
||||
.get_or_try_init(|| {
|
||||
load_codex_bundled_models()
|
||||
.and_then(normalize_codex_native_rows)
|
||||
.ok_or(())
|
||||
})
|
||||
.cloned()
|
||||
.unwrap_or_else(|_| codex_static_official_models())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn codex_openai_official_models() -> Vec<Value> {
|
||||
CODEX_OPENAI_OFFICIAL_MODELS_OVERRIDE
|
||||
.with(|rows| rows.borrow().clone())
|
||||
.unwrap_or_else(codex_static_official_models)
|
||||
}
|
||||
|
||||
/// 按 Codex 自己查模型信息的规则找官方条目(codex-rs `models-manager/src/manager.rs` 的
|
||||
/// `construct_model_info_from_candidates`):先按最长前缀,没有再去掉一层命名空间
|
||||
/// (`openai/gpt-5.5`)重试;区分大小写。结果就是没有 CC Switch 的目录时 Codex 会用的那条。
|
||||
fn find_codex_official_model<'a>(model: &str, candidates: &'a [Value]) -> Option<&'a Value> {
|
||||
fn longest_prefix<'a>(model: &str, candidates: &'a [Value]) -> Option<&'a Value> {
|
||||
candidates
|
||||
.iter()
|
||||
.filter_map(|candidate| {
|
||||
let slug = candidate.get("slug").and_then(Value::as_str)?;
|
||||
(!slug.is_empty() && model.starts_with(slug)).then_some((slug.len(), candidate))
|
||||
})
|
||||
.max_by_key(|(len, _)| *len)
|
||||
.map(|(_, candidate)| candidate)
|
||||
}
|
||||
|
||||
longest_prefix(model, candidates).or_else(|| {
|
||||
let (namespace, suffix) = model.split_once('/')?;
|
||||
let simple_namespace = !namespace.is_empty()
|
||||
&& namespace
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-');
|
||||
if suffix.contains('/') || !simple_namespace {
|
||||
return None;
|
||||
}
|
||||
longest_prefix(suffix, candidates)
|
||||
})
|
||||
}
|
||||
|
||||
/// 第三方供应商上命中官方的 GPT 行:官方条目整条照搬(提示词、工具、档位、窗口都以官方
|
||||
/// 为准,不接受行里的覆盖值;窗口不同时在 `config.toml` 里设 `model_context_window`),
|
||||
/// 只改掉属于官方账号或官方后端的字段。
|
||||
fn codex_official_model_entry(
|
||||
official: &Value,
|
||||
model: &str,
|
||||
priority: usize,
|
||||
profile: CodexCatalogToolProfile,
|
||||
) -> Value {
|
||||
let mut entry = official.clone();
|
||||
let Some(obj) = entry.as_object_mut() else {
|
||||
return json!({});
|
||||
};
|
||||
// 前缀、命名空间命中的别名(`gpt-5.5-high`)不冒用官方的显示名。
|
||||
if obj.get("slug").and_then(Value::as_str) != Some(model) {
|
||||
obj.insert("display_name".to_string(), json!(model));
|
||||
}
|
||||
obj.insert("slug".to_string(), json!(model));
|
||||
obj.insert("priority".to_string(), json!(1000 + priority));
|
||||
// 官方隐藏的条目用户明确写了,也要出现在选择器里。
|
||||
obj.insert("visibility".to_string(), json!("list"));
|
||||
// 速度档、升级提示归官方账号。
|
||||
obj.insert("service_tiers".to_string(), json!([]));
|
||||
obj.insert("additional_speed_tiers".to_string(), json!([]));
|
||||
obj.insert("availability_nux".to_string(), Value::Null);
|
||||
obj.insert("upgrade".to_string(), Value::Null);
|
||||
// 第三方不支持 Responses Lite 协议。
|
||||
obj.insert("use_responses_lite".to_string(), Value::Bool(false));
|
||||
if profile == CodexCatalogToolProfile::ProxyChat {
|
||||
// 同 `codex_catalog_model_entry`:严格的 Chat 网关拒收 `original` 精度的图片。
|
||||
obj.insert("supports_image_detail_original".to_string(), json!(false));
|
||||
}
|
||||
entry
|
||||
}
|
||||
|
||||
/// 目录里的一行是不是按 `official` 原样镜像出来的:反向解析会保留的几项(显示名、窗口、
|
||||
/// 模态、并行工具调用)都和重新镜像的结果相同,只还原模型名才不丢东西。
|
||||
fn is_codex_official_mirror(entry: &Value, model: &str, official: &Value) -> bool {
|
||||
let expected =
|
||||
codex_official_model_entry(official, model, 0, CodexCatalogToolProfile::NativeResponses);
|
||||
[
|
||||
"display_name",
|
||||
"context_window",
|
||||
"input_modalities",
|
||||
"supports_parallel_tool_calls",
|
||||
]
|
||||
.iter()
|
||||
.all(|key| entry.get(*key) == expected.get(*key))
|
||||
}
|
||||
|
||||
/// The catalog for one provider's models: its official vendor catalog when the
|
||||
/// gateway ships one, otherwise the profile's template.
|
||||
fn codex_catalog_from_specs_for_row(
|
||||
@@ -2136,12 +2255,32 @@ fn codex_catalog_from_specs_for_row(
|
||||
}
|
||||
CodexCatalogToolProfile::ProxyChat => load_codex_model_catalog_template()?,
|
||||
};
|
||||
Ok(codex_model_catalog_from_specs(
|
||||
specs,
|
||||
&template,
|
||||
profile,
|
||||
default_context_window,
|
||||
))
|
||||
// 命中 OpenAI 官方条目的行照搬官方(写了目录之后 Codex 只认文件里的条目,通用模板
|
||||
// 会顶掉 GPT 自己的提示词),其余行照旧按模板生成。Responses→Anthropic 的转换会丢掉
|
||||
// 官方条目里的 custom 工具,这条路不照搬。
|
||||
let official = match profile {
|
||||
CodexCatalogToolProfile::Anthropic => Vec::new(),
|
||||
CodexCatalogToolProfile::NativeResponses | CodexCatalogToolProfile::ProxyChat => {
|
||||
codex_openai_official_models()
|
||||
}
|
||||
};
|
||||
let entries: Vec<Value> = specs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(
|
||||
|(index, spec)| match find_codex_official_model(&spec.model, &official) {
|
||||
Some(found) => codex_official_model_entry(found, &spec.model, index, profile),
|
||||
None => codex_catalog_model_entry(
|
||||
&template,
|
||||
spec,
|
||||
index,
|
||||
profile,
|
||||
default_context_window,
|
||||
),
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
Ok(json!({ "models": entries }))
|
||||
}
|
||||
|
||||
/// 一个供应商的模型目录:没有配置模型时为 `None`,不生成、也不指向目录文件。
|
||||
@@ -2537,6 +2676,7 @@ fn build_simplified_catalog_from_texts(config_text: &str, catalog_text: &str) ->
|
||||
let default_context_window =
|
||||
extract_codex_top_level_u64(config_text, "model_context_window").unwrap_or(128_000);
|
||||
|
||||
let mut official: Option<Vec<Value>> = None;
|
||||
let mut entries = Vec::with_capacity(models.len());
|
||||
for entry in models {
|
||||
let Some(model) = entry
|
||||
@@ -2554,6 +2694,22 @@ fn build_simplified_catalog_from_texts(config_text: &str, catalog_text: &str) ->
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
// 照搬官方的行只还原模型名,不能把官方值当成用户填的存回库里。通用模板没有
|
||||
// `model_messages`(走 Anthropic 的行不照搬);旧版 ProxyChat 克隆的 gpt-5.5 模板
|
||||
// 有,但带着用户填的显示名、窗口,下面逐项比对不上,照旧还原。
|
||||
if entry
|
||||
.get("model_messages")
|
||||
.and_then(|messages| messages.get("instructions_template"))
|
||||
.is_some()
|
||||
{
|
||||
let official = official.get_or_insert_with(codex_openai_official_models);
|
||||
if find_codex_official_model(model, official)
|
||||
.is_some_and(|found| is_codex_official_mirror(entry, model, found))
|
||||
{
|
||||
entries.push(json!({ "model": model }));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let mut obj = serde_json::Map::new();
|
||||
obj.insert("model".to_string(), json!(model));
|
||||
@@ -4500,6 +4656,289 @@ wire_api = "responses"
|
||||
);
|
||||
}
|
||||
|
||||
/// 在官方列表换成 `rows` 的情况下跑 `f`(本线程)。
|
||||
fn with_official_models<T>(rows: Vec<Value>, f: impl FnOnce() -> T) -> T {
|
||||
CODEX_OPENAI_OFFICIAL_MODELS_OVERRIDE.with(|slot| *slot.borrow_mut() = Some(rows));
|
||||
let out = f();
|
||||
CODEX_OPENAI_OFFICIAL_MODELS_OVERRIDE.with(|slot| *slot.borrow_mut() = None);
|
||||
out
|
||||
}
|
||||
|
||||
/// 官方 GPT 条目的形状:提示词在 `model_messages`,带 freeform 工具、Lite 协议、速度档。
|
||||
fn official_gpt_row(slug: &str, prompt: &str) -> Value {
|
||||
native_row(
|
||||
slug,
|
||||
json!({
|
||||
"display_name": slug.to_uppercase(),
|
||||
"model_messages": { "instructions_template": prompt },
|
||||
"apply_patch_tool_type": "freeform",
|
||||
"web_search_tool_type": "text_and_image",
|
||||
"tool_mode": "code_mode_only",
|
||||
"use_responses_lite": true,
|
||||
"visibility": "hide",
|
||||
"service_tiers": [{ "id": "priority", "name": "Fast" }],
|
||||
"additional_speed_tiers": ["fast"],
|
||||
"upgrade": { "model": "gpt-next" },
|
||||
"context_window": 272_000,
|
||||
"max_context_window": 872_000,
|
||||
"supports_image_detail_original": true,
|
||||
"supported_reasoning_levels": [
|
||||
{ "effort": "low", "description": "l" },
|
||||
{ "effort": "xhigh", "description": "x" }
|
||||
],
|
||||
"default_reasoning_level": "low",
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn official_gpt_rows() -> Vec<Value> {
|
||||
normalize_codex_native_rows(vec![
|
||||
official_gpt_row("gpt-6", "GPT-6 base prompt"),
|
||||
official_gpt_row("gpt-6-sol", "GPT-6 Sol prompt"),
|
||||
official_gpt_row("gpt-5.5", "GPT-5.5 prompt"),
|
||||
])
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn catalog_for(models: Value, config: &str, profile: CodexCatalogToolProfile) -> Vec<Value> {
|
||||
let settings = json!({ "modelCatalog": { "models": models } });
|
||||
codex_model_catalog_from_settings(&settings, config, profile)
|
||||
.unwrap()
|
||||
.unwrap()["models"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.clone()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn official_models_match_like_codex() {
|
||||
let rows = official_gpt_rows();
|
||||
let found = |model: &str| {
|
||||
find_codex_official_model(model, &rows)
|
||||
.and_then(|row| row["slug"].as_str())
|
||||
.map(str::to_string)
|
||||
};
|
||||
// 最长前缀。
|
||||
assert_eq!(found("gpt-6-sol").as_deref(), Some("gpt-6-sol"));
|
||||
assert_eq!(found("gpt-6-sol-high").as_deref(), Some("gpt-6-sol"));
|
||||
assert_eq!(found("gpt-6-luna").as_deref(), Some("gpt-6"));
|
||||
// 去掉一层简单的命名空间。
|
||||
assert_eq!(found("openai/gpt-5.5").as_deref(), Some("gpt-5.5"));
|
||||
assert_eq!(found("my_relay-1/gpt-6-sol").as_deref(), Some("gpt-6-sol"));
|
||||
assert_eq!(found("a/b/gpt-5.5"), None);
|
||||
assert_eq!(found("bad ns/gpt-5.5"), None);
|
||||
assert_eq!(found("/gpt-5.5"), None);
|
||||
// 区分大小写,和 Codex 一样。
|
||||
assert_eq!(found("GPT-5.5"), None);
|
||||
assert_eq!(found("gpt-5"), None);
|
||||
assert_eq!(found("glm-5"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_gpt_rows_mirror_the_official_entry() {
|
||||
let models = with_official_models(official_gpt_rows(), || {
|
||||
catalog_for(
|
||||
json!([
|
||||
// 行里的覆盖值对官方行不生效。
|
||||
{ "model": "gpt-6-sol", "contextWindow": 128_000, "reasoningLevels": ["none"] },
|
||||
{ "model": "glm-5" }
|
||||
]),
|
||||
"",
|
||||
CodexCatalogToolProfile::NativeResponses,
|
||||
)
|
||||
});
|
||||
|
||||
let gpt = &models[0];
|
||||
assert_eq!(gpt["slug"], "gpt-6-sol");
|
||||
assert_eq!(gpt["display_name"], "GPT-6-SOL");
|
||||
assert_eq!(
|
||||
gpt["model_messages"]["instructions_template"],
|
||||
"GPT-6 Sol prompt"
|
||||
);
|
||||
assert_eq!(gpt["apply_patch_tool_type"], "freeform");
|
||||
assert_eq!(gpt["web_search_tool_type"], "text_and_image");
|
||||
assert_eq!(gpt["tool_mode"], "code_mode_only");
|
||||
assert_eq!(gpt["context_window"], 272_000);
|
||||
assert_eq!(gpt["max_context_window"], 872_000);
|
||||
assert_eq!(gpt["default_reasoning_level"], "low");
|
||||
assert_eq!(gpt["supported_reasoning_levels"][1]["effort"], "xhigh");
|
||||
assert_eq!(gpt["supports_image_detail_original"], true);
|
||||
// 官方账号、官方后端的字段改掉。
|
||||
assert_eq!(gpt["use_responses_lite"], false);
|
||||
assert_eq!(gpt["visibility"], "list");
|
||||
assert_eq!(gpt["service_tiers"], json!([]));
|
||||
assert_eq!(gpt["additional_speed_tiers"], json!([]));
|
||||
assert_eq!(gpt["upgrade"], Value::Null);
|
||||
assert_eq!(gpt["priority"], 1000);
|
||||
|
||||
// 没命中的行照旧:通用模板、去掉 custom 工具。
|
||||
let glm = &models[1];
|
||||
assert_eq!(glm["slug"], "glm-5");
|
||||
assert_eq!(glm["priority"], 1001);
|
||||
assert!(glm.get("model_messages").is_none());
|
||||
assert!(glm.get("apply_patch_tool_type").is_none());
|
||||
assert_eq!(glm["shell_type"], "shell_command");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aliases_keep_their_own_name() {
|
||||
let models = with_official_models(official_gpt_rows(), || {
|
||||
catalog_for(
|
||||
json!([{ "model": "openai/gpt-5.5" }, { "model": "gpt-6-sol-high" }]),
|
||||
"",
|
||||
CodexCatalogToolProfile::NativeResponses,
|
||||
)
|
||||
});
|
||||
assert_eq!(models[0]["slug"], "openai/gpt-5.5");
|
||||
assert_eq!(models[0]["display_name"], "openai/gpt-5.5");
|
||||
assert_eq!(
|
||||
models[0]["model_messages"]["instructions_template"],
|
||||
"GPT-5.5 prompt"
|
||||
);
|
||||
assert_eq!(models[1]["slug"], "gpt-6-sol-high");
|
||||
assert_eq!(models[1]["display_name"], "gpt-6-sol-high");
|
||||
assert_eq!(
|
||||
models[1]["model_messages"]["instructions_template"],
|
||||
"GPT-6 Sol prompt"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_chat_gpt_rows_mirror_but_never_send_original_image_detail() {
|
||||
let models = with_official_models(official_gpt_rows(), || {
|
||||
catalog_for(
|
||||
json!([{ "model": "gpt-6-sol" }]),
|
||||
"",
|
||||
CodexCatalogToolProfile::ProxyChat,
|
||||
)
|
||||
});
|
||||
assert_eq!(
|
||||
models[0]["model_messages"]["instructions_template"],
|
||||
"GPT-6 Sol prompt"
|
||||
);
|
||||
assert_eq!(models[0]["apply_patch_tool_type"], "freeform");
|
||||
assert_eq!(models[0]["use_responses_lite"], false);
|
||||
assert_eq!(models[0]["supports_image_detail_original"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anthropic_and_deepseek_rows_do_not_mirror_openai() {
|
||||
let (anthropic, deepseek) = with_official_models(official_gpt_rows(), || {
|
||||
(
|
||||
catalog_for(
|
||||
json!([{ "model": "gpt-6-sol" }]),
|
||||
"",
|
||||
CodexCatalogToolProfile::Anthropic,
|
||||
),
|
||||
catalog_for(
|
||||
json!([{ "model": "gpt-6-sol" }]),
|
||||
DEEPSEEK_NATIVE_CONFIG,
|
||||
CodexCatalogToolProfile::NativeResponses,
|
||||
),
|
||||
)
|
||||
});
|
||||
// Responses→Anthropic 的转换会丢掉 custom 工具:照旧用通用模板。
|
||||
assert!(anthropic[0].get("model_messages").is_none());
|
||||
assert!(anthropic[0].get("apply_patch_tool_type").is_none());
|
||||
// DeepSeek 官方网关按它自己的目录。
|
||||
assert_ne!(
|
||||
deepseek[0]["model_messages"]["instructions_template"],
|
||||
"GPT-6 Sol prompt"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stacked_gpt_rows_mirror_under_their_prefixed_id() {
|
||||
let models = with_official_models(official_gpt_rows(), || {
|
||||
let route = json!({ "modelCatalog": { "models": [{ "model": "gpt-6-sol" }] } });
|
||||
let member = json!({ "modelCatalog": { "models": [{ "model": "gpt-6-sol" }] } });
|
||||
plan_codex_stack_catalog(
|
||||
CodexStackRoute::ThirdParty(CodexCatalogRow {
|
||||
settings: &route,
|
||||
config_text: "",
|
||||
profile: CodexCatalogToolProfile::NativeResponses,
|
||||
}),
|
||||
&[CodexStackCatalogMember {
|
||||
key: "relay",
|
||||
provider_name: "Relay",
|
||||
row: CodexCatalogRow {
|
||||
settings: &member,
|
||||
config_text: "",
|
||||
profile: CodexCatalogToolProfile::ProxyChat,
|
||||
},
|
||||
}],
|
||||
)
|
||||
.unwrap()["models"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.clone()
|
||||
});
|
||||
// 两家都有 GPT-6 Sol:各一条,内容都是官方的。
|
||||
assert_eq!(models[0]["slug"], "gpt-6-sol");
|
||||
assert_eq!(models[1]["slug"], "ccs-relay/gpt-6-sol");
|
||||
for model in &models {
|
||||
assert_eq!(
|
||||
model["model_messages"]["instructions_template"],
|
||||
"GPT-6 Sol prompt"
|
||||
);
|
||||
assert_eq!(model["use_responses_lite"], false);
|
||||
}
|
||||
// 各家按自己的链路:走 Chat 的那家不发 original 精度的图片。
|
||||
assert_eq!(models[0]["supports_image_detail_original"], true);
|
||||
assert_eq!(models[1]["supports_image_detail_original"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mirrored_rows_round_trip_as_bare_models() {
|
||||
let (catalog, simplified) = with_official_models(official_gpt_rows(), || {
|
||||
let catalog = json!({
|
||||
"models": catalog_for(
|
||||
json!([
|
||||
{ "model": "gpt-6-sol" },
|
||||
{ "model": "glm-5", "displayName": "GLM 5", "contextWindow": 200_000 }
|
||||
]),
|
||||
"",
|
||||
CodexCatalogToolProfile::NativeResponses,
|
||||
)
|
||||
});
|
||||
let simplified = build_simplified_catalog_from_texts("", &catalog.to_string());
|
||||
(catalog, simplified)
|
||||
});
|
||||
// 官方的显示名、窗口、档位不能被当成用户填的存回去。
|
||||
assert_eq!(catalog["models"][0]["display_name"], "GPT-6-SOL");
|
||||
let rows = simplified.unwrap()["models"].as_array().unwrap().clone();
|
||||
assert_eq!(rows[0], json!({ "model": "gpt-6-sol" }));
|
||||
assert_eq!(rows[1]["model"], "glm-5");
|
||||
assert_eq!(rows[1]["displayName"], "GLM 5");
|
||||
assert_eq!(rows[1]["contextWindow"], 200_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_proxy_chat_clones_keep_their_user_values_on_round_trip() {
|
||||
// 旧版 ProxyChat 克隆 gpt-5.5 模板(带 model_messages),再叠用户填的值。
|
||||
let rows = official_gpt_rows();
|
||||
let settings = json!({ "modelCatalog": { "models": [
|
||||
{ "model": "gpt-5.5", "displayName": "My GPT", "contextWindow": 200_000 },
|
||||
{ "model": "gpt-6-sol" }
|
||||
] } });
|
||||
let old = codex_model_catalog_from_specs(
|
||||
&codex_catalog_model_specs(&settings),
|
||||
&rows[2],
|
||||
CodexCatalogToolProfile::ProxyChat,
|
||||
128_000,
|
||||
);
|
||||
let simplified = with_official_models(rows, || {
|
||||
build_simplified_catalog_from_texts("", &old.to_string())
|
||||
})
|
||||
.unwrap();
|
||||
let models = simplified["models"].as_array().unwrap();
|
||||
assert_eq!(models[0]["displayName"], "My GPT");
|
||||
assert_eq!(models[0]["contextWindow"], 200_000);
|
||||
// 克隆 gpt-5.5 的 gpt-6-sol 行:显示名、窗口都不是官方的,照旧还原,不当成镜像。
|
||||
assert_eq!(models[1]["model"], "gpt-6-sol");
|
||||
assert_ne!(models[1], json!({ "model": "gpt-6-sol" }));
|
||||
}
|
||||
|
||||
fn native_row(slug: &str, extra: Value) -> Value {
|
||||
let mut row = json!({
|
||||
"slug": slug,
|
||||
|
||||
Reference in new issue
Block a user