feat(models): 模型映射新增最大输出 Token 列(max_output_size 直写 config.toml,models.dev 参考值一键填入/空值自动补全),移除无效的声明支持 1M 复选框;思考等级顺序修正为 low/medium/high/xhigh/max 且标签全英文 — bump to v0.8.0
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
Release / Attach macOS install script (push) Canceled after 0s

This commit is contained in:
KimiSwitch Dev committed 2026-10-03 23:28:36 +08:00
1 parent 30371a1ba6
commit 3c3c18925d
19 files changed
+412 -38

No files matched your search

+1 -1
View File
@@ -1978,7 +1978,7 @@ dependencies = [
[[package]]
name = "kimiswitch"
version = "0.7.23"
version = "0.8.0"
dependencies = [
"anyhow",
"chrono",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "kimiswitch"
version = "0.7.23"
version = "0.8.0"
description = "Kimi Switch - model config manager"
authors = ["codingplan.site"]
edition = "2021"
+108
View File
@@ -648,6 +648,114 @@ api_key = ""
assert!(caps.iter().any(|v| v.as_str() == Some("thinking")));
}
#[test]
fn kimi_code_export_writes_and_drops_max_output_size() {
// max_output_size is not a first-class Model field: it round-trips
// through raw_other and must land in config.toml verbatim, and must be
// removable (unset = upstream default) without disturbing its siblings.
let mut providers = IndexMap::new();
providers.insert(
"p".to_string(),
Provider {
name: "p".to_string(),
provider_type: ProviderType::Openai,
base_url: Some("https://a.example.com".to_string()),
api_key: Some("sk-a".to_string()),
api_key_env: None,
env: IndexMap::new(),
note: None,
official_url: None,
managed: false,
enabled: true,
active: true,
icon: None,
icon_color: None,
raw_other: Value::Null,
usage_kinds: None,
usage_config: None,
},
);
let make_model = |raw_other: Value| Model {
alias: "p/m".to_string(),
provider: "p".to_string(),
model: "m".to_string(),
max_context_size: 128_000,
display_name: None,
supports_1m: false,
capabilities: vec![],
raw_other,
};
let mut models = IndexMap::new();
models.insert(
"p/m".to_string(),
make_model(serde_json::json!({
"max_output_size": 32768,
"support_efforts": ["low", "high"],
})),
);
let config = Config {
default_model: None,
providers,
models,
raw_other: Value::Null,
imported_section_keys: Vec::new(),
};
let exported = config_to_kimi_code(&config, None);
let root = exported.as_table().unwrap();
let model = root
.get("models")
.unwrap()
.as_table()
.unwrap()
.get("p/m")
.unwrap()
.as_table()
.unwrap();
assert_eq!(model.get("max_output_size").and_then(|v| v.as_integer()), Some(32768));
assert!(model.get("support_efforts").is_some());
// Re-import through real TOML text (the same serialize → parse path
// save_kimi_code_config / load_kimi_code_config use): the key must
// come back in the model's raw_other as an integer.
let toml_text = toml::to_string_pretty(&exported).unwrap();
assert!(toml_text.contains("max_output_size = 32768"), "toml: {toml_text}");
let reparsed: TomlValue = toml_text.parse().unwrap();
let parsed = kimi_code_to_config(&reparsed);
let loaded = parsed.models.get("p/m").unwrap();
assert_eq!(
loaded.raw_other.get("max_output_size").and_then(|v| v.as_i64()),
Some(32768)
);
// Clearing the override drops the key entirely.
let mut models = IndexMap::new();
models.insert(
"p/m".to_string(),
make_model(serde_json::json!({ "support_efforts": ["low", "high"] })),
);
let config = Config {
default_model: None,
providers: IndexMap::new(),
models,
raw_other: Value::Null,
imported_section_keys: Vec::new(),
};
let exported = config_to_kimi_code(&config, None);
let root = exported.as_table().unwrap();
let model = root
.get("models")
.unwrap()
.as_table()
.unwrap()
.get("p/m")
.unwrap()
.as_table()
.unwrap();
assert!(model.get("max_output_size").is_none());
}
#[test]
fn kimi_code_export_adds_opencode_go_session_header() {
let make_provider = |base_url: &str, raw_other: Value| Provider {
+12 -2
View File
@@ -138,6 +138,14 @@ fn build_snapshot(raw: &Value) -> Result<Value, String> {
{
entry.insert("context".into(), num(ctx));
}
// Max output tokens — reference for the max_output_size column.
if let Some(out) = m
.pointer("/limit/output")
.and_then(|x| x.as_f64())
.filter(|o| *o > 0.0)
{
entry.insert("output".into(), num(out));
}
for flag in ["reasoning", "tool_call", "structured_output"] {
if m.get(flag).and_then(|x| x.as_bool()) == Some(true) {
entry.insert(flag.into(), Value::Bool(true));
@@ -286,7 +294,7 @@ mod tests {
},
"kimi-image": {
"name": "Kimi Image",
"limit": { "context": 0 },
"limit": { "context": 0, "output": 0 },
"modalities": { "input": ["text", "image", "video"] }
}
}
@@ -309,6 +317,7 @@ mod tests {
let k3 = &obj["moonshotai/kimi-k3"];
assert_eq!(k3["name"], "Kimi K3");
assert_eq!(k3["context"], 262144);
assert_eq!(k3["output"], 16384);
assert_eq!(k3["reasoning"], true);
assert_eq!(k3["tool_call"], true);
assert_eq!(k3["structured_output"], true);
@@ -318,9 +327,10 @@ mod tests {
assert_eq!(k3["cost"]["cache_read"], 0.3);
assert_eq!(k3["cost"]["cache_write"], 0.0);
// Context 0 → dropped; video modality → flagged.
// Context 0 / output 0 → dropped; video modality → flagged.
let img = &obj["moonshotai/kimi-image"];
assert!(img.get("context").is_none());
assert!(img.get("output").is_none());
assert_eq!(img["video"], true);
assert!(img.get("cost").is_none());
+1 -1
View File
@@ -1,6 +1,6 @@
{
"productName": "Kimi Switch",
"version": "0.7.23",
"version": "0.8.0",
"identifier": "com.kimiswitch.app",
"build": {
"beforeDevCommand": "npm run dev",