From 139af188267c6742710e62e29af8df4530c91e28 Mon Sep 17 00:00:00 2001 From: KimiSwitch Dev Date: Mon, 27 Jul 2026 22:36:42 +0800 Subject: [PATCH] feat: v0.3.0 - fix provider data loss, oauth corruption & switch UX Bug fixes: - Fix config.toml wiped to single provider on switch (now writes all providers) - Fix managed:kimi-code oauth key corruption (oauth/kimi-code was rewritten to oauth/managed-kimi-code, breaking the official subscription) - Fix default_model not switching due to React 18 deferred state update (updateConfig now sets configRef synchronously before setConfig) - Strip KimiSwitch-private default_model from config.toml provider output Architecture: - config.toml is now the authoritative source for Kimi Code provider data - SQLite stores only metadata (notes, official URLs, remembered models) - Load reconciles config.toml with SQLite for external /provider changes - Activate is a no-op for KimiCode (save already writes config.toml) Features: - Selected provider moves to top of list on switch - Add models-dev context size lookup - Add tauri-plugin-single-instance Tests: - 3 new regression tests: oauth key round-trip, all-providers write, private default_model strip --- README.md | 25 +- README_EN.md | 25 +- package.json | 4 +- scripts/fetch-models-dev.mjs | 57 + src-tauri/Cargo.lock | 95 +- src-tauri/Cargo.toml | 3 +- src-tauri/src/commands.rs | 118 +- src-tauri/src/kimi_code_io.rs | 169 ++- src-tauri/src/lib.rs | 10 + src-tauri/tauri.conf.json | 2 +- src/App.tsx | 35 +- src/components/ProviderEdit.tsx | 16 +- src/hooks/useConfig.ts | 13 +- src/lib/models-dev.json | 1866 +++++++++++++++++++++++++++++++ src/lib/models-dev.ts | 57 + 15 files changed, 2398 insertions(+), 97 deletions(-) create mode 100644 scripts/fetch-models-dev.mjs create mode 100644 src/lib/models-dev.json create mode 100644 src/lib/models-dev.ts diff --git a/README.md b/README.md index eca90ff..cf3d538 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Kimi Switch 提供统一的图形界面: - 统一图形界面,分别管理 Kimi Code 和 Pi 两套 Agent 配置,顶部 Tab 切换 -- 一键切换供应商,自动写入对应 Agent 的原生配置 +- 一键切换供应商,更新对应 Agent 原生配置的 `default_model`,保留全部供应商 - 一键拉取模型列表(OpenAI / Anthropic / Google GenAI) - 切换后给出 `/reload` 提示,确保 Kimi Code 会话立即生效 @@ -96,10 +96,11 @@ Kimi Switch 提供统一的图形界面: **关键设计**: -- Kimi Switch 维护自己的 SQLite 数据库存**完整配置**(所有供应商 + 所有模型,含未激活的) -- 点「切换使用」时只把**当前激活的供应商**写入 Agent 原生配置,其他不写 -- 这样 Kimi Code / Pi 始终只看到一个活跃供应商,不会被托管/OAuth 默认值干扰 -- `raw_other` 字段透传未知键,保证前后往返不丢字段 +- 对 Kimi Code,`config.toml` 是供应商/模型数据的**权威来源**:所有供应商和模型始终全量保留,`default_model` 决定哪个生效(与 CLI 原生 `/provider` 行为一致) +- SQLite 只存 Kimi Switch 专有元数据(备注、官网、每个供应商记住的默认模型),并在 `config.toml` 不完整时兜底 +- 加载时以 `config.toml` 为准,因此通过 `/provider` 在 CLI 端增/改/删的供应商都会被正确反映,切换不再覆盖丢失 +- `raw_other` 字段透传未知键(含 `[oauth]` 段),保证前后往返不丢字段 +- Pi 的行为不变:切换时仍只写活跃供应商到 `models.json` ## 支持的供应商类型 @@ -118,8 +119,8 @@ Kimi Switch 提供统一的图形界面: | 文件 | 用途 | 备份 | | --- | --- | --- | -| `%USERPROFILE%\.kimi-switch\kimi-switch.db` | Kimi Switch 自己的 SQLite 数据库,存全量配置 | — | -| `%USERPROFILE%\.kimi-code\config.toml` | Kimi Code CLI 的 TOML 配置(**切换时写入**) | 同目录下 `backups/config.toml.bak.{YYYYMMDD_HHMMSS}`,保留 7 天 | +| `%USERPROFILE%\.kimi-switch\kimi-switch.db` | Kimi Switch 自己的 SQLite 数据库,存元数据(备注/官网/记住的模型)+ 迁移兜底 | — | +| `%USERPROFILE%\.kimi-code\config.toml` | Kimi Code CLI 的 TOML 配置(**权威数据源,切换/保存时写入**) | 同目录下 `backups/config.toml.bak.{YYYYMMDD_HHMMSS}`,保留 7 天 | | `%USERPROFILE%\.pi\agent\models.json` | Pi 的供应商+模型配置(**切换时写入**) | 同目录下 `backups/models.json.bak.{YYYYMMDD_HHMMSS}`,保留 7 天 | | `%USERPROFILE%\.pi\agent\settings.json` | Pi 的默认供应商/模型(**切换时写入**) | 同目录下 `backups/settings.json.bak.{YYYYMMDD_HHMMSS}`,保留 7 天 | | `localStorage[kimi-switch-agent]` | 前端记住上次选中的 Agent(kimi_code / pi) | — | @@ -213,9 +214,9 @@ npm run dev | 命令 | 说明 | | --- | --- | -| `load_agent_config_command(agent)` | 加载配置:优先 SQLite,为空则从 Agent 原生配置导入 | -| `save_agent_config_command(agent, config)` | 保存全量配置到 SQLite | -| `activate_agent_config_command(agent)` | 把当前激活的供应商写入 Agent 原生配置 | +| `load_agent_config_command(agent)` | 加载配置:Kimi Code 以 `config.toml` 为权威数据源,SQLite 补充元数据;Pi 优先 SQLite | +| `save_agent_config_command(agent, config)` | 保存到 SQLite;Kimi Code 同时写入 `config.toml` | +| `activate_agent_config_command(agent)` | 全量写入 Agent 原生配置(Kimi Code 写全部供应商,`default_model` 决定生效项;Pi 仅写活跃供应商) | | `open_agent_config_dir(agent)` | 用系统资源管理器打开 Agent 配置目录 | | `get_app_version()` | 返回 `Cargo.toml` 版本号 | | `list_provider_models(provider)` | 调供应商 API 拉取模型列表(异步) | @@ -241,7 +242,7 @@ npm run dev | 快捷键 | 作用 | | --- | --- | -| `Ctrl + S` | 保存当前修改到 SQLite | +| `Ctrl + S` | 保存当前修改到 SQLite + config.toml(Kimi Code) | | `Ctrl + R` | 重新读取配置(未保存时会提示) | | `Ctrl + O` | 打开当前 Agent 的配置目录 | @@ -303,7 +304,7 @@ A: 在 Kimi Code 会话里执行 `/reload`(Kimi Code CLI 才会重新读取 `~ A: 关闭窗口前会弹原生 `beforeunload` 提示,标题栏也会显示 `*` 前缀。 **Q: 怎么备份/迁移我的配置?** -A: 备份 `%USERPROFILE%\.kimi-switch\kimi-switch.db` 即可,里面存了全量配置(含未激活的供应商)。 +A: 对 Kimi Code,`config.toml` 本身就是全量配置的权威来源,直接备份它即可;SQLite 存额外的备注/官网等元数据,需要时一并备份 `kimi-switch.db`。对 Pi,备份 `kimi-switch.db`。 **Q: Vertex AI 为什么不能拉取模型列表?** A: Vertex 需要 GCP project/location 凭证,当前实现留了 TODO,等 GCP SDK 集成后再补。 diff --git a/README_EN.md b/README_EN.md index 8751aef..336f718 100644 --- a/README_EN.md +++ b/README_EN.md @@ -40,7 +40,7 @@ Kimi Switch provides a unified GUI: - One unified interface to manage two separate agent configs (Kimi Code and Pi), switched via a top tab -- One-click provider switching that writes the active provider into the target agent's native config +- One-click provider switching that updates `default_model` in the target agent's native config while preserving all providers - One-click model-list fetching (OpenAI / Anthropic / Google GenAI) - A `/reload` hint after switching so Kimi Code sessions take effect immediately @@ -96,10 +96,11 @@ Kimi Switch provides a unified GUI: **Key design points**: -- Kimi Switch keeps its own SQLite database for the **full config** (all providers + all models, including inactive ones) -- Clicking "Switch to" writes **only the active provider** into the agent's native config; the others are not written -- This way Kimi Code / Pi always see exactly one active provider and are never confused by managed/OAuth defaults -- The `raw_other` field passes unknown keys through untouched, so round-trips never lose fields +- For Kimi Code, `config.toml` is the **authoritative source** for provider/model data: all providers and models are always written in full, and `default_model` selects the active one (matching the CLI's native `/provider` behaviour) +- SQLite stores only Kimi Switch-private metadata (notes, official URLs, per-provider remembered default model) and acts as a fallback when `config.toml` is incomplete +- On load, `config.toml` takes precedence, so providers added/edited/deleted via the CLI's `/provider` are correctly reflected — switching no longer overwrites or loses them +- The `raw_other` field passes unknown keys through untouched (including `[oauth]` blocks), so round-trips never lose fields +- Pi behaviour is unchanged: switching still writes only the active provider to `models.json` ## Supported Provider Types @@ -118,8 +119,8 @@ Credential precedence: the `api_key` field wins over the same-named key in the ` | File | Purpose | Backup | | --- | --- | --- | -| `%USERPROFILE%\.kimi-switch\kimi-switch.db` | Kimi Switch's own SQLite database holding the full config | — | -| `%USERPROFILE%\.kimi-code\config.toml` | Kimi Code CLI's TOML config (**written on switch**) | `backups/config.toml.bak.{YYYYMMDD_HHMMSS}` next to it, kept 7 days | +| `%USERPROFILE%\.kimi-switch\kimi-switch.db` | Kimi Switch's own SQLite database, holding metadata (notes/official URLs/remembered models) + migration fallback | — | +| `%USERPROFILE%\.kimi-code\config.toml` | Kimi Code CLI's TOML config (**authoritative source, written on switch/save**) | `backups/config.toml.bak.{YYYYMMDD_HHMMSS}` next to it, kept 7 days | | `%USERPROFILE%\.pi\agent\models.json` | Pi's provider+model config (**written on switch**) | `backups/models.json.bak.{YYYYMMDD_HHMMSS}` next to it, kept 7 days | | `%USERPROFILE%\.pi\agent\settings.json` | Pi's default provider/model (**written on switch**) | `backups/settings.json.bak.{YYYYMMDD_HHMMSS}` next to it, kept 7 days | | `localStorage[kimi-switch-agent]` | Frontend remembers the last selected agent (kimi_code / pi) | — | @@ -213,9 +214,9 @@ Good for pure UI debugging. | Command | Description | | --- | --- | -| `load_agent_config_command(agent)` | Load config: SQLite first, falling back to the agent's native config on first use | -| `save_agent_config_command(agent, config)` | Save the full config to SQLite | -| `activate_agent_config_command(agent)` | Write the active provider into the agent's native config | +| `load_agent_config_command(agent)` | Load config: Kimi Code reads `config.toml` as the authoritative source, enriched with SQLite metadata; Pi reads SQLite first | +| `save_agent_config_command(agent, config)` | Save to SQLite; for Kimi Code also writes `config.toml` | +| `activate_agent_config_command(agent)` | Write to the agent's native config (Kimi Code writes all providers — `default_model` picks the active one; Pi writes only the active provider) | | `open_agent_config_dir(agent)` | Open the agent's config dir in the system file manager | | `get_app_version()` | Return the `Cargo.toml` version | | `list_provider_models(provider)` | Fetch model list from the provider API (async) | @@ -241,7 +242,7 @@ Good for pure UI debugging. | Shortcut | Action | | --- | --- | -| `Ctrl + S` | Save current changes to SQLite | +| `Ctrl + S` | Save current changes to SQLite + config.toml (Kimi Code) | | `Ctrl + R` | Reload config (prompts if unsaved) | | `Ctrl + O` | Open the current agent's config dir | @@ -303,7 +304,7 @@ A: Run `/reload` inside the Kimi Code session (the CLI only re-reads `~/.kimi-co A: A native `beforeunload` prompt appears before closing, and the title bar shows a `*` prefix. **Q: How do I back up / migrate my config?** -A: Back up `%USERPROFILE%\.kimi-switch\kimi-switch.db`; it holds the full config (including inactive providers). +A: For Kimi Code, `config.toml` itself is the authoritative full config — back it up directly. SQLite holds extra metadata (notes, official URLs); back up `kimi-switch.db` too if you need those. For Pi, back up `kimi-switch.db`. **Q: Why can't Vertex AI fetch the model list?** A: Vertex requires GCP project/location credentials. The current implementation leaves a TODO pending GCP SDK integration. diff --git a/package.json b/package.json index f4f20bf..d75e3c1 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,13 @@ { "name": "kimiswitch", "private": true, - "version": "0.2.0", + "version": "0.3.0", "type": "module", "scripts": { "dev": "vite", + "prebuild": "node scripts/fetch-models-dev.mjs || echo fetch-models-dev failed, using existing snapshot", "build": "tsc && vite build", + "fetch-models-dev": "node scripts/fetch-models-dev.mjs", "preview": "vite preview", "tauri": "tauri", "tauri-dev": "tauri dev", diff --git a/scripts/fetch-models-dev.mjs b/scripts/fetch-models-dev.mjs new file mode 100644 index 0000000..2e41f4a --- /dev/null +++ b/scripts/fetch-models-dev.mjs @@ -0,0 +1,57 @@ +#!/usr/bin/env node +/** + * Fetch the latest model reference data from models.dev and write a compact + * snapshot to src/lib/models-dev.json. + * + * Run manually (`npm run fetch-models-dev`) or automatically before builds + * (`prebuild`). Requires network access to models.dev. + */ + +import { writeFile, mkdir } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const SOURCE_URL = "https://models.dev/models.json"; +const ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); +const OUTPUT = join(ROOT, "src", "lib", "models-dev.json"); + +const res = await fetch(SOURCE_URL, { + headers: { Accept: "application/json" }, + signal: AbortSignal.timeout(30_000), +}); +if (!res.ok) { + throw new Error(`GET ${SOURCE_URL} returned HTTP ${res.status}`); +} + +/** @type {Record} */ +const raw = await res.json(); + +// Keep only the fields the app needs; the raw file carries benchmarks, +// descriptions, etc. that would bloat the bundle. +const snapshot = {}; +for (const [key, m] of Object.entries(raw)) { + if (!m || typeof m !== "object") continue; + const entry = {}; + if (typeof m.name === "string") entry.name = m.name; + // context 0 means "not applicable" (image/audio models) — treat as missing + // so callers fall back to regex defaults instead of storing 0. + if (typeof m.limit?.context === "number" && m.limit.context > 0) { + entry.context = m.limit.context; + } + if (m.reasoning === true) entry.reasoning = true; + if (m.tool_call === true) entry.tool_call = true; + if (m.structured_output === true) entry.structured_output = true; + const input = m.modalities?.input; + if (Array.isArray(input)) { + if (input.includes("image")) entry.image = true; + if (input.includes("video")) entry.video = true; + } + snapshot[key] = entry; +} + +await mkdir(dirname(OUTPUT), { recursive: true }); +await writeFile(OUTPUT, JSON.stringify(snapshot, null, 2) + "\n", "utf8"); + +console.log( + `models-dev snapshot: ${Object.keys(snapshot).length} models -> ${OUTPUT}`, +); diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 19e5aa3..50a07ef 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1952,7 +1952,7 @@ dependencies = [ [[package]] name = "kimiswitch" -version = "0.2.0" +version = "0.3.0" dependencies = [ "anyhow", "chrono", @@ -1965,6 +1965,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-opener", + "tauri-plugin-single-instance", "toml 0.8.23", ] @@ -3773,6 +3774,22 @@ dependencies = [ "zbus", ] +[[package]] +name = "tauri-plugin-single-instance" +version = "2.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3214becf9ef5783c0ae99a3bb25adf5353a7a16ebf53e74b909e29205735c6c" +dependencies = [ + "serde", + "serde_json", + "tauri", + "thiserror 2.0.18", + "tokio", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + [[package]] name = "tauri-runtime" version = "2.11.3" @@ -4883,6 +4900,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -4931,13 +4957,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows-threading" version = "0.1.0" @@ -4974,6 +5017,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -4992,6 +5041,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -5010,12 +5065,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -5034,6 +5101,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -5052,6 +5125,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -5070,6 +5149,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -5088,6 +5173,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.5.40" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 0985076..ebb51b8 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kimiswitch" -version = "0.2.0" +version = "0.3.0" description = "Kimi Switch - model config manager" authors = ["you"] edition = "2021" @@ -11,6 +11,7 @@ tauri-build = { version = "2.0.0", features = [] } [dependencies] tauri = { version = "2.0.0", features = ["tray-icon"] } tauri-plugin-opener = "2.0.0" +tauri-plugin-single-instance = "2.0.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" indexmap = { version = "2.2", features = ["serde"] } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index bb7d3fa..12d6e86 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -18,33 +18,77 @@ pub fn debug_log(message: String) { #[tauri::command] pub fn load_agent_config_command(agent: Agent) -> Result { - // Load from Kimi Switch's own SQLite database first. - match db::load_config(&agent) { - Ok(config) if !config.providers.is_empty() => Ok(config), - _ => { - // Fallback to the agent's native config on first use. - match agent { - Agent::KimiCode => crate::kimi_code_io::load_kimi_code_config_as_config() - .map_err(fmt_anyhow), - Agent::Pi => { - let file = pi_io::load_pi_models().map_err(fmt_anyhow)?; - let mut config = pi_io::pi_file_to_config(&file); - if config.default_model.is_none() { - if let Ok(settings) = pi_io::load_pi_settings() { - if let (Some(provider), Some(model_id)) = - (settings.default_provider, settings.default_model) - { - if let Some(alias) = config.models.values().find(|m| { - m.provider == provider && m.model == model_id - }) { - config.default_model = Some(alias.alias.clone()); + // Load Kimi Switch's own SQLite database (metadata + migration fallback). + let db_config = db::load_config(&agent).ok(); + + match agent { + Agent::KimiCode => { + // config.toml is the authoritative source for provider/model data + // because the user can add or edit providers at any time via the + // CLI's /provider command. SQLite only enriches with Kimi + // Switch-private metadata (note, official_url, remembered default + // model) and fills gaps when config.toml is incomplete. + let mut config = crate::kimi_code_io::load_kimi_code_config_as_config() + .map_err(fmt_anyhow)?; + + if let Some(db) = &db_config { + // Enrich config.toml providers with SQLite metadata. + for (name, p) in config.providers.iter_mut() { + if let Some(db_p) = db.providers.get(name) { + p.note = db_p.note.clone(); + p.official_url = db_p.official_url.clone(); + // Restore the remembered per-provider default model + // (Kimi-Switch-private, stored in raw_other). + if let Some(dm) = db_p.raw_other.get("default_model") { + match &mut p.raw_other { + serde_json::Value::Object(obj) => { + obj.insert("default_model".to_string(), dm.clone()); + } + _ => { + let mut obj = serde_json::Map::new(); + obj.insert("default_model".to_string(), dm.clone()); + p.raw_other = serde_json::Value::Object(obj); } } } } - Ok(config) + } + // Migration safety: include providers/models that exist in + // SQLite but not in config.toml (e.g. after upgrading from the + // old single-provider-write behaviour). + for (name, p) in &db.providers { + config.providers.entry(name.clone()).or_insert_with(|| p.clone()); + } + for (alias, m) in &db.models { + config.models.entry(alias.clone()).or_insert_with(|| m.clone()); } } + + Ok(config) + } + Agent::Pi => { + // Pi: SQLite first, fall back to native config on first use. + if let Some(config) = db_config { + if !config.providers.is_empty() { + return Ok(config); + } + } + let file = pi_io::load_pi_models().map_err(fmt_anyhow)?; + let mut config = pi_io::pi_file_to_config(&file); + if config.default_model.is_none() { + if let Ok(settings) = pi_io::load_pi_settings() { + if let (Some(provider), Some(model_id)) = + (settings.default_provider, settings.default_model) + { + if let Some(alias) = config.models.values().find(|m| { + m.provider == provider && m.model == model_id + }) { + config.default_model = Some(alias.alias.clone()); + } + } + } + } + Ok(config) } } } @@ -52,19 +96,30 @@ pub fn load_agent_config_command(agent: Agent) -> Result { #[tauri::command] pub fn save_agent_config_command(agent: Agent, config: Config) -> Result<(), String> { // Save the full Kimi Switch configuration to local SQLite. - db::save_config(&agent, &config).map_err(fmt_anyhow) + db::save_config(&agent, &config).map_err(fmt_anyhow)?; + // For Kimi Code, config.toml is the authoritative provider store, so + // persist changes there immediately — not only on activation. This + // ensures edits (Ctrl+S) survive a restart even without switching. + if matches!(agent, Agent::KimiCode) { + crate::kimi_code_io::save_config_as_kimi_code(&config).map_err(fmt_anyhow)?; + } + Ok(()) } #[tauri::command] pub fn activate_agent_config_command(agent: Agent) -> Result<(), String> { - // Load the full config from SQLite and write only the active provider - // to the agent's native config file. - let config = db::load_config(&agent).map_err(fmt_anyhow)?; - let active_config = build_active_config(&config); match agent { - Agent::KimiCode => crate::kimi_code_io::save_config_as_kimi_code(&active_config) - .map_err(fmt_anyhow), + Agent::KimiCode => { + // No-op: save_agent_config_command already writes config.toml for + // Kimi Code. Avoiding a second write here prevents a redundant disk + // write + backup on every switch. + Ok(()) + } Agent::Pi => { + // Load the full config from SQLite and write only the active + // provider to Pi's native config files. + let config = db::load_config(&agent).map_err(fmt_anyhow)?; + let active_config = build_active_config(&config); let file = pi_io::config_to_pi_file(&active_config); pi_io::save_pi_models(&file).map_err(fmt_anyhow)?; @@ -87,9 +142,10 @@ fn active_provider_and_model(config: &Config) -> Option<(String, String)> { } fn build_active_config(config: &Config) -> Config { - // Only the provider explicitly marked as active is written to the agent's - // native config. This ensures Kimi Code / Pi follow Kimi Switch's choice - // instead of falling back to a managed/native provider. + // Used only by Pi: writes only the provider explicitly marked as active + // to Pi's native config so Pi follows Kimi Switch's selection instead of + // falling back to another provider. Kimi Code does not use this — it + // writes all providers and selects via default_model. let providers: IndexMap = config .providers .iter() diff --git a/src-tauri/src/kimi_code_io.rs b/src-tauri/src/kimi_code_io.rs index c4d5d8d..6d90cb0 100644 --- a/src-tauri/src/kimi_code_io.rs +++ b/src-tauri/src/kimi_code_io.rs @@ -174,8 +174,13 @@ pub fn kimi_code_to_config(value: &TomlValue) -> Config { rest.remove("api_key"); rest.remove("managed"); rest.remove("enabled"); - rest.remove("oauth"); rest.remove("env"); + // NOTE: `oauth` is intentionally kept in raw_other so that the + // exact `storage`/`key` block round-trips verbatim on export. + // Previously it was stripped here and regenerated on export + // with a key derived from the provider name, which corrupted + // the managed:kimi-code credential reference (e.g. + // "oauth/kimi-code" became "oauth/managed-kimi-code"). toml_value_to_json(&TomlValue::Table(rest)) }; @@ -234,11 +239,10 @@ pub fn config_to_kimi_code(config: &Config, existing: Option<&TomlValue>) -> Tom let mut providers_table = Table::new(); for (name, provider) in &config.providers { - // Only write the active provider to Kimi Code config so the agent - // follows Kimi Switch's selection. - if !provider.active { - continue; - } + // Write ALL providers to Kimi Code config. The active provider is + // selected via `default_model`, so keeping the full list matches the + // CLI's native multi-provider behavior and prevents data loss when + // switching. let mut pt = Table::new(); pt.insert("type".to_string(), TomlValue::String(provider.provider_type.as_str().to_string())); if let Some(base_url) = provider.base_url.clone().filter(|s| !s.is_empty()) { @@ -273,6 +277,10 @@ pub fn config_to_kimi_code(config: &Config, existing: Option<&TomlValue>) -> Tom if let TomlValue::Table(mut extra) = json_to_toml(&provider.raw_other).unwrap_or(TomlValue::Table(Table::new())) { extra.remove("oauth"); extra.remove("env"); + // Strip Kimi-Switch-private field: the remembered per-provider + // default model is stored in raw_other.default_model and must NOT + // leak into the agent's config.toml. + extra.remove("default_model"); for (k, v) in extra { pt.insert(k, v); } @@ -281,19 +289,10 @@ pub fn config_to_kimi_code(config: &Config, existing: Option<&TomlValue>) -> Tom } root.insert("providers".to_string(), TomlValue::Table(providers_table)); - // Collect provider names that survived the filter above. - let active_provider_names: std::collections::HashSet<&str> = config - .providers - .values() - .filter(|p| p.active) - .map(|p| p.name.as_str()) - .collect(); - + // Write all models — each provider's models are kept so the CLI's + // /provider command can list and switch between them. let mut models_table = Table::new(); for (alias, model) in &config.models { - if !active_provider_names.contains(model.provider.as_str()) { - continue; - } let mut mt = Table::new(); mt.insert("provider".to_string(), TomlValue::String(model.provider.clone())); mt.insert("model".to_string(), TomlValue::String(model.model.clone())); @@ -587,4 +586,140 @@ api_key = "" } } } + + #[test] + fn kimi_code_oauth_key_roundtrip() { + // Regression: importing then exporting managed:kimi-code must preserve + // the exact oauth key "oauth/kimi-code". Previously the oauth block + // was dropped on import and regenerated on export as + // "oauth/managed-kimi-code", breaking the official subscription. + let toml_str = r#" +default_model = "kimi-code/k3" + +[providers."managed:kimi-code"] +type = "kimi" +api_key = "" +base_url = "https://api.kimi.com/coding/v1" + +[providers."managed:kimi-code".oauth] +storage = "file" +key = "oauth/kimi-code" + +[models."kimi-code/k3"] +provider = "managed:kimi-code" +model = "k3" +max_context_size = 1048576 +"#; + let value: TomlValue = toml_str.parse().unwrap(); + let config = kimi_code_to_config(&value); + + // Export back to TOML. + let exported = config_to_kimi_code(&config, None); + let root = exported.as_table().unwrap(); + let providers = root.get("providers").unwrap().as_table().unwrap(); + let managed = providers.get("managed:kimi-code").unwrap().as_table().unwrap(); + let oauth = managed.get("oauth").unwrap().as_table().unwrap(); + assert_eq!( + oauth.get("key").and_then(|v| v.as_str()), + Some("oauth/kimi-code"), + "oauth key must round-trip verbatim, not be regenerated" + ); + assert_eq!( + oauth.get("storage").and_then(|v| v.as_str()), + Some("file") + ); + } + + #[test] + fn kimi_code_export_writes_all_providers() { + // Regression: inactive providers must still be written to config.toml + // so that switching does not wipe the provider list. + let mut providers = IndexMap::new(); + providers.insert( + "active-one".to_string(), + Provider { + name: "active-one".to_string(), + provider_type: ProviderType::Anthropic, + base_url: Some("https://a.example.com".to_string()), + api_key: Some("sk-a".to_string()), + env: IndexMap::new(), + note: None, + official_url: None, + managed: false, + enabled: true, + active: true, + raw_other: Value::Null, + }, + ); + providers.insert( + "inactive-one".to_string(), + Provider { + name: "inactive-one".to_string(), + provider_type: ProviderType::Openai, + base_url: Some("https://b.example.com".to_string()), + api_key: Some("sk-b".to_string()), + env: IndexMap::new(), + note: None, + official_url: None, + managed: false, + enabled: true, + active: false, + raw_other: Value::Null, + }, + ); + let config = Config { + default_model: None, + providers, + models: IndexMap::new(), + raw_other: Value::Null, + }; + + let exported = config_to_kimi_code(&config, None); + let root = exported.as_table().unwrap(); + let providers_table = root.get("providers").unwrap().as_table().unwrap(); + assert_eq!(providers_table.len(), 2, "both providers must be written"); + assert!(providers_table.contains_key("active-one")); + assert!(providers_table.contains_key("inactive-one")); + } + + #[test] + fn kimi_code_export_strips_private_default_model() { + // The remembered per-provider default model (raw_other.default_model) + // is a Kimi-Switch-private field and must NOT leak into config.toml. + let mut providers = IndexMap::new(); + providers.insert( + "p".to_string(), + Provider { + name: "p".to_string(), + provider_type: ProviderType::Anthropic, + base_url: None, + api_key: Some("sk-x".to_string()), + env: IndexMap::new(), + note: None, + official_url: None, + managed: false, + enabled: true, + active: true, + raw_other: serde_json::json!({"default_model": "some-alias"}), + }, + ); + let config = Config { + default_model: None, + providers, + models: IndexMap::new(), + raw_other: Value::Null, + }; + + let exported = config_to_kimi_code(&config, None); + let root = exported.as_table().unwrap(); + let provider = root + .get("providers").unwrap() + .as_table().unwrap() + .get("p").unwrap() + .as_table().unwrap(); + assert!( + !provider.contains_key("default_model"), + "private default_model must not leak into config.toml" + ); + } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5033eb1..f5ad9d7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -11,6 +11,16 @@ use tauri::Manager; pub fn run() { tauri::Builder::default() + // Single instance: must be registered before other plugins. When a + // second process is launched, this callback runs in the existing + // instance and simply brings its window back (including from tray). + .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { + if let Some(window) = app.get_webview_window("main") { + let _ = window.unminimize(); + let _ = window.show(); + let _ = window.set_focus(); + } + })) .plugin(tauri_plugin_opener::init()) .setup(|app| { println!("[Tauri] Setup started"); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index e009e93..a27a74e 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,6 +1,6 @@ { "productName": "Kimi Switch", - "version": "0.2.0", + "version": "0.3.0", "identifier": "com.kimiswitch.app", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/App.tsx b/src/App.tsx index 43863db..8aacfe2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,6 +6,7 @@ import { ProviderList } from "./components/ProviderList"; import { ProviderEdit } from "./components/ProviderEdit"; import { useTranslation } from "./i18n"; import { getDefaultMaxContextSize } from "./lib/model-defaults"; +import { getModelRef } from "./lib/models-dev"; import type { Agent, Model, Provider } from "./types"; const AGENT_STORAGE_KEY = "kimi-switch-agent"; @@ -242,26 +243,31 @@ export default function App() { } } - // Only the selected provider is active. All other providers (including - // Kimi native/managed and custom) are deactivated so the target agent - // follows Kimi Switch's choice exactly. + // Rebuild providers with the selected one moved to the front so it is + // visually prioritised. All others are deactivated. const providers: Record = {}; - for (const [key, p] of Object.entries(cfg.providers)) { - providers[key] = { ...p, active: key === name }; - } + providers[name] = { ...target, active: true }; - // Remember the active provider's current default model (if it belongs to - // that provider) so switching back later restores the user's choice. - if (activeProviderName && cfg.default_model) { - const activeProvider = providers[activeProviderName]; - if (activeProvider && cfg.models[cfg.default_model]?.provider === activeProviderName) { + // Remember the old active provider's current default model (if it + // belongs to that provider) so switching back later restores the user's + // choice. + if (activeProviderName && activeProviderName !== name && cfg.default_model) { + const oldActive = cfg.providers[activeProviderName]; + if (oldActive && cfg.models[cfg.default_model]?.provider === activeProviderName) { providers[activeProviderName] = setProviderDefaultModel( - activeProvider, + { ...oldActive, active: false }, cfg.default_model ); } } + // Add remaining providers (skip selected and old-active if already added). + for (const [key, p] of Object.entries(cfg.providers)) { + if (key === name) continue; + if (providers[key]) continue; + providers[key] = { ...p, active: false }; + } + // Prefer the target provider's remembered default model; fall back to // its first model when there is no saved preference or it is invalid. let default_model = getProviderDefaultModel(target); @@ -315,7 +321,10 @@ export default function App() { ...m, alias, provider: provider.name, - max_context_size: m.max_context_size ?? getDefaultMaxContextSize(m.model), + max_context_size: + m.max_context_size ?? + getModelRef(m.model)?.context ?? + getDefaultMaxContextSize(m.model), }; } diff --git a/src/components/ProviderEdit.tsx b/src/components/ProviderEdit.tsx index c9156c5..370dcdf 100644 --- a/src/components/ProviderEdit.tsx +++ b/src/components/ProviderEdit.tsx @@ -3,6 +3,7 @@ import { invoke } from "@tauri-apps/api/core"; import { useTranslation } from "../i18n"; import type { TranslationKey } from "../i18n/zh"; import { getDefaultMaxContextSize } from "../lib/model-defaults"; +import { capabilitiesFromRef, getModelRef } from "../lib/models-dev"; import { AgentSettingsPanel } from "./AgentSettingsPanel"; import type { Agent, DiscoveredModel, Model, Provider, ProviderType } from "../types"; @@ -400,15 +401,24 @@ function ModelMapping({ for (const dm of discovered) { if (!selected.has(dm.id)) continue; const alias = dm.id.replace(/[^a-zA-Z0-9_-]/g, "-"); - const max_context_size = dm.max_context_size ?? getDefaultMaxContextSize(dm.id); + const ref = getModelRef(dm.id); + // Priority: API-provided value > models.dev reference > regex fallback + const max_context_size = + dm.max_context_size ?? ref?.context ?? getDefaultMaxContextSize(dm.id); toAdd.push({ alias, provider: provider.name, model: dm.id, max_context_size, - display_name: dm.display_name, + display_name: dm.display_name ?? ref?.name ?? null, supports_1m: max_context_size >= 1_000_000, - capabilities: fetchThinking ? ["thinking"] : [], + // Reference data wins when available; otherwise keep the old + // fetchThinking-checkbox behavior as the fallback. + capabilities: ref + ? capabilitiesFromRef(ref) + : fetchThinking + ? ["thinking"] + : [], }); } onBulkAdd(toAdd); diff --git a/src/hooks/useConfig.ts b/src/hooks/useConfig.ts index 5cf23a8..7b53d13 100644 --- a/src/hooks/useConfig.ts +++ b/src/hooks/useConfig.ts @@ -48,11 +48,16 @@ export function useConfig(agent: Agent): UseConfigReturn { }, [refresh]); const updateConfig = useCallback((updater: (config: Config) => Config) => { - setConfig((prev) => { - const next = prev ? updater(prev) : prev; + // Compute next state synchronously from the ref (always current) and set + // the ref BEFORE setConfig. Previously the ref was updated inside the + // setConfig updater, which React 18 defers — so save() reading the ref + // immediately after would get a stale value. + const prev = configRef.current; + if (prev) { + const next = updater(prev); configRef.current = next; - return next; - }); + setConfig(next); + } setDirty(true); }, []); diff --git a/src/lib/models-dev.json b/src/lib/models-dev.json new file mode 100644 index 0000000..ba57a0e --- /dev/null +++ b/src/lib/models-dev.json @@ -0,0 +1,1866 @@ +{ + "zhipuai/glm-4.6v": { + "name": "GLM-4.6V", + "context": 128000, + "reasoning": true, + "tool_call": true, + "image": true, + "video": true + }, + "zhipuai/glm-5": { + "name": "GLM-5", + "context": 204800, + "reasoning": true, + "tool_call": true + }, + "zhipuai/glm-4.5-air": { + "name": "GLM-4.5-Air", + "context": 131072, + "reasoning": true, + "tool_call": true + }, + "zhipuai/glm-5.1": { + "name": "GLM-5.1", + "context": 200000, + "reasoning": true, + "tool_call": true, + "structured_output": true + }, + "zhipuai/glm-4.7-flash": { + "name": "GLM-4.7-Flash", + "context": 200000, + "reasoning": true, + "tool_call": true + }, + "zhipuai/glm-5.2": { + "name": "GLM-5.2", + "context": 1000000, + "reasoning": true, + "tool_call": true, + "structured_output": true + }, + "zhipuai/glm-4.7-flashx": { + "name": "GLM-4.7-FlashX", + "context": 200000, + "reasoning": true, + "tool_call": true + }, + "zhipuai/glm-4.6": { + "name": "GLM-4.6", + "context": 204800, + "reasoning": true, + "tool_call": true + }, + "zhipuai/glm-4.5": { + "name": "GLM-4.5", + "context": 131072, + "reasoning": true, + "tool_call": true + }, + "zhipuai/glm-4.5v": { + "name": "GLM-4.5V", + "context": 64000, + "reasoning": true, + "tool_call": true, + "image": true, + "video": true + }, + "zhipuai/glm-4.7": { + "name": "GLM-4.7", + "context": 204800, + "reasoning": true, + "tool_call": true + }, + "zhipuai/glm-5-turbo": { + "name": "GLM-5-Turbo", + "context": 200000, + "reasoning": true, + "tool_call": true, + "structured_output": true + }, + "zhipuai/glm-5v-turbo": { + "name": "GLM-5V-Turbo", + "context": 200000, + "reasoning": true, + "tool_call": true, + "image": true, + "video": true + }, + "zhipuai/glm-4.5-flash": { + "name": "GLM-4.5-Flash", + "context": 131072, + "reasoning": true, + "tool_call": true + }, + "microsoft/mai-code-1-flash": { + "name": "MAI-Code-1-Flash", + "context": 256000, + "reasoning": true, + "tool_call": true, + "structured_output": true + }, + "cohere/command-r7b-arabic-02-2025": { + "name": "Command R7B Arabic", + "context": 128000, + "tool_call": true + }, + "cohere/command-r-08-2024": { + "name": "Command R", + "context": 128000, + "tool_call": true + }, + "cohere/command-a-plus-05-2026": { + "name": "Command A Plus", + "context": 128000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "cohere/command-a-translate-08-2025": { + "name": "Command A Translate", + "context": 8000, + "tool_call": true + }, + "cohere/c4ai-aya-expanse-8b": { + "name": "Aya Expanse 8B", + "context": 8000 + }, + "cohere/c4ai-aya-vision-32b": { + "name": "Aya Vision 32B", + "context": 16000, + "image": true + }, + "cohere/command-a-03-2025": { + "name": "Command A", + "context": 256000, + "tool_call": true + }, + "cohere/command-r-plus-08-2024": { + "name": "Command R+", + "context": 128000, + "tool_call": true + }, + "cohere/command-a-reasoning-08-2025": { + "name": "Command A Reasoning", + "context": 256000, + "reasoning": true, + "tool_call": true + }, + "cohere/command-a-vision-07-2025": { + "name": "Command A Vision", + "context": 128000, + "image": true + }, + "cohere/command-r7b-12-2024": { + "name": "Command R7B", + "context": 128000, + "tool_call": true + }, + "cohere/c4ai-aya-vision-8b": { + "name": "Aya Vision 8B", + "context": 16000, + "image": true + }, + "cohere/north-mini-code-1-0": { + "name": "North Mini Code", + "context": 256000, + "reasoning": true, + "tool_call": true, + "structured_output": true + }, + "cohere/c4ai-aya-expanse-32b": { + "name": "Aya Expanse 32B", + "context": 128000 + }, + "nvidia/mistral-nemotron": { + "name": "Mistral Nemotron", + "context": 128000, + "tool_call": true + }, + "nvidia/nemotron-nano-12b-v2-vl": { + "name": "Nemotron Nano 12B v2 VL", + "context": 128000, + "reasoning": true, + "tool_call": true, + "image": true, + "video": true + }, + "nvidia/nemotron-3-nano-30b-a3b": { + "name": "Nemotron 3 Nano 30B A3B", + "context": 262144, + "reasoning": true, + "tool_call": true + }, + "nvidia/llama-3.3-nemotron-super-49b-v1.5": { + "name": "Llama 3.3 Nemotron Super 49B v1.5", + "context": 131072, + "reasoning": true, + "tool_call": true + }, + "nvidia/llama-3.1-nemotron-safety-guard-8b-v3": { + "name": "Llama 3.1 Nemotron Safety Guard 8B v3", + "context": 128000 + }, + "nvidia/llama-3.3-nemotron-super-49b-v1": { + "name": "Llama 3.3 Nemotron Super 49B v1", + "context": 131072, + "reasoning": true, + "tool_call": true + }, + "nvidia/llama-3.1-nemotron-70b-instruct": { + "name": "Llama 3.1 Nemotron 70B Instruct", + "context": 128000, + "tool_call": true + }, + "nvidia/nemotron-3-super-120b-a12b": { + "name": "Nemotron 3 Super 120B A12B", + "context": 262144, + "reasoning": true, + "tool_call": true + }, + "nvidia/nemotron-3-content-safety": { + "name": "Nemotron 3 Content Safety", + "context": 128000 + }, + "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning": { + "name": "Nemotron 3 Nano Omni 30B A3B Reasoning", + "context": 256000, + "reasoning": true, + "tool_call": true, + "image": true, + "video": true + }, + "nvidia/llama-3.1-nemotron-ultra-253b": { + "name": "Llama 3.1 Nemotron Ultra 253B", + "context": 128000, + "reasoning": true, + "tool_call": true + }, + "nvidia/nemotron-voicechat": { + "name": "Nemotron VoiceChat", + "context": 128000, + "tool_call": true + }, + "nvidia/llama-nemotron-embed-vl-1b-v2": { + "name": "Llama Nemotron Embed VL 1B v2", + "context": 32768, + "image": true + }, + "nvidia/nemotron-content-safety-reasoning-4b": { + "name": "Nemotron Content Safety Reasoning 4B", + "context": 128000, + "reasoning": true + }, + "nvidia/nemotron-3-ultra-550b-a55b": { + "name": "Nemotron 3 Ultra 550B A55B", + "context": 1000000, + "reasoning": true, + "tool_call": true + }, + "nvidia/llama-nemotron-rerank-vl-1b-v2": { + "name": "Llama Nemotron Rerank VL 1B v2", + "context": 128000, + "image": true + }, + "nvidia/nemotron-mini-4b-instruct": { + "name": "Nemotron Mini 4B Instruct", + "context": 128000, + "tool_call": true + }, + "nvidia/nemotron-nano-9b-v2": { + "name": "Nemotron Nano 9B v2", + "context": 131072, + "reasoning": true, + "tool_call": true + }, + "nvidia/nemotron-cascade-2-30b-a3b": { + "name": "Nemotron Cascade 2 30B A3B", + "context": 256000, + "reasoning": true, + "tool_call": true + }, + "nvidia/nemotron-3.5-content-safety": { + "name": "Nemotron 3.5 Content Safety", + "context": 128000, + "reasoning": true, + "image": true + }, + "google/gemini-2.5-computer-use-preview-10-2025": { + "name": "Gemini 2.5 Computer Use Preview", + "context": 128000, + "reasoning": true, + "tool_call": true, + "image": true + }, + "google/deep-research-preview-04-2026": { + "name": "Gemini Deep Research Preview", + "context": 1048576, + "reasoning": true, + "tool_call": true, + "image": true, + "video": true + }, + "google/gemini-3.1-flash-tts-preview": { + "name": "Gemini 3.1 Flash TTS Preview", + "context": 8192 + }, + "google/gemini-flash-latest": { + "name": "Gemini Flash Latest", + "context": 1048576, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "google/gemini-embedding-2": { + "name": "Gemini Embedding 2", + "context": 8192, + "image": true, + "video": true + }, + "google/lyria-3-pro-preview": { + "name": "Lyria 3 Pro Preview", + "context": 131072, + "image": true + }, + "google/gemini-3.5-flash": { + "name": "Gemini 3.5 Flash", + "context": 1048576, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "google/gemini-2.5-flash": { + "name": "Gemini 2.5 Flash", + "context": 1048576, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "google/gemini-3.5-flash-lite": { + "name": "Gemini 3.5 Flash Lite", + "context": 1048576, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "google/lyria-3-clip-preview": { + "name": "Lyria 3 Clip Preview", + "context": 131072, + "image": true + }, + "google/gemini-omni-flash-preview": { + "name": "Gemini Omni Flash Preview", + "context": 1048576, + "reasoning": true, + "image": true, + "video": true + }, + "google/veo-3.1-generate-preview": { + "name": "Veo 3.1 Preview", + "context": 1024, + "image": true + }, + "google/gemini-2.5-pro-tts": { + "name": "Gemini 2.5 Pro TTS", + "context": 32768 + }, + "google/deep-research-max-preview-04-2026": { + "name": "Deep Research Max Preview", + "context": 1048576, + "reasoning": true, + "tool_call": true, + "image": true, + "video": true + }, + "google/gemini-3-pro-image-preview": { + "name": "Nano Banana Pro", + "context": 65536, + "reasoning": true, + "image": true + }, + "google/gemini-3.1-flash-lite-preview": { + "name": "Gemini 3.1 Flash Lite Preview", + "context": 1048576, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "google/gemini-2.5-flash-tts": { + "name": "Gemini 2.5 Flash TTS", + "context": 32768 + }, + "google/gemini-3.5-live-translate-preview": { + "name": "Gemini 3.5 Live Translate Preview", + "context": 131072 + }, + "google/gemini-3-flash-preview": { + "name": "Gemini 3 Flash Preview", + "context": 1048576, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "google/gemini-3.1-pro-preview-customtools": { + "name": "Gemini 3.1 Pro Preview Custom Tools", + "context": 1048576, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "google/gemini-3.1-flash-lite-image": { + "name": "Nano Banana 2 Lite", + "context": 65536, + "reasoning": true, + "tool_call": true, + "image": true + }, + "google/gemini-3.1-flash-image-preview": { + "name": "Nano Banana 2", + "context": 65536, + "reasoning": true, + "image": true + }, + "google/gemini-robotics-er-1.6-preview": { + "name": "Gemini Robotics-ER 1.6 Preview", + "context": 131072, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "google/gemma-4-26b-a4b-it": { + "name": "Gemma 4 26B A4B IT", + "context": 262144, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "google/gemini-embedding-001": { + "name": "Gemini Embedding 001", + "context": 2048 + }, + "google/veo-3.1-lite-generate-preview": { + "name": "Veo 3.1 Lite Preview", + "context": 1024, + "image": true + }, + "google/gemini-2.0-flash": { + "name": "Gemini 2.0 Flash", + "context": 1048576, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "google/gemini-3.6-flash": { + "name": "Gemini 3.6 Flash", + "context": 1048576, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "google/veo-3.1-fast-generate-preview": { + "name": "Veo 3.1 Fast Preview", + "context": 1024, + "image": true, + "video": true + }, + "google/gemini-3.1-flash-lite": { + "name": "Gemini 3.1 Flash Lite", + "context": 1048576, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "google/gemma-4-E2B-it": { + "name": "Gemma 4 E2B IT", + "context": 131072, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "google/gemma-4-31b-it": { + "name": "Gemma 4 31B IT", + "context": 262144, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "google/gemini-3.1-flash-image": { + "name": "Nano Banana 2", + "context": 131072, + "reasoning": true, + "image": true, + "video": true + }, + "google/gemini-2.5-flash-image": { + "name": "Nano Banana", + "context": 32768, + "reasoning": true, + "image": true + }, + "google/gemini-3-pro-image": { + "name": "Nano Banana Pro", + "context": 65536, + "reasoning": true, + "image": true + }, + "google/gemini-3.1-pro-preview": { + "name": "Gemini 3.1 Pro Preview", + "context": 1048576, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "google/gemini-flash-lite-latest": { + "name": "Gemini Flash-Lite Latest", + "context": 1048576, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "google/gemini-2.5-pro": { + "name": "Gemini 2.5 Pro", + "context": 1048576, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "google/gemma-4-E4B-it": { + "name": "Gemma 4 E4B IT", + "context": 131072, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "google/gemini-3-pro-preview": { + "name": "Gemini 3 Pro Preview", + "context": 1048576, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "google/gemini-2.5-flash-lite": { + "name": "Gemini 2.5 Flash-Lite", + "context": 1048576, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "google/gemini-2.0-flash-lite": { + "name": "Gemini 2.0 Flash-Lite", + "context": 1048576, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "google/gemini-3.1-flash-live-preview": { + "name": "Gemini 3.1 Flash Live Preview", + "context": 131072, + "reasoning": true, + "tool_call": true, + "image": true, + "video": true + }, + "thinkingmachines/inkling": { + "name": "Inkling", + "context": 1048576, + "reasoning": true, + "tool_call": true, + "image": true + }, + "sakana/fugu-ultra": { + "name": "Fugu Ultra", + "context": 1000000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "sakana/fugu": { + "name": "Fugu", + "context": 1000000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "tencent/hy3": { + "name": "Hy3", + "context": 256000, + "reasoning": true, + "tool_call": true + }, + "tencent/hy3-preview": { + "name": "Hy3 preview", + "context": 256000, + "reasoning": true, + "tool_call": true + }, + "meta/muse-spark-1.1": { + "name": "Muse Spark 1.1", + "context": 1000000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "meta/llama-4-scout-17b-instruct": { + "name": "Llama 4 Scout 17B Instruct", + "context": 3500000, + "tool_call": true, + "image": true + }, + "meta/llama-3.3-70b-instruct": { + "name": "Llama-3.3-70B-Instruct", + "context": 128000, + "tool_call": true + }, + "meta/llama-4-maverick-17b-instruct": { + "name": "Llama 4 Maverick 17B Instruct", + "context": 1000000, + "tool_call": true, + "image": true + }, + "meituan/longcat-2.0": { + "name": "LongCat-2.0", + "context": 1000000, + "reasoning": true, + "tool_call": true + }, + "xai/grok-4.3": { + "name": "Grok 4.3", + "context": 1000000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "xai/grok-4.20-0309-non-reasoning": { + "name": "Grok 4.20 (Non-Reasoning)", + "context": 1000000, + "tool_call": true, + "structured_output": true, + "image": true + }, + "xai/grok-imagine-video-1.5": { + "name": "Grok Imagine Video 1.5", + "context": 1024, + "image": true, + "video": true + }, + "xai/grok-4.5": { + "name": "Grok 4.5", + "context": 500000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "xai/grok-build-0.1": { + "name": "Grok Build 0.1", + "context": 256000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "xai/grok-4.20-0309-reasoning": { + "name": "Grok 4.20 (Reasoning)", + "context": 1000000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "mistral/mistral-small-2506": { + "name": "Mistral Small 3.2", + "context": 128000, + "tool_call": true, + "image": true + }, + "mistral/pixtral-large-latest": { + "name": "Pixtral Large (latest)", + "context": 128000, + "tool_call": true, + "image": true + }, + "mistral/pixtral-12b": { + "name": "Pixtral 12B", + "context": 128000, + "tool_call": true, + "image": true + }, + "mistral/magistral-medium-latest": { + "name": "Magistral Medium (latest)", + "context": 128000, + "reasoning": true, + "tool_call": true + }, + "mistral/mistral-large-2411": { + "name": "Mistral Large 2.1", + "context": 131072, + "tool_call": true + }, + "mistral/mistral-medium-latest": { + "name": "Mistral Medium (latest)", + "context": 262144, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "mistral/devstral-small-2507": { + "name": "Devstral Small", + "context": 128000, + "tool_call": true + }, + "mistral/mistral-medium-2505": { + "name": "Mistral Medium 3", + "context": 131072, + "tool_call": true, + "image": true + }, + "mistral/mistral-small-latest": { + "name": "Mistral Small (latest)", + "context": 256000, + "reasoning": true, + "tool_call": true, + "image": true + }, + "mistral/mistral-nemo": { + "name": "Mistral Nemo", + "context": 128000, + "tool_call": true + }, + "mistral/mistral-small-2603": { + "name": "Mistral Small 4", + "context": 256000, + "reasoning": true, + "tool_call": true, + "image": true + }, + "mistral/mistral-medium-2604": { + "name": "Mistral Medium 3.5", + "context": 262144, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "mistral/codestral-latest": { + "name": "Codestral (latest)", + "context": 256000, + "tool_call": true + }, + "mistral/devstral-medium-latest": { + "name": "Devstral 2 (latest)", + "context": 262144, + "tool_call": true + }, + "mistral/devstral-2512": { + "name": "Devstral 2", + "context": 262144, + "tool_call": true + }, + "mistral/mistral-large-latest": { + "name": "Mistral Large (latest)", + "context": 262144, + "tool_call": true, + "image": true + }, + "mistral/devstral-medium-2507": { + "name": "Devstral Medium", + "context": 128000, + "tool_call": true + }, + "mistral/mistral-large-2512": { + "name": "Mistral Large 3", + "context": 262144, + "tool_call": true, + "image": true + }, + "poolside/laguna-xs.2": { + "name": "Laguna XS.2", + "context": 262144, + "reasoning": true, + "tool_call": true + }, + "poolside/laguna-xs-2.1": { + "name": "Laguna XS 2.1", + "context": 262144, + "reasoning": true, + "tool_call": true + }, + "poolside/laguna-s-2.1": { + "name": "Laguna S 2.1", + "context": 1048576, + "reasoning": true, + "tool_call": true + }, + "poolside/laguna-m.1": { + "name": "Laguna M.1", + "context": 262144, + "reasoning": true, + "tool_call": true + }, + "minimax/MiniMax-M2": { + "name": "MiniMax-M2", + "context": 196608, + "reasoning": true, + "tool_call": true + }, + "minimax/MiniMax-M2.7": { + "name": "MiniMax-M2.7", + "context": 204800, + "reasoning": true, + "tool_call": true + }, + "minimax/MiniMax-M2.1": { + "name": "MiniMax-M2.1", + "context": 204800, + "reasoning": true, + "tool_call": true + }, + "minimax/MiniMax-M2.5": { + "name": "MiniMax-M2.5", + "context": 204800, + "reasoning": true, + "tool_call": true + }, + "minimax/MiniMax-M2.5-highspeed": { + "name": "MiniMax-M2.5-highspeed", + "context": 204800, + "reasoning": true, + "tool_call": true + }, + "minimax/MiniMax-M2.7-highspeed": { + "name": "MiniMax-M2.7-highspeed", + "context": 204800, + "reasoning": true, + "tool_call": true + }, + "minimax/MiniMax-M3": { + "name": "MiniMax-M3", + "context": 512000, + "reasoning": true, + "tool_call": true, + "image": true, + "video": true + }, + "deepseek/deepseek-v4-flash": { + "name": "DeepSeek V4 Flash", + "context": 1000000, + "reasoning": true, + "tool_call": true, + "structured_output": true + }, + "deepseek/deepseek-v4-pro": { + "name": "DeepSeek V4 Pro", + "context": 1000000, + "reasoning": true, + "tool_call": true, + "structured_output": true + }, + "deepseek/deepseek-r1": { + "name": "DeepSeek-R1", + "context": 128000, + "reasoning": true, + "tool_call": true + }, + "deepseek/deepseek-chat": { + "name": "DeepSeek Chat", + "context": 1000000, + "tool_call": true + }, + "deepseek/deepseek-reasoner": { + "name": "DeepSeek Reasoner", + "context": 1000000, + "reasoning": true, + "tool_call": true + }, + "deepreinforce/ornith-1.0-9b": { + "name": "Ornith 1.0 9B", + "context": 262144, + "reasoning": true, + "tool_call": true, + "image": true + }, + "deepreinforce/ornith-1.0-397b": { + "name": "Ornith 1.0 397B", + "context": 262144, + "reasoning": true, + "tool_call": true, + "image": true + }, + "deepreinforce/ornith-1.0-31b": { + "name": "Ornith 1.0 31B", + "context": 262144, + "reasoning": true, + "tool_call": true, + "image": true + }, + "deepreinforce/ornith-1.0-35b": { + "name": "Ornith 1.0 35B", + "context": 262144, + "reasoning": true, + "tool_call": true, + "image": true + }, + "alibaba/qwen3-coder-480b-a35b-instruct": { + "name": "Qwen3-Coder 480B-A35B Instruct", + "context": 262144, + "tool_call": true + }, + "alibaba/qwen3.7-plus": { + "name": "Qwen3.7 Plus", + "context": 1000000, + "reasoning": true, + "tool_call": true, + "image": true, + "video": true + }, + "alibaba/qwen3-vl-plus": { + "name": "Qwen3-VL Plus", + "context": 262144, + "reasoning": true, + "tool_call": true, + "image": true + }, + "alibaba/qwen3-32b": { + "name": "Qwen3 32B", + "context": 131072, + "reasoning": true, + "tool_call": true + }, + "alibaba/qwen3.8-max-preview": { + "name": "Qwen3.8 Max Preview", + "context": 1000000, + "reasoning": true, + "tool_call": true, + "image": true, + "video": true + }, + "alibaba/qwen3.6-35b-a3b": { + "name": "Qwen3.6 35B-A3B", + "context": 262144, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "alibaba/qwen2-5-vl-72b-instruct": { + "name": "Qwen2.5-VL 72B Instruct", + "context": 131072, + "tool_call": true, + "image": true + }, + "alibaba/qwen-max": { + "name": "Qwen Max", + "context": 32768, + "tool_call": true + }, + "alibaba/qwen3.5-plus": { + "name": "Qwen3.5 Plus", + "context": 1000000, + "reasoning": true, + "tool_call": true, + "image": true, + "video": true + }, + "alibaba/qwen-omni-turbo": { + "name": "Qwen-Omni Turbo", + "context": 32768, + "tool_call": true, + "image": true, + "video": true + }, + "alibaba/qwen-vl-max": { + "name": "Qwen-VL Max", + "context": 131072, + "tool_call": true, + "image": true + }, + "alibaba/qwen3-coder-30b-a3b-instruct": { + "name": "Qwen3-Coder 30B-A3B Instruct", + "context": 262144, + "tool_call": true + }, + "alibaba/qwen3.5-27b": { + "name": "Qwen3.5 27B", + "context": 262144, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "alibaba/qwen3.7-max": { + "name": "Qwen3.7 Max", + "context": 1000000, + "reasoning": true, + "tool_call": true + }, + "alibaba/qwen3-next-80b-a3b-thinking": { + "name": "Qwen3-Next 80B-A3B (Thinking)", + "context": 131072, + "reasoning": true, + "tool_call": true + }, + "alibaba/qwen3.5-9b": { + "name": "Qwen3.5 9B", + "context": 262144, + "reasoning": true, + "tool_call": true, + "structured_output": true + }, + "alibaba/qwen3.6-27b": { + "name": "Qwen3.6 27B", + "context": 262144, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "alibaba/qwen3.5-35b-a3b": { + "name": "Qwen3.5 35B-A3B", + "context": 262144, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "alibaba/qwen3-235b-a22b": { + "name": "Qwen3 235B-A22B", + "context": 131072, + "reasoning": true, + "tool_call": true + }, + "alibaba/qwen3-max": { + "name": "Qwen3 Max", + "context": 262144, + "tool_call": true + }, + "alibaba/qwen3-coder-plus": { + "name": "Qwen3 Coder Plus", + "context": 1048576, + "tool_call": true + }, + "alibaba/qwen3-coder-flash": { + "name": "Qwen3 Coder Flash", + "context": 1000000, + "tool_call": true + }, + "alibaba/qwen-flash": { + "name": "Qwen Flash", + "context": 1000000, + "reasoning": true, + "tool_call": true + }, + "alibaba/qwen-plus": { + "name": "Qwen Plus", + "context": 1000000, + "reasoning": true, + "tool_call": true + }, + "alibaba/qwen-turbo": { + "name": "Qwen Turbo", + "context": 1000000, + "reasoning": true, + "tool_call": true + }, + "alibaba/qwq-plus": { + "name": "QwQ Plus", + "context": 131072, + "reasoning": true, + "tool_call": true + }, + "alibaba/qwen3.6-flash": { + "name": "Qwen3.6 Flash", + "context": 1000000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "alibaba/qwen3.5-397b-a17b": { + "name": "Qwen3.5 397B-A17B", + "context": 262144, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "alibaba/qwen3.6-max-preview": { + "name": "Qwen3.6 Max Preview", + "context": 262144, + "reasoning": true, + "tool_call": true + }, + "alibaba/qwen3.6-plus": { + "name": "Qwen3.6 Plus", + "context": 1000000, + "reasoning": true, + "tool_call": true, + "image": true, + "video": true + }, + "alibaba/qwen3.5-122b-a10b": { + "name": "Qwen3.5 122B-A10B", + "context": 262144, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "alibaba/qwen3-next-80b-a3b-instruct": { + "name": "Qwen3-Next 80B-A3B Instruct", + "context": 131072, + "tool_call": true + }, + "alibaba/qwen-vl-plus": { + "name": "Qwen-VL Plus", + "context": 131072, + "tool_call": true, + "image": true + }, + "xiaomi/mimo-v2-omni": { + "name": "MiMo-V2-Omni", + "context": 262144, + "reasoning": true, + "tool_call": true, + "image": true, + "video": true + }, + "xiaomi/mimo-v2-flash": { + "name": "MiMo-V2-Flash", + "context": 262144, + "reasoning": true, + "tool_call": true + }, + "xiaomi/mimo-v2.5-pro-ultraspeed": { + "name": "MiMo-V2.5-Pro-UltraSpeed", + "context": 1048576, + "reasoning": true, + "tool_call": true + }, + "xiaomi/mimo-v2-pro": { + "name": "MiMo-V2-Pro", + "context": 1048576, + "reasoning": true, + "tool_call": true + }, + "xiaomi/mimo-v2.5": { + "name": "MiMo-V2.5", + "context": 1048576, + "reasoning": true, + "tool_call": true, + "image": true, + "video": true + }, + "xiaomi/mimo-v2.5-pro": { + "name": "MiMo-V2.5-Pro", + "context": 1048576, + "reasoning": true, + "tool_call": true + }, + "anthropic/claude-sonnet-4-6": { + "name": "Claude Sonnet 4.6", + "context": 1000000, + "reasoning": true, + "tool_call": true, + "image": true + }, + "anthropic/claude-opus-4-0": { + "name": "Claude Opus 4 (latest)", + "context": 200000, + "reasoning": true, + "tool_call": true, + "image": true + }, + "anthropic/claude-haiku-4-5": { + "name": "Claude Haiku 4.5 (latest)", + "context": 200000, + "reasoning": true, + "tool_call": true, + "image": true + }, + "anthropic/claude-3-7-sonnet-20250219": { + "name": "Claude Sonnet 3.7", + "context": 200000, + "reasoning": true, + "tool_call": true, + "image": true + }, + "anthropic/claude-opus-4-6": { + "name": "Claude Opus 4.6", + "context": 1000000, + "reasoning": true, + "tool_call": true, + "image": true + }, + "anthropic/claude-3-5-sonnet-20241022": { + "name": "Claude Sonnet 3.5 v2", + "context": 200000, + "tool_call": true, + "image": true + }, + "anthropic/claude-fable-5": { + "name": "Claude Fable 5", + "context": 1000000, + "reasoning": true, + "tool_call": true, + "image": true + }, + "anthropic/claude-opus-4-8": { + "name": "Claude Opus 4.8", + "context": 1000000, + "reasoning": true, + "tool_call": true, + "image": true + }, + "anthropic/claude-sonnet-4-0": { + "name": "Claude Sonnet 4 (latest)", + "context": 200000, + "reasoning": true, + "tool_call": true, + "image": true + }, + "anthropic/claude-opus-4-1": { + "name": "Claude Opus 4.1 (latest)", + "context": 200000, + "reasoning": true, + "tool_call": true, + "image": true + }, + "anthropic/claude-sonnet-4-5": { + "name": "Claude Sonnet 4.5 (latest)", + "context": 200000, + "reasoning": true, + "tool_call": true, + "image": true + }, + "anthropic/claude-sonnet-4-5-20250929": { + "name": "Claude Sonnet 4.5", + "context": 200000, + "reasoning": true, + "tool_call": true, + "image": true + }, + "anthropic/claude-opus-4-5-20251101": { + "name": "Claude Opus 4.5", + "context": 200000, + "reasoning": true, + "tool_call": true, + "image": true + }, + "anthropic/claude-3-5-haiku-20241022": { + "name": "Claude Haiku 3.5", + "context": 200000, + "tool_call": true, + "image": true + }, + "anthropic/claude-haiku-4-5-20251001": { + "name": "Claude Haiku 4.5", + "context": 200000, + "reasoning": true, + "tool_call": true, + "image": true + }, + "anthropic/claude-opus-4-7": { + "name": "Claude Opus 4.7", + "context": 1000000, + "reasoning": true, + "tool_call": true, + "image": true + }, + "anthropic/claude-sonnet-4-20250514": { + "name": "Claude Sonnet 4", + "context": 200000, + "reasoning": true, + "tool_call": true, + "image": true + }, + "anthropic/claude-opus-4-5": { + "name": "Claude Opus 4.5 (latest)", + "context": 200000, + "reasoning": true, + "tool_call": true, + "image": true + }, + "anthropic/claude-sonnet-5": { + "name": "Claude Sonnet 5", + "context": 1000000, + "reasoning": true, + "tool_call": true, + "image": true + }, + "anthropic/claude-3-haiku-20240307": { + "name": "Claude Haiku 3", + "context": 200000, + "tool_call": true, + "image": true + }, + "anthropic/claude-opus-4-20250514": { + "name": "Claude Opus 4", + "context": 200000, + "reasoning": true, + "tool_call": true, + "image": true + }, + "anthropic/claude-opus-5": { + "name": "Claude Opus 5", + "context": 1000000, + "reasoning": true, + "tool_call": true, + "image": true + }, + "anthropic/claude-opus-4-1-20250805": { + "name": "Claude Opus 4.1", + "context": 200000, + "reasoning": true, + "tool_call": true, + "image": true + }, + "perplexity/sonar": { + "name": "Sonar", + "context": 128000 + }, + "perplexity/sonar-pro": { + "name": "Sonar Pro", + "context": 200000, + "image": true + }, + "perplexity/sonar-reasoning-pro": { + "name": "Sonar Reasoning Pro", + "context": 128000, + "reasoning": true, + "image": true + }, + "moonshotai/kimi-k2.5": { + "name": "Kimi K2.5", + "context": 262144, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "moonshotai/kimi-k2-thinking-turbo": { + "name": "Kimi K2 Thinking Turbo", + "context": 262144, + "reasoning": true, + "tool_call": true + }, + "moonshotai/kimi-k2.7-code-highspeed": { + "name": "Kimi K2.7 Code Highspeed", + "context": 262144, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "moonshotai/kimi-k2.6": { + "name": "Kimi K2.6", + "context": 262144, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "moonshotai/kimi-k2.7-code": { + "name": "Kimi K2.7 Code", + "context": 262144, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "moonshotai/kimi-k2-thinking": { + "name": "Kimi K2 Thinking", + "context": 262144, + "reasoning": true, + "tool_call": true + }, + "moonshotai/kimi-k3": { + "name": "Kimi K3", + "context": 1048576, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true, + "video": true + }, + "openai/gpt-5.1-codex-mini": { + "name": "GPT-5.1 Codex mini", + "context": 400000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/gpt-image-2": { + "name": "GPT-Image-2", + "image": true + }, + "openai/gpt-5.2-pro": { + "name": "GPT-5.2 Pro", + "context": 400000, + "reasoning": true, + "tool_call": true, + "image": true + }, + "openai/gpt-5.5-pro": { + "name": "GPT-5.5 Pro", + "context": 1050000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/gpt-4.1-mini": { + "name": "GPT-4.1 mini", + "context": 1047576, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/gpt-4o": { + "name": "GPT-4o", + "context": 128000, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/gpt-5.4-pro": { + "name": "GPT-5.4 Pro", + "context": 1050000, + "reasoning": true, + "tool_call": true, + "image": true + }, + "openai/gpt-5.6-sol": { + "name": "GPT-5.6 Sol", + "context": 1050000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/gpt-oss-safeguard-120b": { + "name": "GPT OSS Safeguard 120B", + "context": 131072, + "reasoning": true, + "tool_call": true, + "structured_output": true + }, + "openai/gpt-4.1-nano": { + "name": "GPT-4.1 nano", + "context": 1047576, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/gpt-oss-20b": { + "name": "GPT OSS 20B", + "context": 131072, + "reasoning": true, + "tool_call": true, + "structured_output": true + }, + "openai/gpt-5.5-instant": { + "name": "GPT-5.5 Instant", + "context": 400000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/gpt-5.2-chat-latest": { + "name": "GPT-5.2 Chat", + "context": 128000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/o3-mini": { + "name": "o3-mini", + "context": 200000, + "reasoning": true, + "tool_call": true, + "structured_output": true + }, + "openai/gpt-5.5": { + "name": "GPT-5.5", + "context": 1050000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/o3-pro": { + "name": "o3-pro", + "context": 200000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/gpt-4o-mini": { + "name": "GPT-4o mini", + "context": 128000, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/gpt-5": { + "name": "GPT-5", + "context": 400000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/o4-mini-deep-research": { + "name": "o4-mini-deep-research", + "context": 200000, + "reasoning": true, + "tool_call": true, + "image": true + }, + "openai/gpt-realtime-2.1": { + "name": "GPT-Realtime-2.1", + "context": 128000, + "reasoning": true, + "tool_call": true, + "image": true + }, + "openai/o3-deep-research": { + "name": "o3-deep-research", + "context": 200000, + "reasoning": true, + "tool_call": true, + "image": true + }, + "openai/gpt-5-codex": { + "name": "GPT-5-Codex", + "context": 400000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/gpt-3.5-turbo": { + "name": "GPT-3.5-turbo", + "context": 16385 + }, + "openai/whisper-large-v3": { + "name": "Whisper 3 Large", + "context": 448 + }, + "openai/gpt-4o-2024-05-13": { + "name": "GPT-4o (2024-05-13)", + "context": 128000, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/gpt-5-chat-latest": { + "name": "GPT-5 Chat (latest)", + "context": 400000, + "reasoning": true, + "structured_output": true, + "image": true + }, + "openai/gpt-4o-2024-11-20": { + "name": "GPT-4o (2024-11-20)", + "context": 128000, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/gpt-5.4": { + "name": "GPT-5.4", + "context": 1050000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/gpt-5.3-chat-latest": { + "name": "GPT-5.3 Chat (latest)", + "context": 128000, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/gpt-realtime-whisper": { + "name": "GPT Realtime Whisper" + }, + "openai/gpt-image-1.5": { + "name": "GPT-Image-1.5", + "image": true + }, + "openai/gpt-5.2-codex": { + "name": "GPT-5.2 Codex", + "context": 400000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/gpt-5.4-nano": { + "name": "GPT-5.4 nano", + "context": 400000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/gpt-5-pro": { + "name": "GPT-5 Pro", + "context": 400000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/gpt-5.4-mini": { + "name": "GPT-5.4 mini", + "context": 400000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/whisper-large-v3-turbo": { + "name": "Whisper Large v3 Turbo", + "context": 448 + }, + "openai/gpt-oss-120b": { + "name": "GPT OSS 120B", + "context": 131072, + "reasoning": true, + "tool_call": true, + "structured_output": true + }, + "openai/o1": { + "name": "o1", + "context": 200000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/gpt-5.6-luna": { + "name": "GPT-5.6 Luna", + "context": 1050000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/gpt-5.2": { + "name": "GPT-5.2", + "context": 400000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/gpt-5.3-codex": { + "name": "GPT-5.3 Codex", + "context": 400000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/gpt-5-mini": { + "name": "GPT-5 Mini", + "context": 400000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/o1-pro": { + "name": "o1-pro", + "context": 200000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/gpt-5.1": { + "name": "GPT-5.1", + "context": 400000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/gpt-4-turbo": { + "name": "GPT-4 Turbo", + "context": 128000, + "tool_call": true, + "image": true + }, + "openai/gpt-4o-2024-08-06": { + "name": "GPT-4o (2024-08-06)", + "context": 128000, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/gpt-5.1-codex": { + "name": "GPT-5.1 Codex", + "context": 400000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/gpt-5-nano": { + "name": "GPT-5 Nano", + "context": 400000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/o3": { + "name": "o3", + "context": 200000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/gpt-5.6-terra": { + "name": "GPT-5.6 Terra", + "context": 1050000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/gpt-image-1": { + "name": "GPT-Image-1", + "image": true + }, + "openai/gpt-4.1": { + "name": "GPT-4.1", + "context": 1047576, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/o4-mini": { + "name": "o4-mini", + "context": 200000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/gpt-5.1-codex-max": { + "name": "GPT-5.1 Codex Max", + "context": 400000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "openai/gpt-4": { + "name": "GPT-4", + "context": 8192, + "tool_call": true + }, + "openai/gpt-5.1-chat-latest": { + "name": "GPT-5.1 Chat", + "context": 128000, + "reasoning": true, + "tool_call": true, + "structured_output": true, + "image": true + }, + "sarvam/sarvam-30b": { + "name": "Sarvam 30B", + "context": 128000, + "reasoning": true, + "tool_call": true + }, + "sarvam/sarvam-105b": { + "name": "Sarvam 105B", + "context": 131072, + "reasoning": true, + "tool_call": true + }, + "stepfun/step-3.5-flash": { + "name": "Step 3.5 Flash", + "context": 256000, + "reasoning": true, + "tool_call": true + }, + "stepfun/step-3.7-flash": { + "name": "Step 3.7 Flash", + "context": 256000, + "reasoning": true, + "tool_call": true, + "image": true, + "video": true + }, + "stepfun/step-3.5-flash-2603": { + "name": "Step 3.5 Flash 2603", + "context": 256000, + "reasoning": true, + "tool_call": true + } +} diff --git a/src/lib/models-dev.ts b/src/lib/models-dev.ts new file mode 100644 index 0000000..384fec4 --- /dev/null +++ b/src/lib/models-dev.ts @@ -0,0 +1,57 @@ +import snapshot from "./models-dev.json"; + +/** + * Compact per-model reference data from models.dev + * (see scripts/fetch-models-dev.mjs). + */ +export interface ModelRef { + name?: string; + context?: number; + reasoning?: boolean; + tool_call?: boolean; + structured_output?: boolean; + image?: boolean; + video?: boolean; +} + +const data: Record = snapshot; + +// models.dev keys are "/" and may be mixed-case (e.g. +// "minimax/MiniMax-M3"), while provider APIs return bare model ids +// (e.g. "kimi-k2.5"). Index lowercase for tolerant matching. +const byLowerKey: Record = {}; +for (const [key, value] of Object.entries(data)) { + byLowerKey[key.toLowerCase()] = value; +} + +/** + * Look up reference data for a provider model id. + * Matches exact key first, then any "/" suffix. + * Returns undefined when nothing matches — callers fall back to + * getDefaultMaxContextSize / manual capabilities. + */ +export function getModelRef(modelId: string): ModelRef | undefined { + const id = modelId.trim().toLowerCase(); + if (!id) return undefined; + const exact = byLowerKey[id]; + if (exact) return exact; + const suffix = `/${id}`; + for (const [key, value] of Object.entries(byLowerKey)) { + if (key.endsWith(suffix)) return value; + } + return undefined; +} + +/** + * Map reference data to Kimi Code capability flags. + * Multimodal (image_in/video_in) comes from models.dev input modalities; + * always_thinking cannot be derived and stays manual. + */ +export function capabilitiesFromRef(ref: ModelRef): string[] { + const caps: string[] = []; + if (ref.reasoning) caps.push("thinking"); + if (ref.tool_call) caps.push("tool_use"); + if (ref.image) caps.push("image_in"); + if (ref.video) caps.push("video_in"); + return caps; +}