Compare commits

...
12 Commits
Author SHA1 Message Date
KimiSwitch Dev ac82135dee feat(settings): 适配 kimi-code 新增 auto_session_title / repeat_breaker 顶层开关;最大输出未匹配回填 131072;watch 默认值修正 — bump to v0.8.1
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
包含未发布的 ef6749b / 24cb04b / 977f433 三个提交,官网同步更新至 v0.8.1
2026-10-04 00:59:59 +08:00
KimiSwitch Dev ef6749b208 feat(models): 最大输出未匹配 models.dev 时回填默认值 131072(打开编辑页一次性回填 + 获取模型列表预填同口径) 2026-10-04 00:51:50 +08:00
KimiSwitch Dev 24cb04b98f style(i18n): 「最大输出 Token」列名精简为「最大输出」 2026-10-04 00:46:52 +08:00
KimiSwitch Dev 977f433bb2 fix(settings): 适配 kimi-code 近期变更 — [watch] 默认值修正为开启(上游 #4015 翻回默认开);新增顶层开关 auto_session_title(#3962)与 repeat_breaker(#3995,默认开、false 显式写入/true 删键);最大输出 Token 文案修正为「留空则不发送输出上限」(上游 #4091 未配置即省略 max_tokens) 2026-10-04 00:26:05 +08:00
KimiSwitch Dev 3c3c18925d 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
2026-10-03 23:28:36 +08:00
KimiSwitch Dev 30371a1ba6 feat(website): 官网更新至 v0.8.0(下载版本号/截图说明/JSON-LD/模型库数据 8,385·226,更新日志补 v0.8.0 与 v0.7.23 条目) 2026-10-03 23:28:15 +08:00
KimiSwitch Dev 4d87183768 chore(data): 同步 models-dev 快照至 2026-10-03(8,385 模型 / 226 供应商,新增 output 字段) 2026-10-03 23:28:04 +08:00
KimiSwitch Dev 88683b331e fix(release): GitHub workflow 兼容 docs/release-notes/release-notes-<tag>.md 命名 2026-10-02 19:54:56 +08:00
KimiSwitch Dev 26cb88ad0e feat(thinking): 思考等级扩展五档(Max/XHigh)+ 按默认模型 support_efforts 能力感知;删除过时的 max→high 迁移;Segmented 支持逐选项禁用/tooltip — bump to v0.7.23
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-10-02 13:00:35 +08:00
KimiSwitch Dev 50f59cac79 fix(release): publish-gitea.sh 兼容 docs/release-notes/release-notes-<tag>.md 命名(此前 Release 正文回退为 tag 名) 2026-10-02 13:00:33 +08:00
KimiSwitch Dev ecc98fd8cd chore(data): 同步 models-dev 快照至 2026-10-02(8,359 模型 / 225 供应商) 2026-10-02 13:00:32 +08:00
KimiSwitch Dev 13e15657af fix(usage): 列表支持显式模板查询(Sub2API/NewAPI)——对齐后端绕过 usage_kinds 的口径 — bump to v0.7.22
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-09-29 10:42:13 +08:00
39 changed files with 33780 additions and 5797 deletions

No files matched your search

+1
View File
@@ -74,6 +74,7 @@ jobs:
shell: bash
run: |
FILE="docs/release-notes/${{ github.ref_name }}.md"
[ -f "$FILE" ] || FILE="docs/release-notes/release-notes-${{ github.ref_name }}.md"
[ -f "$FILE" ] || FILE="release-notes-${{ github.ref_name }}.md"
{
echo 'body<<RELEASE_BODY_EOF'
+22
View File
@@ -6,6 +6,28 @@
---
## v0.8.1 (2026-10-04)
- **全局配置新增两个顶层开关**:自动生成会话标题(`auto_session_title`)与重复调用拦截(`repeat_breaker`),默认开启,关闭显式写 `false`、开启删键跟随上游默认(适配 kimi-code #3962 / #3995)
- **最大输出未匹配 models.dev 时回填默认值 131072**(128K 兜底,编辑页回填与获取模型列表预填同口径)
- **修复 `[watch] enabled` 默认值显示**:上游 #4015 已翻回默认开启,设置页同步修正
- **「最大输出 Token」列名精简为「最大输出」**,描述对齐上游 #4091(留空即不发送输出上限)
## v0.8.0 (2026-10-03)
- **模型映射新增「最大输出 Token」列**:逐模型设置 `max_output_size`(写入 config.toml,留空用上游默认),models.dev 有 output 上限时显示可点击参考值一键填入;获取模型列表时自动预填
- **models.dev 快照新增 output 上限字段**:同步至 2026-10-03(8,385 模型 / 226 供应商,output 覆盖 97.4%);应用内同步(Rust 侧)同步支持,避免同步后字段丢失
- **移除无效的「声明支持 1M」复选框**:该控件读时由上下文长度派生、写时不持久化,勾选从不生效,由「最大输出 Token」列取代
## v0.7.23 (2026-09-30)
- **思考等级五档 + 能力感知**:全局配置的思考等级扩展为低 / 中 / 高 / Max / XHigh,按当前默认模型声明的 `support_efforts` 自动置灰不支持档位并提示原因;models.dev 标记不支持思考的模型整体禁用思考区;未声明档位的模型保留可选并提示"上游可能拒绝"
- **修正过时适配**:删除 max→high 读取迁移(kimi-code 2.1.1 中 max 为合法档位),存储的 max 与任意手写档位值原样保留显示;子代理页继承档位显示同步修正
## v0.7.22 (2026-09-24)
- **修复 Sub2API / NewAPI 模板查询不显示**:为无预设识别类型的自定义供应商配置 Sub2API(或 NewAPI)模板后,弹窗测试可查但供应商列表始终不显示——前端列表的查询/显示门槛只看预设 `usageKinds`,显式模板被漏掉;现在与后端口径对齐(模板查询绕过 usage_kinds),列表正常显示余额 / 配额
## v0.7.21 (2026-09-24)
- **models.dev 在线同步**:高级设置新增「模型参考数据」卡片——一键在线拉取 models.dev 最新快照(上下文长度 / 能力标记 / 单价),前端与仪表盘计价即时生效,无需等待新版本;支持一键恢复随版本内置数据;显示当前数据来源与版本(同步 / 内置 + 日期 + 模型/供应商数)
+2 -2
View File
@@ -79,7 +79,7 @@
| **连通性测试** | 实测 `base_url` 延迟,绿/橙/红彩色气泡,6 秒自动消失 |
| **重复供应商** | 一键深拷贝供应商 + 全部模型,key 改 `xxx-copy` |
| **图标按钮操作** | 启用 / 编辑 / 复制 / 测试连通 / 删除 全图标化(lucide-react) |
| **模型映射** | 别名("provider/model" 形式)↔ 实际请求模型 ID,自定义显示名、上下文长度、1M 上下文声明、能力 |
| **模型映射** | 别名("provider/model" 形式)↔ 实际请求模型 ID,自定义显示名、上下文长度、最大输出 Token(`max_output_size`)、能力(仅 Kimi Code) |
| **自动上下文** | 拉取模型时 **API 返回 > models.dev ref > 正则兜底** 三级优先级自动适配 |
| **能力自动推导** | `image_in / video_in / tool_use` 全部由 models.dev 推得;UI 仅暴露 `thinking` 一个手动开关 |
| **全局设置** | `[thinking]` 表完整支持(enabled / effort / keep),仅 Kimi Code 生效 |
@@ -111,7 +111,7 @@
![编辑供应商-模型映射](docs/screenshots/provider-model-mapping.png)
一张表管理全部模型映射:显示名、实际请求模型、上下文长度、1M 上下文声明、能力(仅"思考")、设为默认、删除。
一张表管理全部模型映射:显示名、实际请求模型、上下文长度、最大输出 Token(`max_output_size`,可点参考值填 models.dev 上限)、能力(仅"思考")、设为默认、删除(后两项仅 Kimi Code)。
**用量仪表盘**
+3 -3
View File
@@ -73,7 +73,7 @@ Build instructions: [`docs/BUILD.md`](./docs/BUILD.md).
| **Connectivity test** | Real `base_url` latency, coloured bubble (green / orange / red), auto-dismiss in 6 seconds |
| **Duplicate provider** | Deep-copy a provider + all its models; key auto-suffixed to `xxx-copy` |
| **Iconified actions** | Activate / Edit / Duplicate / Test Connectivity / Delete via lucide-react |
| **Model mapping** | Alias (`"provider/model"`) ↔ real model ID, with display name, context size, 1M-context flag, and capabilities |
| **Model mapping** | Alias (`"provider/model"`) ↔ real model ID, with display name, context size, max output tokens (`max_output_size`), and capabilities (Kimi Code only) |
| **Auto context size** | On model fetch: **API response > models.dev ref > regex fallback** — three-tier priority |
| **Auto capabilities** | `image_in / video_in / tool_use` all derived from models.dev; UI only exposes `thinking` as a manual toggle |
| **Global settings** | Full `[thinking]` table (enabled / effort / keep); Kimi Code only |
@@ -105,7 +105,7 @@ Provider name, notes, official URL, managed-provider toggle, API format, API key
![Edit provider — model mapping](docs/screenshots/provider-model-mapping.png)
A single table for all model mappings: display name, real model ID, context length, 1M-context flag, capability (thinking only), default toggle, delete.
A single table for all model mappings: display name, real model ID, context length, max output tokens (`max_output_size`, with a click-to-fill models.dev reference), capability (thinking only), default toggle, delete (the last two are Kimi Code only).
**Usage dashboard**
@@ -175,7 +175,7 @@ Per-workspace Kimi Code session browsing, active / archived / all filters; strea
- **`config.toml` is the authoritative source for Kimi Code**: all providers and models are always written in full; `default_model` selects the active one (matching the CLI's native `/provider` behaviour). Switching only changes `default_model`; newly added providers are auto-promoted to the top of the list and never get overwritten
- **SQLite holds Kimi Switch-private metadata only**: notes, official URLs, per-agent remembered default model (the `settings` table), ordering. Theme / language / last-update-check live in frontend `localStorage` (WebView2), not under `~/.kimi-switch`. It acts as a fallback when `config.toml` is incomplete
- **`raw_other` passes unknown fields through untouched**, including `[oauth]` blocks — round-trips never drop fields
- **models.dev snapshot**: derived from `https://models.dev/api.json`, cached to a local JSON; `capabilitiesFromRef` derives `thinking / image_in / video_in / tool_use`, `getModelRef` derives `max_context_size / display_name`
- **models.dev snapshot**: derived from `https://models.dev/api.json`, cached to a local JSON; `capabilitiesFromRef` derives `thinking / image_in / video_in / tool_use`, `getModelRef` derives `max_context_size / display_name / max output tokens`
- **Override env vars**: `KIMI_CODE_HOME` / `PI_CODING_AGENT_DIR` override the Kimi Code / Pi dirs; Kimi Switch's own data dir is fixed at `~/.kimi-switch` (no env override yet). See [Data Storage Locations](#data-storage-locations)
## Feature Details
@@ -0,0 +1,9 @@
# KimiSwitch v0.7.22
## 修复
- **Sub2API / NewAPI 模板查询在供应商列表不显示**:为没有预设识别类型的自定义供应商(如自家中转站)配置「Sub2API 中转站」或「NewAPI 中转站」模板后,配置弹窗里「测试查询」正常返回余额,但回到供应商列表却始终空白。根因:列表的查询与显示门槛只认预设注入的 `usageKinds`,而后端模板查询本就绕过 `usageKinds` 直接执行——前端口径与后端不一致。现在二者对齐,显式模板的供应商在列表正常显示余额 / 配额窗口、支持自动查询间隔与预警阈值。
## 备注
- 仅前端修复(`useUsageQuery` 支持判定 + 卡片渲染门槛),后端查询逻辑无变化;已补 3 个单测覆盖支持判定。
@@ -0,0 +1,18 @@
# KimiSwitch v0.7.23
## 新功能
- **思考等级五档 + 能力感知**:全局配置的思考等级由「低 / 中 / 高」三档扩展为「低 / 中 / 高 / Max / XHigh」五档,并按当前默认模型的实际能力自适应——
- 模型在 config.toml 声明了 `support_efforts` 时,不支持的档位自动置灰并给出原因提示(如 deepseek-v4.1-flash 仅支持「高」)
- models.dev 标记不支持思考(reasoning=false)的模型,整个思考等级区禁用并给出警告
- 未声明档位的模型保留全部可选,Max / XHigh 附带「上游可能拒绝此档位」提示
- 等级行下方新增说明:实际生效档位取决于当前默认模型
## 修复
- **max 档位被误显示为「高」**:此前代码基于过时信息("上游已移除 max 档位")把存储的 `effort = "max"` 读取时强制改为 `high`;实测 kimi-code 2.1.1 中 max 为合法档位(如 kimi-for-coding 默认即为 max),现已原样保留与显示。手写的任意档位值(上游本就是自由字符串)也会作为额外可选项正常显示,不再被丢弃
- **子代理设置页**:继承档位显示同步修正,max / xhigh 正常显示,未知值按原样展示
## 备注
- 仅前端改动(新增 `thinking-efforts` 能力判定模块 + `Segmented` 控件逐选项禁用/tooltip),无 Rust 变更;新增 14 个单测覆盖三层能力判定规则,全量 117 个测试通过
@@ -0,0 +1,14 @@
# KimiSwitch v0.8.0
## 新功能
- **模型映射表新增「最大输出 Token」列**(Kimi Code):为每个模型单独设置 `max_output_size`,写入 config.toml 的 `[models."<alias>"]`,留空则用上游默认值。models.dev 收录了该模型的输出上限时,输入框下方显示可点击的参考值(如 `参考 128000`),一键填入;「获取模型列表」添加模型时也会自动预填参考值
- **models.dev 快照新增 output 上限字段**:内置快照同步至 2026-10-03(8,385 模型 / 226 供应商,97.4% 模型带 output 上限);应用内「同步 models.dev」(Rust 侧)同步支持该字段,同步后参考值不丢失
## 修复
- **移除无效的「声明支持 1M」复选框**(Kimi Code):该控件在读取时由上下文长度派生、写入时不持久化,勾选永远不会生效,已由「最大输出 Token」列取代
## 备注
- 前端 + 抓取脚本 + Rust 快照构建逻辑;新增 `max_output_size` 读写纯函数 9 个单测 + config.toml 全链路往返测试,全量测试通过(前端 126 / Rust 162)
@@ -0,0 +1,17 @@
# KimiSwitch v0.8.1
## 新功能
- **全局配置新增两个顶层开关**(适配 kimi-code 近期版本):
- **自动生成会话标题**(`auto_session_title`,上游 #3962):允许客户端自动生成会话标题,默认开启;关闭后显式写入 `false`,开启即删键跟随上游默认
- **重复调用拦截**(`repeat_breaker`,上游 #3995):连续重复相同工具调用时提醒并强制停止,默认开启;可被环境变量 `KIMI_CODE_REPEAT_BREAKER` 覆盖
- **最大输出未匹配时回填默认值 131072**(Kimi Code):打开供应商编辑页自动回填时,models.dev 未收录的模型按 128K(131072)兜底;「获取模型列表」新增模型同口径预填
## 修复
- **`[watch] enabled` 默认值修正为开启**:kimi-code #4015 已把文件监听默认值翻回开启(2.0.2 曾短暂默认关闭),设置页的开关显示默认值同步修正
- **「最大输出 Token」列名精简为「最大输出」**;列描述修正为「留空则不发送输出上限」(对齐上游 #4091:未显式配置时不发送 `max_tokens`,避免严格服务栈如 bare vLLM 报 400)
## 备注
- 前端 130 测试 / Rust 162 测试全量通过
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "kimiswitch",
"private": true,
"version": "0.7.21",
"version": "0.8.1",
"type": "module",
"scripts": {
"dev": "vite",
+13310 -1799
View File
File diff suppressed because it is too large. Load diff
+5
View File
@@ -133,6 +133,11 @@ async function main() {
if (typeof m.limit?.context === "number" && m.limit.context > 0) {
entry.context = m.limit.context;
}
// Max output tokens — the reference value behind the max_output_size
// column (model mapping table).
if (typeof m.limit?.output === "number" && m.limit.output > 0) {
entry.output = m.limit.output;
}
if (m.reasoning === true) entry.reasoning = true;
if (m.tool_call === true) entry.tool_call = true;
if (m.structured_output === true) entry.structured_output = true;
+1
View File
@@ -17,6 +17,7 @@ TAG="${1:?usage: publish-gitea.sh <tag> <asset> [asset...]}"
shift
NOTES_FILE="docs/release-notes/${TAG}.md"
[ -f "$NOTES_FILE" ] || NOTES_FILE="docs/release-notes/release-notes-${TAG}.md"
[ -f "$NOTES_FILE" ] || NOTES_FILE="release-notes-${TAG}.md"
json_escape() {
python - "$1" <<'PYEOF'
+1 -1
View File
@@ -1978,7 +1978,7 @@ dependencies = [
[[package]]
name = "kimiswitch"
version = "0.7.21"
version = "0.8.1"
dependencies = [
"anyhow",
"chrono",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "kimiswitch"
version = "0.7.21"
version = "0.8.1"
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.21",
"version": "0.8.1",
"identifier": "com.kimiswitch.app",
"build": {
"beforeDevCommand": "npm run dev",
+104 -20
View File
@@ -1,12 +1,20 @@
import { useEffect, useState } from "react";
import { useEffect, useReducer, useState } from "react";
import { invoke } from "@tauri-apps/api/core";
import { useTranslation } from "../i18n";
import type { TranslationKey } from "../i18n/zh";
import { getAgentSettings, setAgentSettings } from "../lib/agent-settings";
import { getModelRef, modelsDevReady } from "../lib/models-dev";
import {
THINKING_EFFORTS,
readSupportEfforts,
resolveThinkingEffortSupport,
type EffortAvailability,
} from "../lib/thinking-efforts";
import type {
AgentSettings,
ExperimentalEnvStatus,
Hook,
Model,
PermissionRule,
} from "../types";
import { Card, Checkbox, NumberField, Segmented } from "./ui/controls";
@@ -14,14 +22,18 @@ import { Card, Checkbox, NumberField, Segmented } from "./ui/controls";
interface AgentSettingsPanelProps {
rawOther: unknown;
onChange: (nextRawOther: unknown) => void;
/** Configured models (alias → entry) — used to resolve effort support. */
models?: Record<string, Model>;
/** Alias of the model the effort tier actually applies to. */
defaultModel?: string | null;
}
// Upstream removed the "max" effort tier (auto-migrates to "high").
const THINKING_LEVELS = ["low", "medium", "high"] as const;
const THINKING_LABELS: Record<(typeof THINKING_LEVELS)[number], TranslationKey> = {
const THINKING_LABELS: Record<(typeof THINKING_EFFORTS)[number], TranslationKey> = {
low: "thinkingLow",
medium: "thinkingMedium",
high: "thinkingHigh",
max: "thinkingMax",
xhigh: "thinkingXHigh",
};
const PERMISSION_DECISIONS = ["allow", "deny", "ask"] as const;
@@ -44,7 +56,7 @@ const COMMON_EVENTS = [
"SessionEnd",
] as const;
export function AgentSettingsPanel({ rawOther, onChange }: AgentSettingsPanelProps) {
export function AgentSettingsPanel({ rawOther, onChange, models, defaultModel }: AgentSettingsPanelProps) {
const { t } = useTranslation();
const settings = getAgentSettings(rawOther);
/**
@@ -52,6 +64,9 @@ export function AgentSettingsPanel({ rawOther, onChange }: AgentSettingsPanelPro
* v1-engine users; the default (v2) writes only max_attempts_per_step.
*/
const [legacyV1, setLegacyV1] = useState(false);
// The models.dev index loads in the background; re-render once it lands so
// the reasoning flag can disable the thinking area.
const [, forceModelsDevReady] = useReducer((x: number) => x + 1, 0);
useEffect(() => {
invoke<ExperimentalEnvStatus>("get_experimental_env_status")
@@ -62,6 +77,18 @@ export function AgentSettingsPanel({ rawOther, onChange }: AgentSettingsPanelPro
.catch(() => setLegacyV1(false));
}, []);
useEffect(() => {
// The panel only needs the flag once; App owns the permanent
// onModelsDevReady listener for later hot swaps.
let alive = true;
modelsDevReady().then(() => {
if (alive) forceModelsDevReady();
});
return () => {
alive = false;
};
}, []);
const update = (patch: Partial<AgentSettings>) => {
onChange(setAgentSettings(rawOther, patch, { legacyV1 }));
};
@@ -92,6 +119,42 @@ export function AgentSettingsPanel({ rawOther, onChange }: AgentSettingsPanelPro
const thinkingEnabled = settings.thinking?.enabled ?? true;
// Which tiers the current default model accepts: `[models."<alias>"]
// support_efforts` first, then the models.dev `reasoning` flag.
const defaultEntry = defaultModel ? models?.[defaultModel] : undefined;
const effortSupport = resolveThinkingEffortSupport({
supportEfforts: readSupportEfforts(defaultEntry),
reasoning: defaultEntry ? getModelRef(defaultEntry.model)?.reasoning : undefined,
});
const effortNote = (availability: EffortAvailability): string | undefined => {
const note = availability.note;
if (!note) return undefined;
if (note.kind === "thinkingUnsupported") return t("thinkingEffortUnsupported");
if (note.kind === "tierUnsupported") {
return t("thinkingEffortTierUnsupported", { levels: note.supported.join(", ") });
}
return t("thinkingEffortTierUndeclared");
};
// Upstream takes a free-form string: keep a hand-written tier visible and
// selectable instead of dropping it from the control.
const effort = settings.thinking?.effort ?? "medium";
const effortOptions: {
key: string;
label: string;
disabled?: boolean;
title?: string;
}[] = THINKING_EFFORTS.map((tier) => ({
key: tier,
label: t(THINKING_LABELS[tier]),
disabled: !effortSupport.levels[tier].enabled,
title: effortNote(effortSupport.levels[tier]),
}));
if (!THINKING_EFFORTS.includes(effort as (typeof THINKING_EFFORTS)[number])) {
effortOptions.push({ key: effort, label: effort });
}
return (
<div className="mt-6 space-y-4">
<h3 className="text-content-muted text-sm font-medium">{t("agentSettings")}</h3>
@@ -105,17 +168,20 @@ export function AgentSettingsPanel({ rawOther, onChange }: AgentSettingsPanelPro
<div className="flex items-center gap-3 flex-wrap">
<span className="text-sm text-content-muted">{t("thinkingLevel")}</span>
<Segmented
options={THINKING_LEVELS.map((lvl) => ({
key: lvl,
label: t(THINKING_LABELS[lvl]),
}))}
value={settings.thinking?.effort ?? "medium"}
onChange={(effort) =>
updateThinking({ effort: effort as NonNullable<AgentSettings["thinking"]>["effort"] })
}
disabled={!thinkingEnabled}
options={effortOptions}
value={effort}
onChange={(next) => updateThinking({ effort: next })}
disabled={!thinkingEnabled || !effortSupport.thinkingSupported}
/>
</div>
{!effortSupport.thinkingSupported && (
<p className="text-xs text-amber-500 dark:text-amber-400">
{t("thinkingEffortUnsupported")}
</p>
)}
<p className="text-xs text-content-muted">
{t("thinkingEffortDependsOnDefaultModel")}
</p>
<Checkbox
label={t("thinkingKeep")}
checked={settings.thinking?.keep === "all"}
@@ -170,19 +236,37 @@ export function AgentSettingsPanel({ rawOther, onChange }: AgentSettingsPanelPro
</Card>
<Card title={t("watchSettings")}>
{/* Top-level `[watch] enabled` (kimi-code 2.0.1+; off by default since
2.0.2). KIMI_CODE_WATCH outranks this config at runtime — the env
var is probed by get_experimental_env_status as a non-flag entry
but deliberately locks nothing here, the toggle just documents the
config value that applies when the env var is unset. */}
{/* Top-level `[watch] enabled` (kimi-code 2.0.1+; 2.0.2 flipped the
default off, #4015 flipped it back on). KIMI_CODE_WATCH outranks
this config at runtime — the env var is probed by
get_experimental_env_status as a non-flag entry but deliberately
locks nothing here, the toggle just documents the config value
that applies when the env var is unset. */}
<Checkbox
label={t("watchEnabled")}
checked={settings.watch?.enabled ?? false}
checked={settings.watch?.enabled ?? true}
onChange={(checked) => updateWatch({ enabled: checked })}
/>
<p className="text-xs text-content-muted">{t("watchEnabledDesc")}</p>
</Card>
<Card title={t("behaviorSettings")}>
{/* Top-level booleans, both default true upstream: false is written
explicitly, true removes the key (see setAgentSettings). */}
<Checkbox
label={t("autoSessionTitle")}
checked={settings.auto_session_title ?? true}
onChange={(checked) => update({ auto_session_title: checked })}
/>
<p className="text-xs text-content-muted">{t("autoSessionTitleDesc")}</p>
<Checkbox
label={t("repeatBreaker")}
checked={settings.repeat_breaker ?? true}
onChange={(checked) => update({ repeat_breaker: checked })}
/>
<p className="text-xs text-content-muted">{t("repeatBreakerDesc")}</p>
</Card>
<Card title={t("permissionRules")}>
{/* kimi-code `[permission] dangerous_command_guard`. The env
var KIMI_CODE_DANGEROUS_COMMAND_GUARD (literal "true"/"false")
+120 -15
View File
@@ -1,11 +1,17 @@
import { useEffect, useId, useState } from "react";
import { useEffect, useId, useMemo, useReducer, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { invoke } from "@tauri-apps/api/core";
import { useTranslation } from "../i18n";
import type { TranslationKey } from "../i18n/zh";
import { findPresetForProvider } from "../config/providerPresets";
import { getDefaultMaxContextSize } from "../lib/model-defaults";
import { capabilitiesFromRef, getModelRef } from "../lib/models-dev";
import {
parseMaxOutputInput,
readMaxOutputSize,
setMaxOutputSize,
withMaxOutputSize,
} from "../lib/model-max-output";
import { capabilitiesFromRef, getModelRef, modelsDevReady } from "../lib/models-dev";
import { getIconMetadata } from "../icons/extracted/metadata";
import { AgentSettingsPanel } from "./AgentSettingsPanel";
import { KimiOAuthDialog } from "./KimiOAuthDialog";
@@ -33,6 +39,9 @@ const CAPABILITY_LABELS: Record<
tool_use: "capToolUse",
};
/** Fallback max-output cap (128K) for models models.dev doesn't know. */
const FALLBACK_MAX_OUTPUT = 131_072;
const PROVIDER_TYPES: ProviderType[] = [
"openai",
"openai_responses",
@@ -142,6 +151,13 @@ export function ProviderEdit({
const apiKeyEnvSupported = agent === "kimi_code";
const apiKeyEnvMode = apiKeyEnvSupported && provider.api_key_env != null;
// Alias → entry lookup for panels that resolve a model by alias
// (AgentSettingsPanel reads the default model's support_efforts).
const modelsByAlias = useMemo(
() => Object.fromEntries(models.map((m) => [m.alias, m])),
[models]
);
useEffect(() => {
const def = defaultBaseUrl(agent, provider.provider_type);
if (!def) return;
@@ -462,6 +478,8 @@ export function ProviderEdit({
<AgentSettingsPanel
rawOther={rawOther}
onChange={onRawOtherChange}
models={modelsByAlias}
defaultModel={defaultModel}
/>
)}
</>
@@ -558,6 +576,38 @@ function ModelMapping({
const [discoverError, setDiscoverError] = useState<string | null>(null);
const [fetchThinking, setFetchThinking] = useState(true);
// The models.dev index loads in the background; re-render once it lands so
// the max-output "参考" hints appear even when this panel mounted first.
const [, forceModelsDevReady] = useReducer((x: number) => x + 1, 0);
// One-shot backfill: models with no max_output_size yet get the models.dev
// `output` cap, or the 128K fallback when nothing matches. onModelChange
// only mutates the in-memory config — the user still reviews and presses
// 保存配置.
const backfillDone = useRef(false);
const modelsRef = useRef(models);
modelsRef.current = models;
useEffect(() => {
let alive = true;
modelsDevReady().then(() => {
if (!alive) return;
forceModelsDevReady();
if (backfillDone.current || agent !== "kimi_code") return;
backfillDone.current = true;
for (const m of modelsRef.current) {
if (readMaxOutputSize(m.raw_other) !== undefined) continue;
onModelChange(
withMaxOutputSize(
m,
getModelRef(m.model)?.output ?? FALLBACK_MAX_OUTPUT
)
);
}
});
return () => {
alive = false;
};
}, []);
const handleDiscover = async () => {
setDiscovering(true);
setDiscoverError(null);
@@ -627,6 +677,17 @@ function ModelMapping({
: fetchThinking
? ["thinking"]
: [],
// Seed the max_output_size override with the models.dev cap, falling
// back to 128K when nothing matches. kimi_code only: Pi's equivalent
// is `maxTokens` (not written by this UI).
...(agent === "kimi_code"
? {
raw_other: setMaxOutputSize(
undefined,
ref?.output ?? FALLBACK_MAX_OUTPUT
),
}
: {}),
});
}
onBulkAdd(toAdd);
@@ -639,7 +700,10 @@ function ModelMapping({
<div className="flex items-center justify-between">
<div>
<h3 className="font-medium text-content-primary">{t("modelMapping")}</h3>
<p className="text-xs text-content-muted mt-1">{t("modelMappingDesc")}</p>
<p className="text-xs text-content-muted mt-1">
{t("modelMappingDesc")}
{agent === "kimi_code" ? ` ${t("maxOutputSizeDesc")}` : ""}
</p>
</div>
<div className="flex items-center gap-2">
<button
@@ -747,7 +811,9 @@ function ModelMapping({
<th className="text-left px-4 py-3 font-medium">{t("displayName")}</th>
<th className="text-left px-4 py-3 font-medium">{t("actualModel")}</th>
<th className="text-left px-4 py-3 font-medium w-28">{t("contextSize")}</th>
<th className="text-center px-4 py-3 font-medium w-28">{t("supports1M")}</th>
{agent === "kimi_code" && (
<th className="text-left px-4 py-3 font-medium w-32">{t("maxOutputSize")}</th>
)}
{agent === "kimi_code" && (
<th className="text-left px-4 py-3 font-medium">{t("capabilities")}</th>
)}
@@ -788,19 +854,14 @@ function ModelMapping({
}}
/>
</td>
<td className="px-4 py-2 text-center">
<input
type="checkbox"
checked={m.supports_1m || false}
onChange={(e) =>
onModelChange({
...m,
supports_1m: e.target.checked,
})
}
className="w-4 h-4 rounded border-border bg-input text-blue-600 focus:ring-blue-500"
{agent === "kimi_code" && (
<td className="px-4 py-2">
<MaxOutputCell
model={m}
onChange={(next) => onModelChange(next)}
/>
</td>
)}
{agent === "kimi_code" && (
<td className="px-4 py-2">
<CapabilitiesCell
@@ -976,3 +1037,47 @@ function CapabilitiesCell({
</div>
);
}
/**
* Max output tokens override (`[models."<alias>"] max_output_size`).
*
* Not a first-class Model field, so the value lives in `raw_other` (see
* lib/model-max-output.ts). Blank input removes the key — "unset" means the
* upstream default applies. When models.dev knows the model's cap it is shown
* as a click-to-fill reference underneath.
*/
function MaxOutputCell({
model,
onChange,
}: {
model: Model;
onChange: (next: Model) => void;
}) {
const { t } = useTranslation();
const current = readMaxOutputSize(model.raw_other);
const refOutput = getModelRef(model.model)?.output;
return (
<div>
<input
type="number"
min={0}
step={1024}
className="w-full bg-transparent border border-border rounded px-2 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
value={current ?? ""}
onChange={(e) =>
onChange(withMaxOutputSize(model, parseMaxOutputInput(e.target.value)))
}
/>
{refOutput !== undefined && refOutput !== current && (
<button
type="button"
onClick={() => onChange(withMaxOutputSize(model, refOutput))}
className="mt-1 text-[11px] text-content-muted hover:text-blue-500"
>
{t("maxOutputRef", { value: refOutput })}
</button>
)}
</div>
);
}
+4 -2
View File
@@ -96,7 +96,9 @@ function ProviderCard({
provider.usageKinds,
provider.usageConfig?.autoQueryIntervalMinutes,
provider.usageConfig?.enabled === false,
provider.usageConfig?.threshold
provider.usageConfig?.threshold,
provider.usageConfig?.templateType === "newapi" ||
provider.usageConfig?.templateType === "sub2api"
);
const providerModels = Object.values(models).filter(
(m) => m.provider === provider.name
@@ -221,7 +223,7 @@ function ProviderCard({
)}
{/* compact usage summary — sits left of the switch button,
mirroring cc-switch's card layout (usage → action buttons) */}
{(provider.usageKinds?.length ?? 0) > 0 && (
{usage.supported && (
<UsageFooter usage={usage} variant="compact" />
)}
<button
+7 -5
View File
@@ -43,12 +43,15 @@ interface SubagentSettingsPageProps {
onBack: () => void;
}
// Upstream removed the "max" effort tier (auto-migrates to "high").
const EFFORTS = ["low", "medium", "high"] as const;
// Effort tiers the CLI accepts; the value is forwarded verbatim upstream, and
// per-model support comes from `[models."<alias>"] support_efforts`.
const EFFORTS = ["low", "medium", "high", "xhigh", "max"] as const;
const EFFORT_LABELS: Record<(typeof EFFORTS)[number], TranslationKey> = {
low: "thinkingLow",
medium: "thinkingMedium",
high: "thinkingHigh",
max: "thinkingMax",
xhigh: "thinkingXHigh",
};
const FLAG_LABELS: Record<string, { name: TranslationKey; desc: TranslationKey }> = {
@@ -281,9 +284,8 @@ export function SubagentSettingsPage({
? `${Math.round(n / 1000)}K`
: String(n);
const effortLabel = (e: string): string => {
// Stored "max" tiers from old configs are shown as "high" (upstream
// removed the tier; it auto-migrates to "high").
const key = EFFORT_LABELS[e === "max" ? "high" : (e as (typeof EFFORTS)[number])];
// Unknown values (hand-written config) are shown verbatim.
const key = EFFORT_LABELS[e as (typeof EFFORTS)[number]];
return key ? t(key) : e;
};
+12 -6
View File
@@ -106,7 +106,7 @@ export function Segmented({
onChange,
disabled,
}: {
options: { key: string; label: string }[];
options: { key: string; label: string; disabled?: boolean; title?: string }[];
value: string;
onChange: (value: string) => void;
disabled?: boolean;
@@ -117,21 +117,27 @@ export function Segmented({
disabled ? "opacity-50" : ""
}`}
>
{options.map((opt) => (
{options.map((opt) => {
const optDisabled = disabled || opt.disabled === true;
return (
// The title lives on the wrapper: a disabled button swallows pointer
// events in Chromium, so its own tooltip would never show.
<span key={opt.key} title={opt.title} className="inline-flex">
<button
key={opt.key}
type="button"
disabled={disabled}
disabled={optDisabled}
onClick={() => onChange(opt.key)}
className={`px-3 py-1 text-sm ${
value === opt.key
? "bg-blue-600 text-white"
: "bg-input text-content-muted hover:bg-hover-2"
}`}
} ${optDisabled ? "cursor-not-allowed opacity-40 hover:bg-input" : ""}`}
>
{opt.label}
</button>
))}
</span>
);
})}
</div>
);
}
+20 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { isAlertTier, type UsageData } from "./useUsageQuery";
import { isAlertTier, usageQuerySupported, type UsageData } from "./useUsageQuery";
/** 百分比窗口行(five_hour 等);套餐类 tier 的 unit 恒为 "%"。 */
function percentTier(used: number): UsageData {
@@ -42,3 +42,22 @@ describe("isAlertTier", () => {
expect(isAlertTier({ planName: "five_hour", unit: "%", remaining: 20 }, 50)).toBe(false);
});
});
describe("usageQuerySupported", () => {
it("有识别类型即可查询", () => {
expect(usageQuerySupported(["balance:deepseek"])).toBe(true);
expect(usageQuerySupported([], true)).toBe(true);
});
it("显式模板(newapi/sub2api)绕过 usage_kinds——自定义供应商也该查", () => {
expect(usageQuerySupported(undefined, true)).toBe(true);
});
it("无识别类型且非显式模板时不查询", () => {
expect(usageQuerySupported(undefined)).toBe(false);
expect(usageQuerySupported([])).toBe(false);
// auto 模板没有识别到类型 = 不支持(与弹窗短路口径一致)
expect(usageQuerySupported([], false)).toBe(false);
expect(usageQuerySupported(undefined, undefined)).toBe(false);
});
});
+17 -2
View File
@@ -75,15 +75,30 @@ export function isAlertTier(
return d.unit === "%" && d.used != null && d.used >= threshold;
}
/**
* 是否值得为该供应商发起用量查询:有预设识别类型,或后端按显式模板
* (newapi / sub2api,二者绕过 usage_kinds)查询。
*/
export function usageQuerySupported(
usageKinds?: string[],
explicitTemplate?: boolean
): boolean {
return (usageKinds?.length ?? 0) > 0 || explicitTemplate === true;
}
export function useUsageQuery(
agent: Agent,
providerName: string,
usageKinds?: string[],
autoIntervalMinutes?: number,
disabled?: boolean,
threshold?: number
threshold?: number,
/** Backend resolves the query from an explicit template (newapi / sub2api)
* regardless of usageKinds — those bypass kinds entirely, so the row is
* supported even when the provider has no preset-detected kinds. */
explicitTemplate?: boolean
): UsageQueryState {
const supported = (usageKinds?.length ?? 0) > 0;
const supported = usageQuerySupported(usageKinds, explicitTemplate);
// Cache key includes the agent so a Kimi Code provider and a Pi provider
// with the same name do not clobber each other's cached result.
const cacheKey = `${agent}:${providerName}`;
+21 -3
View File
@@ -111,7 +111,8 @@ export const enTranslations: Record<TranslationKey, string> = {
baseUrlHint: "Enter a {type} API-compatible endpoint without a trailing slash.",
envPairs: "Env pairs",
addEnv: "+ Add",
modelMappingDesc: "Display name only affects the /model menu; 1M declares context capability for Kimi Code.",
modelMappingDesc: "Display name only affects the /model menu.",
maxOutputSizeDesc: "Max output writes max_output_size — leave blank to send no output cap upstream.",
oneClickSetup: "One-click setup",
fetchModels: "Fetch models",
fetchingModels: "Fetching...",
@@ -121,7 +122,8 @@ export const enTranslations: Record<TranslationKey, string> = {
displayName: "Display name",
actualModel: "Actual model",
contextSize: "Context size",
supports1M: "Supports 1M",
maxOutputSize: "Max output",
maxOutputRef: "Ref {value}",
default: "Default",
operation: "Operation",
isDefault: "Default",
@@ -164,6 +166,15 @@ export const enTranslations: Record<TranslationKey, string> = {
thinkingLow: "Low",
thinkingMedium: "Medium",
thinkingHigh: "High",
thinkingMax: "Max",
thinkingXHigh: "XHigh",
thinkingEffortUnsupported: "The current default model does not support thinking.",
thinkingEffortTierUnsupported:
"The current default model does not support this tier (supported: {levels}).",
thinkingEffortTierUndeclared:
"This model declares no thinking effort tiers; the upstream may reject this one.",
thinkingEffortDependsOnDefaultModel:
"The tier that actually applies depends on the current default model.",
thinkingContextHint: "Thinking uses more context. Ensure the model context length and reserved size are sufficient.",
loopControlSettings: "Loop Control",
maxAttemptsPerStep: "Max attempts per step",
@@ -177,7 +188,14 @@ export const enTranslations: Record<TranslationKey, string> = {
watchSettings: "File Watching",
watchEnabled: "Enable file watching",
watchEnabledDesc:
"Watch config files and the workspace for changes (configurable since kimi-code 2.0.1, off by default since 2.0.2). The KIMI_CODE_WATCH environment variable overrides this setting.",
"Watch config files and the workspace for changes (configurable since kimi-code 2.0.1, on by default). The KIMI_CODE_WATCH environment variable overrides this setting.",
behaviorSettings: "Behavior",
autoSessionTitle: "Auto session title",
autoSessionTitleDesc:
"Let clients auto-generate session titles (on by default; turning it off writes false).",
repeatBreaker: "Repeat breaker",
repeatBreakerDesc:
"Remind and force-stop on consecutively repeated identical tool calls (on by default). The KIMI_CODE_REPEAT_BREAKER environment variable overrides this setting.",
permissionRules: "Permission Rules",
permissionDecision: "Decision",
permissionPattern: "Pattern",
+21 -6
View File
@@ -108,7 +108,8 @@ export const zhTranslations = {
baseUrlHint: "填写兼容 {type} API 的服务端点地址,不要以斜杠结尾",
envPairs: "Env 键值对",
addEnv: "+ 添加",
modelMappingDesc: "显示名称只影响 /model 菜单;1M 只是给 Kimi Code 的上下文能力声明。",
modelMappingDesc: "显示名称只影响 /model 菜单。",
maxOutputSizeDesc: "「最大输出」写入 max_output_size,留空则不发送输出上限。",
oneClickSetup: "一键设置",
fetchModels: "获取模型列表",
fetchingModels: "获取中...",
@@ -118,7 +119,8 @@ export const zhTranslations = {
displayName: "显示名称",
actualModel: "实际请求模型",
contextSize: "上下文长度",
supports1M: "声明支持 1M",
maxOutputSize: "最大输出",
maxOutputRef: "参考 {value}",
default: "默认",
operation: "操作",
isDefault: "已默认",
@@ -158,9 +160,15 @@ export const zhTranslations = {
enableThinking: "启用思考",
thinkingLevel: "思考等级",
thinkingKeep: "保留思考内容",
thinkingLow: "低",
thinkingMedium: "中",
thinkingHigh: "高",
thinkingLow: "Low",
thinkingMedium: "Medium",
thinkingHigh: "High",
thinkingMax: "Max",
thinkingXHigh: "XHigh",
thinkingEffortUnsupported: "当前默认模型不支持思考。",
thinkingEffortTierUnsupported: "当前默认模型不支持该档位(支持:{levels})。",
thinkingEffortTierUndeclared: "该模型未声明思考等级,上游可能拒绝此档位。",
thinkingEffortDependsOnDefaultModel: "实际生效的档位取决于当前默认模型。",
thinkingContextHint: "启用思考会占用更多上下文,请确保模型上下文长度和预留空间足够。",
loopControlSettings: "循环控制",
maxAttemptsPerStep: "单步最大尝试次数",
@@ -173,7 +181,14 @@ export const zhTranslations = {
watchSettings: "文件监听",
watchEnabled: "启用文件监听",
watchEnabledDesc:
"监听配置文件与工作目录变化(kimi-code 2.0.1 起可配置,2.0.2 起默认关闭)。环境变量 KIMI_CODE_WATCH 会覆盖本配置。",
"监听配置文件与工作目录变化(kimi-code 2.0.1 起可配置,默认开启)。环境变量 KIMI_CODE_WATCH 会覆盖本配置。",
behaviorSettings: "行为开关",
autoSessionTitle: "自动生成会话标题",
autoSessionTitleDesc:
"允许客户端自动生成会话标题(默认开启;关闭后写入 false)。",
repeatBreaker: "重复调用拦截",
repeatBreakerDesc:
"连续重复相同工具调用时提醒并强制停止(默认开启)。环境变量 KIMI_CODE_REPEAT_BREAKER 会覆盖本配置。",
permissionRules: "权限规则",
permissionDecision: "处置",
permissionPattern: "模式",
+69 -9
View File
@@ -20,7 +20,7 @@ function loopOf(raw: unknown): Record<string, unknown> {
}
// ---------------------------------------------------------------------------
// Reading — legacy off values normalized to "off", "max" → "high"
// Reading — legacy off values normalized to "off"; effort tiers pass through
// ---------------------------------------------------------------------------
describe("getAgentSettings — thinking.keep normalization", () => {
@@ -52,12 +52,29 @@ describe("getAgentSettings — thinking.keep normalization", () => {
});
});
describe("getAgentSettings — effort \"max\" read mapping", () => {
it("normalizes a stored \"max\" to \"high\" on read", () => {
describe("getAgentSettings — thinking.effort read mapping", () => {
it("keeps a stored \"max\" as-is (the tier is valid upstream)", () => {
expect(getAgentSettings({ thinking: { effort: "max" } }).thinking?.effort).toBe(
"high"
"max"
);
});
it("passes an \"xhigh\" tier through untouched", () => {
expect(getAgentSettings({ thinking: { effort: "xhigh" } }).thinking?.effort).toBe(
"xhigh"
);
});
it("keeps the default \"medium\" when the key is absent", () => {
expect(getAgentSettings({}).thinking?.effort).toBe("medium");
});
it("round-trips an arbitrary hand-written tier on save", () => {
const next = setAgentSettings({ thinking: { effort: "ultra" } }, {
thinking: { effort: "ultra" },
});
expect(thinkingOf(next).effort).toBe("ultra");
});
});
// ---------------------------------------------------------------------------
@@ -184,8 +201,8 @@ describe("loop_control.compaction_max_attempts", () => {
});
// ---------------------------------------------------------------------------
// [watch] enabled (kimi-code 2.0.1+) — top-level section, off by default since
// 2.0.2; mirrors the [background] handling
// [watch] enabled (kimi-code 2.0.1+) — top-level section; 2.0.2 flipped the
// default off, #4015 flipped it back on. Mirrors the [background] handling.
// ---------------------------------------------------------------------------
function watchOf(raw: unknown): Record<string, unknown> {
@@ -196,9 +213,9 @@ function watchOf(raw: unknown): Record<string, unknown> {
}
describe("[watch] enabled", () => {
it("reads as false when the section or key is absent (2.0.2 default)", () => {
expect(getAgentSettings({}).watch?.enabled).toBe(false);
expect(getAgentSettings({ watch: {} }).watch?.enabled).toBe(false);
it("reads as true when the section or key is absent (#4015 default)", () => {
expect(getAgentSettings({}).watch?.enabled).toBe(true);
expect(getAgentSettings({ watch: {} }).watch?.enabled).toBe(true);
});
it("reads an explicit value", () => {
@@ -231,6 +248,49 @@ describe("[watch] enabled", () => {
});
});
// ---------------------------------------------------------------------------
// Top-level booleans (auto_session_title #3962 / repeat_breaker #3995):
// upstream default true — false is written explicitly, true removes the key.
// ---------------------------------------------------------------------------
describe("top-level boolean switches", () => {
it("reads as undefined when the key is absent (default true)", () => {
const s = getAgentSettings({});
expect(s.auto_session_title).toBeUndefined();
expect(s.repeat_breaker).toBeUndefined();
});
it("reads explicit false values", () => {
const s = getAgentSettings({
auto_session_title: false,
repeat_breaker: false,
});
expect(s.auto_session_title).toBe(false);
expect(s.repeat_breaker).toBe(false);
});
it("writes false explicitly and removes the key on true", () => {
const off = setAgentSettings({}, { repeat_breaker: false }) as Record<
string,
unknown
>;
expect(off.repeat_breaker).toBe(false);
const on = setAgentSettings(
{ repeat_breaker: false },
{ repeat_breaker: true }
) as Record<string, unknown>;
expect("repeat_breaker" in on).toBe(false);
});
it("keeps an explicit false on saves that do not touch it", () => {
const next = setAgentSettings(
{ auto_session_title: false },
{ thinking: { effort: "high" } }
) as Record<string, unknown>;
expect(next.auto_session_title).toBe(false);
});
});
describe("permission.dangerous_command_guard", () => {
it("reads as undefined when the key is absent (default on)", () => {
+27 -8
View File
@@ -13,10 +13,11 @@ const DEFAULT_SETTINGS: AgentSettings = {
background: {
keep_alive_on_exit: false,
},
// `[watch] enabled` — kimi-code 2.0.1 added the key, 2.0.2 flipped the
// default to off, so an absent key reads as "off".
// `[watch] enabled` — kimi-code 2.0.1 added the key; 2.0.2 flipped the
// default to off, then #4015 flipped it back on, so an absent key reads
// as "on".
watch: {
enabled: false,
enabled: true,
},
permission: { rules: [] },
hooks: [],
@@ -37,6 +38,7 @@ function getSection<T>(rawOther: unknown, key: string): T | undefined {
}
export function getAgentSettings(rawOther: unknown): AgentSettings {
const root = asRecord(rawOther);
const sectionLoop = getSection<AgentSettings["loop_control"]>(
rawOther,
"loop_control"
@@ -65,11 +67,6 @@ export function getAgentSettings(rawOther: unknown): AgentSettings {
if (thinking.keep !== undefined && thinking.keep !== "all") {
thinking.keep = "off";
}
// The "max" effort tier was removed upstream (auto-migrates to "high");
// old configs still carrying it are shown as "high" (not rewritten on read).
if (thinking.effort === "max") {
thinking.effort = "high";
}
const sectionPermission = getSection<AgentSettings["permission"]>(
rawOther,
"permission"
@@ -97,6 +94,16 @@ export function getAgentSettings(rawOther: unknown): AgentSettings {
: {}),
},
hooks: getSection<AgentSettings["hooks"]>(rawOther, "hooks") ?? [],
// Top-level booleans (upstream default true): keep only explicit values,
// an absent key means "on" — the UI shows `?? true`.
auto_session_title:
typeof root.auto_session_title === "boolean"
? root.auto_session_title
: undefined,
repeat_breaker:
typeof root.repeat_breaker === "boolean"
? root.repeat_breaker
: undefined,
};
}
@@ -171,5 +178,17 @@ export function setAgentSettings(
delete root.hooks;
}
// Top-level booleans with an upstream default of true: an explicit false
// is written; true/undefined removes the key so the config tracks the
// upstream default instead of pinning it.
for (const key of ["auto_session_title", "repeat_breaker"] as const) {
const value = patch[key] ?? current[key];
if (value === false) {
root[key] = false;
} else {
delete root[key];
}
}
return root;
}
+83
View File
@@ -0,0 +1,83 @@
import { describe, expect, it } from "vitest";
import {
parseMaxOutputInput,
readMaxOutputSize,
setMaxOutputSize,
withMaxOutputSize,
} from "./model-max-output";
const model = (raw_other: unknown) =>
({
alias: "a",
provider: "p",
model: "m",
max_context_size: 0,
display_name: null,
raw_other,
}) as const;
describe("readMaxOutputSize", () => {
it("reads the key from a model entry's preserved config fields", () => {
expect(readMaxOutputSize({ max_output_size: 32768 })).toBe(32768);
});
it("treats absent / malformed / non-positive values as unset", () => {
expect(readMaxOutputSize(undefined)).toBeUndefined();
expect(readMaxOutputSize(null)).toBeUndefined();
expect(readMaxOutputSize({})).toBeUndefined();
expect(readMaxOutputSize("32768")).toBeUndefined();
expect(readMaxOutputSize({ max_output_size: 0 })).toBeUndefined();
expect(readMaxOutputSize({ max_output_size: -1 })).toBeUndefined();
expect(readMaxOutputSize({ max_output_size: NaN })).toBeUndefined();
expect(readMaxOutputSize([1, 2])).toBeUndefined();
});
});
describe("parseMaxOutputInput", () => {
it("keeps positive integers", () => {
expect(parseMaxOutputInput("32768")).toBe(32768);
});
it("maps blank / zero / garbage to unset", () => {
expect(parseMaxOutputInput("")).toBeUndefined();
expect(parseMaxOutputInput("0")).toBeUndefined();
expect(parseMaxOutputInput("-5")).toBeUndefined();
expect(parseMaxOutputInput("abc")).toBeUndefined();
});
});
describe("setMaxOutputSize", () => {
it("sets the key while preserving other preserved fields", () => {
expect(setMaxOutputSize({ support_efforts: ["low"] }, 16384)).toEqual({
support_efforts: ["low"],
max_output_size: 16384,
});
});
it("drops the key when the value is unset", () => {
expect(setMaxOutputSize({ max_output_size: 16384, force: true }, undefined)).toEqual({
force: true,
});
});
it("starts from an empty object for null / non-object raw_other", () => {
expect(setMaxOutputSize(null, 1024)).toEqual({ max_output_size: 1024 });
expect(setMaxOutputSize("junk", 1024)).toEqual({ max_output_size: 1024 });
expect(setMaxOutputSize({ max_output_size: 1 }, undefined)).toEqual({});
});
});
describe("withMaxOutputSize", () => {
it("returns a new model with the override applied", () => {
const before = model({ max_output_size: 4096 });
const after = withMaxOutputSize(before, 8192);
expect(readMaxOutputSize(after.raw_other)).toBe(8192);
expect(readMaxOutputSize(before.raw_other)).toBe(4096);
expect(after).not.toBe(before);
});
it("clears the override without touching other fields", () => {
const after = withMaxOutputSize(model({ max_output_size: 4096 }), undefined);
expect(after.raw_other).toEqual({});
});
});
+59
View File
@@ -0,0 +1,59 @@
import type { Model } from "../types";
/**
* Helpers for the `max_output_size` model field.
*
* kimi-code keeps it in `[models."<alias>"] max_output_size`; it is not a
* first-class field of Kimi Switch's Rust `Model` struct, so it round-trips
* through `raw_other` (the pass-through bucket for unknown keys). Absent key =
* unset = the upstream default applies, so clearing the field removes the key
* rather than writing 0.
*/
function asRecord(value: unknown): Record<string, unknown> {
if (value && typeof value === "object" && !Array.isArray(value)) {
return value as Record<string, unknown>;
}
return {};
}
/** Read the override; undefined when absent or malformed (treated as unset). */
export function readMaxOutputSize(rawOther: unknown): number | undefined {
const value = asRecord(rawOther).max_output_size;
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
return undefined;
}
return value;
}
/** Parse the number input: blank / 0 / NaN mean "unset". */
export function parseMaxOutputInput(text: string): number | undefined {
const value = parseInt(text, 10);
if (isNaN(value) || value <= 0) return undefined;
return value;
}
/**
* Set (positive value) or drop (undefined) `max_output_size` on a raw_other
* blob; every other preserved key is kept.
*/
export function setMaxOutputSize(
rawOther: unknown,
value: number | undefined,
): Record<string, unknown> {
const next = { ...asRecord(rawOther) };
if (value === undefined) {
delete next.max_output_size;
} else {
next.max_output_size = Math.floor(value);
}
return next;
}
/** Immutable update of a model entry's max_output_size override. */
export function withMaxOutputSize(
model: Model,
value: number | undefined,
): Model {
return { ...model, raw_other: setMaxOutputSize(model.raw_other, value) };
}
+6034 -2093
View File
File diff suppressed because it is too large. Load diff
+13310 -1799
View File
File diff suppressed because it is too large. Load diff
+2
View File
@@ -25,6 +25,8 @@ export interface ModelCost {
export interface ModelRef {
name?: string;
context?: number;
/** Upper bound on generated tokens (`limit.output` on models.dev). */
output?: number;
reasoning?: boolean;
tool_call?: boolean;
structured_output?: boolean;
+139
View File
@@ -0,0 +1,139 @@
import { describe, expect, it } from "vitest";
import {
THINKING_EFFORTS,
normalizeSupportEfforts,
readSupportEfforts,
resolveThinkingEffortSupport,
} from "./thinking-efforts";
// ---------------------------------------------------------------------------
// support_efforts normalization — an absent / malformed key means "unknown",
// never "no tiers supported"
// ---------------------------------------------------------------------------
describe("normalizeSupportEfforts", () => {
it("keeps a non-empty list of strings", () => {
expect(normalizeSupportEfforts(["low", "high"])).toEqual(["low", "high"]);
});
it("drops blanks and non-strings but keeps the order", () => {
expect(normalizeSupportEfforts(["low", "", 3, null, "max"])).toEqual([
"low",
"max",
]);
});
it("returns null for absent / empty / non-array values", () => {
expect(normalizeSupportEfforts(undefined)).toBeNull();
expect(normalizeSupportEfforts([])).toBeNull();
expect(normalizeSupportEfforts([" "])).toBeNull();
expect(normalizeSupportEfforts("low")).toBeNull();
});
});
describe("readSupportEfforts", () => {
const model = (raw_other: unknown) =>
({ alias: "a", provider: "p", model: "m", max_context_size: 0, display_name: null, raw_other }) as const;
it("reads the key from a model entry's preserved config fields", () => {
expect(readSupportEfforts(model({ support_efforts: ["low", "max"] }))).toEqual([
"low",
"max",
]);
});
it("returns null when the entry or the key is absent", () => {
expect(readSupportEfforts(undefined)).toBeNull();
expect(readSupportEfforts(model(undefined))).toBeNull();
expect(readSupportEfforts(model({ other: 1 }))).toBeNull();
});
});
// ---------------------------------------------------------------------------
// Tier resolution — three layers, in priority order
// ---------------------------------------------------------------------------
describe("resolveThinkingEffortSupport — declared support_efforts wins", () => {
const support = resolveThinkingEffortSupport({
supportEfforts: ["low", "high", "max"],
reasoning: true,
});
it("exposes the declared list and keeps thinking enabled", () => {
expect(support.declared).toEqual(["low", "high", "max"]);
expect(support.thinkingSupported).toBe(true);
});
it("enables exactly the declared tiers", () => {
for (const tier of THINKING_EFFORTS) {
expect(support.levels[tier].enabled).toBe(tier === "low" || tier === "high" || tier === "max");
}
});
it("explains a disabled tier with the supported list", () => {
expect(support.levels.medium).toEqual({
enabled: false,
note: { kind: "tierUnsupported", supported: ["low", "high", "max"] },
});
expect(support.levels.xhigh.enabled).toBe(false);
});
it("does not disable thinking even when models.dev says reasoning: false", () => {
const conflicting = resolveThinkingEffortSupport({
supportEfforts: ["high"],
reasoning: false,
});
expect(conflicting.thinkingSupported).toBe(true);
expect(conflicting.levels.low.enabled).toBe(false);
expect(conflicting.levels.high.enabled).toBe(true);
});
it("matches tiers case-insensitively", () => {
const upper = resolveThinkingEffortSupport({ supportEfforts: ["HIGH"] });
expect(upper.levels.high.enabled).toBe(true);
});
});
describe("resolveThinkingEffortSupport — model without thinking", () => {
const support = resolveThinkingEffortSupport({ reasoning: false });
it("disables the whole thinking area", () => {
expect(support.thinkingSupported).toBe(false);
expect(support.declared).toBeNull();
for (const tier of THINKING_EFFORTS) {
expect(support.levels[tier]).toEqual({
enabled: false,
note: { kind: "thinkingUnsupported" },
});
}
});
});
describe("resolveThinkingEffortSupport — nothing known about the model", () => {
it("offers low/medium/high plain and flags max/xhigh as undeclared", () => {
const support = resolveThinkingEffortSupport({});
expect(support.thinkingSupported).toBe(true);
expect(support.declared).toBeNull();
for (const tier of ["low", "medium", "high"] as const) {
expect(support.levels[tier]).toEqual({ enabled: true });
}
for (const tier of ["max", "xhigh"] as const) {
expect(support.levels[tier]).toEqual({
enabled: true,
note: { kind: "tierUndeclared" },
});
}
});
it("treats an undefined reasoning flag as unknown, not as unsupported", () => {
expect(
resolveThinkingEffortSupport({ reasoning: undefined }).thinkingSupported
).toBe(true);
});
it("ignores a malformed support_efforts value and falls back to unknown", () => {
const support = resolveThinkingEffortSupport({ supportEfforts: "low" });
expect(support.declared).toBeNull();
expect(support.levels.low).toEqual({ enabled: true });
});
});
+127
View File
@@ -0,0 +1,127 @@
/**
* Thinking-effort capability resolution.
*
* kimi-code's `[thinking] effort` is a free-form string (low/medium/high/
* xhigh/max in practice) that the CLI forwards verbatim to OpenAI-compatible
* upstreams as `reasoning_effort`. Which tiers a given model accepts is
* declared per model in config.toml (`[models."<alias>"] support_efforts`);
* models.dev only carries a boolean `reasoning` flag with no tier list. The
* two sources are merged here so the UI can grey out tiers the current default
* model would reject, without ever rejecting a stored value on read.
*/
import type { Model } from "../types";
export const THINKING_EFFORTS = [
"low",
"medium",
"high",
"xhigh",
"max",
] as const;
export type ThinkingEffort = (typeof THINKING_EFFORTS)[number];
/** Why a tier is blocked, or (for undeclared models) merely unverified. */
export type EffortNote =
| { kind: "thinkingUnsupported" }
| { kind: "tierUnsupported"; supported: string[] }
| { kind: "tierUndeclared" };
export interface EffortAvailability {
/** Whether the tier can be picked. */
enabled: boolean;
/** Advisory message — set for blocked tiers and for unverified ones. */
note?: EffortNote;
}
export interface ThinkingEffortSupport {
/** False only when the model is known not to support thinking at all. */
thinkingSupported: boolean;
/** Declared tiers, or null when nothing is known about the model. */
declared: string[] | null;
levels: Record<ThinkingEffort, EffortAvailability>;
}
/** Tiers offered unverified when a model declares nothing at all. */
const ALWAYS_OFFERED = new Set<string>(["low", "medium", "high"]);
/**
* Normalize a `support_efforts` value into a comparable list, or null when the
* key is absent / malformed (treating it as "unknown", not "none supported").
*/
export function normalizeSupportEfforts(value: unknown): string[] | null {
if (!Array.isArray(value)) return null;
const tiers = value
.filter((v): v is string => typeof v === "string")
.map((v) => v.trim())
.filter((v) => v.length > 0);
return tiers.length > 0 ? tiers : null;
}
/** Read `support_efforts` from a model entry's preserved config.toml fields. */
export function readSupportEfforts(model: Model | undefined): string[] | null {
if (!model) return null;
const raw = model.raw_other;
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
return normalizeSupportEfforts(
(raw as Record<string, unknown>).support_efforts
);
}
function buildLevels(
resolve: (effort: ThinkingEffort) => EffortAvailability
): Record<ThinkingEffort, EffortAvailability> {
const levels = {} as Record<ThinkingEffort, EffortAvailability>;
for (const effort of THINKING_EFFORTS) levels[effort] = resolve(effort);
return levels;
}
/**
* Resolve per-tier availability from the two capability sources, in priority
* order:
* 1. `supportEfforts` declared → tiers follow the list exactly.
* 2. models.dev `reasoning === false` → the whole thinking area is off.
* 3. Nothing known → low/medium/high are offered plain, max/xhigh stay
* selectable but carry a "not declared" note (upstream may reject them).
*/
export function resolveThinkingEffortSupport(input: {
supportEfforts?: unknown;
reasoning?: boolean;
}): ThinkingEffortSupport {
const declared = normalizeSupportEfforts(input.supportEfforts);
if (declared) {
const supported = new Set(declared.map((t) => t.toLowerCase()));
return {
thinkingSupported: true,
declared,
levels: buildLevels((effort) =>
supported.has(effort)
? { enabled: true }
: {
enabled: false,
note: { kind: "tierUnsupported", supported: declared },
}
),
};
}
if (input.reasoning === false) {
return {
thinkingSupported: false,
declared: null,
levels: buildLevels(() => ({
enabled: false,
note: { kind: "thinkingUnsupported" },
})),
};
}
return {
thinkingSupported: true,
declared: null,
levels: buildLevels((effort) =>
ALWAYS_OFFERED.has(effort)
? { enabled: true }
: { enabled: true, note: { kind: "tierUndeclared" } }
),
};
}
+12 -4
View File
@@ -111,11 +111,12 @@ export interface DiscoveredModel {
export interface ThinkingConfig {
enabled?: boolean;
/**
* Effort tier. `max` is read-compatible only — upstream removed the tier
* (old configs auto-migrate to `high`); the UI normalizes it and the
* serialization path never writes it.
* Effort tier, forwarded verbatim to OpenAI-compatible upstreams as
* `reasoning_effort`. Upstream accepts a free-form string; the UI offers
* low/medium/high/max/xhigh and derives per-model support from
* `[models."<alias>"] support_efforts`.
*/
effort?: "low" | "medium" | "high" | "max";
effort?: string;
/**
* Keep thinking content. The legacy off values (`false`, `0`, "no", "none",
* `null`) are read-compatible — old configs may carry them; the UI
@@ -171,6 +172,13 @@ export interface AgentSettings {
background?: BackgroundConfig;
/** Top-level `[watch]` section (kimi-code 2.0.1+). */
watch?: WatchConfig;
/** Top-level config.toml key (kimi-code #3962): let clients auto-generate
* session titles. Default true; only an explicit false disables. */
auto_session_title?: boolean;
/** Top-level config.toml key (kimi-code #3995): repeat-breaker for
* consecutively repeated identical tool calls. Default true; env
* KIMI_CODE_REPEAT_BREAKER outranks this config. */
repeat_breaker?: boolean;
permission?: {
rules?: PermissionRule[];
/** kimi-code `[permission]` key. Default true when the key is
+1 -1
View File
@@ -50,7 +50,7 @@
"url": "https://billowliu2.github.io/KimiSwitch/",
"applicationCategory": "DeveloperApplication",
"operatingSystem": "Windows 10, Windows 11, macOS, Linux",
"softwareVersion": "0.7.12",
"softwareVersion": "0.8.1",
"dateModified": "2026-09-03",
"description": "桌面端 LLM 供应商配置管理器:22 个预设选好即写入 config.toml,用量与账单直读各厂商官方接口。Tauri + Rust,开源 MIT。",
"downloadUrl": "https://github.com/billowliu2/KimiSwitch/releases",
+64 -12
View File
@@ -16,10 +16,10 @@ const zh = {
performance: "性能",
changelog: "更新日志",
download: "下载",
downloadBtn: "下载 v0.7.12",
downloadBtn: "下载 v0.8.0",
},
hero: {
badge: "v0.7.12 · 开源 MIT · Windows / macOS / Linux",
badge: "v0.8.0 · 开源 MIT · Windows / macOS / Linux",
titleBefore: "统一管理你的",
titleAccent: "AI 供应商",
titleAfter: "",
@@ -29,7 +29,7 @@ const zh = {
source: "查看源码",
stats: [
{ value: "22", label: "预设供应商" },
{ value: "7495", label: "模型价格库" },
{ value: "8385", label: "模型价格库" },
{ value: "MIT", label: "开源协议" },
],
},
@@ -76,7 +76,7 @@ const zh = {
{
id: "model-discovery",
title: "模型自动发现",
desc: "可用模型直接问供应商 API;显示名 / 上下文 / 能力取自 models.dev 快照(当前 7495 个模型、212 家供应商)。",
desc: "可用模型直接问供应商 API;显示名 / 上下文 / 能力取自 models.dev 快照(当前 8,385 个模型、226 家供应商)。",
img: "screenshots/kimi-cli-select-model.png",
},
{
@@ -89,7 +89,7 @@ const zh = {
},
showcase: {
title: "界面演示",
subtitle: "以下截图取自 v0.7.12 实机运行。",
subtitle: "以下截图取自 v0.8.0 实机运行。",
items: [
{
src: "screenshots/usage-config-page.png",
@@ -140,6 +140,32 @@ const zh = {
syncedNote: "数据已同步 GitHub Releases",
fallbackNote: "内置版本记录",
entries: [
{
version: "v0.8.1",
date: "2026-10-04",
items: [
"全局配置新增两个顶层开关:自动生成会话标题(auto_session_title)与重复调用拦截(repeat_breaker),默认开启",
"最大输出未匹配 models.dev 时回填默认值 131072(128K 兜底)",
"修复 [watch] enabled 默认值显示(上游 #4015 翻回默认开启);「最大输出」列描述对齐上游 #4091",
],
},
{
version: "v0.8.0",
date: "2026-10-03",
items: [
"模型映射新增「最大输出 Token」列:逐模型设置 max_output_size(留空用上游默认),models.dev 有 output 上限时显示可点击参考值,打开编辑页自动补全空值",
"models.dev 快照新增 output 上限字段,同步至 2026-10-03(8,385 模型 / 226 供应商,output 覆盖 97.4%)",
"移除无效的「声明支持 1M」复选框(勾选从不生效),由「最大输出 Token」列取代",
],
},
{
version: "v0.7.23",
date: "2026-09-30",
items: [
"思考等级五档 + 能力感知:Low / Medium / High / XHigh / Max,按当前默认模型声明的 support_efforts 自动置灰不支持档位并提示原因",
"models.dev 标记不支持思考的模型整体禁用思考区;未声明档位的模型保留可选并提示上游可能拒绝",
],
},
{
version: "v0.7.12",
date: "2026-09-03",
@@ -322,7 +348,7 @@ const zh = {
},
download: {
title: "下载 Kimi Switch",
subtitle: "当前版本 v0.7.12 · Windows / macOS / Linux 三平台已发布",
subtitle: "当前版本 v0.8.0 · Windows / macOS / Linux 三平台已发布",
autoUpdate: "更新检查跑在启动时和每 8 小时的周期任务里,设置页也可以手动触发。",
ready: "已发布",
wip: "开发中",
@@ -391,10 +417,10 @@ const en: Dict = {
performance: "Performance",
changelog: "Changelog",
download: "Download",
downloadBtn: "Download v0.7.12",
downloadBtn: "Download v0.8.0",
},
hero: {
badge: "v0.7.12 · Open source MIT · Windows / macOS / Linux",
badge: "v0.8.0 · Open source MIT · Windows / macOS / Linux",
titleBefore: "One app for all your ",
titleAccent: "AI providers",
titleAfter: "",
@@ -404,7 +430,7 @@ const en: Dict = {
source: "View source",
stats: [
{ value: "22", label: "Provider presets" },
{ value: "7495", label: "Model price DB" },
{ value: "8385", label: "Model price DB" },
{ value: "MIT", label: "License" },
],
},
@@ -451,7 +477,7 @@ const en: Dict = {
{
id: "model-discovery",
title: "Model discovery",
desc: "Model lists are fetched from the provider API itself; display names, context sizes and capabilities come from a build-time models.dev snapshot (7495 models, 212 providers).",
desc: "Model lists are fetched from the provider API itself; display names, context sizes and capabilities come from a build-time models.dev snapshot (8,385 models, 226 providers).",
img: "screenshots/kimi-cli-select-model.png",
},
{
@@ -464,7 +490,7 @@ const en: Dict = {
},
showcase: {
title: "Screenshots",
subtitle: "All screenshots below are taken from the running v0.7.12 build.",
subtitle: "All screenshots below are taken from the running v0.8.0 build.",
items: [
{
src: "screenshots/usage-config-page.png",
@@ -515,6 +541,32 @@ const en: Dict = {
syncedNote: "Synced from GitHub Releases",
fallbackNote: "Built-in release notes",
entries: [
{
version: "v0.8.1",
date: "2026-10-04",
items: [
"Two new top-level switches in Global Settings: auto session title (auto_session_title) and repeat breaker (repeat_breaker), on by default",
"Max output backfills a 131072 (128K) default when models.dev has no match",
"Fixed the [watch] enabled default display (upstream #4015 flipped it back on); Max output column wording aligned with upstream #4091",
],
},
{
version: "v0.8.0",
date: "2026-10-03",
items: [
"New per-model Max Output Token column: sets max_output_size in config.toml (blank keeps the upstream default), shows a click-to-fill reference when models.dev knows the cap, and backfills empty rows automatically",
"models.dev snapshot gains the output-limit field, synced to 2026-10-03 (8,385 models / 226 providers, 97.4% output coverage)",
"Removed the ineffective 'Supports 1M' checkbox (it was never persisted), replaced by the Max Output Token column",
],
},
{
version: "v0.7.23",
date: "2026-09-30",
items: [
"Thinking levels expanded to five tiers with capability awareness: Low / Medium / High / XHigh / Max, greying out tiers the current default model's support_efforts doesn't declare, with an explanation",
"Models flagged non-reasoning by models.dev disable the whole thinking area; undeclared tiers stay selectable with an upstream-may-reject hint",
],
},
{
version: "v0.7.12",
date: "2026-09-03",
@@ -696,7 +748,7 @@ const en: Dict = {
},
download: {
title: "Download Kimi Switch",
subtitle: "Current version v0.7.12 · Windows, macOS and Linux now released",
subtitle: "Current version v0.8.0 · Windows, macOS and Linux now released",
autoUpdate: "Update checks run at startup and every 8 hours; Settings also has a manual check.",
ready: "Available",
wip: "In development",
+1 -1
View File
@@ -8,7 +8,7 @@ const icons: Record<string, Icon> = {
linux: LinuxLogo,
};
const VERSION = "0.7.12";
const VERSION = "0.8.1";
const GITHUB = "https://github.com/billowliu2/KimiSwitch";
const MIRROR_RELEASES = "https://git.codingplan.site/admin/KimiCodeSwitch/releases";
const dl = (file: string) => `${GITHUB}/releases/download/v${VERSION}/${file}`;