Compare commits

...
5 Commits
Author SHA1 Message Date
KimiSwitch Dev 7435198fcc chore(release): bump to v0.7.10 — 配置写回保护(导入基线 + 未知供应商类型原样保留)
Release / Version consistency (push) Canceled after 0s
Release / Attach macOS install script (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
2026-08-29 00:10:52 +08:00
KimiSwitch Dev 67db19eda4 fix(config): 保存不再误删 CLI 后加的顶层配置节 + 未知供应商类型原样保留
- 导出 stale-key 清理增加导入基线(imported_section_keys):CLI 在导入后
  新增的顶层节(kimi-code 0.38+ 的 [task]/[swarm]/[cron] 等)在保存时
  原样保留;用户在 UI 显式删除的节仍被移除;空基线(SQLite 快照恢复)
  不删任何键(安全退化)
- ProviderType 增加 Unknown(String) 变体,手写 serde 保持纯字符串线格式,
  上游新增的 type 值往返不再被改写为 kimi;SQLite 快照恢复路径同步修复;
  模型发现对未知类型返回明确错误,不再误用 KIMI_API_KEY
- 新增 3 个往返测试(106 个 Rust 测试全绿),前端 tsc/vitest 通过
2026-08-29 00:09:55 +08:00
KimiSwitch Dev 9764929919 chore(release): bump to v0.7.9 — models.dev 数据同步(GLM-5.3/5.3-Flash、Qwen3.8、豆包 Seed 2.x)
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
2026-08-27 09:42:14 +08:00
KimiSwitch Dev f40e4be577 chore(data): 同步 models-dev 2026-08-27 快照(7343 模型 / 203 供应商)— 新增 GLM-5.3/5.3-Flash、Qwen3.8、豆包 Seed 2.x 等 74 模型,58 处价格与 56 处能力更新;补齐 public/ 静态资源(原停留在 08-01) 2026-08-27 09:37:26 +08:00
KimiSwitch Dev 138a2b8547 chore(data): 同步 models-dev 最新模型数据(7284 模型 / 199 供应商) 2026-08-25 13:12:11 +08:00
20 changed files with 81453 additions and 60648 deletions

No files matched your search

+5 -5
View File
@@ -81,11 +81,11 @@ jobs:
### 本次更新 ### 本次更新
- **高级设置**:原「子代理设置」升级为「高级设置」,聚合子代理模型指定、实验功能开关与 WebUI 快捷入口 - **模型数据同步(models.dev 2026-08-27)**:7343 个模型 / 203 个供应商
- **仪表盘热力图双击弹窗**:双击热力图方块即可查看当日模型用量分布,每种模型展示 Token / 请求次数 / 费用 / 命中率 - **新增 GLM-5.3 / GLM-5.3-Flash 全线接入**:智谱官方、OpenRouter、Cloudflare、DeepInfra、HuggingFace、火山引擎等约 20 条渠道;GLM-5.3-Flash 定价 $0.075 / $0.25(输入/输出,每百万 token)
- **子代理模型指定(实验功能)**:为子代理绑定次主力模型,不再默认继承主模型;实验开关改为滑动开关并修复开启态隐形 - 新增 coding plan 渠道:`zhipuai-coding-plan/glm-5.3`、`glm-5.3-highspeed`、`glm-5.3-flash`
- **兼容 kimi-code 0.33+(v2 引擎)**:`loop_control` 写入新键名 `max_attempts_per_step`,旧配置自动迁移并保留旧键兼容 `KIMI_CODE_LEGACY_FLAG=1`;hooks 候选补充新事件 - 新增 Qwen3.8 系列、火山引擎豆包 Seed 2.x 全系、DeepSeek-V4 GA 版、MiniMax-M2.7/M3 等共 74 个模型条目
- models.dev 模型数据同步;官网企业版视觉升级;出品公司标识统一 - 价格更新 58 处、能力信息修正 56 处(详见 release-notes-v0.7.9.md)
### Downloads by platform ### Downloads by platform
+6
View File
@@ -543,6 +543,12 @@ A: 需要分两步:
**Q: 切换会覆盖其他供应商吗?** **Q: 切换会覆盖其他供应商吗?**
A: 不会。Kimi Code 的 `config.toml` 始终写入全部供应商,仅 `default_model` 决定生效项。这与 CLI 原生 `/provider` 行为一致。 A: 不会。Kimi Code 的 `config.toml` 始终写入全部供应商,仅 `default_model` 决定生效项。这与 CLI 原生 `/provider` 行为一致。
**Q: 在 Kimi Code CLI 里新增的顶层配置节(如 `[task]`/`[swarm]`)会被保存时删掉吗?**
A: 不会(v0.7.10 起)。保存时以加载配置那一刻的顶层键为基线,CLI 后加的节原样保留;只有在本应用中显式删除的节才会从 `config.toml` 移除。
**Q: 遇到 Kimi Code 新增的供应商类型(`type` 值)会怎样?**
A: 原样保留,不再改写为 `kimi`。编辑界面会显示原始类型值(标注"未知类型");该供应商不支持的操作(如模型发现)会返回明确错误提示。
**Q: 新增/激活的供应商位置怎么变?** **Q: 新增/激活的供应商位置怎么变?**
A: 自动提升到列表最前,UI 立即反映。 A: 自动提升到列表最前,UI 立即反映。
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "kimiswitch", "name": "kimiswitch",
"private": true, "private": true,
"version": "0.7.8", "version": "0.7.10",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+77684 -58645
View File
File diff suppressed because it is too large. Load diff
+8
View File
@@ -0,0 +1,8 @@
## v0.7.10
### 配置写回保护(适配 kimi-code 0.38+ 配置演进)
- **修复:保存不再误删 CLI 新增的顶层配置节**。此前在 Kimi Switch 加载配置后,若 Kimi Code CLI 往 `config.toml` 写入了新的顶层节(如 0.38+ 的 `[task]` / `[swarm]` / `[cron]` / `[tools]` / `[identity]` / `[token_counting]` 等),回到 Kimi Switch 保存时会把这些节**静默删除**。现在以导入时的顶层键为基线:CLI 后加的节原样保留;在界面中显式删除的节仍会被移除;老数据(无基线,如 SQLite 快照恢复)不删任何键
- **修复:未知供应商类型不再被改写**。`config.toml` 中 Kimi Code 新增的供应商 `type` 值往返读写后不再被改写成 `kimi`,原样保留;编辑器 API 格式下拉框显示原始值(标注"未知类型");模型发现等不支持的操作返回明确错误提示,不再误用 `KIMI_API_KEY`
- SQLite 快照恢复路径同步修复(此前从快照恢复同样会丢失未知类型)
- 新增 3 个配置往返回归测试(Rust 测试 106 项全绿,前端 tsc / vitest 通过)
+10
View File
@@ -0,0 +1,10 @@
## v0.7.9
### 模型数据同步(models.dev 2026-08-27 快照)
- 模型库刷新至 **7343 个模型 / 203 个供应商**,内置快照与官网静态资源同步更新
- **新增 GLM-5.3 / GLM-5.3-Flash 全线接入**:智谱官方、OpenRouter、Cloudflare Workers AI、DeepInfra、HuggingFace、火山引擎等约 20 条渠道,GLM-5.3-Flash 定价 $0.075/$0.25(输入/输出,每百万 token)
- 新增 coding plan 渠道:`zhipuai-coding-plan/glm-5.3`、`glm-5.3-highspeed`、`glm-5.3-flash`
- 新增 Qwen3.8 系列(27B / Flash / 2.4T-A95B)、火山引擎豆包 Seed 2.x 全系(2-0 Pro/Lite/Mini/Code-Preview、2-1 Pro/Turbo)、DeepSeek-V4 GA 版、MiniMax-M2.7/M3 等共 74 个模型条目
- 价格更新 58 处:Amazon Bedrock GPT-5.6 Sol 降价(5.5→4)、DeepSeek V4 Flash 0731 半价、Kimi-Latest 微调降价等
- 能力信息修正 56 处:MiniMax-M2/M3 上下文扩展至 200K/1M、Grok 4.x 补充 structured_output、豆包 Seed 补充图像输入等
+1 -1
View File
@@ -1978,7 +1978,7 @@ dependencies = [
[[package]] [[package]]
name = "kimiswitch" name = "kimiswitch"
version = "0.7.8" version = "0.7.10"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "kimiswitch" name = "kimiswitch"
version = "0.7.8" version = "0.7.10"
description = "Kimi Switch - model config manager" description = "Kimi Switch - model config manager"
authors = ["codingplan.site"] authors = ["codingplan.site"]
edition = "2021" edition = "2021"
+21 -11
View File
@@ -244,6 +244,7 @@ fn build_active_config(config: &Config) -> Config {
providers, providers,
models, models,
raw_other: config.raw_other.clone(), raw_other: config.raw_other.clone(),
imported_section_keys: config.imported_section_keys.clone(),
} }
} }
@@ -277,6 +278,9 @@ pub async fn list_provider_models(provider: Provider) -> Result<Vec<DiscoveredMo
ProviderType::Anthropic => fetch_anthropic_models(&base, &api_key).await, ProviderType::Anthropic => fetch_anthropic_models(&base, &api_key).await,
ProviderType::GoogleGenai => fetch_google_genai_models(&base, &api_key).await, ProviderType::GoogleGenai => fetch_google_genai_models(&base, &api_key).await,
ProviderType::Vertexai => Err("Vertex AI model discovery requires GCP project/location configuration and is not yet supported".to_string()), ProviderType::Vertexai => Err("Vertex AI model discovery requires GCP project/location configuration and is not yet supported".to_string()),
ProviderType::Unknown(s) => Err(format!(
"unsupported provider type for model discovery: {s}"
)),
} }
} }
@@ -338,8 +342,12 @@ fn resolve_api_key(provider: &Provider) -> Option<String> {
return Some(key.clone()); return Some(key.clone());
} }
} }
let env_key = expected_api_key_key(&provider.provider_type); if let Some(env_key) = expected_api_key_key(&provider.provider_type) {
provider.env.get(env_key).filter(|s| !s.is_empty()).cloned() if let Some(key) = provider.env.get(env_key).filter(|s| !s.is_empty()) {
return Some(key.clone());
}
}
None
} }
fn resolve_base_url(provider: &Provider) -> String { fn resolve_base_url(provider: &Provider) -> String {
@@ -356,13 +364,16 @@ fn resolve_base_url(provider: &Provider) -> String {
}) })
} }
fn expected_api_key_key(provider_type: &ProviderType) -> &'static str { /// Well-known env var fallback for the API key per provider type; `None`
/// for unknown types (no well-known variable name to look up).
fn expected_api_key_key(provider_type: &ProviderType) -> Option<&'static str> {
match provider_type { match provider_type {
ProviderType::Kimi => "KIMI_API_KEY", ProviderType::Kimi => Some("KIMI_API_KEY"),
ProviderType::Anthropic => "ANTHROPIC_API_KEY", ProviderType::Anthropic => Some("ANTHROPIC_API_KEY"),
ProviderType::Openai | ProviderType::OpenaiResponses => "OPENAI_API_KEY", ProviderType::Openai | ProviderType::OpenaiResponses => Some("OPENAI_API_KEY"),
ProviderType::GoogleGenai => "GOOGLE_API_KEY", ProviderType::GoogleGenai => Some("GOOGLE_API_KEY"),
ProviderType::Vertexai => "VERTEXAI_API_KEY", ProviderType::Vertexai => Some("VERTEXAI_API_KEY"),
ProviderType::Unknown(_) => None,
} }
} }
@@ -650,9 +661,8 @@ pub async fn query_provider_usage(
.clone() .clone()
.filter(|s| !s.trim().is_empty()) .filter(|s| !s.trim().is_empty())
.or_else(|| { .or_else(|| {
provider expected_api_key_key(&provider.provider_type)
.env .and_then(|env_key| provider.env.get(env_key))
.get(expected_api_key_key(&provider.provider_type))
.cloned() .cloned()
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
}); });
+4 -8
View File
@@ -207,6 +207,9 @@ pub fn load_config(agent: &Agent) -> DbResult<Config> {
providers, providers,
models, models,
raw_other: Value::Null, raw_other: Value::Null,
// The SQLite snapshot stores no import baseline; an empty baseline
// makes export's stale-key cleanup a no-op (safe degradation).
imported_section_keys: Vec::new(),
}) })
} }
@@ -328,12 +331,5 @@ pub fn delete_setting_pub(key: &str) -> DbResult<()> {
} }
fn provider_type_for_str(s: &str) -> ProviderType { fn provider_type_for_str(s: &str) -> ProviderType {
match s { ProviderType::from_kimi_type(s)
"anthropic" => ProviderType::Anthropic,
"openai" => ProviderType::Openai,
"openai_responses" => ProviderType::OpenaiResponses,
"google-genai" => ProviderType::GoogleGenai,
"vertexai" => ProviderType::Vertexai,
_ => ProviderType::Kimi,
}
} }
+147 -23
View File
@@ -148,7 +148,7 @@ pub fn kimi_code_to_config(value: &TomlValue) -> Config {
let provider_type = table let provider_type = table
.get("type") .get("type")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.map(provider_type_for_kimi_type) .map(ProviderType::from_kimi_type)
.unwrap_or(ProviderType::Kimi); .unwrap_or(ProviderType::Kimi);
let base_url = table.get("base_url").and_then(|v| v.as_str()).map(|s| s.to_string()); let base_url = table.get("base_url").and_then(|v| v.as_str()).map(|s| s.to_string());
@@ -216,19 +216,23 @@ pub fn kimi_code_to_config(value: &TomlValue) -> Config {
} }
} }
let raw_other = { let mut rest = root;
let mut rest = root; rest.remove("default_model");
rest.remove("default_model"); rest.remove("providers");
rest.remove("providers"); rest.remove("models");
rest.remove("models"); // Baseline of top-level keys seen at import time (table order). Export
toml_value_to_json(&TomlValue::Table(rest)) // only drops baseline keys that later disappeared from raw_other, i.e.
}; // sections the user removed in the UI — sections the CLI adds after
// import are not in the baseline and survive the round-trip.
let imported_section_keys: Vec<String> = rest.keys().cloned().collect();
let raw_other = toml_value_to_json(&TomlValue::Table(rest));
Config { Config {
default_model, default_model,
providers, providers,
models, models,
raw_other, raw_other,
imported_section_keys,
} }
} }
@@ -354,10 +358,13 @@ pub fn config_to_kimi_code(config: &Config, existing: Option<&TomlValue>) -> Tom
// Sync UI-managed top-level sections (thinking, experimental, // Sync UI-managed top-level sections (thinking, experimental,
// secondary_model, ...) from config.raw_other back into the root. // secondary_model, ...) from config.raw_other back into the root.
// At import time every top-level key except default_model/providers/ // At import time every top-level key except default_model/providers/
// models was collected into raw_other, so raw_other is the authoritative // models was collected into raw_other, and their keys recorded in
// snapshot of those sections. This both persists UI edits to those // `imported_section_keys` (the import-time baseline). Keys present in
// sections and drops sections the user explicitly removed (absent from // raw_other are written back (persisting UI edits); keys that were in
// raw_other but still present in `existing`). // the baseline but are absent from raw_other were removed by the user
// and are dropped from `existing`. Top-level keys NOT in the baseline
// were added by the CLI after import and are preserved untouched. An
// empty baseline (Config not built via import) deletes nothing.
let mut managed_keys = std::collections::HashSet::new(); let mut managed_keys = std::collections::HashSet::new();
if let TomlValue::Table(extra) = if let TomlValue::Table(extra) =
json_to_toml(&config.raw_other).unwrap_or(TomlValue::Table(Table::new())) json_to_toml(&config.raw_other).unwrap_or(TomlValue::Table(Table::new()))
@@ -377,6 +384,7 @@ pub fn config_to_kimi_code(config: &Config, existing: Option<&TomlValue>) -> Tom
&& *k != "providers" && *k != "providers"
&& *k != "models" && *k != "models"
&& !managed_keys.contains(*k) && !managed_keys.contains(*k)
&& config.imported_section_keys.iter().any(|b| b == *k)
}) })
.cloned() .cloned()
.collect(); .collect();
@@ -387,17 +395,6 @@ pub fn config_to_kimi_code(config: &Config, existing: Option<&TomlValue>) -> Tom
TomlValue::Table(root) TomlValue::Table(root)
} }
fn provider_type_for_kimi_type(typ: &str) -> ProviderType {
match typ {
"anthropic" => ProviderType::Anthropic,
"openai" => ProviderType::Openai,
"openai_responses" => ProviderType::OpenaiResponses,
"google-genai" => ProviderType::GoogleGenai,
"vertexai" => ProviderType::Vertexai,
_ => ProviderType::Kimi,
}
}
fn toml_value_to_json(value: &TomlValue) -> Value { fn toml_value_to_json(value: &TomlValue) -> Value {
match value { match value {
TomlValue::String(s) => Value::String(s.clone()), TomlValue::String(s) => Value::String(s.clone()),
@@ -572,6 +569,7 @@ api_key = ""
providers, providers,
models, models,
raw_other: Value::Null, raw_other: Value::Null,
imported_section_keys: Vec::new(),
}; };
let exported = config_to_kimi_code(&config, None); let exported = config_to_kimi_code(&config, None);
@@ -705,6 +703,7 @@ default_effort = "low"
providers, providers,
models, models,
raw_other: Value::Null, raw_other: Value::Null,
imported_section_keys: Vec::new(),
}; };
let exported = config_to_kimi_code(&config, None); let exported = config_to_kimi_code(&config, None);
@@ -818,6 +817,7 @@ max_context_size = 1048576
providers, providers,
models: IndexMap::new(), models: IndexMap::new(),
raw_other: Value::Null, raw_other: Value::Null,
imported_section_keys: Vec::new(),
}; };
let exported = config_to_kimi_code(&config, None); let exported = config_to_kimi_code(&config, None);
@@ -858,6 +858,7 @@ max_context_size = 1048576
providers, providers,
models: IndexMap::new(), models: IndexMap::new(),
raw_other: Value::Null, raw_other: Value::Null,
imported_section_keys: Vec::new(),
}; };
let exported = config_to_kimi_code(&config, None); let exported = config_to_kimi_code(&config, None);
@@ -908,6 +909,7 @@ max_context_size = 1048576
)]), )]),
models: IndexMap::new(), models: IndexMap::new(),
raw_other: Value::Null, raw_other: Value::Null,
imported_section_keys: Vec::new(),
}; };
let exported = config_to_kimi_code(&config, None); let exported = config_to_kimi_code(&config, None);
let provider = exported let provider = exported
@@ -927,6 +929,7 @@ max_context_size = 1048576
)]), )]),
models: IndexMap::new(), models: IndexMap::new(),
raw_other: Value::Null, raw_other: Value::Null,
imported_section_keys: Vec::new(),
}; };
let exported = config_to_kimi_code(&config, None); let exported = config_to_kimi_code(&config, None);
let provider = exported let provider = exported
@@ -940,4 +943,125 @@ max_context_size = 1048576
Some("sk-user-key") Some("sk-user-key")
); );
} }
#[test]
fn kimi_code_unknown_provider_type_roundtrip() {
// A provider `type` string the CLI knows but Kimi Switch does not
// must survive import → export verbatim, not be rewritten to "kimi".
let toml_str = r#"
default_model = "p1/custom-model"
[providers.p1]
type = "custom-xyz"
api_key = "sk-x"
[models."p1/custom-model"]
provider = "p1"
model = "custom-model"
max_context_size = 128000
"#;
let value: TomlValue = toml_str.parse().unwrap();
let config = kimi_code_to_config(&value);
let provider = config.providers.get("p1").unwrap();
assert_eq!(
provider.provider_type,
ProviderType::Unknown("custom-xyz".to_string())
);
let exported = config_to_kimi_code(&config, Some(&value));
let provider = exported
.as_table().unwrap()
.get("providers").unwrap()
.as_table().unwrap()
.get("p1").unwrap()
.as_table().unwrap();
assert_eq!(
provider.get("type").and_then(|v| v.as_str()),
Some("custom-xyz"),
"unknown provider type must round-trip verbatim"
);
}
#[test]
fn kimi_code_export_preserves_sections_added_after_import() {
// Top-level sections the CLI added AFTER we imported (not in the
// import-time baseline) must survive export instead of being wiped
// by the stale-key cleanup.
let imported_str = r#"
default_model = "glm-5.2"
[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
"#;
let imported: TomlValue = imported_str.parse().unwrap();
let config = kimi_code_to_config(&imported);
// The on-disk file now contains an extra [swarm] section the CLI
// wrote after our import.
let existing: TomlValue = format!("{}\n[swarm]\ntimeout_ms = 123\n", imported_str.trim_end())
.parse()
.unwrap();
let exported = config_to_kimi_code(&config, Some(&existing));
let root = exported.as_table().unwrap();
let swarm = root.get("swarm").unwrap().as_table().unwrap();
assert_eq!(
swarm.get("timeout_ms").and_then(|v| v.as_integer()),
Some(123),
"CLI-added [swarm] section must be preserved"
);
// Baseline sections are still synced as before.
let thinking = root.get("thinking").unwrap().as_table().unwrap();
assert_eq!(thinking.get("enabled").and_then(|v| v.as_bool()), Some(true));
}
#[test]
fn kimi_code_export_drops_user_removed_sections() {
// A section that WAS in the import baseline but was removed from
// raw_other (the user deleted it in the UI) must still be dropped
// from the exported file.
let toml_str = r#"
default_model = "glm-5.2"
[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
"#;
let value: TomlValue = toml_str.parse().unwrap();
let mut config = kimi_code_to_config(&value);
assert!(
config.imported_section_keys.contains(&"thinking".to_string()),
"baseline must record the imported [thinking] section"
);
// UI removed the thinking section.
let mut raw = config.raw_other.as_object().unwrap().clone();
raw.remove("thinking");
config.raw_other = Value::Object(raw);
let exported = config_to_kimi_code(&config, Some(&value));
let root = exported.as_table().unwrap();
assert!(
!root.contains_key("thinking"),
"user-removed section must be dropped on export"
);
}
} }
+54 -13
View File
@@ -1,5 +1,7 @@
use std::borrow::Cow;
use indexmap::IndexMap; use indexmap::IndexMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value; use serde_json::Value;
/// Target agent whose provider/model config is being edited. /// Target agent whose provider/model config is being edited.
@@ -19,18 +21,35 @@ impl Agent {
} }
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ProviderType { pub enum ProviderType {
Anthropic, Anthropic,
Openai, Openai,
#[serde(rename = "openai_responses")]
OpenaiResponses, OpenaiResponses,
#[serde(rename = "google-genai")]
GoogleGenai, GoogleGenai,
Vertexai, Vertexai,
/// Kept for compatibility; mapped to OpenAI-compatible in Pi. /// Kept for compatibility; mapped to OpenAI-compatible in Pi.
Kimi, Kimi,
/// A provider type string the CLI knows but Kimi Switch does not.
/// Kept verbatim so new upstream `type` values survive the round-trip
/// instead of being rewritten to "kimi".
Unknown(String),
}
// Serde is hand-written to keep a pure string wire format: known variants
// map to their CLI string, `Unknown(s)` maps to `s` itself. A derived
// representation would expose the internal variant names.
impl Serialize for ProviderType {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.as_str())
}
}
impl<'de> Deserialize<'de> for ProviderType {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Ok(ProviderType::from_kimi_type(&s))
}
} }
const fn default_true() -> bool { const fn default_true() -> bool {
@@ -38,14 +57,29 @@ const fn default_true() -> bool {
} }
impl ProviderType { impl ProviderType {
pub fn as_str(&self) -> &'static str { pub fn as_str(&self) -> Cow<'static, str> {
match self { match self {
ProviderType::Anthropic => "anthropic", ProviderType::Anthropic => Cow::Borrowed("anthropic"),
ProviderType::Openai => "openai", ProviderType::Openai => Cow::Borrowed("openai"),
ProviderType::OpenaiResponses => "openai_responses", ProviderType::OpenaiResponses => Cow::Borrowed("openai_responses"),
ProviderType::GoogleGenai => "google-genai", ProviderType::GoogleGenai => Cow::Borrowed("google-genai"),
ProviderType::Vertexai => "vertexai", ProviderType::Vertexai => Cow::Borrowed("vertexai"),
ProviderType::Kimi => "kimi", ProviderType::Kimi => Cow::Borrowed("kimi"),
ProviderType::Unknown(s) => Cow::Owned(s.clone()),
}
}
/// Map a CLI `type` string to a `ProviderType`; unrecognized values
/// become `Unknown(s)` so the original string round-trips on export.
pub fn from_kimi_type(s: &str) -> ProviderType {
match s {
"anthropic" => ProviderType::Anthropic,
"openai" => ProviderType::Openai,
"openai_responses" => ProviderType::OpenaiResponses,
"google-genai" => ProviderType::GoogleGenai,
"vertexai" => ProviderType::Vertexai,
"kimi" => ProviderType::Kimi,
other => ProviderType::Unknown(other.to_string()),
} }
} }
@@ -55,7 +89,7 @@ impl ProviderType {
Some("https://api.openai.com/v1") Some("https://api.openai.com/v1")
} }
ProviderType::GoogleGenai => Some("https://generativelanguage.googleapis.com"), ProviderType::GoogleGenai => Some("https://generativelanguage.googleapis.com"),
ProviderType::Anthropic | ProviderType::Vertexai => None, ProviderType::Anthropic | ProviderType::Vertexai | ProviderType::Unknown(_) => None,
} }
} }
@@ -203,6 +237,12 @@ pub struct Config {
pub models: IndexMap<String, Model>, pub models: IndexMap<String, Model>,
#[serde(default, skip_serializing_if = "Value::is_null")] #[serde(default, skip_serializing_if = "Value::is_null")]
pub raw_other: Value, pub raw_other: Value,
/// Top-level section keys captured at import time. Export uses this
/// baseline to distinguish "user removed this section in the UI" from
/// "the CLI added this section after we imported" — only baseline keys
/// absent from the current raw_other are dropped on export.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub imported_section_keys: Vec<String>,
} }
impl PartialEq for Config { impl PartialEq for Config {
@@ -211,6 +251,7 @@ impl PartialEq for Config {
&& self.providers == other.providers && self.providers == other.providers
&& self.models == other.models && self.models == other.models
&& self.raw_other == other.raw_other && self.raw_other == other.raw_other
&& self.imported_section_keys == other.imported_section_keys
} }
} }
+5 -2
View File
@@ -205,8 +205,10 @@ pub fn pi_api_for_provider(provider_type: &ProviderType) -> &'static str {
ProviderType::Anthropic => "anthropic-messages", ProviderType::Anthropic => "anthropic-messages",
ProviderType::GoogleGenai => "google-generative-ai", ProviderType::GoogleGenai => "google-generative-ai",
ProviderType::Vertexai => "google-vertex", ProviderType::Vertexai => "google-vertex",
// Treat Kimi as OpenAI-compatible since it is not a native Pi API. // Treat Kimi as OpenAI-compatible since it is not a native Pi API;
ProviderType::Kimi => "openai-completions", // unknown upstream types get the same lenient handling (Pi is a
// legacy path).
ProviderType::Kimi | ProviderType::Unknown(_) => "openai-completions",
} }
} }
@@ -383,6 +385,7 @@ pub fn pi_file_to_config(file: &PiModelsFile) -> Config {
providers, providers,
models, models,
raw_other: file.extra.clone(), raw_other: file.extra.clone(),
imported_section_keys: Vec::new(),
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"productName": "Kimi Switch", "productName": "Kimi Switch",
"version": "0.7.8", "version": "0.7.10",
"identifier": "com.kimiswitch.app", "identifier": "com.kimiswitch.app",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",
+9
View File
@@ -54,6 +54,8 @@ function defaultBaseUrl(agent: Agent, type: ProviderType): string {
case "anthropic": case "anthropic":
case "vertexai": case "vertexai":
return ""; return "";
default:
return "";
} }
} }
// Pi defaults // Pi defaults
@@ -68,6 +70,8 @@ function defaultBaseUrl(agent: Agent, type: ProviderType): string {
case "anthropic": case "anthropic":
case "vertexai": case "vertexai":
return ""; return "";
default:
return "";
} }
} }
@@ -307,6 +311,11 @@ export function ProviderEdit({
{t} {t}
</option> </option>
))} ))}
{!PROVIDER_TYPES.includes(provider.provider_type) && (
<option value={provider.provider_type}>
{t("apiFormatUnknown", { type: provider.provider_type })}
</option>
)}
</select> </select>
</div> </div>
</div> </div>
+1
View File
@@ -74,6 +74,7 @@ export const enTranslations: Record<TranslationKey, string> = {
kimiOAuthStart: "Start sign-in", kimiOAuthStart: "Start sign-in",
apiSettings: "API Settings", apiSettings: "API Settings",
apiFormat: "API Format", apiFormat: "API Format",
apiFormatUnknown: "Unknown format ({type}, kept verbatim)",
authField: "Auth Field", authField: "Auth Field",
apiKey: "API Key", apiKey: "API Key",
getApiKeyLink: "Get API Key", getApiKeyLink: "Get API Key",
+1
View File
@@ -72,6 +72,7 @@ export const zhTranslations = {
kimiOAuthStart: "开始登录", kimiOAuthStart: "开始登录",
apiSettings: "API 设置", apiSettings: "API 设置",
apiFormat: "API 格式", apiFormat: "API 格式",
apiFormatUnknown: "未知格式({type},原样保留)",
authField: "认证字段", authField: "认证字段",
apiKey: "API Key", apiKey: "API Key",
getApiKeyLink: "获取 API Key", getApiKeyLink: "获取 API Key",
+1991 -1143
View File
File diff suppressed because it is too large. Load diff
+1494 -793
View File
File diff suppressed because it is too large. Load diff
+9 -1
View File
@@ -6,7 +6,9 @@ export type ProviderType =
| "openai" | "openai"
| "openai_responses" | "openai_responses"
| "google-genai" | "google-genai"
| "vertexai"; | "vertexai"
/** Any other string the CLI writes — preserved verbatim on round-trip. */
| (string & {});
export interface Provider { export interface Provider {
name: string; name: string;
@@ -62,6 +64,12 @@ export interface Config {
providers: Record<string, Provider>; providers: Record<string, Provider>;
models: Record<string, Model>; models: Record<string, Model>;
raw_other?: unknown; raw_other?: unknown;
/**
* Top-level section keys captured at import time by the Rust side.
* Export drops only baseline keys absent from raw_other (sections the
* user removed in the UI); CLI-added sections are preserved.
*/
imported_section_keys?: string[];
} }
/** /**