feat(subagent): 实验功能设置新增子代理模型配置
Release / Version consistency (push) Canceled after 0s
Release / Build (macos-latest) (push) Canceled after 0s
Release / Build (ubuntu-latest) (push) Canceled after 0s
Release / Build (windows-latest) (push) Canceled after 0s

- 子代理设置页支持选择次主力模型,仅保存模型引用(不再写补丁字段),
  子代理直接继承所选模型的上下文长度与思考模式
- 界面只读展示继承的上下文长度与思考模式,移除补丁编辑项
- 仪表盘统计识别子代理请求(__secondary__):归为「子代理模型(估算)」
  并缓存次主力模型定价;自定义网关模型价格按官方同名模型跨 provider 匹配
- bump v0.7.1
This commit is contained in:
KimiSwitch Dev committed 2026-08-05 23:38:05 +08:00
1 parent 8b0daabd95
commit 8aed04898c
22 files changed
+16648 -8882

No files matched your search

+103
View File
@@ -343,6 +343,39 @@ pub fn config_to_kimi_code(config: &Config, existing: Option<&TomlValue>) -> Tom
}
root.insert("models".to_string(), TomlValue::Table(models_table));
// Sync UI-managed top-level sections (thinking, experimental,
// secondary_model, ...) from config.raw_other back into the root.
// At import time every top-level key except default_model/providers/
// models was collected into raw_other, so raw_other is the authoritative
// snapshot of those sections. This both persists UI edits to those
// sections and drops sections the user explicitly removed (absent from
// raw_other but still present in `existing`).
let mut managed_keys = std::collections::HashSet::new();
if let TomlValue::Table(extra) =
json_to_toml(&config.raw_other).unwrap_or(TomlValue::Table(Table::new()))
{
for (k, v) in extra {
if k == "default_model" || k == "providers" || k == "models" {
continue;
}
managed_keys.insert(k.clone());
root.insert(k, v);
}
}
let stale_keys: Vec<String> = root
.keys()
.filter(|k| {
*k != "default_model"
&& *k != "providers"
&& *k != "models"
&& !managed_keys.contains(*k)
})
.cloned()
.collect();
for k in stale_keys {
root.remove(&k);
}
TomlValue::Table(root)
}
@@ -552,6 +585,76 @@ api_key = ""
assert!(caps.iter().any(|v| v.as_str() == Some("thinking")));
}
#[test]
fn kimi_code_export_syncs_raw_other_top_level_sections() {
// Regression: UI-managed top-level sections ([thinking],
// [experimental], [secondary_model], ...) collected into raw_other at
// import time must be written back on export; sections removed from
// raw_other must be dropped from the output.
let toml_str = r#"
default_model = "glm-5.2"
default_thinking = true
[providers."glmzhongzhuan"]
type = "anthropic"
api_key = "sk-test"
[models."glm-5.2"]
provider = "glmzhongzhuan"
model = "glm-5.2"
max_context_size = 900000
[thinking]
enabled = true
[experimental]
secondary-model = true
[secondary_model]
model = "glm-5.2"
default_effort = "low"
"#;
let value: TomlValue = toml_str.parse().unwrap();
let mut config = kimi_code_to_config(&value);
// raw_other collected the unknown top-level sections
let raw = config.raw_other.as_object().unwrap();
assert!(raw.contains_key("thinking"));
assert!(raw.contains_key("experimental"));
assert!(raw.contains_key("secondary_model"));
// UI edit: flip the experimental flag off and drop secondary_model
let mut raw = config.raw_other.as_object().unwrap().clone();
raw.insert(
"experimental".to_string(),
serde_json::json!({"secondary-model": false}),
);
raw.remove("secondary_model");
config.raw_other = serde_json::Value::Object(raw);
let exported = config_to_kimi_code(&config, Some(&value));
let root = exported.as_table().unwrap();
// Updated section is written back
let exp = root.get("experimental").unwrap().as_table().unwrap();
assert_eq!(
exp.get("secondary-model").and_then(|v| v.as_bool()),
Some(false)
);
// Removed section is dropped
assert!(root.get("secondary_model").is_none());
// Untouched sections are preserved
let thinking = root.get("thinking").unwrap().as_table().unwrap();
assert_eq!(
thinking.get("enabled").and_then(|v| v.as_bool()),
Some(true)
);
assert_eq!(
root.get("default_thinking").and_then(|v| v.as_bool()),
Some(true)
);
}
#[test]
fn kimi_code_export_does_not_inject_thinking_from_supports_1m() {
let mut providers = IndexMap::new();