From 97422440e148e0110ae74981c5e319ea2c0c07f9 Mon Sep 17 00:00:00 2001 From: KimiSwitch Dev Date: Fri, 16 Oct 2026 17:23:56 +0800 Subject: [PATCH] =?UTF-8?q?fix(providers):=20=E4=BE=9B=E5=BA=94=E5=95=86?= =?UTF-8?q?=E5=90=8D=E7=A7=B0=E5=94=AF=E4=B8=80=E6=80=A7=E9=98=B2=E6=8A=A4?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E5=A4=8D=E5=90=8C=E5=90=8D=E9=9D=99=E9=BB=98?= =?UTF-8?q?=E8=A6=86=E7=9B=96=E5=AF=BC=E8=87=B4=20API=20Key=20=E4=B8=A2?= =?UTF-8?q?=E5=A4=B1=20=E2=80=94=20bump=20to=20v0.8.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 15 + docs/release-notes/v0.8.6.md | 30 + package.json | 2 +- public/models-dev.json | 1428 +++++++++++++++++------------ src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- src/App.tsx | 157 +++- src/components/ProviderEdit.tsx | 36 +- src/i18n/en.ts | 6 + src/i18n/zh.ts | 5 + src/lib/models-dev-full.json | 1512 ++++++++++++++++++------------- src/lib/models-dev.json | 1428 +++++++++++++++++------------ src/lib/validation.test.ts | 36 + src/lib/validation.ts | 28 + 15 files changed, 2883 insertions(+), 1806 deletions(-) create mode 100644 docs/release-notes/v0.8.6.md create mode 100644 src/lib/validation.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ee18a50..3f589e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,21 @@ --- +## v0.8.6 (2026-10-16) + +供应商名称唯一性防护版:修复同一厂商配置第二把 Key 时,因名称撞车导致第一个供应商条目被静默覆盖、API Key 丢失的问题。 + +- **同名覆盖丢失修复**:供应商以名称为唯一存储键,此前新建/改名撞名会在内存无提示顶掉旧条目,随后任意保存(含 Ctrl+S 快捷键,其绕过被禁用的按钮)即把丢失写进 config.toml;现四个写入点全部拦截 +- **编辑页即时提示**:名称为空或重名 → 红框 + 错误说明 + 保存按钮禁用 +- **保存硬阻止**:Ctrl+S / 底部按钮 / 切换供应商三条入口统一过检,撞名未解除或空名条目弹窗阻止、绝不落盘 +- **被顶掉的供应商自动找回**:撞名瞬间暂存被覆盖条目,改名离开撞名态时自动还原(Key 与模型映射一并回来) +- **新建占位名循环取号**:删除过 `provider-N` 后新建不再回退撞名 +- **「配置 JSON」源头拒绝**:JSON 内 `name` 与其他供应商重名时拒绝应用、零状态变更 +- **复制提示显示真实新名**(`-copy` / `-copy-2`),不再固定 `-copy` +- 名称比较为大小写敏感全等、忽略首尾空白,与 config.toml 键语义一致;零 Rust 改动、无迁移成本 +- models.dev 快照同步至 2026-10-16(8,894 模型 / 230 供应商) +- 测试:前端 157 项(新增 7 项名称校验)、Rust 212 项(无改动,全量回归);置换暂存算法另经 27 条独立断言推演验证 + ## v0.8.5 (2026-10-16) 设置界面重构:原来塞满所有选项的「设置」弹窗改为带返回头的全局设置页面,按「全局设置 / 高级设置 / 关于」分三页;同时修掉体检账单漏查与更新下载哑火两处问题。 diff --git a/docs/release-notes/v0.8.6.md b/docs/release-notes/v0.8.6.md new file mode 100644 index 0000000..3faa714 --- /dev/null +++ b/docs/release-notes/v0.8.6.md @@ -0,0 +1,30 @@ +# KimiSwitch v0.8.6 + +供应商名称唯一性防护版:修复同一厂商配置第二把 Key 时,因供应商**名称撞车**导致第一个供应商条目被静默覆盖、API Key 随之丢失的问题。 + +## 同名覆盖丢失(事故修复) + +- **事故复现路径**:Kimi Switch 里供应商以「名称」为唯一存储键,新建第二个供应商时若名称与已有厂商相同(例如两把 Key 都叫 `TokenHub`),应用会在内存里**无提示地用新条目顶掉旧条目**,随后任意一次保存(包括 Ctrl+S 快捷键与切换供应商)都会把丢失状态写进 config.toml——第一把 Key 就此消失,全程没有任何警告 +- **根因**:四个写入点(编辑改名、JSON 应用、新建占位名、保存校验)全部没有名称查重;且 Ctrl+S 是窗口级快捷键,会绕过任何被禁用的按钮,单纯「灰掉保存按钮」拦不住 +- **本次修复后**:名称撞车在每一层都会被拦下,被顶掉的条目还能自动找回(见下),同类丢失不再可能发生 + +## 防护机制 + +- **编辑页即时提示**:名称为空或与其他供应商重名时,名称输入框立即红框 + 错误说明(「供应商名称不能为空」/「名称与其他供应商重复」),顶部保存按钮同时禁用 +- **保存硬阻止**:三条保存入口(Ctrl+S、底部保存按钮、切换供应商)统一过检——撞名未解除或存在空名条目时弹窗阻止,绝不落盘 +- **被顶掉的供应商自动找回**:撞名瞬间被覆盖的原厂商会被暂存保护,改名离开撞名状态时**自动还原**回列表(例如把 `TokenHub-copy` 错改成 `TokenHub` 后再改成 `TokenHub-3`,原来的 `TokenHub` 连同它的 Key 和模型映射都会回来) +- **新建占位名防撞**:`+ 自定义配置` 生成的 `provider-N` 占位名改为循环取号,删除过条目后不再回退撞名 +- **「配置 JSON」源头拒绝**:粘贴的 JSON 里 `name` 与其他供应商重名时直接拒绝应用,不产生任何变更 +- **复制提示修正**:复制供应商的提示文案显示真实新名(`-copy` / `-copy-2`),不再固定显示 `-copy` + +## 其他 + +- 名称比较为大小写敏感全等(`TokenHub` 与 `tokenhub` 视为两个不同供应商,与 config.toml 键语义一致);首尾空白自动忽略 +- models.dev 快照同步至 2026-10-16(8,894 模型 / 230 供应商) +- 本次零 Rust 改动,存储层与 config.toml 格式完全不变,升级 / 回退无迁移成本 + +## 测试 + +- 前端 157 项(新增 7 项名称校验:空名 / 纯空白 / 撞名 / 大小写不判撞 / 双侧空白归一 / 唯一名 / 空名优先级) +- 置换暂存与自动还原算法经 27 条独立断言推演验证(含连续两次撞名、撞名后重载、撞名后返回丢弃等边界) +- Rust 212 项(本次无改动,全量回归通过) diff --git a/package.json b/package.json index c6b5929..b581f33 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "kimiswitch", "private": true, - "version": "0.8.5", + "version": "0.8.6", "type": "module", "scripts": { "dev": "vite", diff --git a/public/models-dev.json b/public/models-dev.json index 9aad7de..9f2e871 100644 --- a/public/models-dev.json +++ b/public/models-dev.json @@ -1,5 +1,5 @@ { - "last_updated": "2026-10-15", + "last_updated": "2026-10-16", "deepinfra/anthropic/claude-fable-5": { "name": "Claude Fable 5", "context": 1000000, @@ -655,6 +655,21 @@ "cache_read": 0.04 } }, + "deepinfra/stepfun-ai/Step-5-Preview": { + "name": "Step 5 Preview", + "context": 1048576, + "output": 1000000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true, + "cost": { + "input": 1, + "output": 2.7, + "cache_read": 0.05 + } + }, "deepinfra/MiniMaxAI/MiniMax-M3": { "name": "MiniMax-M3", "context": 524288, @@ -4838,7 +4853,7 @@ }, "engy/glm-5.3-flash": { "name": "GLM-5.3-Flash", - "context": 262144, + "context": 327680, "output": 32768, "reasoning": true, "tool_call": true, @@ -4879,7 +4894,7 @@ }, "engy/deepseek-v4.1-flash": { "name": "DeepSeek V4.1 Flash", - "context": 327680, + "context": 1048576, "output": 65536, "reasoning": true, "tool_call": true, @@ -7140,9 +7155,9 @@ "structured_output": true, "image": true, "cost": { - "input": 0.22, - "output": 0.66, - "cache_read": 0.007 + "input": 0.44, + "output": 1.32, + "cache_read": 0.014 } }, "nano-gpt/deepseek/deepseek-v3.2:thinking": { @@ -9348,6 +9363,15 @@ "cache_read": 0.2465 } }, + "nano-gpt/aisingapore/Gemma-SEA-LION-v4-27B-IT": { + "name": "Gemma SEA-LION v4 27B", + "context": 131072, + "output": 128000, + "cost": { + "input": 0.24, + "output": 0.48 + } + }, "nano-gpt/MarinaraSpaghetti/NemoMix-Unleashed-12B": { "name": "NemoMix 12B Unleashed", "context": 32768, @@ -9659,6 +9683,20 @@ "cache_read": 0.3026 } }, + "nano-gpt/fireworks/ember-1": { + "name": "Ember 1", + "context": 1048576, + "output": 131072, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "cost": { + "input": 3, + "output": 15, + "cache_read": 0.3 + } + }, "nano-gpt/mistralai/ministral-8b-2512": { "name": "Ministral 8B", "context": 262144, @@ -10211,6 +10249,21 @@ "cache_read": 0.025 } }, + "nano-gpt/nvidia/nemotron-3.5-super-vl": { + "name": "Nvidia Nemotron 3.5 Super VL", + "context": 262144, + "output": 16384, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true, + "cost": { + "input": 0.15, + "output": 0.5, + "cache_read": 0.02 + } + }, "nano-gpt/nvidia/nemotron-3-ultra-550b-a55b": { "name": "Nvidia Nemotron 3 Ultra 550B", "context": 1000000, @@ -11318,9 +11371,9 @@ "tool_call": true, "structured_output": true, "cost": { - "input": 0.09, + "input": 0.1, "output": 1.1, - "cache_read": 0.075 + "cache_read": 0.07 } }, "nano-gpt/qwen/qwen3-max-2026-01-23": { @@ -14980,7 +15033,7 @@ "cost": { "input": 0.15, "output": 0.65, - "cache_read": 0.006 + "cache_read": 0.002 } }, "wandb/MiniMaxAI/MiniMax-M3": { @@ -15049,7 +15102,21 @@ "cost": { "input": 0.15, "output": 0.5, - "cache_read": 0.029 + "cache_read": 0.02 + } + }, + "wandb/nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B": { + "name": "Nemotron 3.5 Super VL", + "context": 1048576, + "output": 1048576, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "cost": { + "input": 0.15, + "output": 0.75, + "cache_read": 0.02 } }, "wandb/nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B": { @@ -17266,7 +17333,7 @@ "cost": { "input": 0.1, "output": 0.25, - "cache_read": 0.02 + "cache_read": 0.01 } }, "llmgateway-providers/consensusprotocol/deepseek-v4.1-flash": { @@ -18340,36 +18407,6 @@ "cache_write": 0.08333 } }, - "llmgateway-providers/google-ai-studio/gemini-2.5-pro": { - "name": "Gemini 2.5 Pro (Google AI Studio)", - "context": 1048576, - "output": 65536, - "reasoning": true, - "tool_call": true, - "structured_output": true, - "image": true, - "video": true, - "cost": { - "input": 1.25, - "output": 10, - "cache_read": 0.125 - } - }, - "llmgateway-providers/google-ai-studio/gemini-2.5-flash": { - "name": "Gemini 2.5 Flash (Google AI Studio)", - "context": 1048576, - "output": 65535, - "reasoning": true, - "tool_call": true, - "structured_output": true, - "image": true, - "video": true, - "cost": { - "input": 0.3, - "output": 2.5, - "cache_read": 0.03 - } - }, "llmgateway-providers/google-ai-studio/gemini-3.7-flash": { "name": "Gemini 3.7 Flash (Google AI Studio)", "context": 1048576, @@ -18431,20 +18468,6 @@ "cache_write": 0.08333 } }, - "llmgateway-providers/google-ai-studio/gemini-2.5-flash-lite": { - "name": "Gemini 2.5 Flash Lite (Google AI Studio)", - "context": 1048576, - "output": 65535, - "tool_call": true, - "structured_output": true, - "image": true, - "video": true, - "cost": { - "input": 0.1, - "output": 0.4, - "cache_read": 0.01 - } - }, "llmgateway-providers/google-ai-studio/gemini-4-argon": { "name": "Gemini 4 Argon (Google AI Studio)", "context": 1048576, @@ -22192,36 +22215,6 @@ "cache_write": 0.08333 } }, - "llmgateway-providers/google-vertex/gemini-2.5-pro": { - "name": "Gemini 2.5 Pro (Google Vertex AI)", - "context": 1048576, - "output": 65536, - "reasoning": true, - "tool_call": true, - "structured_output": true, - "image": true, - "video": true, - "cost": { - "input": 1.25, - "output": 10, - "cache_read": 0.125 - } - }, - "llmgateway-providers/google-vertex/gemini-2.5-flash": { - "name": "Gemini 2.5 Flash (Google Vertex AI)", - "context": 1048576, - "output": 65535, - "reasoning": true, - "tool_call": true, - "structured_output": true, - "image": true, - "video": true, - "cost": { - "input": 0.3, - "output": 2.5, - "cache_read": 0.03 - } - }, "llmgateway-providers/google-vertex/gemini-3.7-flash": { "name": "Gemini 3.7 Flash (Google Vertex AI)", "context": 1048576, @@ -22268,20 +22261,6 @@ "cache_write": 0.08333 } }, - "llmgateway-providers/google-vertex/gemini-2.5-flash-lite": { - "name": "Gemini 2.5 Flash Lite (Google Vertex AI)", - "context": 1048576, - "output": 65535, - "tool_call": true, - "structured_output": true, - "image": true, - "video": true, - "cost": { - "input": 0.1, - "output": 0.4, - "cache_read": 0.01 - } - }, "llmgateway-providers/google-vertex/gemini-4-argon": { "name": "Gemini 4 Argon (Google Vertex AI)", "context": 1048576, @@ -23242,9 +23221,9 @@ "image": true, "video": true, "cost": { - "input": 0.29, - "output": 24, - "cache_read": 0.28 + "input": 0.294, + "output": 28, + "cache_read": 0.284 } }, "kilo/poolside/laguna-s-2.1:free": { @@ -23693,13 +23672,13 @@ }, "kilo/deepseek/deepseek-chat": { "name": "DeepSeek Chat", - "context": 163840, - "output": 16384, + "context": 128000, + "output": 16000, "tool_call": true, "structured_output": true, "cost": { - "input": 0.32, - "output": 0.89 + "input": 0.2574, + "output": 1.0287 } }, "kilo/deepseek/deepseek-v4-pro-0813": { @@ -23802,18 +23781,6 @@ "cache_read": 0.0182 } }, - "kilo/unbiased/pareto-26.10-preview": { - "name": "Pareto 26.10 Preview (retires Oct 15)", - "context": 1048576, - "output": 131072, - "tool_call": true, - "image": true, - "cost": { - "input": 0.8, - "output": 3.2, - "cache_read": 0.03 - } - }, "kilo/unbiased/pareto-26.9": { "name": "Pareto 26.9", "context": 262144, @@ -23899,9 +23866,9 @@ "tool_call": true, "structured_output": true, "cost": { - "input": 0.83, - "output": 2.501, - "cache_read": 0.041 + "input": 0.801, + "output": 2.48, + "cache_read": 0.038 } }, "kilo/anthracite-org/magnum-v4-72b": { @@ -23922,8 +23889,8 @@ "structured_output": true, "image": true, "cost": { - "input": 0.1, - "output": 0.3 + "input": 0.08, + "output": 0.24 } }, "kilo/meta-llama/llama-guard-4-12b": { @@ -23932,8 +23899,8 @@ "output": 16384, "image": true, "cost": { - "input": 0.18, - "output": 0.18 + "input": 0.144, + "output": 0.144 } }, "kilo/meta-llama/llama-4-maverick": { @@ -24126,8 +24093,8 @@ }, "kilo/z-ai/glm-5.3-flash": { "name": "GLM-5.3-Flash", - "context": 1048575, - "output": 943717, + "context": 1048576, + "output": 131072, "reasoning": true, "tool_call": true, "structured_output": true, @@ -24553,6 +24520,21 @@ "cache_read": 0.002 } }, + "kilo/meta/muse-spark-open": { + "name": "Muse Spark Open", + "context": 1048576, + "output": 943718, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true, + "cost": { + "input": 1.25, + "output": 4.25, + "cache_read": 0.15 + } + }, "kilo/nousresearch/hermes-3-llama-3.1-70b": { "name": "Nous: Hermes 3 70B Instruct", "context": 131072, @@ -24578,6 +24560,7 @@ "context": 131072, "output": 117964, "reasoning": true, + "structured_output": true, "cost": { "input": 1, "output": 3 @@ -24606,16 +24589,16 @@ "kilo/~z-ai/glm-flash-latest": { "name": "Z.ai: GLM Flash Latest", "context": 1048576, - "output": 943718, + "output": 131072, "reasoning": true, "tool_call": true, "structured_output": true, "image": true, "video": true, "cost": { - "input": 0.036, - "output": 0.9, - "cache_read": 0.01125 + "input": 0.0131, + "output": 0.8228, + "cache_read": 0.0125 } }, "kilo/~z-ai/glm-latest": { @@ -24963,22 +24946,22 @@ "structured_output": true, "image": true, "cost": { - "input": 0.02376, - "output": 0.72, + "input": 0.036, + "output": 0.684, "cache_read": 0.0054 } }, "kilo/~deepseek/deepseek-pro-latest": { "name": "DeepSeek: DeepSeek Pro Latest", "context": 1048576, - "output": 393216, + "output": 943718, "reasoning": true, "tool_call": true, "structured_output": true, "cost": { - "input": 0.1307, - "output": 8, - "cache_read": 0.13 + "input": 0.25, + "output": 5, + "cache_read": 0.249 } }, "kilo/~deepseek/deepseek-v4-flash-latest": { @@ -24989,9 +24972,9 @@ "tool_call": true, "structured_output": true, "cost": { - "input": 0.0144, - "output": 0.576, - "cache_read": 0.0108 + "input": 0.0049, + "output": 1.28, + "cache_read": 0.0049 } }, "kilo/~x-ai/grok-latest": { @@ -25055,9 +25038,9 @@ "image": true, "video": true, "cost": { - "input": 0.09, + "input": 0.1, "output": 0.34, - "cache_read": 0.05 + "cache_read": 0.1 } }, "kilo/google/gemini-3.1-flash-image-preview": { @@ -25224,8 +25207,8 @@ "structured_output": true, "image": true, "cost": { - "input": 0.05, - "output": 0.15 + "input": 0.04, + "output": 0.12 } }, "kilo/google/gemini-3.5-flash": { @@ -25479,6 +25462,28 @@ "output": 6 } }, + "kilo/moondream/moondream3-preview": { + "name": "Moondream: Moondream 3 Preview", + "context": 32000, + "output": 4096, + "reasoning": true, + "image": true, + "cost": { + "input": 0.3, + "output": 2.5 + } + }, + "kilo/moondream/moondream3.1-9b-a2b": { + "name": "Moondream: Moondream 3.1", + "context": 32000, + "output": 4096, + "reasoning": true, + "image": true, + "cost": { + "input": 0.3, + "output": 2.5 + } + }, "kilo/ibm-granite/granite-4.0-h-micro": { "name": "IBM: Granite 4.0 Micro", "context": 131000, @@ -26044,6 +26049,20 @@ "output": 0.1 } }, + "kilo/nvidia/nemotron-3.5-super-vl:free": { + "name": "NVIDIA: Nemotron 3.5 Super VL (free)", + "context": 1000000, + "output": 131072, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true, + "cost": { + "input": 0, + "output": 0 + } + }, "kilo/nvidia/nemotron-3-nano-30b-a3b": { "name": "Nemotron 3 Nano 30B A3B", "context": 262144, @@ -26105,6 +26124,21 @@ "output": 0.45 } }, + "kilo/nvidia/nemotron-3.5-super-vl": { + "name": "NVIDIA: Nemotron 3.5 Super VL", + "context": 262144, + "output": 235929, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true, + "cost": { + "input": 0.1, + "output": 0.4, + "cache_read": 0.08 + } + }, "kilo/nvidia/nemotron-3-ultra-550b-a55b:free": { "name": "NVIDIA: Nemotron 3 Ultra (free)", "context": 1000000, @@ -26124,9 +26158,9 @@ "tool_call": true, "structured_output": true, "cost": { - "input": 0.5, - "output": 2.2, - "cache_read": 0.1 + "input": 0.4, + "output": 1.76, + "cache_read": 0.08 } }, "kilo/nvidia/nemotron-3-super-120b-a12b:free": { @@ -26372,7 +26406,7 @@ "cost": { "input": 1, "output": 2.7, - "cache_read": 0.05 + "cache_read": 0.1 } }, "kilo/stepfun/step-3.5-flash": { @@ -26401,19 +26435,6 @@ "cache_read": 0.04 } }, - "kilo/stepfun/step-5-preview-free": { - "name": "StepFun: Step 5 Preview (free)", - "context": 1000000, - "output": 64000, - "reasoning": true, - "tool_call": true, - "image": true, - "cost": { - "input": 0, - "output": 0, - "cache_read": 0 - } - }, "kilo/dots-studio/dots-3-note-preview:free": { "name": "Dots Studio: Dots3-Note Preview (free)", "context": 512000, @@ -26837,13 +26858,12 @@ "kilo/qwen/qwen3-next-80b-a3b-instruct": { "name": "Qwen3-Next 80B-A3B Instruct", "context": 262144, - "output": 235929, + "output": 16384, "tool_call": true, "structured_output": true, "cost": { - "input": 0.1, - "output": 1.1, - "cache_read": 0.07 + "input": 0.09, + "output": 1.1 } }, "kilo/qwen/qwen3-vl-30b-a3b-thinking": { @@ -27958,7 +27978,7 @@ "cost": { "input": 0.03, "output": 0.15, - "cache_read": 0.03 + "cache_read": 0.015 } }, "kilo/openai/gpt-5.1-codex-mini": { @@ -28051,8 +28071,8 @@ "output": 14745, "structured_output": true, "cost": { - "input": 0.07, - "output": 0.14 + "input": 0.056, + "output": 0.112 } }, "kilo/microsoft/wizardlm-2-8x22b": { @@ -28086,10 +28106,10 @@ "structured_output": true, "image": true, "cost": { - "input": 0.25, - "output": 1.5, - "cache_read": 0.025, - "cache_write": 0.3125 + "input": 0.2, + "output": 1.2, + "cache_read": 0.02, + "cache_write": 0.25 } }, "venice/grok-4-5": { @@ -28287,10 +28307,10 @@ "structured_output": true, "image": true, "cost": { - "input": 0.125, - "output": 0.625, - "cache_read": 0.0125, - "cache_write": 0.15625 + "input": 0.1, + "output": 0.5, + "cache_read": 0.01, + "cache_write": 0.125 } }, "venice/kimi-k3": { @@ -28316,9 +28336,9 @@ "structured_output": true, "image": true, "cost": { - "input": 0.9375, - "output": 5.625, - "cache_read": 0.09375 + "input": 0.75, + "output": 4.5, + "cache_read": 0.075 } }, "venice/qwen-3-8-flash": { @@ -28446,6 +28466,21 @@ "cache_read": 0.08 } }, + "venice/muse-wonder": { + "name": "Muse Wonder", + "context": 1048576, + "output": 131072, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true, + "cost": { + "input": 5, + "output": 25, + "cache_read": 0.25 + } + }, "venice/openai-gpt-54-pro": { "name": "GPT-5.4 Pro", "context": 1000000, @@ -28455,8 +28490,8 @@ "structured_output": true, "image": true, "cost": { - "input": 37.5, - "output": 225 + "input": 30, + "output": 180 } }, "venice/aion-labs-aion-3-0": { @@ -28569,10 +28604,10 @@ "structured_output": true, "image": true, "cost": { - "input": 5, - "output": 25, - "cache_read": 0.5, - "cache_write": 6.25 + "input": 4, + "output": 20, + "cache_read": 0.4, + "cache_write": 5 } }, "venice/seed-2-1-turbo": { @@ -28613,8 +28648,8 @@ "structured_output": true, "image": true, "cost": { - "input": 37.5, - "output": 225 + "input": 30, + "output": 180 } }, "venice/qwen-3-7-plus": { @@ -28654,10 +28689,10 @@ "structured_output": true, "image": true, "cost": { - "input": 2.5, - "output": 12.5, - "cache_read": 0.125, - "cache_write": 3.125 + "input": 2, + "output": 10, + "cache_read": 0.1, + "cache_write": 2.5 } }, "venice/zai-org-glm-4.7-flash": { @@ -28682,10 +28717,10 @@ "structured_output": true, "image": true, "cost": { - "input": 15, - "output": 75, - "cache_read": 0.75, - "cache_write": 18.75 + "input": 12, + "output": 60, + "cache_read": 0.6, + "cache_write": 15 } }, "venice/claude-opus-5": { @@ -28883,10 +28918,10 @@ "structured_output": true, "image": true, "cost": { - "input": 2.5, - "output": 15, - "cache_read": 0.25, - "cache_write": 3.125 + "input": 2, + "output": 12, + "cache_read": 0.2, + "cache_write": 2.5 } }, "venice/llama-3.3-70b": { @@ -28907,9 +28942,9 @@ "structured_output": true, "image": true, "cost": { - "input": 0.1875, - "output": 0.75, - "cache_read": 0.09375 + "input": 0.15, + "output": 0.6, + "cache_read": 0.075 } }, "venice/mistral-small-3-2-24b-instruct": { @@ -28989,10 +29024,10 @@ "structured_output": true, "image": true, "cost": { - "input": 12.5, - "output": 62.5, - "cache_read": 1.25, - "cache_write": 15.625 + "input": 10, + "output": 50, + "cache_read": 1, + "cache_write": 12.5 } }, "venice/openai-gpt-54": { @@ -29004,9 +29039,9 @@ "structured_output": true, "image": true, "cost": { - "input": 3.13, - "output": 18.8, - "cache_read": 0.313 + "input": 2.5, + "output": 15, + "cache_read": 0.25 } }, "venice/gemma-4-uncensored": { @@ -29159,10 +29194,10 @@ "structured_output": true, "image": true, "cost": { - "input": 2.5, - "output": 12.5, - "cache_read": 0.25, - "cache_write": 3.125 + "input": 2, + "output": 10, + "cache_read": 0.2, + "cache_write": 2.5 } }, "venice/qwen-3-8-27b": { @@ -29188,9 +29223,9 @@ "structured_output": true, "image": true, "cost": { - "input": 2.19, - "output": 17.5, - "cache_read": 0.219 + "input": 1.75, + "output": 14, + "cache_read": 0.175 } }, "venice/aion-labs-aion-3-0-mini": { @@ -29242,10 +29277,10 @@ "structured_output": true, "image": true, "cost": { - "input": 2.5, - "output": 15, - "cache_read": 0.25, - "cache_write": 3.125 + "input": 2, + "output": 12, + "cache_read": 0.2, + "cache_write": 2.5 } }, "venice/gemini-3-flash-preview": { @@ -29326,8 +29361,9 @@ "structured_output": true, "image": true, "cost": { - "input": 3.125, - "output": 12.5 + "input": 2.5, + "output": 10, + "cache_read": 1.25 } }, "venice/openai-gpt-52": { @@ -29338,9 +29374,9 @@ "tool_call": true, "structured_output": true, "cost": { - "input": 2.19, - "output": 17.5, - "cache_read": 0.219 + "input": 1.75, + "output": 14, + "cache_read": 0.175 } }, "venice/gemini-3-6-flash": { @@ -29441,9 +29477,9 @@ "structured_output": true, "image": true, "cost": { - "input": 6.25, - "output": 37.5, - "cache_read": 0.625 + "input": 5, + "output": 30, + "cache_read": 0.5 } }, "venice/openai-gpt-56-sol": { @@ -29455,10 +29491,10 @@ "structured_output": true, "image": true, "cost": { - "input": 5, - "output": 25, - "cache_read": 0.5, - "cache_write": 6.25 + "input": 4, + "output": 20, + "cache_read": 0.4, + "cache_write": 5 } }, "venice/qwen3-coder-480b-a35b-instruct-turbo": { @@ -29558,6 +29594,19 @@ "cache_read": 0.0375 } }, + "venice/hy4-preview": { + "name": "Hy4 Preview", + "context": 1048576, + "output": 64000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "cost": { + "input": 1.0425, + "output": 3.12625, + "cache_read": 0.0525 + } + }, "venice/gemini-3-5-flash": { "name": "Gemini 3.5 Flash", "context": 1000000, @@ -29616,6 +29665,21 @@ "output": 0.9 } }, + "venice/nvidia-nemotron-3-5-super-vl-120b-a12b": { + "name": "NVIDIA Nemotron 3.5 Super VL 120B", + "context": 262144, + "output": 32768, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true, + "cost": { + "input": 0.25, + "output": 1.25, + "cache_read": 0.125 + } + }, "venice/qwen3-5-397b-a17b": { "name": "Qwen 3.5 397B", "context": 128000, @@ -29649,10 +29713,10 @@ "structured_output": true, "image": true, "cost": { - "input": 0.25, - "output": 1.5, - "cache_read": 0.025, - "cache_write": 0.3125 + "input": 0.2, + "output": 1.2, + "cache_read": 0.02, + "cache_write": 0.25 } }, "venice/nvidia-nemotron-3-ultra-550b-a55b": { @@ -29668,6 +29732,20 @@ "cache_read": 0.1875 } }, + "venice/muse-spark-open": { + "name": "Muse Spark Open", + "context": 1048576, + "output": 131072, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "cost": { + "input": 1.5625, + "output": 5.3125, + "cache_read": 0.1875 + } + }, "venice/gemini-4-argon": { "name": "Gemini 4 Argon", "context": 1000000, @@ -35328,6 +35406,22 @@ "output": 30 } }, + "302ai/gemini-4-argon": { + "name": "Gemini 4 Argon", + "context": 1048576, + "output": 65536, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true, + "cost": { + "input": 4, + "output": 10, + "cache_read": 0.2, + "cache_write": 0.375 + } + }, "302ai/gpt-6-sol": { "name": "GPT-6 Sol", "context": 1050000, @@ -35481,9 +35575,9 @@ "image": true, "video": true, "cost": { - "input": 0.29, - "output": 24, - "cache_read": 0.28 + "input": 0.294, + "output": 28, + "cache_read": 0.284 } }, "openrouter/poolside/laguna-s-2.1:free": { @@ -35885,9 +35979,9 @@ "tool_call": true, "structured_output": true, "cost": { - "input": 0.016, - "output": 0.64, - "cache_read": 0.016 + "input": 0.0049, + "output": 1.28, + "cache_read": 0.0049 } }, "openrouter/deepseek/deepseek-v4.1-flash": { @@ -35899,8 +35993,8 @@ "structured_output": true, "image": true, "cost": { - "input": 0.02376, - "output": 0.72, + "input": 0.036, + "output": 0.684, "cache_read": 0.0054 } }, @@ -35933,12 +36027,12 @@ "openrouter/deepseek/deepseek-chat": { "name": "DeepSeek Chat", "context": 163840, - "output": 16384, + "output": 16000, "tool_call": true, "structured_output": true, "cost": { - "input": 0.32, - "output": 0.89 + "input": 0.2574, + "output": 1.0287 } }, "openrouter/deepseek/deepseek-v4-pro-0813": { @@ -35949,9 +36043,9 @@ "tool_call": true, "structured_output": true, "cost": { - "input": 0.66, - "output": 1.98, - "cache_read": 0.022 + "input": 1.32, + "output": 3.96, + "cache_read": 0.044 } }, "openrouter/deepseek/deepseek-v3.2-exp": { @@ -35997,9 +36091,9 @@ "tool_call": true, "structured_output": true, "cost": { - "input": 0.43935, - "output": 0.8787, - "cache_read": 0.036613 + "input": 0.95526, + "output": 1.91052, + "cache_read": 0.079605 } }, "openrouter/deepseek/deepseek-r1-0528": { @@ -36041,18 +36135,6 @@ "cache_read": 0.0182 } }, - "openrouter/unbiased/pareto-26.10-preview": { - "name": "Pareto 26.10 Preview", - "context": 1048576, - "output": 131072, - "tool_call": true, - "image": true, - "cost": { - "input": 0.8, - "output": 3.2, - "cache_read": 0.03 - } - }, "openrouter/unbiased/pareto-26.9": { "name": "Pareto 26.9", "context": 262144, @@ -36368,16 +36450,16 @@ "openrouter/z-ai/glm-5.3-flash": { "name": "GLM-5.3-Flash", "context": 1048576, - "output": 943717, + "output": 943718, "reasoning": true, "tool_call": true, "structured_output": true, "image": true, "video": true, "cost": { - "input": 0.15, - "output": 0.5, - "cache_read": 0.03 + "input": 0.01179, + "output": 0.9, + "cache_read": 0.01125 } }, "openrouter/z-ai/glm-4.6": { @@ -36466,9 +36548,9 @@ "tool_call": true, "structured_output": true, "cost": { - "input": 0.06, - "output": 7, - "cache_read": 0.059 + "input": 0.037, + "output": 9, + "cache_read": 0.036 } }, "openrouter/z-ai/glm-5.1": { @@ -36516,9 +36598,9 @@ "tool_call": true, "structured_output": true, "cost": { - "input": 0.4, - "output": 8, - "cache_read": 0.399 + "input": 0.079, + "output": 4.8, + "cache_read": 0.078 } }, "openrouter/inference-net/schematron-v2-small": { @@ -36800,6 +36882,21 @@ "cache_read": 0.002 } }, + "openrouter/meta/muse-spark-open": { + "name": "Muse Spark Open", + "context": 1048576, + "output": 943718, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true, + "cost": { + "input": 1.25, + "output": 4.25, + "cache_read": 0.15 + } + }, "openrouter/nousresearch/hermes-3-llama-3.1-70b": { "name": "Hermes 3 70B Instruct", "context": 131072, @@ -36825,6 +36922,7 @@ "context": 131072, "output": 117964, "reasoning": true, + "structured_output": true, "cost": { "input": 1, "output": 3 @@ -36860,7 +36958,7 @@ "image": true, "video": true, "cost": { - "input": 0.036, + "input": 0.01179, "output": 0.9, "cache_read": 0.01125 } @@ -37210,22 +37308,22 @@ "structured_output": true, "image": true, "cost": { - "input": 0.02376, - "output": 0.72, + "input": 0.036, + "output": 0.684, "cache_read": 0.0054 } }, "openrouter/~deepseek/deepseek-pro-latest": { "name": "DeepSeek Pro Latest", "context": 1048576, - "output": 393216, + "output": 943718, "reasoning": true, "tool_call": true, "structured_output": true, "cost": { - "input": 0.1307, - "output": 8, - "cache_read": 0.13 + "input": 0.25, + "output": 5, + "cache_read": 0.249 } }, "openrouter/~deepseek/deepseek-v4-flash-latest": { @@ -37236,9 +37334,9 @@ "tool_call": true, "structured_output": true, "cost": { - "input": 0.0144, - "output": 0.576, - "cache_read": 0.0108 + "input": 0.00441, + "output": 1.152, + "cache_read": 0.00441 } }, "openrouter/~x-ai/grok-latest": { @@ -37633,9 +37731,9 @@ "image": true, "video": true, "cost": { - "input": 0.75, - "output": 3.75, - "cache_read": 0.075, + "input": 0.825, + "output": 4.125, + "cache_read": 0.0825, "cache_write": 0.041667 } }, @@ -37751,6 +37849,28 @@ "output": 6 } }, + "openrouter/moondream/moondream3-preview": { + "name": "Moondream 3 Preview", + "context": 32000, + "output": 4096, + "reasoning": true, + "image": true, + "cost": { + "input": 0.3, + "output": 2.5 + } + }, + "openrouter/moondream/moondream3.1-9b-a2b": { + "name": "Moondream 3.1", + "context": 32000, + "output": 4096, + "reasoning": true, + "image": true, + "cost": { + "input": 0.3, + "output": 2.5 + } + }, "openrouter/ibm-granite/granite-4.0-h-micro": { "name": "Granite 4.0 Micro", "context": 131000, @@ -38141,9 +38261,9 @@ "image": true, "video": true, "cost": { - "input": 0.45, - "output": 15, - "cache_read": 0.28 + "input": 0.304, + "output": 11.936, + "cache_read": 0.21 } }, "openrouter/moonshotai/kimi-k2.6": { @@ -38246,6 +38366,20 @@ "output": 0.1 } }, + "openrouter/nvidia/nemotron-3.5-super-vl:free": { + "name": "Nemotron 3.5 Super VL (free)", + "context": 1000000, + "output": 131072, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true, + "cost": { + "input": 0, + "output": 0 + } + }, "openrouter/nvidia/nemotron-3-nano-30b-a3b": { "name": "Nemotron 3 Nano 30B A3B", "context": 262144, @@ -38307,6 +38441,21 @@ "output": 0.45 } }, + "openrouter/nvidia/nemotron-3.5-super-vl": { + "name": "Nemotron 3.5 Super VL", + "context": 1048576, + "output": 235929, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true, + "cost": { + "input": 0.15, + "output": 0.5, + "cache_read": 0.02 + } + }, "openrouter/nvidia/nemotron-3-ultra-550b-a55b:free": { "name": "Nemotron 3 Ultra (free)", "context": 1000000, @@ -38574,7 +38723,7 @@ "cost": { "input": 1, "output": 2.7, - "cache_read": 0.05 + "cache_read": 0.1 } }, "openrouter/stepfun/step-3.5-flash": { @@ -39024,13 +39173,12 @@ "openrouter/qwen/qwen3-next-80b-a3b-instruct": { "name": "Qwen3-Next 80B-A3B Instruct", "context": 262144, - "output": 235929, + "output": 16384, "tool_call": true, "structured_output": true, "cost": { - "input": 0.1, - "output": 1.1, - "cache_read": 0.07 + "input": 0.09, + "output": 1.1 } }, "openrouter/qwen/qwen3-vl-30b-a3b-thinking": { @@ -39279,8 +39427,9 @@ "image": true, "video": true, "cost": { - "input": 0.45, - "output": 2.7 + "input": 0.25, + "output": 2, + "cache_read": 0.025 } }, "openrouter/qwen/qwen3.6-plus": { @@ -40292,16 +40441,29 @@ } }, "iteracompute/deepseek/deepseek-v4-flash-0731": { - "name": "DeepSeek V4 Flash 0731", - "context": 970000, + "name": "DeepSeek V4 Flash 0731 (legacy alias)", + "context": 1048576, "output": 393216, "reasoning": true, "tool_call": true, "structured_output": true, "cost": { - "input": 0.34, - "output": 1.05, - "cache_read": 0.035 + "input": 0.29, + "output": 1.16, + "cache_read": 0.0058 + } + }, + "iteracompute/deepseek/deepseek-v4.1-flash": { + "name": "DeepSeek V4.1 Flash", + "context": 1048576, + "output": 393216, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "cost": { + "input": 0.29, + "output": 1.16, + "cache_read": 0.0058 } }, "iteracompute/deepseek/deepseek-v4-pro-0813": { @@ -40312,9 +40474,9 @@ "tool_call": true, "structured_output": true, "cost": { - "input": 1.1, - "output": 3.3, - "cache_read": 0.11 + "input": 0.89, + "output": 2.69, + "cache_read": 0.03 } }, "iteracompute/z-ai/glm-5.3-flash": { @@ -40325,9 +40487,9 @@ "tool_call": true, "image": true, "cost": { - "input": 0.14, - "output": 0.49, - "cache_read": 0.03 + "input": 0.099, + "output": 0.35, + "cache_read": 0.025 } }, "iteracompute/z-ai/glm-5.3": { @@ -40337,9 +40499,9 @@ "reasoning": true, "tool_call": true, "cost": { - "input": 1.2, - "output": 3.5, - "cache_read": 0.26 + "input": 0.99, + "output": 3.45, + "cache_read": 0.245 } }, "iteracompute/moonshotai/kimi-k3": { @@ -40348,11 +40510,25 @@ "output": 999999, "reasoning": true, "tool_call": true, + "structured_output": true, "image": true, "cost": { - "input": 3, - "output": 14.9, - "cache_read": 0.29 + "input": 2.39, + "output": 11.99, + "cache_read": 0.24 + } + }, + "iteracompute/moonshotai/kimi-k2.7-code": { + "name": "Kimi K2.7 Code", + "context": 262144, + "output": 235929, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "cost": { + "input": 0.69, + "output": 3.29, + "cache_read": 0.15 } }, "iteracompute/minimax/minimax-m3": { @@ -40363,42 +40539,28 @@ "tool_call": true, "image": true, "cost": { - "input": 0.29, - "output": 1.2, - "cache_read": 0.08 - } - }, - "iteracompute/ornith-ai/ornith-1.5-35b-a3b": { - "name": "Ornith 1.5 35B A3B", - "context": 327680, - "output": 65536, - "reasoning": true, - "tool_call": true, - "structured_output": true, - "image": true, - "cost": { - "input": 0.3, - "output": 3, - "cache_read": 0.03 + "input": 0.299, + "output": 1.19, + "cache_read": 0.045 } }, "iteracompute/qwen/qwen3.8-27b": { "name": "Qwen3.8 27B", - "context": 327680, - "output": 65536, + "context": 131072, + "output": 8192, "reasoning": true, "tool_call": true, "structured_output": true, "image": true, "cost": { - "input": 0.3, - "output": 2.5, - "cache_read": 0.03 + "input": 0, + "output": 0, + "cache_read": 0 } }, "iteracompute/qwen/qwen3.8-2.4t-a95b": { "name": "Qwen3.8 2.4T A95B", - "context": 970000, + "context": 1048576, "output": 131072, "reasoning": true, "tool_call": true, @@ -40406,8 +40568,8 @@ "image": true, "cost": { "input": 1.95, - "output": 5.95, - "cache_read": 0.2 + "output": 5.7, + "cache_read": 0.245 } }, "the-grid-ai/bytedance-pro-latest": { @@ -56031,6 +56193,21 @@ "cache_read": 0.064 } }, + "cloudflare-ai-gateway/google/gemini-4-argon": { + "name": "Gemini 4 Argon", + "context": 1048576, + "output": 65536, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true, + "cost": { + "input": 2, + "output": 10, + "cache_read": 0.1 + } + }, "cloudflare-ai-gateway/xai/grok-4.7": { "name": "Grok 4.7", "context": 500000, @@ -56114,6 +56291,19 @@ "cache_read": 0.5 } }, + "cloudflare-ai-gateway/fireworks/ember-1": { + "name": "Ember-1", + "context": 1040000, + "output": 131072, + "reasoning": true, + "tool_call": true, + "image": true, + "cost": { + "input": 3, + "output": 15, + "cache_read": 0.3 + } + }, "cloudflare-ai-gateway/moonshotai/kimi-k3": { "name": "Kimi K3", "context": 1048576, @@ -58785,6 +58975,20 @@ "cache_read": 0.015 } }, + "ollama-cloud/muse-spark-open": { + "name": "Muse Spark Open", + "context": 1048576, + "output": 131072, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "cost": { + "input": 1.25, + "output": 4.25, + "cache_read": 0.15 + } + }, "ollama-cloud/deepseek-v4-flash": { "name": "deepseek-v4-flash", "context": 1048576, @@ -59087,6 +59291,20 @@ "cache_write": 0 } }, + "ambient/minimax/minimax-m3": { + "name": "MiniMax-M3", + "context": 262144, + "output": 262144, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "cost": { + "input": 0.23, + "output": 0.96, + "cache_read": 0.05, + "cache_write": 0 + } + }, "ambient/stepfun/step-3.7-flash": { "name": "Step 3.7 Flash", "context": 262144, @@ -59422,6 +59640,21 @@ "cache_read": 0.5 } }, + "nebius/meta-models/muse-spark-open": { + "name": "Muse Spark Open", + "context": 1048576, + "output": 1048576, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true, + "cost": { + "input": 1.25, + "output": 4.25, + "cache_read": 1.25 + } + }, "nebius/google/gemma-3-27b-it": { "name": "Gemma-3-27b-it", "context": 110000, @@ -59621,6 +59854,21 @@ "cache_read": 1.4 } }, + "nebius/nvidia/Nemotron-3.5-Super-VL": { + "name": "Nemotron 3.5 Super VL 120B A12B", + "context": 1048576, + "output": 1048576, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true, + "cost": { + "input": 0.3, + "output": 0.9, + "cache_read": 0.3 + } + }, "nebius/nvidia/nemotron-3-super-120b-a12b": { "name": "Nemotron-3-Super-120B-A12B", "context": 262144, @@ -60060,6 +60308,17 @@ "output": 0 } }, + "vercel/stealth/cat-jam": { + "name": "Cat Jam", + "context": 1048576, + "output": 131072, + "reasoning": true, + "tool_call": true, + "cost": { + "input": 0, + "output": 0 + } + }, "vercel/zai/glm-4.5": { "name": "GLM 4.5", "context": 128000, @@ -60697,20 +60956,6 @@ "cache_write": 12.5 } }, - "vercel/anthropic/claude-sonnet-4": { - "name": "Claude Sonnet 4", - "context": 1000000, - "output": 64000, - "reasoning": true, - "tool_call": true, - "image": true, - "cost": { - "input": 3, - "output": 15, - "cache_read": 0.3, - "cache_write": 3.75 - } - }, "vercel/anthropic/claude-sonnet-5": { "name": "Claude Sonnet 5", "context": 1000000, @@ -60958,11 +61203,6 @@ "output": 5.4 } }, - "vercel/quiverai/arrow-1.1": { - "name": "Arrow 1.1", - "context": 131072, - "output": 131072 - }, "vercel/quiverai/arrow-2": { "name": "Arrow 2", "context": 131072, @@ -61290,6 +61530,20 @@ "cache_read": 0.002 } }, + "vercel/meta/muse-spark-open": { + "name": "Muse Spark Open", + "context": 1048576, + "output": 1048576, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "cost": { + "input": 1.25, + "output": 4.25, + "cache_read": 0.15 + } + }, "vercel/meta/llama-3.3-70b": { "name": "Llama-3.3-70B-Instruct", "context": 128000, @@ -62460,6 +62714,19 @@ "output": 0.65 } }, + "vercel/nvidia/nemotron-3.5-super-vl": { + "name": "NVIDIA Nemotron 3.5 Super VL", + "context": 1048576, + "output": 1048576, + "reasoning": true, + "tool_call": true, + "image": true, + "cost": { + "input": 0.2, + "output": 0.5, + "cache_read": 0.04 + } + }, "vercel/nvidia/nemotron-3-ultra-550b-a55b": { "name": "Nemotron 3 Ultra", "context": 1000000, @@ -62674,8 +62941,8 @@ }, "vercel/stepfun/step-5-preview": { "name": "Step 5 Preview", - "context": 1000000, - "output": 1000000, + "context": 1048576, + "output": 1048576, "structured_output": true, "image": true, "cost": { @@ -64867,6 +65134,18 @@ "cache_read": 0.016 } }, + "hyper/prism": { + "name": "Prism", + "context": 1000000, + "output": 16000, + "tool_call": true, + "image": true, + "cost": { + "input": 3.2664, + "output": 16.332, + "cache_read": 0.32664 + } + }, "hyper/glm-5.2": { "name": "GLM-5.2", "context": 1000000, @@ -65419,6 +65698,20 @@ "cache_read": 0.015 } }, + "fireworks-ai/accounts/fireworks/models/muse-spark-open": { + "name": "Muse Spark Open", + "context": 1048576, + "output": 131072, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "cost": { + "input": 1.25, + "output": 4.25, + "cache_read": 0.15 + } + }, "fireworks-ai/accounts/fireworks/models/qwen3p8-max": { "name": "Qwen3.8 Max", "context": 262144, @@ -74452,15 +74745,16 @@ }, "merge-gateway/deepseek/deepseek-v4-flash": { "name": "DeepSeek V4 Flash", - "context": 1048576, + "context": 1000000, "output": 384000, "reasoning": true, "tool_call": true, "structured_output": true, "cost": { - "input": 0.035, - "output": 0.07, - "cache_read": 0.007 + "input": 0.3, + "output": 1.2, + "cache_read": 0.006, + "cache_write": 0.1125 } }, "merge-gateway/thinkingmachines/inkling": { @@ -74533,6 +74827,19 @@ "output": 0.99 } }, + "merge-gateway/meta/muse-spark-open": { + "name": "Muse Spark Open", + "context": 1048576, + "output": 262144, + "reasoning": true, + "tool_call": true, + "image": true, + "cost": { + "input": 1.25, + "output": 4.25, + "cache_read": 0.15 + } + }, "merge-gateway/bytedance/dola-seed-2.0-code-preview": { "name": "Dola Seed 2.0 Code (preview)", "context": 131072, @@ -75422,9 +75729,9 @@ "structured_output": true, "image": true, "cost": { - "input": 0.287, - "output": 1.147, - "cache_read": 0.15785 + "input": 0.21, + "output": 1.9, + "cache_read": 0.105 } }, "merge-gateway/qwen/qwen3.7-max": { @@ -75610,8 +75917,8 @@ "structured_output": true, "image": true, "cost": { - "input": 0.108, - "output": 0.43, + "input": 0.15, + "output": 0.6, "cache_read": 0.0216 } }, @@ -75686,9 +75993,9 @@ "tool_call": true, "structured_output": true, "cost": { - "input": 0.144, - "output": 0.574, - "cache_read": 0.0288 + "input": 0.1, + "output": 1.1, + "cache_read": 0.05 } }, "merge-gateway/qwen/qwen3.6-flash": { @@ -80922,19 +81229,6 @@ "cache_read": 0.003625 } }, - "opencode-go/qwen3.7-max": { - "name": "Qwen3.7 Max", - "context": 1000000, - "output": 65536, - "reasoning": true, - "tool_call": true, - "cost": { - "input": 2.5, - "output": 7.5, - "cache_read": 0.5, - "cache_write": 3.125 - } - }, "opencode-go/mimo-v2.5": { "name": "MiMo V2.5", "context": 1000000, @@ -81051,32 +81345,19 @@ "cache_read": 0.003 } }, - "opencode-go/deepseek-v4-flash-vision-exp": { - "name": "DeepSeek V4 Flash Vision Exp", + "opencode-go/step-5-preview": { + "name": "Step 5 Preview", "context": 1000000, - "output": 384000, + "output": 65536, "reasoning": true, "tool_call": true, "structured_output": true, "image": true, - "cost": { - "input": 0.15, - "output": 0.6, - "cache_read": 0.003 - } - }, - "opencode-go/kimi-k2.6": { - "name": "Kimi K2.6", - "context": 262144, - "output": 65536, - "reasoning": true, - "tool_call": true, - "image": true, "video": true, "cost": { - "input": 0.95, - "output": 4, - "cache_read": 0.16 + "input": 1, + "output": 2.7, + "cache_read": 0.1 } }, "opencode-go/longcat-2.0": { @@ -81093,7 +81374,7 @@ }, "opencode-go/cat-jam-free": { "name": "Cat Jam Free", - "context": 262144, + "context": 1048576, "output": 131072, "reasoning": true, "tool_call": true, @@ -81104,20 +81385,6 @@ "cache_read": 0 } }, - "opencode-go/grok-4.5": { - "name": "Grok 4.5", - "context": 500000, - "output": 500000, - "reasoning": true, - "tool_call": true, - "structured_output": true, - "image": true, - "cost": { - "input": 2, - "output": 6, - "cache_read": 0.3 - } - }, "opencode-go/space-bunny": { "name": "Space Bunny", "context": 1048576, @@ -81146,21 +81413,6 @@ "cache_write": 0.375 } }, - "opencode-go/space-bunny-free": { - "name": "Space Bunny Free", - "context": 1048576, - "output": 524288, - "reasoning": true, - "tool_call": true, - "image": true, - "video": true, - "cost": { - "input": 0, - "output": 0, - "cache_read": 0, - "cache_write": 0 - } - }, "opencode-go/mimo-v2.5-pro": { "name": "MiMo V2.5 Pro", "context": 1048576, @@ -81315,21 +81567,6 @@ "cache_read": 0.022 } }, - "opencode-go/qwen3.6-plus": { - "name": "Qwen3.6 Plus", - "context": 1000000, - "output": 65536, - "reasoning": true, - "tool_call": true, - "image": true, - "video": true, - "cost": { - "input": 0.5, - "output": 3, - "cache_read": 0.05, - "cache_write": 0.625 - } - }, "opencode-go/hy4-preview": { "name": "Hy4 preview", "context": 1024000, @@ -81400,20 +81637,6 @@ "cache_read": 0.19 } }, - "opencode-go/grok-4.6": { - "name": "Grok 4.6", - "context": 500000, - "output": 500000, - "reasoning": true, - "tool_call": true, - "structured_output": true, - "image": true, - "cost": { - "input": 2, - "output": 6, - "cache_read": 0.5 - } - }, "opencode-go/qwen3.7-plus": { "name": "Qwen3.7 Plus", "context": 1000000, @@ -81429,19 +81652,6 @@ "cache_write": 0.5 } }, - "opencode-go/deepseek-v4-flash": { - "name": "DeepSeek V4 Flash", - "context": 1000000, - "output": 384000, - "reasoning": true, - "tool_call": true, - "structured_output": true, - "cost": { - "input": 0.15, - "output": 0.6, - "cache_read": 0.003 - } - }, "openreason/deepseek-ai/deepseek-v4-flash-0731": { "name": "DeepSeek V4 Flash 0731", "context": 1000000, @@ -84070,6 +84280,21 @@ "cache_read": 0.5 } }, + "edenai/nebius/meta-models/muse-spark-open": { + "name": "Muse Spark Open (Nebius)", + "context": 1048576, + "output": 131072, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true, + "cost": { + "input": 1.25, + "output": 4.25, + "cache_read": 1.25 + } + }, "edenai/nebius/google/gemma-3-27b-it": { "name": "Gemma 3 27B IT (Nebius)", "context": 110000, @@ -84190,9 +84415,9 @@ "reasoning": true, "tool_call": true, "cost": { - "input": 0.4478, - "output": 0.8956, - "cache_read": 0.08956 + "input": 0.44832, + "output": 0.89664, + "cache_read": 0.089664 } }, "edenai/scaleway/qwen3-235b-a22b-instruct-2507": { @@ -84201,8 +84426,8 @@ "output": 16384, "tool_call": true, "cost": { - "input": 0.839625, - "output": 2.518875 + "input": 0.8406, + "output": 2.5218 } }, "edenai/scaleway/deepseek-v4.1-flash": { @@ -84213,9 +84438,9 @@ "tool_call": true, "image": true, "cost": { - "input": 0.6717, - "output": 2.0151, - "cache_read": 0.13434 + "input": 0.67248, + "output": 2.01744, + "cache_read": 0.134496 } }, "edenai/scaleway/qwen3-coder-30b-a3b-instruct": { @@ -84224,8 +84449,8 @@ "output": 65536, "tool_call": true, "cost": { - "input": 0.2239, - "output": 0.8956 + "input": 0.22416, + "output": 0.89664 } }, "edenai/scaleway/llama-3.3-70b-instruct": { @@ -84234,8 +84459,8 @@ "output": 4096, "tool_call": true, "cost": { - "input": 1.00755, - "output": 1.00755 + "input": 1.00872, + "output": 1.00872 } }, "edenai/scaleway/gpt-oss-120b": { @@ -84245,8 +84470,8 @@ "reasoning": true, "tool_call": true, "cost": { - "input": 0.167925, - "output": 0.6717 + "input": 0.16812, + "output": 0.67248 } }, "edenai/ionos/meta-llama/Llama-3.3-70B-Instruct": { @@ -84256,8 +84481,8 @@ "tool_call": true, "structured_output": true, "cost": { - "input": 0.727675, - "output": 0.727675 + "input": 0.72852, + "output": 0.72852 } }, "edenai/ionos/Qwen/Qwen3-Coder-Next": { @@ -84267,8 +84492,8 @@ "tool_call": true, "structured_output": true, "cost": { - "input": 0.167925, - "output": 0.8956 + "input": 0.16812, + "output": 0.89664 } }, "edenai/ionos/openai/gpt-oss-120b": { @@ -84279,8 +84504,8 @@ "tool_call": true, "structured_output": true, "cost": { - "input": 0.167925, - "output": 0.727675 + "input": 0.16812, + "output": 0.72852 } }, "edenai/vertex/gemini-4-argon@us": { @@ -84753,9 +84978,9 @@ "tool_call": true, "structured_output": true, "cost": { - "input": 0.15673, - "output": 0.391825, - "cache_read": 0.04478 + "input": 0.156912, + "output": 0.39228, + "cache_read": 0.044832 } }, "edenai/greenpt/qwen3-235b-a22b-instruct-2507": { @@ -84765,8 +84990,8 @@ "tool_call": true, "structured_output": true, "cost": { - "input": 1.00755, - "output": 3.02265 + "input": 1.00872, + "output": 3.026159 } }, "edenai/greenpt/deepseek-v4.1-flash": { @@ -84777,9 +85002,9 @@ "tool_call": true, "image": true, "cost": { - "input": 0.24629, - "output": 1.23145, - "cache_read": 0.012314 + "input": 0.246576, + "output": 1.23288, + "cache_read": 0.012329 } }, "edenai/greenpt/llama-3.3-70b-instruct": { @@ -84789,8 +85014,8 @@ "tool_call": true, "structured_output": true, "cost": { - "input": 1.23145, - "output": 1.23145 + "input": 1.23288, + "output": 1.23288 } }, "edenai/greenpt/gpt-oss-120b": { @@ -84801,8 +85026,8 @@ "tool_call": true, "structured_output": true, "cost": { - "input": 0.2239, - "output": 0.78365 + "input": 0.22416, + "output": 0.78456 } }, "edenai/greenference/deepseek-v4.1-flash": { @@ -85458,6 +85683,21 @@ "cache_read": 0.015 } }, + "edenai/fireworks_ai/accounts/fireworks/models/muse-spark-open": { + "name": "Muse Spark Open (Fireworks AI)", + "context": 1048576, + "output": 131072, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true, + "cost": { + "input": 1.25, + "output": 4.25, + "cache_read": 0.15 + } + }, "edenai/mistral/codestral-latest": { "name": "Codestral (latest)", "context": 256000, @@ -86424,8 +86664,8 @@ "structured_output": true, "image": true, "cost": { - "input": 0.33585, - "output": 0.4478 + "input": 0.33624, + "output": 0.44832 } }, "edenai/moonshot/kimi-k3": { @@ -88735,7 +88975,7 @@ }, "opencode/cat-jam-free": { "name": "Cat Jam Free", - "context": 262144, + "context": 1048576, "output": 131072, "reasoning": true, "tool_call": true, @@ -88979,6 +89219,19 @@ "cache_write": 0 } }, + "opencode/ll-alpha-free": { + "name": "LL Alpha Free", + "context": 262144, + "output": 65536, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "cost": { + "input": 0, + "output": 0, + "cache_read": 0 + } + }, "opencode/claude-fable-5": { "name": "Claude Fable 5", "context": 1000000, @@ -89251,6 +89504,21 @@ "output": 0 } }, + "opencode/glm-5.3-flash-free": { + "name": "GLM-5.3-Flash Free", + "context": 200000, + "output": 32000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true, + "cost": { + "input": 0, + "output": 0, + "cache_read": 0 + } + }, "opencode/claude-sonnet-5": { "name": "Claude Sonnet 5", "context": 1000000, @@ -89346,6 +89614,19 @@ "cache_read": 0.26 } }, + "opencode/nemotron-3.5-super-vl-free": { + "name": "Nemotron 3.5 Super VL Free", + "context": 1048576, + "output": 131072, + "reasoning": true, + "tool_call": true, + "image": true, + "cost": { + "input": 0, + "output": 0, + "cache_read": 0 + } + }, "opencode/kimi-k2": { "name": "Kimi K2", "context": 262144, @@ -93745,6 +94026,20 @@ "cache_write": 6.875 } }, + "requesty/muse-spark-open": { + "name": "Muse Spark Open", + "context": 1048576, + "output": 131072, + "reasoning": true, + "tool_call": true, + "image": true, + "video": true, + "cost": { + "input": 1.25, + "output": 4.25, + "cache_read": 0.15 + } + }, "requesty/gemini-4-argon": { "name": "Gemini 4 Argon", "context": 1048576, @@ -98632,21 +98927,6 @@ "cache_write": 0.08333 } }, - "llmgateway/gemini-2.5-pro": { - "name": "Gemini 2.5 Pro", - "context": 1048576, - "output": 65536, - "reasoning": true, - "tool_call": true, - "structured_output": true, - "image": true, - "video": true, - "cost": { - "input": 1.25, - "output": 10, - "cache_read": 0.125 - } - }, "llmgateway/qwen3.6-flash": { "name": "Qwen3.6 Flash", "context": 983616, @@ -98663,21 +98943,6 @@ "cache_write": 0.3125 } }, - "llmgateway/gemini-2.5-flash": { - "name": "Gemini 2.5 Flash", - "context": 1048576, - "output": 65536, - "reasoning": true, - "tool_call": true, - "structured_output": true, - "image": true, - "video": true, - "cost": { - "input": 0.3, - "output": 2.5, - "cache_read": 0.03 - } - }, "llmgateway/qwen3-coder-flash": { "name": "Qwen3 Coder Flash", "context": 997952, @@ -99428,21 +99693,6 @@ "cache_read": 0.04 } }, - "llmgateway/gemini-2.5-flash-lite": { - "name": "Gemini 2.5 Flash-Lite", - "context": 1048576, - "output": 65536, - "reasoning": true, - "tool_call": true, - "structured_output": true, - "image": true, - "video": true, - "cost": { - "input": 0.1, - "output": 0.4, - "cache_read": 0.01 - } - }, "llmgateway/muse-wonder-1.0": { "name": "Muse Wonder", "context": 1048576, @@ -101167,6 +101417,19 @@ "output": 25 } }, + "prime/anthropic/claude-sonnet-5.5": { + "name": "Claude Sonnet 5.5", + "context": 1000000, + "output": 128000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "cost": { + "input": 2, + "output": 10 + } + }, "prime/anthropic/claude-fable-5.1": { "name": "Claude Fable 5.1", "context": 1000000, @@ -101333,6 +101596,20 @@ "cache_write": 1.4 } }, + "prime/meta/muse-spark-open": { + "name": "Muse Spark Open", + "context": 1048576, + "output": 943718, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true, + "cost": { + "input": 1.25, + "output": 4.25 + } + }, "prime/x-ai/grok-4.7": { "name": "Grok 4.7", "context": 500000, @@ -101653,19 +101930,6 @@ "output": 2 } }, - "prime/openai/gpt-5.2-pro": { - "name": "GPT-5.2 Pro", - "context": 400000, - "output": 128000, - "reasoning": true, - "tool_call": true, - "structured_output": true, - "image": true, - "cost": { - "input": 21, - "output": 168 - } - }, "prime/openai/gpt-5.3-codex": { "name": "GPT-5.3-Codex", "context": 400000, @@ -105590,18 +105854,6 @@ "output": 1.097 } }, - "cortecs/llama-3.1-8b-instruct": { - "name": "Llama-3.1-8B-Instruct", - "context": 128000, - "output": 128000, - "reasoning": true, - "tool_call": true, - "structured_output": true, - "cost": { - "input": 0.167, - "output": 0.167 - } - }, "cortecs/glm-5.1": { "name": "GLM-5.1", "context": 202752, @@ -109294,6 +109546,18 @@ "cache_read": 0.19 } }, + "digitalocean/nemotron-3.5-super-vl": { + "name": "NVIDIA Nemotron 3.5 Super VL", + "context": 1048576, + "output": 1048576, + "tool_call": true, + "image": true, + "cost": { + "input": 0.3, + "output": 0.65, + "cache_read": 0.06 + } + }, "digitalocean/deepseek-v3": { "name": "DeepSeek V3", "context": 163840, diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 6176edd..33f6e0c 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1978,7 +1978,7 @@ dependencies = [ [[package]] name = "kimiswitch" -version = "0.8.5" +version = "0.8.6" dependencies = [ "anyhow", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 74f23d6..554d2a8 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kimiswitch" -version = "0.8.5" +version = "0.8.6" description = "Kimi Switch - model config manager" authors = ["codingplan.site"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 7cb2963..e560934 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,6 +1,6 @@ { "productName": "Kimi Switch", - "version": "0.8.5", + "version": "0.8.6", "identifier": "com.kimiswitch.app", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/App.tsx b/src/App.tsx index e0dd278..2f2f4b3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useReducer, useState } from "react"; +import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react"; import { invoke } from "@tauri-apps/api/core"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { useConfig } from "./hooks/useConfig"; @@ -99,7 +99,20 @@ export default function App() { const [pendingNewProviders, setPendingNewProviders] = useState>( () => new Set(), ); + // The provider an earlier name collision overwrote in memory, held so it + // can be put back. Non-null means the in-memory config is missing an entry, + // so trySave hard-blocks on it; the rename / delete paths restore it. + const displacedRef = useRef<{ key: string; provider: Provider } | null>(null); + // Every "abandon the in-memory config" path goes through here. The reload + // brings back the on-disk truth, which includes whatever a collision + // displaced, so the stash is obsolete once the reload has actually + // returned. Clearing it only afterwards keeps a *failed* reload (config + // unchanged, still collided) from looking clean to trySave. + const reloadFromDisk = useCallback(async () => { + await refresh(); + displacedRef.current = null; + }, [refresh]); useEffect(() => { if (!loading) { @@ -170,6 +183,26 @@ export default function App() { async (target?: string): Promise => { if (!config) return false; if (dirty) { + // Hard blocks ahead of the completeness confirm. Both describe a + // config that loses data the moment it reaches disk, so there is + // nothing here for the user to confirm. + if (displacedRef.current) { + alert(t("saveBlockedDisplaced")); + return false; + } + const blankNames = Object.keys(config.providers).filter( + (key) => key.trim() === "" + ); + if (blankNames.length > 0) { + // JSON.stringify keeps a blank name readable in the message — + // joining the raw keys renders as nothing after the colon. + alert( + t("saveBlockedNameConflict", { + names: blankNames.map((k) => JSON.stringify(k)).join(", "), + }) + ); + return false; + } const issues = validateProviders(config, config.models, tGeneric, target); if (issues.length > 0) { const summary = issues @@ -203,7 +236,7 @@ export default function App() { case "R": e.preventDefault(); if (dirty && !confirm(t("unsavedConfirm"))) return; - refresh(); + void reloadFromDisk(); break; case "o": case "O": @@ -216,7 +249,7 @@ export default function App() { }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); - }, [trySave, refresh, dirty, t, agent]); + }, [trySave, reloadFromDisk, dirty, t, agent]); // "Add provider" opens the preset picker; the empty form is reachable via // the picker's "+ custom config" entry (handleAddCustomProvider). @@ -228,7 +261,15 @@ export default function App() { const handleAddCustomProvider = () => { if (!config) return; setShowPresetPicker(false); - const name = `provider-${providers.length + 1}`; + // Counting alone can land on a taken name (delete `provider-2`, add: + // `provider-3` may already exist), and that collision would silently + // overwrite an existing provider before the user types anything. + let n = providers.length + 1; + let name = `provider-${n}`; + while (name in config.providers) { + n++; + name = `provider-${n}`; + } const defaultType = agent === "kimi_code" ? "kimi" : "openai"; updateConfig((cfg) => ({ ...cfg, @@ -292,6 +333,16 @@ export default function App() { const oldEntry = Object.entries(providers).find( ([, p]) => p.name === editingProvider ); + const oldKey = oldEntry ? oldEntry[0] : editingProvider; + // Judged against the pre-change map: the name this write lands on is + // exactly the entry the overwrite below would destroy without a trace. + const displacedKey = + provider.name !== oldKey && provider.name in cfg.providers + ? provider.name + : null; + // Both branches assign `next`; the initial value is only there so + // TypeScript can infer its type. + let next = cfg; if (oldEntry && oldEntry[0] !== provider.name) { delete providers[oldEntry[0]]; // Re-key this provider's model aliases so the `/` @@ -317,10 +368,37 @@ export default function App() { // pool entries. const raw_other = rekeySubagentPoolAliases(cfg.raw_other, aliasMap); setEditingProvider(provider.name); - return { ...cfg, providers, models, default_model, raw_other }; + next = { ...cfg, providers, models, default_model, raw_other }; + } else { + providers[provider.name] = provider; + next = { ...cfg, providers }; } - providers[provider.name] = provider; - return { ...cfg, providers }; + // Put back the entry an earlier collision displaced. This must run + // AFTER the rename above moved the edited provider off that key: while + // the edited entry still holds it, the key is present in `providers` + // and the condition would never fire. Running it before the stashing + // below also empties the ref, so a second collision in a row can stash + // its own victim instead of being skipped. + if ( + displacedRef.current && + provider.name !== displacedRef.current.key && + !(displacedRef.current.key in providers) + ) { + providers[displacedRef.current.key] = displacedRef.current.provider; + displacedRef.current = null; + } + // Stash whatever this write is about to overwrite. The write itself + // still goes through: the name input is controlled and reports every + // keystroke, so rejecting a colliding one would make the text jump back + // as the user types. Keeping the loss out of config.toml is trySave's + // job, not the input's. + if (displacedKey && displacedRef.current == null) { + displacedRef.current = { + key: displacedKey, + provider: cfg.providers[displacedKey], + }; + } + return next; }); }; @@ -329,6 +407,14 @@ export default function App() { updateConfig((cfg) => { const providers = { ...cfg.providers }; delete providers[name]; + // The stash holds a provider an earlier collision overwrote in memory. + // Deleting frees the key the edited entry was squatting on, so the + // displaced provider can go back instead of staying stranded — a + // stranded stash blocks every later save until the user discards. + if (displacedRef.current && !(displacedRef.current.key in providers)) { + providers[displacedRef.current.key] = displacedRef.current.provider; + displacedRef.current = null; + } const models = { ...cfg.models }; const removedAliases: string[] = []; for (const key of Object.keys(models)) { @@ -371,19 +457,25 @@ export default function App() { }; const handleDuplicateProvider = async (name: string) => { + if (!config) return; + const src = config.providers[name]; + if (!src) return; + // Resolved outside the updater so the toast below can name the entry that + // was actually created (`-copy-2` and up when `-copy` is taken). The + // closure's config is the same map the updater starts from. + let newName = `${name}-copy`; + let n = 2; + while (config.providers[newName]) { + newName = `${name}-copy-${n}`; + n++; + } updateConfig((cfg) => { - const src = cfg.providers[name]; - if (!src) return cfg; - let newName = `${name}-copy`; - let n = 2; - while (cfg.providers[newName]) { - newName = `${name}-copy-${n}`; - n++; - } + const source = cfg.providers[name]; + if (!source) return cfg; // Clone the provider (deactivated, clear its remembered default model // since the model aliases will be re-keyed to the new name below). const copied = setProviderDefaultModel( - { ...src, name: newName, active: false }, + { ...source, name: newName, active: false }, null ); const providers = { ...cfg.providers, [newName]: copied }; @@ -395,7 +487,7 @@ export default function App() { // No immediate save: the copy lives in memory only, like every other // edit. Abandoning (back → discard) must leave the config unchanged; // the copy persists when the user saves (Ctrl+S / footer / switch). - setSwitchMessage(t("copiedProvider", { name: `${name}-copy` })); + setSwitchMessage(t("copiedProvider", { name: newName })); setTimeout(() => setSwitchMessage(null), 3000); }; @@ -468,7 +560,7 @@ export default function App() { if (!saved) { // Validation was cancelled: undo the in-memory switch by reloading // the on-disk state, and skip the activation + /reload nudge. - await refresh(); + await reloadFromDisk(); return; } await invoke("activate_agent_config_command", { agent }); @@ -495,17 +587,29 @@ export default function App() { next.delete(target); return next; }); - await refresh(); + await reloadFromDisk(); } else if (dirty) { // Real edits to a previously-saved provider — keep the existing // unsaved-changes confirm so the user doesn't lose work by accident. if (!confirm(t("unsavedConfirm"))) return; - await refresh(); + await reloadFromDisk(); } setView("list"); }; const handleApplyProviderJson = (provider: Provider, models: Model[]) => { + if (!config) return; + // Source-level rejection: applying is a single deliberate action, so + // refusing here leaves both the panel and the config untouched (unlike + // the name input, which takes every keystroke and stashes the loss). + if (provider.name.trim() === "") { + alert(t("validationNameEmpty")); + return; + } + if (provider.name !== editingProvider && provider.name in config.providers) { + alert(t("applyJsonNameConflict")); + return; + } updateConfig((cfg) => { const providers = { ...cfg.providers }; const oldName = editingProvider; @@ -581,7 +685,7 @@ export default function App() { @@ -746,9 +850,18 @@ export default function App() { agent={agent} /> ) : currentProvider ? ( + // `nameConflict` is read from the displaced-entry stash during + // render: the collision write happens inside updateConfig's + // updater, which sets the ref before React re-renders, so this pass + // already sees the new value (same precedent as setEditingProvider + // inside that updater). k !== editingProvider + )} + nameConflict={displacedRef.current != null} models={Object.values(config.models).filter( (m) => m.provider === currentProvider.name )} @@ -914,7 +1027,7 @@ export default function App() { if (dirty && !confirm(t("unsavedConfirm"))) { return; } - refresh(); + void reloadFromDisk(); }} disabled={loading} className="px-3 py-1.5 text-sm border border-border rounded hover:bg-hover-2 focus:ring-2 focus:ring-blue-500 focus:outline-none disabled:opacity-50" diff --git a/src/components/ProviderEdit.tsx b/src/components/ProviderEdit.tsx index 8526aa5..08af513 100644 --- a/src/components/ProviderEdit.tsx +++ b/src/components/ProviderEdit.tsx @@ -12,6 +12,7 @@ import { withMaxOutputSize, } from "../lib/model-max-output"; import { capabilitiesFromRef, getModelRef, modelsDevReady } from "../lib/models-dev"; +import { validateProviderName } from "../lib/validation"; import { getIconMetadata } from "../icons/extracted/metadata"; import { KimiOAuthDialog } from "./KimiOAuthDialog"; import { ProviderIcon } from "./ProviderIcon"; @@ -87,6 +88,15 @@ function defaultBaseUrl(agent: Agent, type: ProviderType): string { interface ProviderEditProps { agent: Agent; provider: Provider; + /** Names of every OTHER provider. Used to flag a colliding name as the + * user types; the enforced gates live in App.tsx (write path + trySave), + * because Ctrl+S is a window-level listener that bypasses this form. */ + allProviderNames: string[]; + /** Set while App holds a displaced entry: the name collides exactly with + * another provider, a case `allProviderNames` cannot see (the edited + * entry has already been re-keyed onto the colliding name and is + * excluded there). */ + nameConflict?: boolean; models: Model[]; defaultModel: string | null; onBack: () => void; @@ -104,6 +114,8 @@ interface ProviderEditProps { export function ProviderEdit({ agent, provider, + allProviderNames, + nameConflict, models, defaultModel, onBack, @@ -129,6 +141,14 @@ export function ProviderEdit({ const [showIconPicker, setShowIconPicker] = useState(false); const [kimiOAuthOpen, setKimiOAuthOpen] = useState(false); + // Display layer only. The enforced gates are the write path in App.tsx + // (which stashes whatever a collision overwrites) and trySave; Ctrl+S is a + // window-level listener, so a disabled button here cannot stop a save. + const nameIssue = validateProviderName(allProviderNames, provider.name); + // An exact collision is invisible to the check above (see `nameConflict`), + // so the stash flag has to widen the highlight too. + const nameError = nameIssue != null || nameConflict === true; + // Preset this provider was created from (if any) — provides the // "Get API Key" / referral links shown under the key input. const preset = findPresetForProvider(provider); @@ -170,7 +190,8 @@ export function ProviderEdit({ @@ -248,10 +269,21 @@ export function ProviderEdit({ onChange({ ...provider, name: e.target.value })} /> + {nameError && ( +

+ {nameIssue === "empty" + ? t("validationNameEmpty") + : t("validationNameDuplicate")} +

+ )}