mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-10-26 01:34:51 +08:00
fix(codex): split the stack comp_hash by tool protocol
In the merged (stack) catalog every third-party row shared one comp_hash, "cc-switch", so Codex never compacted when switching between them (#8485). Third-party GPT rows mirror the official entry and carry `tool_mode: "code_mode_only"`, where shell calls are recorded as the custom tool `exec`; DeepSeek, Kimi and other rows are direct and only have `exec_command`. After switching from a GPT row to a direct row without compaction, the direct model copied the `exec` calls from history and every turn failed with `unsupported call: exec`. Group rows by tool protocol instead: a row with a tool_mode gets "cc-switch-<tool_mode>", direct rows keep "cc-switch" so existing sessions see no change. Switching within a group still skips compaction; switching across groups compacts with the previous model first, which folds the other protocol's calls into the summary. tool_mode and supports_search_tool keep the template values, so third-party GPT rows keep code mode. The cost is that a cross-group switch away from a provider that is no longer usable fails to compact, as before #8485; switching within the group or starting a new session avoids it. Fixes #8676
This commit is contained in:
1 parent
79ba7982aa
commit
91fc2fcbf7
1 file changed
+82
-3
@@ -2541,6 +2541,23 @@ pub(crate) enum CodexStackRoute<'a> {
|
||||
/// 压缩语义也要保留。
|
||||
const CODEX_STACK_COMP_HASH: &str = "cc-switch";
|
||||
|
||||
/// 让一行加入合并目录里共用历史的那组:`comp_hash` 按工具协议分组,同协议的行共用一个值。
|
||||
///
|
||||
/// 镜像官方条目的第三方 GPT 行带着 `tool_mode: "code_mode_only"`,shell 调用在历史里是
|
||||
/// custom 工具 `exec`;其余第三方行是 direct,只有 `exec_command`。两者若共用一个值,切换时
|
||||
/// 不压缩,direct 模型会照着历史调用不存在的 `exec`,每轮都报 `unsupported call: exec`
|
||||
/// (#8676)。所以协议不同就分开:跨组切换先用上一家压缩,把对方协议的调用收进摘要;
|
||||
/// 组内切换照旧不压缩(#8485)。代价是跨组切换时上一家若不可用,压缩会失败,#8485 在这条
|
||||
/// 路径上回来;组内换一家或新开会话都能绕开。direct 行沿用 [`CODEX_STACK_COMP_HASH`],
|
||||
/// 老会话记下的值不变。
|
||||
fn join_stack_history(obj: &mut serde_json::Map<String, Value>) {
|
||||
let comp_hash = match obj.get("tool_mode").and_then(Value::as_str) {
|
||||
Some(mode) => format!("{CODEX_STACK_COMP_HASH}-{mode}"),
|
||||
None => CODEX_STACK_COMP_HASH.to_string(),
|
||||
};
|
||||
obj.insert("comp_hash".to_string(), json!(comp_hash));
|
||||
}
|
||||
|
||||
/// Stack 名单非空时的模型目录:路由那家的行在前,各 Stack 供应商的行按名单顺序在后,
|
||||
/// `priority` 统一重新编号。
|
||||
///
|
||||
@@ -2558,7 +2575,7 @@ pub(crate) fn plan_codex_stack_catalog(
|
||||
let mut rows = codex_stack_third_party_rows(&row)?;
|
||||
for entry in &mut rows {
|
||||
if let Some(obj) = entry.as_object_mut() {
|
||||
obj.insert("comp_hash".to_string(), json!(CODEX_STACK_COMP_HASH));
|
||||
join_stack_history(obj);
|
||||
}
|
||||
}
|
||||
rows
|
||||
@@ -2586,7 +2603,7 @@ pub(crate) fn plan_codex_stack_catalog(
|
||||
let Some(obj) = entry.as_object_mut() else {
|
||||
continue;
|
||||
};
|
||||
obj.insert("comp_hash".to_string(), json!(CODEX_STACK_COMP_HASH));
|
||||
join_stack_history(obj);
|
||||
let Some(model) = obj.get("slug").and_then(Value::as_str).map(str::to_string) else {
|
||||
continue;
|
||||
};
|
||||
@@ -2647,7 +2664,8 @@ pub(crate) fn plan_codex_stack_catalog(
|
||||
/// `config.toml` 里 `[features] multi_agent_v2 = true` 比目录优先,这时写了也不生效。
|
||||
const CODEX_CLASSIC_SUBAGENTS: &str = "v1";
|
||||
|
||||
/// 一家第三方供应商在合并目录里的行(`comp_hash` 保持模板的值,由调用方统一改写)。
|
||||
/// 一家第三方供应商在合并目录里的行(`comp_hash` 保持模板的值,由调用方经
|
||||
/// [`join_stack_history`] 统一改写)。
|
||||
pub(crate) fn codex_stack_third_party_rows(
|
||||
row: &CodexCatalogRow<'_>,
|
||||
) -> Result<Vec<Value>, AppError> {
|
||||
@@ -5836,6 +5854,67 @@ wire_api = "responses"
|
||||
}
|
||||
}
|
||||
|
||||
/// #8676:共用 `comp_hash` 的行切换时不压缩,所以按工具协议分组。镜像官方条目的第三方
|
||||
/// GPT 行(路由那家和 Stack 成员)保留 code mode,自成一组;direct 行仍是原来的值。
|
||||
/// 除 `comp_hash` 外,GPT 行和直连目录里的同一行一致。
|
||||
#[test]
|
||||
fn stack_rows_share_history_only_within_one_tool_protocol() {
|
||||
let gpt_settings = json!({ "modelCatalog": { "models": [{ "model": "gpt-6-sol" }] } });
|
||||
let gpt_text = "model = \"gpt-6-sol\"\n";
|
||||
let ds_settings = json!({ "modelCatalog": { "models": [{ "model": "deepseek-v4-pro" }] } });
|
||||
let ds_text = "model = \"deepseek-v4-pro\"\n";
|
||||
let profile = CodexCatalogToolProfile::NativeResponses;
|
||||
let row = |settings, config_text| CodexCatalogRow {
|
||||
settings,
|
||||
config_text,
|
||||
profile,
|
||||
};
|
||||
let (direct, stacked) = with_official_models(official_gpt_rows(), || {
|
||||
let direct = codex_model_catalog_from_settings(&gpt_settings, gpt_text, profile)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let stacked = plan_codex_stack_catalog(
|
||||
CodexStackRoute::ThirdParty(row(&gpt_settings, gpt_text)),
|
||||
&[
|
||||
CodexStackCatalogMember {
|
||||
key: "relay",
|
||||
provider_name: "Relay",
|
||||
row: row(&gpt_settings, gpt_text),
|
||||
},
|
||||
CodexStackCatalogMember {
|
||||
key: "ds",
|
||||
provider_name: "DS",
|
||||
row: row(&ds_settings, ds_text),
|
||||
},
|
||||
],
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
(direct, stacked)
|
||||
});
|
||||
|
||||
let models = stacked["models"].as_array().unwrap();
|
||||
let slugs: Vec<&str> = models.iter().map(|m| m["slug"].as_str().unwrap()).collect();
|
||||
assert_eq!(
|
||||
slugs,
|
||||
vec!["gpt-6-sol", "ccs-relay/gpt-6-sol", "ccs-ds/deepseek-v4-pro"]
|
||||
);
|
||||
let mirrored = &direct["models"][0];
|
||||
assert_eq!(mirrored["tool_mode"], "code_mode_only");
|
||||
for gpt in &models[..2] {
|
||||
assert_eq!(
|
||||
gpt["comp_hash"], "cc-switch-code_mode_only",
|
||||
"{}",
|
||||
gpt["slug"]
|
||||
);
|
||||
for key in ["tool_mode", "supports_search_tool", "model_messages"] {
|
||||
assert_eq!(gpt[key], mirrored[key], "{} {key}", gpt["slug"]);
|
||||
}
|
||||
}
|
||||
assert!(models[2].get("tool_mode").is_none());
|
||||
assert_eq!(models[2]["comp_hash"], "cc-switch");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stacked_rows_keep_their_own_tool_profile_and_never_use_responses_lite() {
|
||||
let route_settings = json!({ "modelCatalog": { "models": [{ "model": "route-model" }] } });
|
||||
|
||||
Reference in new issue
Block a user