Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
317562781c | ||
|
|
6a8331d28b | ||
|
|
c08de0ebb0 | ||
|
|
ded166c4c8 | ||
|
|
803c346854 | ||
|
|
f9f9af0fc9 | ||
|
|
9f51ea5e84 | ||
|
|
d190a55bf2 | ||
|
|
ac82135dee | ||
|
|
ef6749b208 | ||
|
|
24cb04b98f | ||
|
|
977f433bb2 | ||
|
|
3c3c18925d | ||
|
|
30371a1ba6 | ||
|
|
4d87183768 | ||
|
|
88683b331e |
No files matched your search
@@ -74,6 +74,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
FILE="docs/release-notes/${{ github.ref_name }}.md"
|
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"
|
[ -f "$FILE" ] || FILE="release-notes-${{ github.ref_name }}.md"
|
||||||
{
|
{
|
||||||
echo 'body<<RELEASE_BODY_EOF'
|
echo 'body<<RELEASE_BODY_EOF'
|
||||||
|
|||||||
@@ -6,6 +6,30 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## v0.8.2 (2026-10-09)
|
||||||
|
|
||||||
|
性能专项,功能与数据口径不变(用量数字、字段、IPC 接口、导出格式均与 v0.8.1 一致)。
|
||||||
|
|
||||||
|
- **仪表盘重启后首次打开:约 22~27 秒 → 约 0.85 秒**(实测 6.4 万条记录 / 1.03 GB)。用量解析结果持久化到 `~/.kimi-switch/scan-cache-*.json`,启动直读快照渲染,后台线程全量校验,有变化时事件推送无感更新(先快后准)
|
||||||
|
- **切换时间范围几乎瞬时**:热力图 / 模型总览 / 各范围汇总按记录集缓存;并去掉原「全时段」聚合的整表无用遍历
|
||||||
|
- **界面不再闪加载态**:切范围与重进页面先呈现旧数据再静默刷新(stale-while-revalidate)
|
||||||
|
- **供应商卡片墙变快**:供应商配置读取缓存(含外部 CLI 改动失效路径),N 张卡片由 N 次全量加载降为 1 次
|
||||||
|
- **SQLite 连接复用**:连接与迁移改为每库一次,卡片墙场景由 200+ 次降为 1 次
|
||||||
|
- **models.dev 快照更新**:同步至 2026-10-09(8,467 模型 / 225 供应商)
|
||||||
|
|
||||||
|
## 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)
|
## v0.7.23 (2026-09-30)
|
||||||
|
|
||||||
- **思考等级五档 + 能力感知**:全局配置的思考等级扩展为低 / 中 / 高 / Max / XHigh,按当前默认模型声明的 `support_efforts` 自动置灰不支持档位并提示原因;models.dev 标记不支持思考的模型整体禁用思考区;未声明档位的模型保留可选并提示"上游可能拒绝"
|
- **思考等级五档 + 能力感知**:全局配置的思考等级扩展为低 / 中 / 高 / Max / XHigh,按当前默认模型声明的 `support_efforts` 自动置灰不支持档位并提示原因;models.dev 标记不支持思考的模型整体禁用思考区;未声明档位的模型保留可选并提示"上游可能拒绝"
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
| **连通性测试** | 实测 `base_url` 延迟,绿/橙/红彩色气泡,6 秒自动消失 |
|
| **连通性测试** | 实测 `base_url` 延迟,绿/橙/红彩色气泡,6 秒自动消失 |
|
||||||
| **重复供应商** | 一键深拷贝供应商 + 全部模型,key 改 `xxx-copy` |
|
| **重复供应商** | 一键深拷贝供应商 + 全部模型,key 改 `xxx-copy` |
|
||||||
| **图标按钮操作** | 启用 / 编辑 / 复制 / 测试连通 / 删除 全图标化(lucide-react) |
|
| **图标按钮操作** | 启用 / 编辑 / 复制 / 测试连通 / 删除 全图标化(lucide-react) |
|
||||||
| **模型映射** | 别名("provider/model" 形式)↔ 实际请求模型 ID,自定义显示名、上下文长度、1M 上下文声明、能力 |
|
| **模型映射** | 别名("provider/model" 形式)↔ 实际请求模型 ID,自定义显示名、上下文长度、最大输出 Token(`max_output_size`)、能力(仅 Kimi Code) |
|
||||||
| **自动上下文** | 拉取模型时 **API 返回 > models.dev ref > 正则兜底** 三级优先级自动适配 |
|
| **自动上下文** | 拉取模型时 **API 返回 > models.dev ref > 正则兜底** 三级优先级自动适配 |
|
||||||
| **能力自动推导** | `image_in / video_in / tool_use` 全部由 models.dev 推得;UI 仅暴露 `thinking` 一个手动开关 |
|
| **能力自动推导** | `image_in / video_in / tool_use` 全部由 models.dev 推得;UI 仅暴露 `thinking` 一个手动开关 |
|
||||||
| **全局设置** | `[thinking]` 表完整支持(enabled / effort / keep),仅 Kimi Code 生效 |
|
| **全局设置** | `[thinking]` 表完整支持(enabled / effort / keep),仅 Kimi Code 生效 |
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
一张表管理全部模型映射:显示名、实际请求模型、上下文长度、1M 上下文声明、能力(仅"思考")、设为默认、删除。
|
一张表管理全部模型映射:显示名、实际请求模型、上下文长度、最大输出 Token(`max_output_size`,可点参考值填 models.dev 上限)、能力(仅"思考")、设为默认、删除(后两项仅 Kimi Code)。
|
||||||
|
|
||||||
**用量仪表盘**
|
**用量仪表盘**
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -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 |
|
| **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` |
|
| **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 |
|
| **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 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 |
|
| **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 |
|
| **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
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
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**
|
**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
|
- **`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
|
- **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
|
- **`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)
|
- **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
|
## Feature Details
|
||||||
|
|||||||
@@ -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 测试全量通过
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# KimiSwitch v0.8.2
|
||||||
|
|
||||||
|
本版为**性能专项**:把主要操作的等待时间从「秒级」压到「感知不到」,功能与数据口径完全不变。
|
||||||
|
|
||||||
|
## 性能优化
|
||||||
|
|
||||||
|
- **仪表盘重启后首次打开:约 22~27 秒 → 约 0.85 秒**(实测 1065 个 `wire.jsonl` / 1.03 GB / 6.4 万条记录)。用量解析结果持久化到本地快照(`~/.kimi-switch/scan-cache-*.json`),启动时直接读快照渲染,后台线程同时做全量校验;校验发现新记录时自动推送事件、无感更新数字(先快后准,无需手动刷新)
|
||||||
|
- **切换时间范围几乎瞬时**:热力图、模型总览、各范围汇总结果按记录集与日期缓存,来回切换不再重复全量聚合;未命中时也不再做无用的整表遍历(原「全时段」统计会为取一张模型表而白算日/明细/供应商三份数据)
|
||||||
|
- **界面不再闪加载态**:仪表盘切范围与重新进入页面时先呈现上次数据再后台静默刷新,不再出现数据已经拿过却先显示骨架屏的情况
|
||||||
|
- **供应商卡片墙显著变快**:供应商配置读取结果加缓存(含敏感字段写入与外部 CLI 改动两条失效路径),N 张卡片从 N 次全量读库 + 配置解析降为 1 次
|
||||||
|
- **SQLite 连接复用**:库连接与建表迁移从「每次调用各做一遍」改为每库一次,卡片墙场景下由 200+ 次降为 1 次
|
||||||
|
|
||||||
|
## 其他
|
||||||
|
|
||||||
|
- **models.dev 快照更新**:同步至 2026-10-09(8,467 模型 / 225 供应商),价格与上下文长度随之刷新
|
||||||
|
|
||||||
|
## 备注
|
||||||
|
|
||||||
|
- 数据口径与功能行为与 v0.8.1 完全一致:用量数字、字段、IPC 接口、导出格式均无变化;「强制刷新」仍会忽略一切缓存重新全量解析
|
||||||
|
- 测试全量通过:Rust 193 项(新增 16 项,覆盖缓存失效、磁盘快照往返等价、损坏快照降级等)、前端 130 项
|
||||||
|
- 首次使用本版本时会重建一次磁盘快照(沿用原有全量扫描耗时),此后每次启动均为快速加载
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "kimiswitch",
|
"name": "kimiswitch",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.7.23",
|
"version": "0.8.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
+10621
-1029
File diff suppressed because it is too large.
Load diff
@@ -133,6 +133,11 @@ async function main() {
|
|||||||
if (typeof m.limit?.context === "number" && m.limit.context > 0) {
|
if (typeof m.limit?.context === "number" && m.limit.context > 0) {
|
||||||
entry.context = m.limit.context;
|
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.reasoning === true) entry.reasoning = true;
|
||||||
if (m.tool_call === true) entry.tool_call = true;
|
if (m.tool_call === true) entry.tool_call = true;
|
||||||
if (m.structured_output === true) entry.structured_output = true;
|
if (m.structured_output === true) entry.structured_output = true;
|
||||||
|
|||||||
Generated
+1
-1
@@ -1978,7 +1978,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kimiswitch"
|
name = "kimiswitch"
|
||||||
version = "0.7.23"
|
version = "0.8.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kimiswitch"
|
name = "kimiswitch"
|
||||||
version = "0.7.23"
|
version = "0.8.2"
|
||||||
description = "Kimi Switch - model config manager"
|
description = "Kimi Switch - model config manager"
|
||||||
authors = ["codingplan.site"]
|
authors = ["codingplan.site"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use indexmap::IndexMap;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::{Mutex, OnceLock};
|
use std::sync::{Mutex, OnceLock};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant, UNIX_EPOCH};
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
use tauri_plugin_opener::OpenerExt;
|
use tauri_plugin_opener::OpenerExt;
|
||||||
|
|
||||||
@@ -79,8 +79,61 @@ fn load_pi_native_config() -> Result<Config, String> {
|
|||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cached `load_agent_config_command` results, keyed by agent. The full load
|
||||||
|
/// parses config.toml, reads all of SQLite and runs `merge_usage_kinds` (two
|
||||||
|
/// settings reads per provider); a usage-card wall triggers one load per
|
||||||
|
/// provider card, so caching turns N loads into one. Entries are validated
|
||||||
|
/// against the db generation counter (bumped by every write path in db.rs)
|
||||||
|
/// plus the config.toml file stamp (catches edits made outside the app, e.g.
|
||||||
|
/// the CLI's /provider command).
|
||||||
|
static CONFIG_CACHE: Mutex<Option<HashMap<String, (u64, Option<(u64, u64)>, Config)>>> =
|
||||||
|
Mutex::new(None);
|
||||||
|
|
||||||
|
/// (mtime_ms, len) of the authoritative on-disk config file for the agent, or
|
||||||
|
/// None when the file is absent / the agent has no authoritative file (Pi is
|
||||||
|
/// SQLite-first, so the db generation alone covers it).
|
||||||
|
fn config_file_stamp(agent: &Agent) -> Option<(u64, u64)> {
|
||||||
|
match agent {
|
||||||
|
Agent::KimiCode => {
|
||||||
|
let md = std::fs::metadata(crate::kimi_code_io::kimi_code_config_path()).ok()?;
|
||||||
|
let mtime_ms = md
|
||||||
|
.modified()
|
||||||
|
.ok()?
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.ok()?
|
||||||
|
.as_millis() as u64;
|
||||||
|
Some((mtime_ms, md.len()))
|
||||||
|
}
|
||||||
|
Agent::Pi => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn load_agent_config_command(agent: Agent) -> Result<Config, String> {
|
pub fn load_agent_config_command(agent: Agent) -> Result<Config, String> {
|
||||||
|
let stamp = config_file_stamp(&agent);
|
||||||
|
let gen = db::generation();
|
||||||
|
let key = agent.as_str().to_string();
|
||||||
|
|
||||||
|
{
|
||||||
|
let guard = CONFIG_CACHE.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
if let Some(map) = guard.as_ref() {
|
||||||
|
if let Some((cached_gen, cached_stamp, config)) = map.get(&key) {
|
||||||
|
if *cached_gen == gen && *cached_stamp == stamp {
|
||||||
|
return Ok(config.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = load_agent_config_uncached(agent)?;
|
||||||
|
let mut guard = CONFIG_CACHE.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
guard
|
||||||
|
.get_or_insert_with(HashMap::new)
|
||||||
|
.insert(key, (gen, stamp, config.clone()));
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_agent_config_uncached(agent: Agent) -> Result<Config, String> {
|
||||||
// Load Kimi Switch's own SQLite database (metadata + migration fallback).
|
// Load Kimi Switch's own SQLite database (metadata + migration fallback).
|
||||||
let db_config = db::load_config(&agent).ok();
|
let db_config = db::load_config(&agent).ok();
|
||||||
|
|
||||||
|
|||||||
+2270
-118
File diff suppressed because it is too large.
Load diff
+125
-59
@@ -6,6 +6,8 @@
|
|||||||
//! a provider.
|
//! a provider.
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
@@ -17,6 +19,51 @@ use crate::models::{Agent, Config, Model, Provider, ProviderType};
|
|||||||
|
|
||||||
pub type DbResult<T> = anyhow::Result<T>;
|
pub type DbResult<T> = anyhow::Result<T>;
|
||||||
|
|
||||||
|
/// Process-wide cached connection, keyed by the resolved db path. Opening a
|
||||||
|
/// SQLite connection plus running the DDL migrations costs several ms; before
|
||||||
|
/// this cache every public helper paid that cost on every call (a usage-card
|
||||||
|
/// wall could open 200+ connections per refresh). `Connection` is `Send` but
|
||||||
|
/// not `Sync`, so a `Mutex` is the correct `static` wrapper — all db access is
|
||||||
|
/// serialized, which is fine for a single-user desktop app.
|
||||||
|
static DB_CONN: Mutex<Option<(PathBuf, Connection)>> = Mutex::new(None);
|
||||||
|
|
||||||
|
/// Monotonic generation counter, bumped by every write path. Read-side caches
|
||||||
|
/// (config cache, archived-session cache) key on this to detect staleness.
|
||||||
|
static DB_GEN: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
|
pub(crate) fn generation() -> u64 {
|
||||||
|
DB_GEN.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn bump_generation() {
|
||||||
|
DB_GEN.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run `f` against the cached connection for the current `db_path()`, opening
|
||||||
|
/// (and migrating) it on first use or whenever the path changed (tests point
|
||||||
|
/// `KIMI_SWITCH_DB_PATH` at per-test temp dirs).
|
||||||
|
fn with_conn<T>(f: impl FnOnce(&mut Connection) -> DbResult<T>) -> DbResult<T> {
|
||||||
|
let mut guard = DB_CONN.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
let path = db_path();
|
||||||
|
let needs_open = match guard.as_ref() {
|
||||||
|
Some((cached_path, _)) => cached_path != &path,
|
||||||
|
None => true,
|
||||||
|
};
|
||||||
|
if needs_open {
|
||||||
|
*guard = Some((path, init_db()?));
|
||||||
|
}
|
||||||
|
let (_, conn) = guard.as_mut().expect("connection just opened");
|
||||||
|
f(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drop the cached connection so tests that switch `KIMI_SWITCH_DB_PATH`
|
||||||
|
/// between temp dirs never observe cross-test state.
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn close_cached_conn_for_tests() {
|
||||||
|
let mut guard = DB_CONN.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
*guard = None;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn kimi_switch_data_dir() -> PathBuf {
|
pub fn kimi_switch_data_dir() -> PathBuf {
|
||||||
dirs::home_dir()
|
dirs::home_dir()
|
||||||
.map(|h| h.join(".kimi-switch"))
|
.map(|h| h.join(".kimi-switch"))
|
||||||
@@ -158,7 +205,10 @@ pub fn init_db() -> DbResult<Connection> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_config(agent: &Agent) -> DbResult<Config> {
|
pub fn load_config(agent: &Agent) -> DbResult<Config> {
|
||||||
let mut conn = init_db()?;
|
with_conn(|conn| load_config_inner(conn, agent))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_config_inner(conn: &mut Connection, agent: &Agent) -> DbResult<Config> {
|
||||||
let tx = conn.transaction()?;
|
let tx = conn.transaction()?;
|
||||||
|
|
||||||
let default_model = get_setting_tx(&tx, &default_model_key(agent))?;
|
let default_model = get_setting_tx(&tx, &default_model_key(agent))?;
|
||||||
@@ -249,7 +299,12 @@ pub fn load_config(agent: &Agent) -> DbResult<Config> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_config(agent: &Agent, config: &Config) -> DbResult<()> {
|
pub fn save_config(agent: &Agent, config: &Config) -> DbResult<()> {
|
||||||
let mut conn = init_db()?;
|
with_conn(|conn| save_config_inner(conn, agent, config))?;
|
||||||
|
bump_generation();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_config_inner(conn: &mut Connection, agent: &Agent, config: &Config) -> DbResult<()> {
|
||||||
let tx = conn.transaction()?;
|
let tx = conn.transaction()?;
|
||||||
|
|
||||||
tx.execute("DELETE FROM providers WHERE agent = ?1", params![agent.as_str()])?;
|
tx.execute("DELETE FROM providers WHERE agent = ?1", params![agent.as_str()])?;
|
||||||
@@ -340,29 +395,36 @@ fn set_setting_tx(tx: &rusqlite::Transaction, key: &str, value: &str) -> DbResul
|
|||||||
|
|
||||||
/// Public helper: read a single setting without an explicit transaction.
|
/// Public helper: read a single setting without an explicit transaction.
|
||||||
pub fn get_setting_pub(key: &str) -> DbResult<Option<String>> {
|
pub fn get_setting_pub(key: &str) -> DbResult<Option<String>> {
|
||||||
let conn = init_db()?;
|
with_conn(|conn| {
|
||||||
let mut stmt = conn.prepare("SELECT value FROM settings WHERE key = ?1")?;
|
let mut stmt = conn.prepare("SELECT value FROM settings WHERE key = ?1")?;
|
||||||
let mut rows = stmt.query(params![key])?;
|
let mut rows = stmt.query(params![key])?;
|
||||||
if let Some(row) = rows.next()? {
|
if let Some(row) = rows.next()? {
|
||||||
Ok(Some(row.get(0)?))
|
Ok(Some(row.get(0)?))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Public helper: write a single setting in its own transaction.
|
/// Public helper: write a single setting in its own transaction.
|
||||||
pub fn set_setting_pub(key: &str, value: &str) -> DbResult<()> {
|
pub fn set_setting_pub(key: &str, value: &str) -> DbResult<()> {
|
||||||
let mut conn = init_db()?;
|
with_conn(|conn| {
|
||||||
let tx = conn.transaction()?;
|
let tx = conn.transaction()?;
|
||||||
set_setting_tx(&tx, key, value)?;
|
set_setting_tx(&tx, key, value)?;
|
||||||
tx.commit()?;
|
tx.commit()?;
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
bump_generation();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Public helper: delete a single setting (no-op if the key does not exist).
|
/// Public helper: delete a single setting (no-op if the key does not exist).
|
||||||
pub fn delete_setting_pub(key: &str) -> DbResult<()> {
|
pub fn delete_setting_pub(key: &str) -> DbResult<()> {
|
||||||
let conn = init_db()?;
|
with_conn(|conn| {
|
||||||
conn.execute("DELETE FROM settings WHERE key = ?1", params![key])?;
|
conn.execute("DELETE FROM settings WHERE key = ?1", params![key])?;
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
bump_generation();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,55 +475,59 @@ pub struct ArchivedSessionSnapshot {
|
|||||||
/// Insert or replace one archived-session snapshot (keyed by session id, which
|
/// Insert or replace one archived-session snapshot (keyed by session id, which
|
||||||
/// is a UUID and never reused).
|
/// is a UUID and never reused).
|
||||||
pub fn upsert_archived_session(row: &ArchivedSessionSnapshot) -> DbResult<()> {
|
pub fn upsert_archived_session(row: &ArchivedSessionSnapshot) -> DbResult<()> {
|
||||||
let conn = init_db()?;
|
with_conn(|conn| {
|
||||||
let day_stats = serde_json::to_string(&row.day_stats)?;
|
let day_stats = serde_json::to_string(&row.day_stats)?;
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT OR REPLACE INTO archived_sessions
|
"INSERT OR REPLACE INTO archived_sessions
|
||||||
(session_id, workspace_id, title, archived_at_ms, updated_at_ms, created_at_ms, total_tokens, total_cost_usd, day_stats)
|
(session_id, workspace_id, title, archived_at_ms, updated_at_ms, created_at_ms, total_tokens, total_cost_usd, day_stats)
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
|
||||||
params![
|
params![
|
||||||
row.session_id,
|
row.session_id,
|
||||||
row.workspace_id,
|
row.workspace_id,
|
||||||
row.title,
|
row.title,
|
||||||
row.archived_at_ms as i64,
|
row.archived_at_ms as i64,
|
||||||
row.updated_at_ms.map(|v| v as i64),
|
row.updated_at_ms.map(|v| v as i64),
|
||||||
row.created_at_ms.map(|v| v as i64),
|
row.created_at_ms.map(|v| v as i64),
|
||||||
row.total_tokens as i64,
|
row.total_tokens as i64,
|
||||||
row.total_cost_usd,
|
row.total_cost_usd,
|
||||||
day_stats,
|
day_stats,
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
bump_generation();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Every stored archived-session snapshot. Unreadable `day_stats` JSON degrades
|
/// Every stored archived-session snapshot. Unreadable `day_stats` JSON degrades
|
||||||
/// to an empty breakdown instead of failing the whole list.
|
/// to an empty breakdown instead of failing the whole list.
|
||||||
pub fn list_archived_sessions() -> DbResult<Vec<ArchivedSessionSnapshot>> {
|
pub fn list_archived_sessions() -> DbResult<Vec<ArchivedSessionSnapshot>> {
|
||||||
let conn = init_db()?;
|
with_conn(|conn| {
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
"SELECT session_id, workspace_id, title, archived_at_ms, updated_at_ms, created_at_ms,
|
"SELECT session_id, workspace_id, title, archived_at_ms, updated_at_ms, created_at_ms,
|
||||||
total_tokens, total_cost_usd, day_stats
|
total_tokens, total_cost_usd, day_stats
|
||||||
FROM archived_sessions",
|
FROM archived_sessions",
|
||||||
)?;
|
)?;
|
||||||
let rows = stmt.query_map([], |row| {
|
let rows = stmt.query_map([], |row| {
|
||||||
let day_stats_json: String = row.get(8)?;
|
let day_stats_json: String = row.get(8)?;
|
||||||
Ok(ArchivedSessionSnapshot {
|
Ok(ArchivedSessionSnapshot {
|
||||||
session_id: row.get(0)?,
|
session_id: row.get(0)?,
|
||||||
workspace_id: row.get(1)?,
|
workspace_id: row.get(1)?,
|
||||||
title: row.get(2)?,
|
title: row.get(2)?,
|
||||||
archived_at_ms: row.get::<_, i64>(3)? as u64,
|
archived_at_ms: row.get::<_, i64>(3)? as u64,
|
||||||
updated_at_ms: row.get::<_, Option<i64>>(4)?.map(|v| v as u64),
|
updated_at_ms: row.get::<_, Option<i64>>(4)?.map(|v| v as u64),
|
||||||
created_at_ms: row.get::<_, Option<i64>>(5)?.map(|v| v as u64),
|
created_at_ms: row.get::<_, Option<i64>>(5)?.map(|v| v as u64),
|
||||||
total_tokens: row.get::<_, i64>(6)? as u64,
|
total_tokens: row.get::<_, i64>(6)? as u64,
|
||||||
total_cost_usd: row.get(7)?,
|
total_cost_usd: row.get(7)?,
|
||||||
day_stats: serde_json::from_str(&day_stats_json).unwrap_or_default(),
|
day_stats: serde_json::from_str(&day_stats_json).unwrap_or_default(),
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for row in rows {
|
for row in rows {
|
||||||
out.push(row?);
|
out.push(row?);
|
||||||
}
|
}
|
||||||
Ok(out)
|
Ok(out)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn provider_type_for_str(s: &str) -> ProviderType {
|
fn provider_type_for_str(s: &str) -> ProviderType {
|
||||||
|
|||||||
@@ -648,6 +648,114 @@ api_key = ""
|
|||||||
assert!(caps.iter().any(|v| v.as_str() == Some("thinking")));
|
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]
|
#[test]
|
||||||
fn kimi_code_export_adds_opencode_go_session_header() {
|
fn kimi_code_export_adds_opencode_go_session_header() {
|
||||||
let make_provider = |base_url: &str, raw_other: Value| Provider {
|
let make_provider = |base_url: &str, raw_other: Value| Provider {
|
||||||
|
|||||||
@@ -138,6 +138,14 @@ fn build_snapshot(raw: &Value) -> Result<Value, String> {
|
|||||||
{
|
{
|
||||||
entry.insert("context".into(), num(ctx));
|
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"] {
|
for flag in ["reasoning", "tool_call", "structured_output"] {
|
||||||
if m.get(flag).and_then(|x| x.as_bool()) == Some(true) {
|
if m.get(flag).and_then(|x| x.as_bool()) == Some(true) {
|
||||||
entry.insert(flag.into(), Value::Bool(true));
|
entry.insert(flag.into(), Value::Bool(true));
|
||||||
@@ -286,7 +294,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
"kimi-image": {
|
"kimi-image": {
|
||||||
"name": "Kimi Image",
|
"name": "Kimi Image",
|
||||||
"limit": { "context": 0 },
|
"limit": { "context": 0, "output": 0 },
|
||||||
"modalities": { "input": ["text", "image", "video"] }
|
"modalities": { "input": ["text", "image", "video"] }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -309,6 +317,7 @@ mod tests {
|
|||||||
let k3 = &obj["moonshotai/kimi-k3"];
|
let k3 = &obj["moonshotai/kimi-k3"];
|
||||||
assert_eq!(k3["name"], "Kimi K3");
|
assert_eq!(k3["name"], "Kimi K3");
|
||||||
assert_eq!(k3["context"], 262144);
|
assert_eq!(k3["context"], 262144);
|
||||||
|
assert_eq!(k3["output"], 16384);
|
||||||
assert_eq!(k3["reasoning"], true);
|
assert_eq!(k3["reasoning"], true);
|
||||||
assert_eq!(k3["tool_call"], true);
|
assert_eq!(k3["tool_call"], true);
|
||||||
assert_eq!(k3["structured_output"], 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_read"], 0.3);
|
||||||
assert_eq!(k3["cost"]["cache_write"], 0.0);
|
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"];
|
let img = &obj["moonshotai/kimi-image"];
|
||||||
assert!(img.get("context").is_none());
|
assert!(img.get("context").is_none());
|
||||||
|
assert!(img.get("output").is_none());
|
||||||
assert_eq!(img["video"], true);
|
assert_eq!(img["video"], true);
|
||||||
assert!(img.get("cost").is_none());
|
assert!(img.get("cost").is_none());
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"productName": "Kimi Switch",
|
"productName": "Kimi Switch",
|
||||||
"version": "0.7.23",
|
"version": "0.8.2",
|
||||||
"identifier": "com.kimiswitch.app",
|
"identifier": "com.kimiswitch.app",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|||||||
@@ -236,19 +236,37 @@ export function AgentSettingsPanel({ rawOther, onChange, models, defaultModel }:
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card title={t("watchSettings")}>
|
<Card title={t("watchSettings")}>
|
||||||
{/* Top-level `[watch] enabled` (kimi-code 2.0.1+; off by default since
|
{/* Top-level `[watch] enabled` (kimi-code 2.0.1+; 2.0.2 flipped the
|
||||||
2.0.2). KIMI_CODE_WATCH outranks this config at runtime — the env
|
default off, #4015 flipped it back on). KIMI_CODE_WATCH outranks
|
||||||
var is probed by get_experimental_env_status as a non-flag entry
|
this config at runtime — the env var is probed by
|
||||||
but deliberately locks nothing here, the toggle just documents the
|
get_experimental_env_status as a non-flag entry but deliberately
|
||||||
config value that applies when the env var is unset. */}
|
locks nothing here, the toggle just documents the config value
|
||||||
|
that applies when the env var is unset. */}
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label={t("watchEnabled")}
|
label={t("watchEnabled")}
|
||||||
checked={settings.watch?.enabled ?? false}
|
checked={settings.watch?.enabled ?? true}
|
||||||
onChange={(checked) => updateWatch({ enabled: checked })}
|
onChange={(checked) => updateWatch({ enabled: checked })}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-content-muted">{t("watchEnabledDesc")}</p>
|
<p className="text-xs text-content-muted">{t("watchEnabledDesc")}</p>
|
||||||
</Card>
|
</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")}>
|
<Card title={t("permissionRules")}>
|
||||||
{/* kimi-code `[permission] dangerous_command_guard`. The env
|
{/* kimi-code `[permission] dangerous_command_guard`. The env
|
||||||
var KIMI_CODE_DANGEROUS_COMMAND_GUARD (literal "true"/"false")
|
var KIMI_CODE_DANGEROUS_COMMAND_GUARD (literal "true"/"false")
|
||||||
|
|||||||
+113
-17
@@ -1,11 +1,17 @@
|
|||||||
import { useEffect, useId, useMemo, useState } from "react";
|
import { useEffect, useId, useMemo, useReducer, useRef, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { useTranslation } from "../i18n";
|
import { useTranslation } from "../i18n";
|
||||||
import type { TranslationKey } from "../i18n/zh";
|
import type { TranslationKey } from "../i18n/zh";
|
||||||
import { findPresetForProvider } from "../config/providerPresets";
|
import { findPresetForProvider } from "../config/providerPresets";
|
||||||
import { getDefaultMaxContextSize } from "../lib/model-defaults";
|
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 { getIconMetadata } from "../icons/extracted/metadata";
|
||||||
import { AgentSettingsPanel } from "./AgentSettingsPanel";
|
import { AgentSettingsPanel } from "./AgentSettingsPanel";
|
||||||
import { KimiOAuthDialog } from "./KimiOAuthDialog";
|
import { KimiOAuthDialog } from "./KimiOAuthDialog";
|
||||||
@@ -33,6 +39,9 @@ const CAPABILITY_LABELS: Record<
|
|||||||
tool_use: "capToolUse",
|
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[] = [
|
const PROVIDER_TYPES: ProviderType[] = [
|
||||||
"openai",
|
"openai",
|
||||||
"openai_responses",
|
"openai_responses",
|
||||||
@@ -567,6 +576,38 @@ function ModelMapping({
|
|||||||
const [discoverError, setDiscoverError] = useState<string | null>(null);
|
const [discoverError, setDiscoverError] = useState<string | null>(null);
|
||||||
const [fetchThinking, setFetchThinking] = useState(true);
|
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 () => {
|
const handleDiscover = async () => {
|
||||||
setDiscovering(true);
|
setDiscovering(true);
|
||||||
setDiscoverError(null);
|
setDiscoverError(null);
|
||||||
@@ -636,6 +677,17 @@ function ModelMapping({
|
|||||||
: fetchThinking
|
: fetchThinking
|
||||||
? ["thinking"]
|
? ["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);
|
onBulkAdd(toAdd);
|
||||||
@@ -648,7 +700,10 @@ function ModelMapping({
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-medium text-content-primary">{t("modelMapping")}</h3>
|
<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>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
@@ -756,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("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">{t("actualModel")}</th>
|
||||||
<th className="text-left px-4 py-3 font-medium w-28">{t("contextSize")}</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" && (
|
{agent === "kimi_code" && (
|
||||||
<th className="text-left px-4 py-3 font-medium">{t("capabilities")}</th>
|
<th className="text-left px-4 py-3 font-medium">{t("capabilities")}</th>
|
||||||
)}
|
)}
|
||||||
@@ -797,19 +854,14 @@ function ModelMapping({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 text-center">
|
{agent === "kimi_code" && (
|
||||||
<input
|
<td className="px-4 py-2">
|
||||||
type="checkbox"
|
<MaxOutputCell
|
||||||
checked={m.supports_1m || false}
|
model={m}
|
||||||
onChange={(e) =>
|
onChange={(next) => onModelChange(next)}
|
||||||
onModelChange({
|
/>
|
||||||
...m,
|
</td>
|
||||||
supports_1m: e.target.checked,
|
)}
|
||||||
})
|
|
||||||
}
|
|
||||||
className="w-4 h-4 rounded border-border bg-input text-blue-600 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
{agent === "kimi_code" && (
|
{agent === "kimi_code" && (
|
||||||
<td className="px-4 py-2">
|
<td className="px-4 py-2">
|
||||||
<CapabilitiesCell
|
<CapabilitiesCell
|
||||||
@@ -985,3 +1037,47 @@ function CapabilitiesCell({
|
|||||||
</div>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -45,7 +45,7 @@ interface SubagentSettingsPageProps {
|
|||||||
|
|
||||||
// Effort tiers the CLI accepts; the value is forwarded verbatim upstream, and
|
// Effort tiers the CLI accepts; the value is forwarded verbatim upstream, and
|
||||||
// per-model support comes from `[models."<alias>"] support_efforts`.
|
// per-model support comes from `[models."<alias>"] support_efforts`.
|
||||||
const EFFORTS = ["low", "medium", "high", "max", "xhigh"] as const;
|
const EFFORTS = ["low", "medium", "high", "xhigh", "max"] as const;
|
||||||
const EFFORT_LABELS: Record<(typeof EFFORTS)[number], TranslationKey> = {
|
const EFFORT_LABELS: Record<(typeof EFFORTS)[number], TranslationKey> = {
|
||||||
low: "thinkingLow",
|
low: "thinkingLow",
|
||||||
medium: "thinkingMedium",
|
medium: "thinkingMedium",
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ export function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
{loadStats && !loading && (
|
{loadStats && !loading && (
|
||||||
<span className="hidden text-[10px] text-content-muted sm:inline">
|
<span className="hidden text-[10px] text-content-muted sm:inline">
|
||||||
{t("loadStats", { ms: loadStats.ms, kb: loadStats.kb })}
|
{t("loadStats", { ms: loadStats.ms })}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+93
-21
@@ -1,32 +1,68 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import type { SummaryResult } from "../types/dashboard";
|
import type { SummaryResult } from "../types/dashboard";
|
||||||
|
|
||||||
export type DashboardRange = "today" | "yesterday" | "7d" | "30d" | "all";
|
export type DashboardRange = "today" | "yesterday" | "7d" | "30d" | "all";
|
||||||
|
|
||||||
const RANGE_STORAGE_KEY = "kimi-switch-dashboard-range";
|
const RANGE_STORAGE_KEY = "kimi-switch-dashboard-range";
|
||||||
|
|
||||||
export function useDashboard() {
|
/**
|
||||||
const [range, setRange] = useState<DashboardRange>(() => {
|
* Emitted by the backend when the background verification walk found records the
|
||||||
try {
|
* fast (disk-cache) path did not have. The payload is empty — the hook only
|
||||||
const stored = localStorage.getItem(RANGE_STORAGE_KEY) as DashboardRange | null;
|
* needs to know that a re-fetch is worth making.
|
||||||
if (stored === "today" || stored === "yesterday" || stored === "7d" || stored === "30d" || stored === "all") {
|
*/
|
||||||
return stored;
|
const RECORDS_UPDATED_EVENT = "dashboard://records-updated";
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
return "30d";
|
|
||||||
});
|
|
||||||
|
|
||||||
const [data, setData] = useState<SummaryResult | null>(null);
|
const RANGES: readonly DashboardRange[] = ["today", "yesterday", "7d", "30d", "all"];
|
||||||
|
|
||||||
|
function readStoredRange(): DashboardRange {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(RANGE_STORAGE_KEY) as DashboardRange | null;
|
||||||
|
if (stored && (RANGES as readonly string[]).includes(stored)) {
|
||||||
|
return stored;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return "30d";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last successful payload per range, kept across mounts so a range switch (and
|
||||||
|
* reopening the page) paints instantly from memory and revalidates in the
|
||||||
|
* background instead of showing a spinner over data that is already known.
|
||||||
|
* Bounded by the five ranges the dashboard offers.
|
||||||
|
*/
|
||||||
|
const summaryCache = new Map<DashboardRange, SummaryResult>();
|
||||||
|
|
||||||
|
export function useDashboard() {
|
||||||
|
const [range, setRange] = useState<DashboardRange>(readStoredRange);
|
||||||
|
|
||||||
|
const [data, setData] = useState<SummaryResult | null>(
|
||||||
|
() => summaryCache.get(readStoredRange()) ?? null,
|
||||||
|
);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
/** Round-trip timing for the last get_summary call (user-perceived lag). */
|
/** Round-trip timing for the last get_summary call (user-perceived lag). */
|
||||||
const [loadStats, setLoadStats] = useState<{ ms: number; kb: number } | null>(null);
|
const [loadStats, setLoadStats] = useState<{ ms: number } | null>(null);
|
||||||
|
// Generation counter: stale responses (superseded range / unmounted) are dropped.
|
||||||
|
const genRef = useRef(0);
|
||||||
|
// The event handler must call the *current* refresh, not the one captured when
|
||||||
|
// the listener was installed, so it reads the callback through a ref.
|
||||||
|
const refreshRef = useRef<(force?: boolean) => Promise<void>>(async () => {});
|
||||||
|
|
||||||
const refresh = useCallback(async (force = false) => {
|
const refresh = useCallback(async (force = false) => {
|
||||||
setLoading(true);
|
const gen = ++genRef.current;
|
||||||
|
// Stale-while-revalidate: hand the cached payload for this range to the
|
||||||
|
// view before the request goes out, so a range switch is instant and the
|
||||||
|
// response only refreshes numbers that are already on screen. `loading` is
|
||||||
|
// raised only when there is nothing to show (first load of a range) or when
|
||||||
|
// the user asked for a refresh — the button keeps its busy state, the silent
|
||||||
|
// revalidation behind a cached range does not.
|
||||||
|
const cached = summaryCache.get(range);
|
||||||
|
if (cached) setData(cached);
|
||||||
|
setLoading(force || !cached);
|
||||||
setError(null);
|
setError(null);
|
||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
try {
|
try {
|
||||||
@@ -34,23 +70,59 @@ export function useDashboard() {
|
|||||||
range,
|
range,
|
||||||
refresh: force,
|
refresh: force,
|
||||||
});
|
});
|
||||||
|
if (genRef.current !== gen) return;
|
||||||
|
summaryCache.set(range, result);
|
||||||
setData(result);
|
setData(result);
|
||||||
setLoadStats({
|
setLoadStats({ ms: Math.round(performance.now() - start) });
|
||||||
ms: Math.round(performance.now() - start),
|
|
||||||
kb: Math.round(JSON.stringify(result).length / 1024),
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (genRef.current !== gen) return;
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
setError(msg);
|
setError(msg);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
if (genRef.current === gen) setLoading(false);
|
||||||
}
|
}
|
||||||
}, [range]);
|
}, [range]);
|
||||||
|
|
||||||
|
refreshRef.current = refresh;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refresh();
|
refresh();
|
||||||
}, [refresh]);
|
}, [refresh]);
|
||||||
|
|
||||||
|
// The first load of a session answers from the on-disk snapshot, which can be
|
||||||
|
// behind a session written since the app last ran. The backend verifies in the
|
||||||
|
// background and signals here when the numbers moved; re-fetching silently
|
||||||
|
// (never raising `loading`) keeps the already-drawn dashboard on screen and
|
||||||
|
// just corrects it. A silent call still takes a fresh generation, so it cannot
|
||||||
|
// be interleaved with a range switch in flight.
|
||||||
|
useEffect(() => {
|
||||||
|
let unlisten: (() => void) | undefined;
|
||||||
|
let cancelled = false;
|
||||||
|
listen(RECORDS_UPDATED_EVENT, () => {
|
||||||
|
refreshRef.current();
|
||||||
|
})
|
||||||
|
.then((fn) => {
|
||||||
|
// The effect may have been torn down before `listen` resolved.
|
||||||
|
if (cancelled) fn();
|
||||||
|
else unlisten = fn;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// No event bridge (e.g. a browser build): the dashboard still works,
|
||||||
|
// it just will not be corrected mid-session.
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
unlisten?.();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Ignore late responses after unmount.
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
genRef.current += 1;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const changeRange = useCallback((next: DashboardRange) => {
|
const changeRange = useCallback((next: DashboardRange) => {
|
||||||
setRange(next);
|
setRange(next);
|
||||||
try {
|
try {
|
||||||
|
|||||||
+13
-4
@@ -111,7 +111,8 @@ export const enTranslations: Record<TranslationKey, string> = {
|
|||||||
baseUrlHint: "Enter a {type} API-compatible endpoint without a trailing slash.",
|
baseUrlHint: "Enter a {type} API-compatible endpoint without a trailing slash.",
|
||||||
envPairs: "Env pairs",
|
envPairs: "Env pairs",
|
||||||
addEnv: "+ Add",
|
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",
|
oneClickSetup: "One-click setup",
|
||||||
fetchModels: "Fetch models",
|
fetchModels: "Fetch models",
|
||||||
fetchingModels: "Fetching...",
|
fetchingModels: "Fetching...",
|
||||||
@@ -121,7 +122,8 @@ export const enTranslations: Record<TranslationKey, string> = {
|
|||||||
displayName: "Display name",
|
displayName: "Display name",
|
||||||
actualModel: "Actual model",
|
actualModel: "Actual model",
|
||||||
contextSize: "Context size",
|
contextSize: "Context size",
|
||||||
supports1M: "Supports 1M",
|
maxOutputSize: "Max output",
|
||||||
|
maxOutputRef: "Ref {value}",
|
||||||
default: "Default",
|
default: "Default",
|
||||||
operation: "Operation",
|
operation: "Operation",
|
||||||
isDefault: "Default",
|
isDefault: "Default",
|
||||||
@@ -186,7 +188,14 @@ export const enTranslations: Record<TranslationKey, string> = {
|
|||||||
watchSettings: "File Watching",
|
watchSettings: "File Watching",
|
||||||
watchEnabled: "Enable file watching",
|
watchEnabled: "Enable file watching",
|
||||||
watchEnabledDesc:
|
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",
|
permissionRules: "Permission Rules",
|
||||||
permissionDecision: "Decision",
|
permissionDecision: "Decision",
|
||||||
permissionPattern: "Pattern",
|
permissionPattern: "Pattern",
|
||||||
@@ -248,7 +257,7 @@ export const enTranslations: Record<TranslationKey, string> = {
|
|||||||
scanning: "Scanning…",
|
scanning: "Scanning…",
|
||||||
refresh: "Refresh",
|
refresh: "Refresh",
|
||||||
refreshing: "Refreshing…",
|
refreshing: "Refreshing…",
|
||||||
loadStats: "Loaded in {ms} ms · {kb} KB payload",
|
loadStats: "Loaded in {ms} ms",
|
||||||
refreshed: "Refreshed",
|
refreshed: "Refreshed",
|
||||||
cancel: "Cancel",
|
cancel: "Cancel",
|
||||||
confirmDeleteTitle: "Delete session forever?",
|
confirmDeleteTitle: "Delete session forever?",
|
||||||
|
|||||||
+16
-7
@@ -108,7 +108,8 @@ export const zhTranslations = {
|
|||||||
baseUrlHint: "填写兼容 {type} API 的服务端点地址,不要以斜杠结尾",
|
baseUrlHint: "填写兼容 {type} API 的服务端点地址,不要以斜杠结尾",
|
||||||
envPairs: "Env 键值对",
|
envPairs: "Env 键值对",
|
||||||
addEnv: "+ 添加",
|
addEnv: "+ 添加",
|
||||||
modelMappingDesc: "显示名称只影响 /model 菜单;1M 只是给 Kimi Code 的上下文能力声明。",
|
modelMappingDesc: "显示名称只影响 /model 菜单。",
|
||||||
|
maxOutputSizeDesc: "「最大输出」写入 max_output_size,留空则不发送输出上限。",
|
||||||
oneClickSetup: "一键设置",
|
oneClickSetup: "一键设置",
|
||||||
fetchModels: "获取模型列表",
|
fetchModels: "获取模型列表",
|
||||||
fetchingModels: "获取中...",
|
fetchingModels: "获取中...",
|
||||||
@@ -118,7 +119,8 @@ export const zhTranslations = {
|
|||||||
displayName: "显示名称",
|
displayName: "显示名称",
|
||||||
actualModel: "实际请求模型",
|
actualModel: "实际请求模型",
|
||||||
contextSize: "上下文长度",
|
contextSize: "上下文长度",
|
||||||
supports1M: "声明支持 1M",
|
maxOutputSize: "最大输出",
|
||||||
|
maxOutputRef: "参考 {value}",
|
||||||
default: "默认",
|
default: "默认",
|
||||||
operation: "操作",
|
operation: "操作",
|
||||||
isDefault: "已默认",
|
isDefault: "已默认",
|
||||||
@@ -158,9 +160,9 @@ export const zhTranslations = {
|
|||||||
enableThinking: "启用思考",
|
enableThinking: "启用思考",
|
||||||
thinkingLevel: "思考等级",
|
thinkingLevel: "思考等级",
|
||||||
thinkingKeep: "保留思考内容",
|
thinkingKeep: "保留思考内容",
|
||||||
thinkingLow: "低",
|
thinkingLow: "Low",
|
||||||
thinkingMedium: "中",
|
thinkingMedium: "Medium",
|
||||||
thinkingHigh: "高",
|
thinkingHigh: "High",
|
||||||
thinkingMax: "Max",
|
thinkingMax: "Max",
|
||||||
thinkingXHigh: "XHigh",
|
thinkingXHigh: "XHigh",
|
||||||
thinkingEffortUnsupported: "当前默认模型不支持思考。",
|
thinkingEffortUnsupported: "当前默认模型不支持思考。",
|
||||||
@@ -179,7 +181,14 @@ export const zhTranslations = {
|
|||||||
watchSettings: "文件监听",
|
watchSettings: "文件监听",
|
||||||
watchEnabled: "启用文件监听",
|
watchEnabled: "启用文件监听",
|
||||||
watchEnabledDesc:
|
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: "权限规则",
|
permissionRules: "权限规则",
|
||||||
permissionDecision: "处置",
|
permissionDecision: "处置",
|
||||||
permissionPattern: "模式",
|
permissionPattern: "模式",
|
||||||
@@ -240,7 +249,7 @@ export const zhTranslations = {
|
|||||||
scanning: "扫描中…",
|
scanning: "扫描中…",
|
||||||
refresh: "刷新",
|
refresh: "刷新",
|
||||||
refreshing: "刷新中…",
|
refreshing: "刷新中…",
|
||||||
loadStats: "加载 {ms} ms · 数据 {kb} KB",
|
loadStats: "加载 {ms} ms",
|
||||||
refreshed: "已刷新",
|
refreshed: "已刷新",
|
||||||
cancel: "取消",
|
cancel: "取消",
|
||||||
confirmDeleteTitle: "永久删除会话?",
|
confirmDeleteTitle: "永久删除会话?",
|
||||||
|
|||||||
@@ -201,8 +201,8 @@ describe("loop_control.compaction_max_attempts", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// [watch] enabled (kimi-code 2.0.1+) — top-level section, off by default since
|
// [watch] enabled (kimi-code 2.0.1+) — top-level section; 2.0.2 flipped the
|
||||||
// 2.0.2; mirrors the [background] handling
|
// default off, #4015 flipped it back on. Mirrors the [background] handling.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function watchOf(raw: unknown): Record<string, unknown> {
|
function watchOf(raw: unknown): Record<string, unknown> {
|
||||||
@@ -213,9 +213,9 @@ function watchOf(raw: unknown): Record<string, unknown> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("[watch] enabled", () => {
|
describe("[watch] enabled", () => {
|
||||||
it("reads as false when the section or key is absent (2.0.2 default)", () => {
|
it("reads as true when the section or key is absent (#4015 default)", () => {
|
||||||
expect(getAgentSettings({}).watch?.enabled).toBe(false);
|
expect(getAgentSettings({}).watch?.enabled).toBe(true);
|
||||||
expect(getAgentSettings({ watch: {} }).watch?.enabled).toBe(false);
|
expect(getAgentSettings({ watch: {} }).watch?.enabled).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("reads an explicit value", () => {
|
it("reads an explicit value", () => {
|
||||||
@@ -248,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", () => {
|
describe("permission.dangerous_command_guard", () => {
|
||||||
it("reads as undefined when the key is absent (default on)", () => {
|
it("reads as undefined when the key is absent (default on)", () => {
|
||||||
|
|||||||
@@ -13,10 +13,11 @@ const DEFAULT_SETTINGS: AgentSettings = {
|
|||||||
background: {
|
background: {
|
||||||
keep_alive_on_exit: false,
|
keep_alive_on_exit: false,
|
||||||
},
|
},
|
||||||
// `[watch] enabled` — kimi-code 2.0.1 added the key, 2.0.2 flipped the
|
// `[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".
|
// default to off, then #4015 flipped it back on, so an absent key reads
|
||||||
|
// as "on".
|
||||||
watch: {
|
watch: {
|
||||||
enabled: false,
|
enabled: true,
|
||||||
},
|
},
|
||||||
permission: { rules: [] },
|
permission: { rules: [] },
|
||||||
hooks: [],
|
hooks: [],
|
||||||
@@ -37,6 +38,7 @@ function getSection<T>(rawOther: unknown, key: string): T | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getAgentSettings(rawOther: unknown): AgentSettings {
|
export function getAgentSettings(rawOther: unknown): AgentSettings {
|
||||||
|
const root = asRecord(rawOther);
|
||||||
const sectionLoop = getSection<AgentSettings["loop_control"]>(
|
const sectionLoop = getSection<AgentSettings["loop_control"]>(
|
||||||
rawOther,
|
rawOther,
|
||||||
"loop_control"
|
"loop_control"
|
||||||
@@ -92,6 +94,16 @@ export function getAgentSettings(rawOther: unknown): AgentSettings {
|
|||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
hooks: getSection<AgentSettings["hooks"]>(rawOther, "hooks") ?? [],
|
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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,5 +178,17 @@ export function setAgentSettings(
|
|||||||
delete root.hooks;
|
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;
|
return root;
|
||||||
}
|
}
|
||||||
@@ -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({});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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) };
|
||||||
|
}
|
||||||
+2771
-1174
File diff suppressed because it is too large.
Load diff
+10621
-1029
File diff suppressed because it is too large.
Load diff
@@ -25,6 +25,8 @@ export interface ModelCost {
|
|||||||
export interface ModelRef {
|
export interface ModelRef {
|
||||||
name?: string;
|
name?: string;
|
||||||
context?: number;
|
context?: number;
|
||||||
|
/** Upper bound on generated tokens (`limit.output` on models.dev). */
|
||||||
|
output?: number;
|
||||||
reasoning?: boolean;
|
reasoning?: boolean;
|
||||||
tool_call?: boolean;
|
tool_call?: boolean;
|
||||||
structured_output?: boolean;
|
structured_output?: boolean;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Thinking-effort capability resolution.
|
* Thinking-effort capability resolution.
|
||||||
*
|
*
|
||||||
* kimi-code's `[thinking] effort` is a free-form string (low/medium/high/
|
* kimi-code's `[thinking] effort` is a free-form string (low/medium/high/
|
||||||
* max/xhigh in practice) that the CLI forwards verbatim to OpenAI-compatible
|
* xhigh/max in practice) that the CLI forwards verbatim to OpenAI-compatible
|
||||||
* upstreams as `reasoning_effort`. Which tiers a given model accepts is
|
* upstreams as `reasoning_effort`. Which tiers a given model accepts is
|
||||||
* declared per model in config.toml (`[models."<alias>"] support_efforts`);
|
* declared per model in config.toml (`[models."<alias>"] support_efforts`);
|
||||||
* models.dev only carries a boolean `reasoning` flag with no tier list. The
|
* models.dev only carries a boolean `reasoning` flag with no tier list. The
|
||||||
@@ -16,8 +16,8 @@ export const THINKING_EFFORTS = [
|
|||||||
"low",
|
"low",
|
||||||
"medium",
|
"medium",
|
||||||
"high",
|
"high",
|
||||||
"max",
|
|
||||||
"xhigh",
|
"xhigh",
|
||||||
|
"max",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type ThinkingEffort = (typeof THINKING_EFFORTS)[number];
|
export type ThinkingEffort = (typeof THINKING_EFFORTS)[number];
|
||||||
|
|||||||
@@ -172,6 +172,13 @@ export interface AgentSettings {
|
|||||||
background?: BackgroundConfig;
|
background?: BackgroundConfig;
|
||||||
/** Top-level `[watch]` section (kimi-code 2.0.1+). */
|
/** Top-level `[watch]` section (kimi-code 2.0.1+). */
|
||||||
watch?: WatchConfig;
|
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?: {
|
permission?: {
|
||||||
rules?: PermissionRule[];
|
rules?: PermissionRule[];
|
||||||
/** kimi-code `[permission]` key. Default true when the key is
|
/** kimi-code `[permission]` key. Default true when the key is
|
||||||
|
|||||||
+2
-2
@@ -50,8 +50,8 @@
|
|||||||
"url": "https://billowliu2.github.io/KimiSwitch/",
|
"url": "https://billowliu2.github.io/KimiSwitch/",
|
||||||
"applicationCategory": "DeveloperApplication",
|
"applicationCategory": "DeveloperApplication",
|
||||||
"operatingSystem": "Windows 10, Windows 11, macOS, Linux",
|
"operatingSystem": "Windows 10, Windows 11, macOS, Linux",
|
||||||
"softwareVersion": "0.7.12",
|
"softwareVersion": "0.8.2",
|
||||||
"dateModified": "2026-09-03",
|
"dateModified": "2026-10-09",
|
||||||
"description": "桌面端 LLM 供应商配置管理器:22 个预设选好即写入 config.toml,用量与账单直读各厂商官方接口。Tauri + Rust,开源 MIT。",
|
"description": "桌面端 LLM 供应商配置管理器:22 个预设选好即写入 config.toml,用量与账单直读各厂商官方接口。Tauri + Rust,开源 MIT。",
|
||||||
"downloadUrl": "https://github.com/billowliu2/KimiSwitch/releases",
|
"downloadUrl": "https://github.com/billowliu2/KimiSwitch/releases",
|
||||||
"softwareHelp": "https://github.com/billowliu2/KimiSwitch",
|
"softwareHelp": "https://github.com/billowliu2/KimiSwitch",
|
||||||
|
|||||||
+84
-12
@@ -16,10 +16,10 @@ const zh = {
|
|||||||
performance: "性能",
|
performance: "性能",
|
||||||
changelog: "更新日志",
|
changelog: "更新日志",
|
||||||
download: "下载",
|
download: "下载",
|
||||||
downloadBtn: "下载 v0.7.12",
|
downloadBtn: "下载 v0.8.2",
|
||||||
},
|
},
|
||||||
hero: {
|
hero: {
|
||||||
badge: "v0.7.12 · 开源 MIT · Windows / macOS / Linux",
|
badge: "v0.8.2 · 开源 MIT · Windows / macOS / Linux",
|
||||||
titleBefore: "统一管理你的",
|
titleBefore: "统一管理你的",
|
||||||
titleAccent: "AI 供应商",
|
titleAccent: "AI 供应商",
|
||||||
titleAfter: "",
|
titleAfter: "",
|
||||||
@@ -29,7 +29,7 @@ const zh = {
|
|||||||
source: "查看源码",
|
source: "查看源码",
|
||||||
stats: [
|
stats: [
|
||||||
{ value: "22", label: "预设供应商" },
|
{ value: "22", label: "预设供应商" },
|
||||||
{ value: "7495", label: "模型价格库" },
|
{ value: "8467", label: "模型价格库" },
|
||||||
{ value: "MIT", label: "开源协议" },
|
{ value: "MIT", label: "开源协议" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -76,7 +76,7 @@ const zh = {
|
|||||||
{
|
{
|
||||||
id: "model-discovery",
|
id: "model-discovery",
|
||||||
title: "模型自动发现",
|
title: "模型自动发现",
|
||||||
desc: "可用模型直接问供应商 API;显示名 / 上下文 / 能力取自 models.dev 快照(当前 7495 个模型、212 家供应商)。",
|
desc: "可用模型直接问供应商 API;显示名 / 上下文 / 能力取自 models.dev 快照(当前 8,467 个模型、225 家供应商)。",
|
||||||
img: "screenshots/kimi-cli-select-model.png",
|
img: "screenshots/kimi-cli-select-model.png",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -89,7 +89,7 @@ const zh = {
|
|||||||
},
|
},
|
||||||
showcase: {
|
showcase: {
|
||||||
title: "界面演示",
|
title: "界面演示",
|
||||||
subtitle: "以下截图取自 v0.7.12 实机运行。",
|
subtitle: "以下截图取自 v0.8.2 实机运行。",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
src: "screenshots/usage-config-page.png",
|
src: "screenshots/usage-config-page.png",
|
||||||
@@ -140,6 +140,42 @@ const zh = {
|
|||||||
syncedNote: "数据已同步 GitHub Releases",
|
syncedNote: "数据已同步 GitHub Releases",
|
||||||
fallbackNote: "内置版本记录",
|
fallbackNote: "内置版本记录",
|
||||||
entries: [
|
entries: [
|
||||||
|
{
|
||||||
|
version: "v0.8.2",
|
||||||
|
date: "2026-10-09",
|
||||||
|
items: [
|
||||||
|
"仪表盘重启后首次打开从约 25 秒降到 1 秒内:用量解析快照持久化到本地,启动直读并在后台校验,有变化自动无感更新(6.4 万条记录实测)",
|
||||||
|
"切换时间范围几乎瞬时:热力图 / 模型总览 / 各范围汇总按记录集缓存;去除原「全时段」聚合的整表无用遍历",
|
||||||
|
"界面不再闪加载态:切范围与重新进入页面先呈现上次数据再后台静默刷新;供应商卡片墙由 N 次配置全量加载降为 1 次",
|
||||||
|
"models.dev 快照同步至 2026-10-09(8,467 模型 / 225 供应商)",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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",
|
version: "v0.7.12",
|
||||||
date: "2026-09-03",
|
date: "2026-09-03",
|
||||||
@@ -322,7 +358,7 @@ const zh = {
|
|||||||
},
|
},
|
||||||
download: {
|
download: {
|
||||||
title: "下载 Kimi Switch",
|
title: "下载 Kimi Switch",
|
||||||
subtitle: "当前版本 v0.7.12 · Windows / macOS / Linux 三平台已发布",
|
subtitle: "当前版本 v0.8.2 · Windows / macOS / Linux 三平台已发布",
|
||||||
autoUpdate: "更新检查跑在启动时和每 8 小时的周期任务里,设置页也可以手动触发。",
|
autoUpdate: "更新检查跑在启动时和每 8 小时的周期任务里,设置页也可以手动触发。",
|
||||||
ready: "已发布",
|
ready: "已发布",
|
||||||
wip: "开发中",
|
wip: "开发中",
|
||||||
@@ -391,10 +427,10 @@ const en: Dict = {
|
|||||||
performance: "Performance",
|
performance: "Performance",
|
||||||
changelog: "Changelog",
|
changelog: "Changelog",
|
||||||
download: "Download",
|
download: "Download",
|
||||||
downloadBtn: "Download v0.7.12",
|
downloadBtn: "Download v0.8.2",
|
||||||
},
|
},
|
||||||
hero: {
|
hero: {
|
||||||
badge: "v0.7.12 · Open source MIT · Windows / macOS / Linux",
|
badge: "v0.8.2 · Open source MIT · Windows / macOS / Linux",
|
||||||
titleBefore: "One app for all your ",
|
titleBefore: "One app for all your ",
|
||||||
titleAccent: "AI providers",
|
titleAccent: "AI providers",
|
||||||
titleAfter: "",
|
titleAfter: "",
|
||||||
@@ -404,7 +440,7 @@ const en: Dict = {
|
|||||||
source: "View source",
|
source: "View source",
|
||||||
stats: [
|
stats: [
|
||||||
{ value: "22", label: "Provider presets" },
|
{ value: "22", label: "Provider presets" },
|
||||||
{ value: "7495", label: "Model price DB" },
|
{ value: "8467", label: "Model price DB" },
|
||||||
{ value: "MIT", label: "License" },
|
{ value: "MIT", label: "License" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -451,7 +487,7 @@ const en: Dict = {
|
|||||||
{
|
{
|
||||||
id: "model-discovery",
|
id: "model-discovery",
|
||||||
title: "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,467 models, 225 providers).",
|
||||||
img: "screenshots/kimi-cli-select-model.png",
|
img: "screenshots/kimi-cli-select-model.png",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -464,7 +500,7 @@ const en: Dict = {
|
|||||||
},
|
},
|
||||||
showcase: {
|
showcase: {
|
||||||
title: "Screenshots",
|
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.2 build.",
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
src: "screenshots/usage-config-page.png",
|
src: "screenshots/usage-config-page.png",
|
||||||
@@ -515,6 +551,42 @@ const en: Dict = {
|
|||||||
syncedNote: "Synced from GitHub Releases",
|
syncedNote: "Synced from GitHub Releases",
|
||||||
fallbackNote: "Built-in release notes",
|
fallbackNote: "Built-in release notes",
|
||||||
entries: [
|
entries: [
|
||||||
|
{
|
||||||
|
version: "v0.8.2",
|
||||||
|
date: "2026-10-09",
|
||||||
|
items: [
|
||||||
|
"First dashboard open after a restart drops from ~25s to under 1s: the usage snapshot is persisted locally and read straight at startup, then verified in the background with silent updates when things changed (measured at 64k records)",
|
||||||
|
"Switching the time range is near-instant: heatmap / model overview / per-range totals are cached per record set, and the wasteful full-table pass behind the 'all time' aggregate is gone",
|
||||||
|
"No more loading flicker: switching ranges or reopening the page shows the previous data first and revalidates silently; the provider card wall loads config once instead of once per card",
|
||||||
|
"models.dev snapshot synced to 2026-10-09 (8,467 models / 225 providers)",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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",
|
version: "v0.7.12",
|
||||||
date: "2026-09-03",
|
date: "2026-09-03",
|
||||||
@@ -696,7 +768,7 @@ const en: Dict = {
|
|||||||
},
|
},
|
||||||
download: {
|
download: {
|
||||||
title: "Download Kimi Switch",
|
title: "Download Kimi Switch",
|
||||||
subtitle: "Current version v0.7.12 · Windows, macOS and Linux now released",
|
subtitle: "Current version v0.8.2 · Windows, macOS and Linux now released",
|
||||||
autoUpdate: "Update checks run at startup and every 8 hours; Settings also has a manual check.",
|
autoUpdate: "Update checks run at startup and every 8 hours; Settings also has a manual check.",
|
||||||
ready: "Available",
|
ready: "Available",
|
||||||
wip: "In development",
|
wip: "In development",
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ const icons: Record<string, Icon> = {
|
|||||||
linux: LinuxLogo,
|
linux: LinuxLogo,
|
||||||
};
|
};
|
||||||
|
|
||||||
const VERSION = "0.7.12";
|
const VERSION = "0.8.2";
|
||||||
const GITHUB = "https://github.com/billowliu2/KimiSwitch";
|
const GITHUB = "https://github.com/billowliu2/KimiSwitch";
|
||||||
const MIRROR_RELEASES = "https://git.codingplan.site/admin/KimiCodeSwitch/releases";
|
const MIRROR_RELEASES = "https://git.codingplan.site/admin/KimiCodeSwitch/releases";
|
||||||
const dl = (file: string) => `${GITHUB}/releases/download/v${VERSION}/${file}`;
|
const dl = (file: string) => `${GITHUB}/releases/download/v${VERSION}/${file}`;
|
||||||
|
|
||||||
/** Per-platform download assets for the current release (names match the CI
|
/** Per-platform download assets for the current release (names match the CI
|
||||||
* release workflow exactly — verified against the published v0.7.12 assets:
|
* release workflow exactly — verified against the published v0.8.2 assets:
|
||||||
* tauri-bundler on CI produces `Kimi.Switch_…` (dots) on every platform,
|
* tauri-bundler on CI produces `Kimi.Switch_…` (dots) on every platform,
|
||||||
* except the rpm which uses `Kimi.Switch-<version>-1` (hyphens).
|
* except the rpm which uses `Kimi.Switch-<version>-1` (hyphens).
|
||||||
* Filenames interpolate VERSION, so a release bump only changes the
|
* Filenames interpolate VERSION, so a release bump only changes the
|
||||||
|
|||||||
Reference in new issue
Block a user