From d6940daf5382ccc59f275ecedc8897daab0d5c73 Mon Sep 17 00:00:00 2001 From: backryun Date: Mon, 20 Jul 2026 20:41:00 +0900 Subject: [PATCH 1/6] refactor(cli): remove legacy Qwen Code integration --- .env.example | 1 - README.md | 3 +- bin/cli/commands/registry.mjs | 2 - bin/cli/commands/setup-qwen.mjs | 156 -------- config/quality/eslint-suppressions.json | 12 +- docs/frameworks/ACP.md | 8 +- docs/guides/CLI-INTEGRATIONS.md | 4 +- docs/guides/FEATURES.md | 2 +- docs/guides/REMOTE-MODE.md | 1 - docs/guides/SETUP_GUIDE.md | 1 - docs/reference/CLI-TOOLS.md | 61 +-- docs/reference/ENVIRONMENT.md | 1 - .../cli-code/components/DefaultToolCard.tsx | 6 +- src/app/api/cli-tools/all-statuses/route.ts | 9 - src/app/api/cli-tools/backups/route.ts | 2 +- .../guide-settings/[toolId]/route.ts | 56 --- src/app/api/cli-tools/qwen-settings/route.ts | 363 ------------------ src/app/api/cli-tools/status/route.ts | 1 - src/app/api/skills/collect/detect/route.ts | 1 - src/app/api/skills/collect/install/route.ts | 1 - src/i18n/messages/ar.json | 2 - src/i18n/messages/az.json | 2 - src/i18n/messages/bg.json | 2 - src/i18n/messages/bn.json | 2 - src/i18n/messages/cs.json | 2 - src/i18n/messages/da.json | 2 - src/i18n/messages/de.json | 2 - src/i18n/messages/en.json | 5 +- src/i18n/messages/es.json | 2 - src/i18n/messages/fa.json | 2 - src/i18n/messages/fi.json | 2 - src/i18n/messages/fr.json | 2 - src/i18n/messages/gu.json | 2 - src/i18n/messages/he.json | 2 - src/i18n/messages/hi.json | 2 - src/i18n/messages/hu.json | 2 - src/i18n/messages/id.json | 2 - src/i18n/messages/in.json | 2 - src/i18n/messages/it.json | 2 - src/i18n/messages/ja.json | 2 - src/i18n/messages/ko.json | 2 - src/i18n/messages/mr.json | 2 - src/i18n/messages/ms.json | 2 - src/i18n/messages/nl.json | 2 - src/i18n/messages/no.json | 2 - src/i18n/messages/phi.json | 2 - src/i18n/messages/pl.json | 2 - src/i18n/messages/pt-BR.json | 5 +- src/i18n/messages/pt.json | 2 - src/i18n/messages/ro.json | 2 - src/i18n/messages/ru.json | 2 - src/i18n/messages/sk.json | 2 - src/i18n/messages/sv.json | 2 - src/i18n/messages/sw.json | 2 - src/i18n/messages/ta.json | 2 - src/i18n/messages/te.json | 2 - src/i18n/messages/th.json | 2 - src/i18n/messages/tr.json | 2 - src/i18n/messages/uk-UA.json | 2 - src/i18n/messages/ur.json | 2 - src/i18n/messages/vi.json | 2 - src/i18n/messages/zh-CN.json | 2 - src/i18n/messages/zh-TW.json | 2 - src/lib/acp/manager.ts | 2 +- src/lib/acp/registry.ts | 11 +- src/lib/cliTools/checkToolConfigStatus.ts | 11 - src/shared/constants/cliTools.ts | 129 ------- src/shared/schemas/cliCatalog.ts | 2 +- src/shared/services/cliRuntime.ts | 10 - tests/unit/acp-registry.test.ts | 2 +- tests/unit/check-tool-config-status.test.ts | 19 - tests/unit/cli-catalog-acpspawnable.test.ts | 1 - tests/unit/cli-catalog-counts.test.ts | 11 +- tests/unit/cli-catalog-schema.test.ts | 7 +- tests/unit/cli-runtime-detection.test.ts | 1 - tests/unit/cli-tools-schema.test.ts | 3 +- tests/unit/cli/setup-qwen.test.ts | 28 -- tests/unit/guide-settings-route.test.ts | 73 +--- tests/unit/qwen-api-key-auto-create.test.ts | 153 -------- 79 files changed, 38 insertions(+), 1208 deletions(-) delete mode 100644 bin/cli/commands/setup-qwen.mjs delete mode 100644 src/app/api/cli-tools/qwen-settings/route.ts delete mode 100644 tests/unit/cli/setup-qwen.test.ts delete mode 100644 tests/unit/qwen-api-key-auto-create.test.ts diff --git a/.env.example b/.env.example index d507aacafc7..cebc3df2f43 100644 --- a/.env.example +++ b/.env.example @@ -632,7 +632,6 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true # CLI_CLINE_BIN=cline # CLI_CONTINUE_BIN=cn # CLI_QODER_BIN=qoder -# CLI_QWEN_BIN=qwen # CLI_AUGGIE_BIN=auggie # AUGGIE_BIN=auggie diff --git a/README.md b/README.md index 4236e4790e8..0661f550d19 100644 --- a/README.md +++ b/README.md @@ -292,7 +292,6 @@ All **18** strategies — mix & match per combo step: Kilo Code
Kilo Code
                           
Roo CodeRoo Code
Roo Code
                            Continue
Continue
                            - Qwen Code
Qwen Code
                            Aider
Aider
                            ForgeCode
ForgeCode
                            @@ -323,7 +322,7 @@ All **18** strategies — mix & match per combo step: + also works with · Kiro · Command Code · Antigravity · Windsurf · AMP · any OpenAI-compatible tool -📖 Per-tool setup for all 33 tools (25 CLI Code's + 8 CLI Agents) → [`docs/reference/CLI-TOOLS.md`](docs/reference/CLI-TOOLS.md) · 🧩 OpenCode plugin → [`@omniroute/opencode-provider`](https://www.npmjs.com/package/@omniroute/opencode-provider) +📖 Per-tool setup for all 32 tools (24 CLI Code's + 8 CLI Agents) → [`docs/reference/CLI-TOOLS.md`](docs/reference/CLI-TOOLS.md) · 🧩 OpenCode plugin → [`@omniroute/opencode-provider`](https://www.npmjs.com/package/@omniroute/opencode-provider) diff --git a/bin/cli/commands/registry.mjs b/bin/cli/commands/registry.mjs index 010392f601d..da4ad3bacf0 100644 --- a/bin/cli/commands/registry.mjs +++ b/bin/cli/commands/registry.mjs @@ -70,7 +70,6 @@ import { registerSetupCursor } from "./setup-cursor.mjs"; import { registerSetupRoo } from "./setup-roo.mjs"; import { registerSetupCrush } from "./setup-crush.mjs"; import { registerSetupGoose } from "./setup-goose.mjs"; -import { registerSetupQwen } from "./setup-qwen.mjs"; import { registerSetupAider } from "./setup-aider.mjs"; import { registerConnect } from "./connect.mjs"; import { registerContexts } from "./contexts.mjs"; @@ -153,7 +152,6 @@ export function registerCommands(program) { registerSetupRoo(program); registerSetupCrush(program); registerSetupGoose(program); - registerSetupQwen(program); registerSetupAider(program); registerConnect(program); registerContexts(program); diff --git a/bin/cli/commands/setup-qwen.mjs b/bin/cli/commands/setup-qwen.mjs deleted file mode 100644 index dd34300560a..00000000000 --- a/bin/cli/commands/setup-qwen.mjs +++ /dev/null @@ -1,156 +0,0 @@ -/** - * omniroute setup-qwen — configure Qwen Code (QwenLM/qwen-code) for OmniRoute. - * - * Qwen Code is a terminal AI agent with a file-based config at - * ~/.qwen/settings.json. For a custom OpenAI-compatible endpoint it uses a - * `modelProviders` entry with authType "openai", baseUrl WITH /v1, and an - * `envKey` naming the env var holding the key (secret stays in the env, never the - * file). Remote-aware; headless test via `qwen -p "..."`. - */ - -import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; -import os from "node:os"; -import { printHeading, printInfo, printSuccess, printError, createPrompt } from "../io.mjs"; -import { resolveActiveContext } from "../contexts.mjs"; - -function ensureV1(url) { - const s = String(url || "").replace(/\/+$/, ""); - return s.endsWith("/v1") ? s : `${s}/v1`; -} - -/** Resolve baseUrl (WITH /v1) + apiKey from flags → active context → localhost. */ -export function resolveQwenTarget(opts = {}) { - let root; - if (opts.remote) root = String(opts.remote).replace(/\/+$/, ""); - else { - try { - root = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT)?.baseUrl; - } catch { - /* none */ - } - if (!root) root = `http://localhost:${Number(opts.port ?? process.env.PORT ?? 20128) || 20128}`; - } - let apiKey = opts.apiKey ?? opts["api-key"]; - if (!apiKey) { - try { - const c = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT); - apiKey = c?.accessToken || c?.apiKey; - } catch { - /* none */ - } - } - if (!apiKey) apiKey = process.env.OMNIROUTE_API_KEY || ""; - return { baseUrl: ensureV1(root), apiKey }; -} - -/** Merge the OmniRoute modelProvider into Qwen's settings.json (preserve rest). */ -export function buildQwenSettings(existing, { baseUrl, model }) { - const s = existing && typeof existing === "object" ? { ...existing } : {}; - const providers = Array.isArray(s.modelProviders) - ? s.modelProviders.filter((p) => p?.id !== "omniroute") - : []; - providers.push({ - id: "omniroute", - name: "OmniRoute", - authType: "openai", - baseUrl, - envKey: "OMNIROUTE_API_KEY", - }); - s.modelProviders = providers; - if (model) { - s.selectedProvider = "omniroute"; - s.model = model; - } - return s; -} - -function readJson(path) { - try { - if (existsSync(path)) return JSON.parse(readFileSync(path, "utf8")); - } catch { - /* corrupt/missing */ - } - return {}; -} - -async function fetchModelIds(baseUrl, apiKey) { - try { - const headers = { "Content-Type": "application/json" }; - if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`; - const res = await fetch(`${baseUrl.replace(/\/v1$/, "")}/v1/models`, { - headers, - signal: AbortSignal.timeout(8000), - }); - if (!res.ok) return []; - const body = await res.json(); - const list = Array.isArray(body) ? body : (body.data ?? body.models ?? []); - return list.map((m) => (typeof m === "string" ? m : m?.id)).filter(Boolean); - } catch { - return []; - } -} - -export async function runSetupQwenCommand(opts = {}) { - const { baseUrl, apiKey } = resolveQwenTarget(opts); - const dryRun = Boolean(opts.dryRun ?? opts["dry-run"]); - const configPath = - opts.configPath ?? opts["config-path"] ?? join(os.homedir(), ".qwen", "settings.json"); - - printHeading("OmniRoute → Qwen Code (openai-compatible)"); - printInfo(`baseUrl: ${baseUrl}`); - - let model = opts.model; - if (!model) { - const ids = await fetchModelIds(baseUrl, apiKey); - if (ids.length && !opts.yes) { - printInfo(`Examples: ${ids.slice(0, 20).join(", ")}${ids.length > 20 ? " …" : ""}`); - const prompt = createPrompt(); - try { - model = await prompt.ask("Model id for Qwen"); - } finally { - prompt.close(); - } - } - } - if (!model) { - printError("A model is required. Pass --model ."); - return 2; - } - - const merged = buildQwenSettings(readJson(configPath), { baseUrl, model }); - const out = JSON.stringify(merged, null, 2) + "\n"; - - if (dryRun) { - console.log("\n" + out); - printInfo(`[dry-run] → ${configPath}`); - } else { - mkdirSync(join(configPath, ".."), { recursive: true }); - writeFileSync(configPath, out, "utf8"); - printSuccess(`Wrote ${configPath}`); - } - printInfo( - "\nProvide the key (settings reference OMNIROUTE_API_KEY): export OMNIROUTE_API_KEY=..." - ); - printInfo('Then run: qwen (or headless: qwen -p "reply OK")'); - return 0; -} - -export function registerSetupQwen(program) { - program - .command("setup-qwen") - .description( - "Configure Qwen Code for OmniRoute: write ~/.qwen/settings.json (openai modelProvider)" - ) - .option("--port ", "Local OmniRoute port (ignored when --remote is set)", "20128") - .option("--remote ", "Remote OmniRoute URL, e.g. http://192.168.0.15:20128") - .option("--api-key ", "OmniRoute API key (defaults to OMNIROUTE_API_KEY env var)") - .option("--model ", "Model id for Qwen (required unless picked interactively)") - .option("--config-path ", "settings.json path (default: ~/.qwen/settings.json)") - .option("--yes", "Non-interactive: do not prompt (requires --model)") - .option("--dry-run", "Print what would be written without touching the filesystem") - .action(async (opts) => { - const code = await runSetupQwenCommand(opts); - if (code !== 0) process.exit(code); - }); -} diff --git a/config/quality/eslint-suppressions.json b/config/quality/eslint-suppressions.json index 37c9219ae19..e3a4ccbabff 100644 --- a/config/quality/eslint-suppressions.json +++ b/config/quality/eslint-suppressions.json @@ -202,11 +202,6 @@ "count": 1 } }, - "src/app/(dashboard)/dashboard/cli-code/components/DefaultToolCard.tsx": { - "react-hooks/exhaustive-deps": { - "count": 2 - } - }, "src/app/(dashboard)/dashboard/costs/costExplorerUtils.ts": { "no-restricted-syntax": { "count": 1 @@ -1457,11 +1452,6 @@ "count": 25 } }, - "tests/unit/guide-settings-route.test.ts": { - "@typescript-eslint/no-explicit-any": { - "count": 4 - } - }, "tests/unit/i18n-nest-dotted-keys.test.ts": { "@typescript-eslint/no-explicit-any": { "count": 4 @@ -2297,4 +2287,4 @@ "count": 5 } } -} \ No newline at end of file +} diff --git a/docs/frameworks/ACP.md b/docs/frameworks/ACP.md index fcfa08d3fc5..6facda83c87 100644 --- a/docs/frameworks/ACP.md +++ b/docs/frameworks/ACP.md @@ -27,7 +27,7 @@ ACP (Agent Client Protocol) is a **"CLI-as-backend" transport** for OmniRoute. I ## Supported CLI Agents -ACP supports **14 built-in CLI agents** out of the box: +ACP supports **12 built-in CLI agents** out of the box: | Agent ID | Display Name | Binary | Protocol | | ------------- | ------------------ | ------------- | -------- | @@ -38,7 +38,6 @@ ACP supports **14 built-in CLI agents** out of the box: | `aider` | Aider | `aider` | stdio | | `opencode` | OpenCode | `opencode` | stdio | | `cline` | Cline | `cline` | stdio | -| `qwen-code` | Qwen Code | `qwen` | stdio | | `forge` | ForgeCode | `forge` | stdio | | `amazon-q` | Amazon Q Developer | `q` | stdio | | `interpreter` | Open Interpreter | `interpreter` | stdio | @@ -198,7 +197,7 @@ const session = acpManager.spawn("claude", "claude", ["--print", "--output-forma // Returns: AcpSession ``` -**Allowed agent IDs**: `["claude", "codex", "gemini", "qwen"]` +**Allowed agent IDs**: `["claude", "codex", "gemini"]` #### `acpManager.sendPrompt(sessionId, prompt, timeoutMs)` @@ -407,12 +406,11 @@ Each ACP session runs in its own child process. The process is killed when the s **Problem**: `acpManager.spawn()` throws `Unknown agent: ` -**Solution**: Only 4 agents are allowed in `spawn()`: +**Solution**: Only 3 agents are allowed in `spawn()`: - `claude` - `codex` - `gemini` -- `qwen` Other agents must be spawned manually or via custom agent definitions. diff --git a/docs/guides/CLI-INTEGRATIONS.md b/docs/guides/CLI-INTEGRATIONS.md index ca4d90a3635..ba056e61898 100644 --- a/docs/guides/CLI-INTEGRATIONS.md +++ b/docs/guides/CLI-INTEGRATIONS.md @@ -47,7 +47,6 @@ server and writes the config locally. | `omniroute setup-roo` | Roo Code | `~/.omniroute/roo-settings.json` (import doc) + sets `roo-cline.autoImportSettingsPath` if a VS Code `settings.json` exists | `--remote` `--api-key` `--model` `--yes` `--dry-run` `--port` `--import-path` `--vscode-settings` | Both | | `omniroute setup-crush` | Crush | `~/.config/crush/crush.json` — `openai-compat` provider, key via `$OMNIROUTE_API_KEY` | `--remote` `--api-key` `--only` `--dry-run` `--port` `--config-path` | Both | | `omniroute setup-goose` | Goose | `~/.config/goose/config.yaml` (`GOOSE_PROVIDER`/`OPENAI_HOST`/`GOOSE_MODEL`) + prints env recipe | `--remote` `--api-key` `--model` `--yes` `--dry-run` `--port` `--config-path` | Both | -| `omniroute setup-qwen` | Qwen Code | `~/.qwen/settings.json` — openai `modelProvider`, key via `envKey` (`OMNIROUTE_API_KEY`) | `--remote` `--api-key` `--model` `--yes` `--dry-run` `--port` `--config-path` | Both | | `omniroute setup-aider` | Aider | `~/.aider.conf.yml` (`openai-api-base` + `model: openai/`) + prints env recipe | `--remote` `--api-key` `--model` `--yes` `--dry-run` `--port` `--config-path` | Both | | `omniroute launch` | Claude Code | Nothing — spawns `claude` with `ANTHROPIC_BASE_URL`/`ANTHROPIC_AUTH_TOKEN` injected | `--remote` `--api-key` `--token` `--profile` `--port` | Both | | `omniroute launch-codex` | OpenAI Codex CLI | Nothing — spawns `codex` with the `omniroute` provider injected via `-c` flags | `--remote` `--api-key` `--profile` (`-p`) `--port` | Both | @@ -101,7 +100,6 @@ opencode -m omniroute/glm/glm-5.2 "..." # Tools without auto-discovery need an explicit model: omniroute setup-aider --model glm/glm-5.2 -omniroute setup-qwen --model kmc/kimi-k2.7 # Preview without writing anything: omniroute setup-continue --dry-run @@ -161,7 +159,7 @@ tool expects (verified in the command source): | `setup-cline` (`openAiBaseUrl`) | root | No — Cline appends `/v1/chat/completions` | | `setup-goose` (`OPENAI_HOST`) | root | No — Goose appends the path | | `setup-aider` (`OPENAI_API_BASE`) | root | No — LiteLLM appends `/v1/chat/completions` | -| `setup-kilo`, `setup-roo`, `setup-continue`, `setup-crush`, `setup-qwen`, `setup-cursor` | with `/v1` | Yes | +| `setup-kilo`, `setup-roo`, `setup-continue`, `setup-crush`, `setup-cursor` | with `/v1` | Yes | | `setup-claude` (`ANTHROPIC_BASE_URL`), `launch` | root | No — Claude Code appends `/v1/messages` | | `setup-codex`, `launch-codex` (`model_providers.omniroute.base_url`) | with `/v1` | Yes | diff --git a/docs/guides/FEATURES.md b/docs/guides/FEATURES.md index 6f7ece1eb3c..df540dd1f5f 100644 --- a/docs/guides/FEATURES.md +++ b/docs/guides/FEATURES.md @@ -136,7 +136,7 @@ One-click configuration for AI coding tools: Claude Code, Codex CLI, OpenClaw, K ## 🤖 CLI Agents _(v2.0.11+)_ -Dashboard for discovering and managing CLI agents. Shows a grid of 17 built-in agents (Codex, Claude, Goose, OpenClaw, Aider, OpenCode, Cline, Qwen Code, ForgeCode, Amazon Q, Open Interpreter, Cursor CLI, Warp, **Windsurf**, **Devin CLI**, **Kimi Coding**, **Command Code**) with: +Dashboard for discovering and managing CLI agents. Shows a grid of 16 built-in agents (Codex, Claude, Goose, OpenClaw, Aider, OpenCode, Cline, ForgeCode, Amazon Q, Open Interpreter, Cursor CLI, Warp, **Windsurf**, **Devin CLI**, **Kimi Coding**, **Command Code**) with: - **Installation status** — Installed / Not Found with version detection - **Protocol badges** — stdio, HTTP, etc. diff --git a/docs/guides/REMOTE-MODE.md b/docs/guides/REMOTE-MODE.md index 7edf52eb2df..05f4d559cd4 100644 --- a/docs/guides/REMOTE-MODE.md +++ b/docs/guides/REMOTE-MODE.md @@ -215,7 +215,6 @@ context, or `--remote --api-key `): | Roo Code | `omniroute setup-roo` | writes a Roo import JSON (`~/.omniroute/roo-settings.json`) + sets `roo-cline.autoImportSettingsPath` + prints UI steps (OpenAI-compatible, Base URL **with** `/v1`) | | Crush | `omniroute setup-crush` | `~/.config/crush/crush.json` — `openai-compat` provider, `base_url` **with** `/v1`, key via `$OMNIROUTE_API_KEY` | | Goose | `omniroute setup-goose` | `~/.config/goose/config.yaml` (`GOOSE_PROVIDER=openai` + `OPENAI_HOST` **without** `/v1` + `GOOSE_MODEL`) + env recipe | -| Qwen Code | `omniroute setup-qwen` | `~/.qwen/settings.json` — openai `modelProvider`, `baseUrl` **with** `/v1`, key via `envKey` (OMNIROUTE_API_KEY) | | Aider | `omniroute setup-aider` | `~/.aider.conf.yml` (`openai-api-base` **without** `/v1` + `model: openai/`) + env recipe (`aider --message --yes`) | ```bash diff --git a/docs/guides/SETUP_GUIDE.md b/docs/guides/SETUP_GUIDE.md index 0e6564311cb..550d62fd159 100644 --- a/docs/guides/SETUP_GUIDE.md +++ b/docs/guides/SETUP_GUIDE.md @@ -179,7 +179,6 @@ omniroute setup-cursor # prints Cursor's in-app steps omniroute setup-roo # Roo Code import + autoImport pointer omniroute setup-crush # ~/.config/crush/crush.json omniroute setup-goose # ~/.config/goose/config.yaml -omniroute setup-qwen # ~/.qwen/settings.json omniroute setup-aider # ~/.aider.conf.yml ``` diff --git a/docs/reference/CLI-TOOLS.md b/docs/reference/CLI-TOOLS.md index dad82a929ee..46ae3be954d 100644 --- a/docs/reference/CLI-TOOLS.md +++ b/docs/reference/CLI-TOOLS.md @@ -55,12 +55,12 @@ OmniRoute (local or remote) and writes the tool's own config on your machine: omniroute setup-codex omniroute setup-claude omniroute setup-opencode omniroute setup-cline omniroute setup-kilo omniroute setup-continue omniroute setup-cursor omniroute setup-roo omniroute setup-crush -omniroute setup-goose omniroute setup-qwen omniroute setup-aider +omniroute setup-goose omniroute setup-aider ``` Each accepts `--remote --api-key ` (configure a local tool against a remote OmniRoute), `--dry-run` (preview without writing), and `--port`. Tools -without model auto-discovery (Cline, Kilo, Roo, Goose, Qwen, Aider, Gemini) take +without model auto-discovery (Cline, Kilo, Roo, Goose, Aider, Gemini) take `--model ` (and `--yes` for non-interactive runs). The launchers `omniroute launch` (Claude Code) and `omniroute launch-codex` (Codex) spawn the CLI with the right env injected and write no config at all. @@ -90,7 +90,7 @@ Entries with `baseUrlSupport: "none"` are **not shown** in the dashboard pages --- -## 1. CLI Code's Catalog (25 tools) +## 1. CLI Code's Catalog (24 tools) All tools that appear in `/dashboard/cli-code`. Those with `baseUrlSupport: none` are wired through MITM or a manual guide instead of a custom base URL: @@ -102,7 +102,6 @@ All tools that appear in `/dashboard/cli-code`. Those with `baseUrlSupport: none | kilo | Kilo Code | Kilo-Org | full | custom | false | | roo | Roo Code | Roo (OSS) | full | guide | false | | continue | Continue | continue.dev | full | guide | false | -| qwen | Qwen Code | Alibaba | full | guide | true | | aider | Aider | OSS (P. Gauthier) | full | guide | true | | forge | ForgeCode | Antinomy HQ | full | custom | true | | jcode | jcode | 1jehuang (OSS) | full | custom | false | @@ -123,7 +122,6 @@ All tools that appear in `/dashboard/cli-code`. Those with `baseUrlSupport: none | custom | Custom CLI | — | full | custom-builder | false | Tools with `baseUrlSupport: "partial"` show a badge "⚠ Base URL parcial" in the dashboard card. - --- ## 2. CLI Agents Catalog (8 tools) @@ -147,7 +145,6 @@ Autonomous agents that appear in `/dashboard/cli-agents`: This page (renamed from `/dashboard/agents`) shows CLIs that OmniRoute can **spawn** as backend execution engines via stdio/ACP protocol. The catalog is maintained separately in `src/lib/acp/registry.ts` and is **not** the same as `CLI_TOOLS`. - --- ## 4. MITM Backlog (not shown in dashboard) @@ -302,9 +299,6 @@ npm install -g cline # KiloCode npm install -g kilocode -# Qwen Code (Alibaba) -npm install -g @qwen-code/qwen-code - # Aider pip install aider-chat @@ -532,55 +526,6 @@ under `/dashboard/cli-tools → Kiro`. --- -#### Qwen Code (Alibaba) - -Qwen Code supports OpenAI-compatible API endpoints via environment variables or `settings.json`. - -> Qwen OAuth free tier was discontinued on 2026-04-15. Use OmniRoute with -> `bailian-coding-plan` / `alibaba` / `alibaba-cn` / `openrouter` / `anthropic` / -> `gemini` providers instead. - -**Option 1: Environment variables (`~/.qwen/.env`)** - -```bash -mkdir -p ~/.qwen && cat > ~/.qwen/.env << EOF -OPENAI_API_KEY="sk-your-omniroute-key" -OPENAI_BASE_URL="http://localhost:20128/v1" -OPENAI_MODEL="auto" -EOF -``` - -**Option 2: `settings.json` with `security.auth`** - -```json -// ~/.qwen/settings.json -{ - "security": { - "auth": { - "selectedType": "openai", - "apiKey": "sk-your-omniroute-key", - "baseUrl": "http://localhost:20128/v1" - } - }, - "model": { - "name": "claude-sonnet-4-6" - } -} -``` - -**Option 3: Inline CLI flags** - -```bash -OPENAI_BASE_URL="http://localhost:20128/v1" \ -OPENAI_API_KEY="sk-your-omniroute-key" \ -OPENAI_MODEL="auto" \ -qwen -``` - -> For a **remote server** replace `localhost:20128` with the server IP or domain. - ---- - ## 10. Internal OmniRoute CLI The `omniroute` binary provides commands for server lifecycle, setup, diagnostics, and provider management. Entry point: `bin/omniroute.mjs`. diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index eb8f2eae68f..cd2fa040ee0 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -364,7 +364,6 @@ Controls how OmniRoute discovers and launches CLI sidecars (Claude Code, Codex, | `CLI_CLINE_BIN` | `cline` | `src/shared/services/cliRuntime.ts` | Custom path to Cline CLI binary. | | `CLI_CONTINUE_BIN` | `cn` | `src/shared/services/cliRuntime.ts` | Custom path to Continue CLI binary. | | `CLI_QODER_BIN` | `qoder` | `src/shared/services/cliRuntime.ts` | Custom path to Qoder CLI binary. | -| `CLI_QWEN_BIN` | `qwen` | `src/shared/services/cliRuntime.ts` | Custom path to the Qwen Code CLI binary. | | `CLI_DEVIN_BIN` | `devin` | `open-sse/executors/devin-cli.ts` | Custom path to the Devin CLI binary (v3.8.0). Used by the Windsurf/Devin executor. | | `AUGGIE_BIN` | `auggie` | `open-sse/executors/auggie.ts` | Absolute-path override for the Augment (Auggie) CLI binary used by the local `auggie` provider. Falls back to `CLI_AUGGIE_BIN`, then a PATH lookup. | | `CLI_AUGGIE_BIN` | `auggie` | `open-sse/executors/auggie.ts` | Alias override for the Augment (Auggie) CLI binary path (checked after `AUGGIE_BIN`). | diff --git a/src/app/(dashboard)/dashboard/cli-code/components/DefaultToolCard.tsx b/src/app/(dashboard)/dashboard/cli-code/components/DefaultToolCard.tsx index cd74884a0ce..b833b0301f8 100644 --- a/src/app/(dashboard)/dashboard/cli-code/components/DefaultToolCard.tsx +++ b/src/app/(dashboard)/dashboard/cli-code/components/DefaultToolCard.tsx @@ -195,7 +195,7 @@ export default function DefaultToolCard({ .replace(/\{\{apiKey\}\}/g, keyToUse) .replace(/\{\{model\}\}/g, getSelectedModelLabels()[0] || t("modelPlaceholder")); }, - [baseUrl, getSelectedModelLabels, resolveApiKeyValue, t] + [baseUrlWithV1, getSelectedModelLabels, resolveApiKeyValue, t] ); const handleCopy = async (text, field) => { @@ -226,7 +226,7 @@ export default function DefaultToolCard({ 2 ); }, [ - baseUrl, + baseUrlWithV1, getSelectedModels, getSelectedModelLabelMap, replaceVars, @@ -296,7 +296,7 @@ export default function DefaultToolCard({ }; // Check if this tool supports direct config file write - const supportsDirectSave = ["continue", "opencode", "qwen"].includes(toolId); + const supportsDirectSave = ["continue", "opencode"].includes(toolId); const renderApiKeySelector = () => { return ( diff --git a/src/app/api/cli-tools/all-statuses/route.ts b/src/app/api/cli-tools/all-statuses/route.ts index 06532820f9c..16a7e0a13a6 100644 --- a/src/app/api/cli-tools/all-statuses/route.ts +++ b/src/app/api/cli-tools/all-statuses/route.ts @@ -42,15 +42,6 @@ async function extractEndpointFromConfig( const env = config.env as Record | undefined; return (env?.ANTHROPIC_BASE_URL as string | undefined) ?? null; } - case "qwen": { - const mp = config.modelProviders as Record[] | undefined; - if (!Array.isArray(mp)) return null; - for (const provider of mp) { - const baseUrl = (provider as Record).apiBase as string | undefined; - if (baseUrl) return baseUrl; - } - return null; - } case "cline": return (config.openAiBaseUrl as string | undefined) ?? null; case "droid": diff --git a/src/app/api/cli-tools/backups/route.ts b/src/app/api/cli-tools/backups/route.ts index a183a261ba7..71549d8ce69 100644 --- a/src/app/api/cli-tools/backups/route.ts +++ b/src/app/api/cli-tools/backups/route.ts @@ -8,7 +8,7 @@ import { cliBackupMutationSchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error"; -const VALID_TOOLS = ["claude", "codex", "droid", "openclaw", "cline", "kilo", "qwen"]; +const VALID_TOOLS = ["claude", "codex", "droid", "openclaw", "cline", "kilo"]; // GET /api/cli-tools/backups?tool=claude — list backups export async function GET(request) { diff --git a/src/app/api/cli-tools/guide-settings/[toolId]/route.ts b/src/app/api/cli-tools/guide-settings/[toolId]/route.ts index 79078d0e105..002ee40113e 100644 --- a/src/app/api/cli-tools/guide-settings/[toolId]/route.ts +++ b/src/app/api/cli-tools/guide-settings/[toolId]/route.ts @@ -66,8 +66,6 @@ export async function POST(request, { params }) { // (#524) OpenCode config was never saved because only 'continue' was handled here. // OpenCode reads ~/.config/opencode/opencode.json — write the OmniRoute settings there. return await saveOpenCodeConfig({ baseUrl, apiKey, model, models, modelLabels }); - case "qwen": - return await saveQwenConfig({ baseUrl, apiKey, model }); case "hermes": return await saveHermesConfig({ baseUrl, apiKey, model }); // hermes-agent now uses the dedicated /api/cli-tools/hermes-agent-settings endpoint @@ -205,60 +203,6 @@ async function saveOpenCodeConfig({ baseUrl, apiKey, model, models, modelLabels }); } -/** - * Save Qwen Code config to ~/.qwen/settings.json - * - * Uses security.auth format (not modelProviders) since Qwen Code - * prioritizes security.auth.selectedType over modelProviders entries. - * Per official docs: security.auth takes highest precedence. - */ -async function saveQwenConfig({ baseUrl, apiKey, model }) { - const home = os.homedir(); - const configPath = path.join(home, ".qwen", "settings.json"); - - await fs.mkdir(path.dirname(configPath), { recursive: true }); - - const normalizedBaseUrl = String(baseUrl || "") - .trim() - .replace(/\/+$/, ""); - const resolvedApiKey = apiKey || "sk_omniroute"; - const resolvedModel = model || "qwen/qwen3-coder-plus"; - - // Read existing config to preserve other settings (permissions, mcpServers, etc.) - let existingConfig: Record = {}; - try { - const raw = await fs.readFile(configPath, "utf-8"); - existingConfig = JSON.parse(raw); - } catch { - // File doesn't exist or invalid JSON - } - - // Set security.auth for openai auth type with direct credentials - // This takes priority over modelProviders entries (per Qwen docs) - existingConfig.security = { - ...existingConfig.security, - auth: { - selectedType: "openai", - apiKey: resolvedApiKey, - baseUrl: normalizedBaseUrl, - }, - }; - - // Set model to the selected model - existingConfig.model = { - ...existingConfig.model, - name: resolvedModel, - }; - - await fs.writeFile(configPath, JSON.stringify(existingConfig, null, 2), "utf-8"); - - return NextResponse.json({ - success: true, - message: `Qwen Code config saved to ${configPath}`, - configPath, - }); -} - /** * Save Hermes config to ~/.hermes/config.yaml * diff --git a/src/app/api/cli-tools/qwen-settings/route.ts b/src/app/api/cli-tools/qwen-settings/route.ts deleted file mode 100644 index 11ca68b298b..00000000000 --- a/src/app/api/cli-tools/qwen-settings/route.ts +++ /dev/null @@ -1,363 +0,0 @@ -"use server"; - -import { NextResponse } from "next/server"; -import fs from "fs/promises"; -import path from "path"; -import os from "os"; -import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth"; -import { - ensureCliConfigWriteAllowed, - getCliPrimaryConfigPath, - getCliRuntimeStatus, -} from "@/shared/services/cliRuntime"; -import { createBackup } from "@/shared/services/backupService"; -import { saveCliToolLastConfigured, deleteCliToolLastConfigured } from "@/lib/db/cliToolState"; -import { cliModelConfigSchema } from "@/shared/validation/schemas"; -import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; -import { getApiKeyById } from "@/lib/localDb"; - -const getQwenSettingsPath = () => getCliPrimaryConfigPath("qwen"); -const getQwenDir = () => path.dirname(getQwenSettingsPath()); -const getQwenEnvPath = () => path.join(getQwenDir(), ".env"); - -// Read current settings.json -const readSettings = async () => { - try { - const settingsPath = getQwenSettingsPath(); - const content = await fs.readFile(settingsPath, "utf-8"); - return JSON.parse(content); - } catch (error: any) { - if (error.code === "ENOENT") return null; - throw error; - } -}; - -// Read current .env file -const readEnv = async () => { - try { - const envPath = getQwenEnvPath(); - return await fs.readFile(envPath, "utf-8"); - } catch (error: any) { - if (error.code === "ENOENT") return ""; - throw error; - } -}; - -// Check if settings has OmniRoute config -const hasOmniRouteConfig = (settings: any) => { - if (!settings || !settings.modelProviders) return false; - const openai = settings.modelProviders.openai; - if (!Array.isArray(openai)) return false; - return openai.some((p: any) => { - if (p.name?.includes("OmniRoute") || p.id === "omniroute") return true; - if (!p.baseUrl) return false; - try { - const urlObj = new URL(p.baseUrl); - const host = urlObj.hostname; - const isDashScope = - host === "dashscope.aliyuncs.com" || host.endsWith(".dashscope.aliyuncs.com"); - const isOpenAI = host === "api.openai.com" || host.endsWith(".openai.com"); - return !isDashScope && !isOpenAI; - } catch { - return true; // invalid URLs are treated as custom endpoints - } - }); -}; - -// GET - Check Qwen CLI and read current settings -export async function GET(request: Request) { - const authError = await requireCliToolsAuth(request); - if (authError) return authError; - - try { - const runtime = await getCliRuntimeStatus("qwen"); - - if (!runtime.installed || !runtime.runnable) { - return NextResponse.json({ - installed: runtime.installed, - runnable: runtime.runnable, - command: runtime.command, - commandPath: runtime.commandPath, - runtimeMode: runtime.runtimeMode, - reason: runtime.reason, - settings: null, - message: - runtime.installed && !runtime.runnable - ? "Qwen Code CLI is installed but not runnable" - : "Qwen Code CLI is not installed", - }); - } - - const settings = await readSettings(); - - return NextResponse.json({ - installed: runtime.installed, - runnable: runtime.runnable, - command: runtime.command, - commandPath: runtime.commandPath, - runtimeMode: runtime.runtimeMode, - reason: runtime.reason, - settings, - hasOmniRoute: hasOmniRouteConfig(settings), - settingsPath: getQwenSettingsPath(), - envPath: getQwenEnvPath(), - }); - } catch (error) { - console.log("Error checking qwen settings:", error); - return NextResponse.json({ error: "Failed to check qwen settings" }, { status: 500 }); - } -} - -// POST - Write OmniRoute config to settings.json + .env -export async function POST(request: Request) { - const authError = await requireCliToolsAuth(request); - if (authError) return authError; - - let rawBody; - try { - rawBody = await request.json(); - } catch { - return NextResponse.json( - { - error: { - message: "Invalid request", - details: [{ field: "body", message: "Invalid JSON body" }], - }, - }, - { status: 400 } - ); - } - - try { - const writeGuard = ensureCliConfigWriteAllowed(); - if (writeGuard) { - return NextResponse.json({ error: writeGuard }, { status: 403 }); - } - - // Extract keyId BEFORE validation — Zod strips unknown fields - const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null; - - const validation = validateBody(cliModelConfigSchema, rawBody); - if (isValidationFailure(validation)) { - return NextResponse.json({ error: validation.error }, { status: 400 }); - } - let { baseUrl, apiKey, model } = validation.data; - - // Resolve real key from DB by ID - if (keyId) { - try { - const keyRecord = await getApiKeyById(keyId); - if (keyRecord?.key) apiKey = keyRecord.key as string; - } catch { - /* non-critical */ - } - } - - const resolvedApiKey = apiKey || "sk_omniroute"; - const resolvedModel = model || "coder-model"; - const normalizedBaseUrl = String(baseUrl || "") - .trim() - .replace(/\/+$/, ""); - const qwenDir = getQwenDir(); - const settingsPath = getQwenSettingsPath(); - const envPath = getQwenEnvPath(); - - // Ensure directory exists - await fs.mkdir(qwenDir, { recursive: true }); - - // Backup current settings before modifying - await createBackup("qwen", settingsPath); - - // --- Write API keys to ~/.qwen/.env --- - let envContent = await readEnv(); - const envLines = envContent.split("\n").filter((line) => { - // Remove old OmniRoute-related keys we're about to write - return ( - !line.startsWith("OPENAI_API_KEY=") && - !line.startsWith("ANTHROPIC_API_KEY=") && - !line.startsWith("GEMINI_API_KEY=") - ); - }); - - envLines.push(`OPENAI_API_KEY=${resolvedApiKey}`); - envLines.push(`ANTHROPIC_API_KEY=${resolvedApiKey}`); - envLines.push(`GEMINI_API_KEY=${resolvedApiKey}`); - - await fs.writeFile(envPath, envLines.join("\n").trim() + "\n", "utf-8"); - - // --- Write modelProviders to settings.json --- - let existingConfig: Record = {}; - try { - const raw = await fs.readFile(settingsPath, "utf-8"); - existingConfig = JSON.parse(raw); - } catch { - // File doesn't exist or invalid JSON - } - - if (!existingConfig.modelProviders) existingConfig.modelProviders = {}; - - // openai provider — primary, supports all models via OmniRoute - const openaiEntry = { - id: resolvedModel, - name: `${resolvedModel} (OmniRoute)`, - envKey: "OPENAI_API_KEY", - baseUrl: normalizedBaseUrl, - generationConfig: { - contextWindowSize: 200000, - }, - }; - - if (!existingConfig.modelProviders.openai) existingConfig.modelProviders.openai = []; - const openaiProviders = existingConfig.modelProviders.openai; - const openaiIdx = openaiProviders.findIndex( - (p: any) => p && (p.baseUrl === normalizedBaseUrl || p.id === "omniroute") - ); - if (openaiIdx >= 0) { - openaiProviders[openaiIdx] = openaiEntry; - } else { - openaiProviders.push(openaiEntry); - } - - // anthropic provider — for Claude models via OmniRoute - const anthropicEntry = { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6 (OmniRoute)", - envKey: "ANTHROPIC_API_KEY", - baseUrl: normalizedBaseUrl, - generationConfig: { - contextWindowSize: 200000, - }, - }; - - if (!existingConfig.modelProviders.anthropic) existingConfig.modelProviders.anthropic = []; - const anthropicProviders = existingConfig.modelProviders.anthropic; - const anthropicIdx = anthropicProviders.findIndex( - (p: any) => p && p.baseUrl === normalizedBaseUrl - ); - if (anthropicIdx >= 0) { - anthropicProviders[anthropicIdx] = anthropicEntry; - } else { - anthropicProviders.push(anthropicEntry); - } - - // gemini provider — for Gemini models via OmniRoute - const geminiEntry = { - id: "gemini-3-flash", - name: "Gemini 3 Flash (OmniRoute)", - envKey: "GEMINI_API_KEY", - baseUrl: normalizedBaseUrl, - }; - - if (!existingConfig.modelProviders.gemini) existingConfig.modelProviders.gemini = []; - const geminiProviders = existingConfig.modelProviders.gemini; - const geminiIdx = geminiProviders.findIndex((p: any) => p && p.baseUrl === normalizedBaseUrl); - if (geminiIdx >= 0) { - geminiProviders[geminiIdx] = geminiEntry; - } else { - geminiProviders.push(geminiEntry); - } - - await fs.writeFile(settingsPath, JSON.stringify(existingConfig, null, 2), "utf-8"); - - // Persist last-configured timestamp - try { - saveCliToolLastConfigured("qwen"); - } catch { - /* non-critical */ - } - - return NextResponse.json({ - success: true, - message: "Qwen Code config saved successfully!", - settingsPath, - envPath, - }); - } catch (error) { - console.log("Error updating qwen settings:", error); - return NextResponse.json({ error: "Failed to update qwen settings" }, { status: 500 }); - } -} - -// DELETE - Remove OmniRoute config from settings.json and .env -export async function DELETE(request: Request) { - const authError = await requireCliToolsAuth(request); - if (authError) return authError; - - try { - const writeGuard = ensureCliConfigWriteAllowed(); - if (writeGuard) { - return NextResponse.json({ error: writeGuard }, { status: 403 }); - } - - const settingsPath = getQwenSettingsPath(); - const envPath = getQwenEnvPath(); - - // Backup current settings before resetting - await createBackup("qwen", settingsPath); - - // --- Clean settings.json --- - let existingConfig: Record = {}; - try { - const raw = await fs.readFile(settingsPath, "utf-8"); - existingConfig = JSON.parse(raw); - } catch (error: any) { - if (error.code === "ENOENT") { - return NextResponse.json({ - success: true, - message: "No settings file to reset", - }); - } - throw error; - } - - // Remove OmniRoute entries from each provider type - const providerTypes = ["openai", "anthropic", "gemini"]; - for (const type of providerTypes) { - if (Array.isArray(existingConfig.modelProviders?.[type])) { - existingConfig.modelProviders[type] = existingConfig.modelProviders[type].filter( - (p: any) => !p.name?.includes("OmniRoute") && p.id !== "omniroute" - ); - // Remove empty provider arrays - if (existingConfig.modelProviders[type].length === 0) { - delete existingConfig.modelProviders[type]; - } - } - } - - // Clean up empty modelProviders - if (existingConfig.modelProviders && Object.keys(existingConfig.modelProviders).length === 0) { - delete existingConfig.modelProviders; - } - - await fs.writeFile(settingsPath, JSON.stringify(existingConfig, null, 2), "utf-8"); - - // --- Clean .env --- - const RESET_ENV_KEYS = ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GEMINI_API_KEY"]; - - try { - let envContent = await fs.readFile(envPath, "utf-8"); - const envLines = envContent - .split("\n") - .filter((line) => !RESET_ENV_KEYS.some((key) => line.startsWith(`${key}=`))); - - await fs.writeFile(envPath, envLines.join("\n").trim() + "\n", "utf-8"); - } catch { - // .env doesn't exist — nothing to clean - } - - // Clear last-configured timestamp - try { - deleteCliToolLastConfigured("qwen"); - } catch { - /* non-critical */ - } - - return NextResponse.json({ - success: true, - message: "OmniRoute settings removed from Qwen Code", - }); - } catch (error) { - console.log("Error resetting qwen settings:", error); - return NextResponse.json({ error: "Failed to reset qwen settings" }, { status: 500 }); - } -} diff --git a/src/app/api/cli-tools/status/route.ts b/src/app/api/cli-tools/status/route.ts index 737c0c75ac2..e5ae1675a44 100644 --- a/src/app/api/cli-tools/status/route.ts +++ b/src/app/api/cli-tools/status/route.ts @@ -60,7 +60,6 @@ export async function GET(request: Request) { "openclaw", "cline", "kilo", - "qwen", "hermes", ]; diff --git a/src/app/api/skills/collect/detect/route.ts b/src/app/api/skills/collect/detect/route.ts index 5f755c6ec76..6975dd49322 100644 --- a/src/app/api/skills/collect/detect/route.ts +++ b/src/app/api/skills/collect/detect/route.ts @@ -32,7 +32,6 @@ const CODING_TOOL_KEYWORDS: Record = { droid: ["droid", "factory-ai"], continue: ["continue"], antigravity: ["antigravity"], - qwen: ["qwen", "alibaba"], windsurf: ["windsurf"], devin: ["devin", "cognition"], }; diff --git a/src/app/api/skills/collect/install/route.ts b/src/app/api/skills/collect/install/route.ts index cecca071960..33cd417e1ba 100644 --- a/src/app/api/skills/collect/install/route.ts +++ b/src/app/api/skills/collect/install/route.ts @@ -39,7 +39,6 @@ const CODING_TOOL_PATHS: Record = { windsurf: "~/.windsurf/skills/{category}", devin: "~/.devin/skills/{category}", antigravity: "~/.antigravity/skills/{category}", - qwen: "~/.qwen/skills/{category}", kilocode: "~/.kilocode/skills/{category}", openclaw: "~/.openclaw/skills/{category}", droid: "~/.droid/skills/{category}", diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index 6f97c273f62..5a92e4ee798 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -2036,7 +2036,6 @@ "antigravity": "يُستخدم عندما يجب اعتراض حركة مرور Antigravity/Kiro عبر MITM وتوجيهها إلى OmniRoute.", "copilot": "استخدمه عندما تريد UX بأسلوب دردشة Copilot أثناء فرض مفاتيح OmniRoute وقواعد التوجيه.", "amp": "استخدمه عندما تريد توجيه Amp CLI عبر OmniRoute بأسماء مستعارة مختصرة ثابتة وإعداد متوافق مع OpenAI.", - "qwen": "استخدمه عندما تريد Qwen Code CLI مع إعدادات مزوّد يديرها OmniRoute ووصول إلى نماذج متعددة المزوّدين.", "hermes": "استخدمه عندما تحتاج إلى مساعد AI خفيف الوزن ومعتمد على الأجهزة الطرفية للقيام بالمهام السريعة.", "hermes-agent": "استخدمه عندما تحتاج إلى Hermes Agent من Nousresearch مع توجيه نماذج الوضع الافتراضي والتفويض والرؤية والنماذج المساعدة عبر OmniRoute.", "custom": "يستخدم لتطبيقات الأدوات المخصصة أو التكوينات العامة المتوافقة مع OpenAI." @@ -2055,7 +2054,6 @@ "kiro": "Amazon Kiro — بيئة تطوير متكاملة مدعومة بالذكاء الاصطناعي", "windsurf": "محرر الشيفرة Windsurf المدعوم بالذكاء الاصطناعي", "copilot": "مساعد GitHub Copilot بالذكاء الاصطناعي", - "qwen": "واجهة Alibaba Qwen Code لسطر الأوامر", "amp": "مساعد Sourcegraph Amp للبرمجة عبر سطر الأوامر", "hermes": "Hermes AI مساعد المحطة", "hermes-agent": "Hermes Agent من Nousresearch — ذكاء اصطناعي متقدم للطرفية يدعم نماذج متعددة والتفويض والرؤية والضغط وغيرها", diff --git a/src/i18n/messages/az.json b/src/i18n/messages/az.json index 02e78787e0e..1ddacbeb05f 100644 --- a/src/i18n/messages/az.json +++ b/src/i18n/messages/az.json @@ -2036,7 +2036,6 @@ "antigravity": "Use when Antigravity/Kiro traffic must be intercepted through MITM and routed to OmniRoute.", "copilot": "Use when you want Copilot chat style UX while enforcing OmniRoute keys and routing rules.", "amp": "Use when you want Amp shorthand workflows but still need OmniRoute alias and routing rules enforcement.", - "qwen": "Use when you need Alibaba Qwen Code CLI for coding tasks.", "hermes": "Use when you need a lightweight terminal-native AI assistant for quick tasks.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Use for custom tool implementations or generic OpenAI-compatible configurations." @@ -2055,7 +2054,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Hermes AI Terminal Assistant", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/bg.json b/src/i18n/messages/bg.json index f3cffc82bb7..e4e29c15fa8 100644 --- a/src/i18n/messages/bg.json +++ b/src/i18n/messages/bg.json @@ -2036,7 +2036,6 @@ "antigravity": "Използвайте, когато трафикът на Antigravity/Kiro трябва да бъде прихванат чрез MITM и насочен към OmniRoute.", "copilot": "Използвайте, когато искате UX в стил на чат Copilot, като същевременно налагате OmniRoute ключове и правила за маршрутизиране.", "amp": "Use when you want Amp CLI routed through OmniRoute with stable shorthand aliases and OpenAI-compatible setup.", - "qwen": "Use when you want Qwen Code CLI with OmniRoute-managed provider settings and multi-provider model access.", "hermes": "Използвайте, когато се нуждаете от лек асистент с изкуствен интелект за бързи задачи.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Използвайте за внедряване на персонализирани инструменти или общи конфигурации, съвместими с OpenAI." @@ -2055,7 +2054,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Hermes AI Terminal Assistant", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/bn.json b/src/i18n/messages/bn.json index 546ab863743..35530f66b9c 100644 --- a/src/i18n/messages/bn.json +++ b/src/i18n/messages/bn.json @@ -2036,7 +2036,6 @@ "antigravity": "Use when Antigravity/Kiro traffic must be intercepted through MITM and routed to OmniRoute.", "copilot": "Use when you want Copilot chat style UX while enforcing OmniRoute keys and routing rules.", "amp": "Use when you want Amp shorthand workflows but still need OmniRoute alias and routing rules enforcement.", - "qwen": "Use when you need Alibaba Qwen Code CLI for coding tasks.", "hermes": "Use when you need a lightweight terminal-native AI assistant for quick tasks.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Use for custom tool implementations or generic OpenAI-compatible configurations." @@ -2055,7 +2054,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Hermes AI Terminal Assistant", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/cs.json b/src/i18n/messages/cs.json index b3e235d77f0..e4f7a613774 100644 --- a/src/i18n/messages/cs.json +++ b/src/i18n/messages/cs.json @@ -2036,7 +2036,6 @@ "antigravity": "Použijte, pokud musí být provoz Antigravity/Kiro zachycen prostřednictvím MITM a směrován do OmniRoute.", "copilot": "Použijte, pokud chcete uživatelské rozhraní ve stylu Copilot chat a zároveň vynutit klíče a pravidla směrování OmniRoute.", "amp": "Use when you want Amp CLI routed through OmniRoute with stable shorthand aliases and OpenAI-compatible setup.", - "qwen": "Use when you want Qwen Code CLI with OmniRoute-managed provider settings and multi-provider model access.", "hermes": "Použijte, když potřebujete lehkého terminálového nativního AI asistenta pro rychlé úkoly.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Použijte pro vlastní implementace nástrojů nebo obecné konfigurace kompatibilní s OpenAI." @@ -2055,7 +2054,6 @@ "kiro": "Amazon Kiro – AI poháněné IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Hermes AI Terminal Assistant", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/da.json b/src/i18n/messages/da.json index bbebc4ad204..49cea021c9a 100644 --- a/src/i18n/messages/da.json +++ b/src/i18n/messages/da.json @@ -2036,7 +2036,6 @@ "antigravity": "Bruges, når Antigravity/Kiro-trafik skal opsnappes gennem MITM og dirigeres til OmniRoute.", "copilot": "Brug, når du ønsker Copilot-chatstil UX, mens du håndhæver OmniRoute-nøgler og routingregler.", "amp": "Use when you want Amp CLI routed through OmniRoute with stable shorthand aliases and OpenAI-compatible setup.", - "qwen": "Use when you want Qwen Code CLI with OmniRoute-managed provider settings and multi-provider model access.", "hermes": "Brug, når du har brug for en letvægts terminal-native AI-assistent til hurtige opgaver.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Brug til brugerdefinerede værktøjsimplementeringer eller generiske OpenAI-kompatible konfigurationer." @@ -2055,7 +2054,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Hermes AI terminalassistent", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 4f7cbd05a38..753509cafeb 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -2041,7 +2041,6 @@ "antigravity": "Wird verwendet, wenn Antigravity/Kiro-Verkehr über MITM abgefangen und an OmniRoute weitergeleitet werden muss.", "copilot": "Verwenden Sie diese Option, wenn Sie eine UX im Copilot-Chat-Stil wünschen und gleichzeitig OmniRoute-Schlüssel und Routing-Regeln durchsetzen möchten.", "amp": "Use when you want Amp CLI routed through OmniRoute with stable shorthand aliases and OpenAI-compatible setup.", - "qwen": "Use when you want Qwen Code CLI with OmniRoute-managed provider settings and multi-provider model access.", "hermes": "Verwenden Sie es, wenn Sie einen leichten terminal-nativen KI-Assistenten für schnelle Aufgaben benötigen.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Verwendung für benutzerdefinierte Tool-Implementierungen oder generische OpenAI-kompatible Konfigurationen." @@ -2060,7 +2059,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Hermes AI Terminal Assistant", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index f7cbe053d1a..4a2a96b0e05 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -2160,7 +2160,6 @@ "antigravity": "Use when Antigravity/Kiro traffic must be intercepted through MITM and routed to OmniRoute.", "copilot": "Use when you want Copilot chat style UX while enforcing OmniRoute keys and routing rules.", "amp": "Use when you want Amp shorthand workflows but still need OmniRoute alias and routing rules enforcement.", - "qwen": "Use when you need Alibaba Qwen Code CLI for coding tasks.", "hermes": "Use when you need a lightweight terminal-native AI assistant for quick tasks.", "hermes-agent": "Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Use for custom tool implementations or generic OpenAI-compatible configurations." @@ -2179,7 +2178,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Hermes AI Terminal Assistant", "hermes-agent": "Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", @@ -4628,7 +4626,7 @@ "blackboxWebCookiePlaceholder": "Blackbox Web Cookie Placeholder", "t3ChatWebCookieHint": "Open t3.chat → DevTools → Application → Local Storage → https://t3.chat, copy 'convex-session-id'. Then open DevTools → Network, copy the full Cookie header from any chat request. Paste both values in the fields below.", "t3ChatWebCookiePlaceholder": "convex-session-id=abc123...", - "grokWebCookieHint": "grok.com's cf_clearance cookie is pinned to the IP, User-Agent, and TLS fingerprint of the browser where you copied it — pasting it from a different machine/IP causes a 403. Paste sso and sso-rw here, then open Advanced Settings and fill Custom User-Agent with the EXACT User-Agent string of that same browser, and use the same IP/proxy for this connection.", + "grokWebCookieHint": "Grok Web Cookie Hint", "blockClaudeExtraUsageDescription": "Hide extra Claude usage rows reported by some providers when they duplicate primary token accounting.", "blockClaudeExtraUsageLabel": "Block duplicate Claude usage rows", "disableCoolingDescription": "Skip the transient cooldown so this connection stays eligible even after recoverable errors (terminal states like banned/expired still apply).", @@ -4706,7 +4704,6 @@ "tierFast": "Fast", "antigravityProjectIdLabel": "Google Cloud Project ID", "antigravityProjectIdPlaceholder": "my-gcp-project-id", - "grokWebCookieHint": "Grok Web Cookie Hint", "grokWebCookiePlaceholder": "Grok Web Cookie Placeholder", "herokuBaseUrlHint": "Heroku Base Url Hint", "hideEmail": "Hide Email", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 17fd905e427..ea0bce3b16c 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -2036,7 +2036,6 @@ "antigravity": "Úselo cuando el tráfico de Antigravity/Kiro debe interceptarse a través de MITM y enrutarse a OmniRoute.", "copilot": "Úselo cuando desee una experiencia de usuario estilo chat Copilot mientras aplica las claves de OmniRoute y las reglas de enrutamiento.", "amp": "Use when you want Amp CLI routed through OmniRoute with stable shorthand aliases and OpenAI-compatible setup.", - "qwen": "Use when you want Qwen Code CLI with OmniRoute-managed provider settings and multi-provider model access.", "hermes": "Úselo cuando necesite un asistente de IA nativo de terminal liviano para tareas rápidas.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Úselo para implementaciones de herramientas personalizadas o configuraciones genéricas compatibles con OpenAI." @@ -2055,7 +2054,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Asistente de terminal de IA de Hermes", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/fa.json b/src/i18n/messages/fa.json index b6a32a23af1..86ad158f38b 100644 --- a/src/i18n/messages/fa.json +++ b/src/i18n/messages/fa.json @@ -2036,7 +2036,6 @@ "antigravity": "Use when Antigravity/Kiro traffic must be intercepted through MITM and routed to OmniRoute.", "copilot": "Use when you want Copilot chat style UX while enforcing OmniRoute keys and routing rules.", "amp": "Use when you want Amp shorthand workflows but still need OmniRoute alias and routing rules enforcement.", - "qwen": "Use when you need Alibaba Qwen Code CLI for coding tasks.", "hermes": "Use when you need a lightweight terminal-native AI assistant for quick tasks.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Use for custom tool implementations or generic OpenAI-compatible configurations." @@ -2055,7 +2054,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Hermes AI Terminal Assistant", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/fi.json b/src/i18n/messages/fi.json index c6731c0391e..e635ff7fb4c 100644 --- a/src/i18n/messages/fi.json +++ b/src/i18n/messages/fi.json @@ -2036,7 +2036,6 @@ "antigravity": "Käytä, kun Antigravity/Kiro-liikenne on siepattava MITM:n kautta ja ohjattava OmniRouteen.", "copilot": "Käytä, kun haluat Copilot-chat-tyylisen UX:n ja pakota OmniRoute-avaimia ja reitityssääntöjä.", "amp": "Use when you want Amp CLI routed through OmniRoute with stable shorthand aliases and OpenAI-compatible setup.", - "qwen": "Use when you want Qwen Code CLI with OmniRoute-managed provider settings and multi-provider model access.", "hermes": "Käytä, kun tarvitset kevyen pääte-alkuperäisen AI-avustajan nopeisiin tehtäviin.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Käytä mukautettuihin työkalutoteutuksiin tai yleisiin OpenAI-yhteensopiviin kokoonpanoihin." @@ -2055,7 +2054,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Hermes AI Terminal Assistant", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 3d493c54448..7c706f39e9f 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -2036,7 +2036,6 @@ "antigravity": "À utiliser lorsque le trafic Antigravity/Kiro doit être intercepté via MITM et acheminé vers OmniRoute.", "copilot": "À utiliser lorsque vous souhaitez une UX de style chat Copilot tout en appliquant les clés OmniRoute et les règles de routage.", "amp": "Use when you want Amp CLI routed through OmniRoute with stable shorthand aliases and OpenAI-compatible setup.", - "qwen": "Use when you want Qwen Code CLI with OmniRoute-managed provider settings and multi-provider model access.", "hermes": "À utiliser lorsque vous avez besoin d’un assistant IA léger natif du terminal pour des tâches rapides.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "À utiliser pour les implémentations d'outils personnalisés ou les configurations génériques compatibles OpenAI." @@ -2055,7 +2054,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Assistant de terminal Hermes AI", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/gu.json b/src/i18n/messages/gu.json index ca748b96242..a8915501e2b 100644 --- a/src/i18n/messages/gu.json +++ b/src/i18n/messages/gu.json @@ -2036,7 +2036,6 @@ "antigravity": "Use when Antigravity/Kiro traffic must be intercepted through MITM and routed to OmniRoute.", "copilot": "Use when you want Copilot chat style UX while enforcing OmniRoute keys and routing rules.", "amp": "Use when you want Amp shorthand workflows but still need OmniRoute alias and routing rules enforcement.", - "qwen": "Use when you need Alibaba Qwen Code CLI for coding tasks.", "hermes": "Use when you need a lightweight terminal-native AI assistant for quick tasks.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Use for custom tool implementations or generic OpenAI-compatible configurations." @@ -2055,7 +2054,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Hermes AI Terminal Assistant", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/he.json b/src/i18n/messages/he.json index b44ec033490..f393c223e4c 100644 --- a/src/i18n/messages/he.json +++ b/src/i18n/messages/he.json @@ -2036,7 +2036,6 @@ "antigravity": "השתמש כאשר יש ליירט תעבורת Antigravity/Kiro דרך MITM ולנתב אל OmniRoute.", "copilot": "השתמש כאשר אתה רוצה UX בסגנון צ'אט Copilot תוך אכיפת מפתחות וכללי ניתוב OmniRoute.", "amp": "Use when you want Amp CLI routed through OmniRoute with stable shorthand aliases and OpenAI-compatible setup.", - "qwen": "Use when you want Qwen Code CLI with OmniRoute-managed provider settings and multi-provider model access.", "hermes": "השתמש כאשר אתה צריך עוזר AI קל משקל יליד מסוף למשימות מהירות.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "השתמש עבור הטמעת כלים מותאמים אישית או תצורות כלליות תואמות OpenAI." @@ -2055,7 +2054,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "עוזר מסוף של הרמס AI", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json index 95dff76b95e..21b9de2df5e 100644 --- a/src/i18n/messages/hi.json +++ b/src/i18n/messages/hi.json @@ -2036,7 +2036,6 @@ "antigravity": "इसका उपयोग तब करें जब एंटीग्रेविटी/किरो ट्रैफिक को एमआईटीएम के माध्यम से रोका जाना चाहिए और ओमनीरूट पर भेजा जाना चाहिए।", "copilot": "जब आप ओम्निरूट कुंजी और रूटिंग नियमों को लागू करते समय कोपायलट चैट शैली यूएक्स चाहते हैं तो इसका उपयोग करें।", "amp": "Use when you want Amp CLI routed through OmniRoute with stable shorthand aliases and OpenAI-compatible setup.", - "qwen": "Use when you want Qwen Code CLI with OmniRoute-managed provider settings and multi-provider model access.", "hermes": "जब आपको त्वरित कार्यों के लिए हल्के टर्मिनल-नेटिव एआई सहायक की आवश्यकता हो तो इसका उपयोग करें।", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "कस्टम टूल कार्यान्वयन या सामान्य OpenAI-संगत कॉन्फ़िगरेशन के लिए उपयोग करें।" @@ -2055,7 +2054,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "हर्मीस एआई टर्मिनल सहायक", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/hu.json b/src/i18n/messages/hu.json index 725c186baac..24168bc1425 100644 --- a/src/i18n/messages/hu.json +++ b/src/i18n/messages/hu.json @@ -2036,7 +2036,6 @@ "antigravity": "Akkor használja, ha az Antigravity/Kiro forgalmat MITM-en keresztül kell elfogni, és az OmniRoute-hoz kell irányítani.", "copilot": "Használja, ha másodpilóta csevegési stílusú UX-et szeretne, miközben betartja az OmniRoute kulcsokat és útválasztási szabályokat.", "amp": "Use when you want Amp CLI routed through OmniRoute with stable shorthand aliases and OpenAI-compatible setup.", - "qwen": "Use when you want Qwen Code CLI with OmniRoute-managed provider settings and multi-provider model access.", "hermes": "Használja, ha könnyű, terminál-natív AI-asszisztensre van szüksége a gyors feladatokhoz.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Egyéni eszközmegvalósításokhoz vagy általános OpenAI-kompatibilis konfigurációkhoz használható." @@ -2055,7 +2054,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Hermes AI terminál asszisztens", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/id.json b/src/i18n/messages/id.json index e095f86e159..f2fcda63726 100644 --- a/src/i18n/messages/id.json +++ b/src/i18n/messages/id.json @@ -2036,7 +2036,6 @@ "antigravity": "Gunakan ketika lalu lintas Antigravitasi/Kiro harus dicegat melalui MITM dan dialihkan ke OmniRoute.", "copilot": "Gunakan saat Anda menginginkan UX gaya obrolan kopilot sambil menerapkan kunci OmniRoute dan aturan perutean.", "amp": "Use when you want Amp CLI routed through OmniRoute with stable shorthand aliases and OpenAI-compatible setup.", - "qwen": "Use when you want Qwen Code CLI with OmniRoute-managed provider settings and multi-provider model access.", "hermes": "Gunakan saat Anda memerlukan asisten AI asli terminal yang ringan untuk tugas cepat.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Gunakan untuk implementasi alat khusus atau konfigurasi umum yang kompatibel dengan OpenAI." @@ -2055,7 +2054,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Asisten Terminal AI Hermes", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/in.json b/src/i18n/messages/in.json index ab9f489aa5c..4e2bf68f1b3 100644 --- a/src/i18n/messages/in.json +++ b/src/i18n/messages/in.json @@ -2036,7 +2036,6 @@ "antigravity": "Use when Antigravity/Kiro traffic must be intercepted through MITM and routed to OmniRoute.", "copilot": "Use when you want Copilot chat style UX while enforcing OmniRoute keys and routing rules.", "amp": "Use when you want Amp shorthand workflows but still need OmniRoute alias and routing rules enforcement.", - "qwen": "Use when you need Alibaba Qwen Code CLI for coding tasks.", "hermes": "Use when you need a lightweight terminal-native AI assistant for quick tasks.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Use for custom tool implementations or generic OpenAI-compatible configurations." @@ -2055,7 +2054,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Hermes AI Terminal Assistant", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index 96d7e3f7274..05e3d607dc3 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -2146,7 +2146,6 @@ "antigravity": "Da utilizzare quando il traffico Antigravity/Kiro deve essere intercettato tramite MITM e instradato a OmniRoute.", "copilot": "Utilizzalo quando desideri un'esperienza utente in stile chat Copilot applicando al tempo stesso le chiavi OmniRoute e le regole di routing.", "amp": "Use when you want Amp CLI routed through OmniRoute with stable shorthand aliases and OpenAI-compatible setup.", - "qwen": "Use when you want Qwen Code CLI with OmniRoute-managed provider settings and multi-provider model access.", "hermes": "Utilizzalo quando hai bisogno di un leggero assistente AI nativo del terminale per attività rapide.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Utilizzare per implementazioni di strumenti personalizzati o configurazioni generiche compatibili con OpenAI." @@ -2165,7 +2164,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Assistente terminale Hermes AI", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index 3d75e4e061a..5c4bea24c74 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -2138,7 +2138,6 @@ "antigravity": "Antigravity/Kiro トラフィックを MITM 経由でインターセプトし、OmniRoute にルーティングする必要がある場合に使用します。", "copilot": "OmniRoute キーとルーティング ルールを適用しながら、Copilot チャット スタイルの UX が必要な場合に使用します。", "amp": "Use when you want Amp CLI routed through OmniRoute with stable shorthand aliases and OpenAI-compatible setup.", - "qwen": "Use when you want Qwen Code CLI with OmniRoute-managed provider settings and multi-provider model access.", "hermes": "素早いタスクのために軽量のターミナル ネイティブ AI アシスタントが必要な場合に使用します。", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "カスタム ツールの実装または汎用の OpenAI 互換構成に使用します。" @@ -2157,7 +2156,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "エルメス AI ターミナルアシスタント", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index f88d36e2d4e..aaa11ecc7b6 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -2138,7 +2138,6 @@ "antigravity": "Antigravity/Kiro 트래픽이 MITM을 통해 가로채어 OmniRoute로 라우팅되어야 하는 경우에 사용합니다.", "copilot": "OmniRoute 키와 라우팅 규칙을 적용하면서 Copilot 채팅 스타일 UX를 원할 때 사용하세요.", "amp": "Use when you want Amp CLI routed through OmniRoute with stable shorthand aliases and OpenAI-compatible setup.", - "qwen": "Use when you want Qwen Code CLI with OmniRoute-managed provider settings and multi-provider model access.", "hermes": "빠른 작업을 위해 경량 터미널 기반 AI 도우미가 필요할 때 사용하세요.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "사용자 정의 도구 구현 또는 일반 OpenAI 호환 구성에 사용합니다." @@ -2157,7 +2156,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "헤르메스 AI 터미널 어시스턴트", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/mr.json b/src/i18n/messages/mr.json index 1a162c1c49d..c22d915bbc2 100644 --- a/src/i18n/messages/mr.json +++ b/src/i18n/messages/mr.json @@ -2138,7 +2138,6 @@ "antigravity": "Use when Antigravity/Kiro traffic must be intercepted through MITM and routed to OmniRoute.", "copilot": "Use when you want Copilot chat style UX while enforcing OmniRoute keys and routing rules.", "amp": "Use when you want Amp shorthand workflows but still need OmniRoute alias and routing rules enforcement.", - "qwen": "Use when you need Alibaba Qwen Code CLI for coding tasks.", "hermes": "Use when you need a lightweight terminal-native AI assistant for quick tasks.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Use for custom tool implementations or generic OpenAI-compatible configurations." @@ -2157,7 +2156,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Hermes AI Terminal Assistant", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/ms.json b/src/i18n/messages/ms.json index 9cc01bb0172..f3df934e1e8 100644 --- a/src/i18n/messages/ms.json +++ b/src/i18n/messages/ms.json @@ -2138,7 +2138,6 @@ "antigravity": "Gunakan apabila trafik Antigraviti/Kiro mesti dipintas melalui MITM dan dihalakan ke OmniRoute.", "copilot": "Gunakan apabila anda mahu Copilot gaya sembang UX sambil menguatkuasakan kekunci OmniRoute dan peraturan penghalaan.", "amp": "Use when you want Amp CLI routed through OmniRoute with stable shorthand aliases and OpenAI-compatible setup.", - "qwen": "Use when you want Qwen Code CLI with OmniRoute-managed provider settings and multi-provider model access.", "hermes": "Gunakan apabila anda memerlukan pembantu AI asli terminal yang ringan untuk tugasan pantas.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Gunakan untuk pelaksanaan alat tersuai atau konfigurasi serasi OpenAI generik." @@ -2157,7 +2156,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Pembantu Terminal AI Hermes", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json index cc454df883c..a96520606c5 100644 --- a/src/i18n/messages/nl.json +++ b/src/i18n/messages/nl.json @@ -2138,7 +2138,6 @@ "antigravity": "Gebruik wanneer Antigravity/Kiro-verkeer moet worden onderschept via MITM en naar OmniRoute moet worden gerouteerd.", "copilot": "Gebruik wanneer u UX in Copilot-chatstijl wilt terwijl u OmniRoute-sleutels en routeringsregels afdwingt.", "amp": "Use when you want Amp CLI routed through OmniRoute with stable shorthand aliases and OpenAI-compatible setup.", - "qwen": "Use when you want Qwen Code CLI with OmniRoute-managed provider settings and multi-provider model access.", "hermes": "Gebruik deze wanneer u een lichtgewicht terminal-native AI-assistent nodig heeft voor snelle taken.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Gebruik voor aangepaste toolimplementaties of generieke OpenAI-compatibele configuraties." @@ -2157,7 +2156,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Hermes AI Terminalassistent", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/no.json b/src/i18n/messages/no.json index c7ed826431a..51ba649197b 100644 --- a/src/i18n/messages/no.json +++ b/src/i18n/messages/no.json @@ -2138,7 +2138,6 @@ "antigravity": "Brukes når Antigravity/Kiro-trafikk må avskjæres gjennom MITM og rutes til OmniRoute.", "copilot": "Bruk når du vil ha Copilot chat-stil UX mens du håndhever OmniRoute-nøkler og rutingsregler.", "amp": "Use when you want Amp CLI routed through OmniRoute with stable shorthand aliases and OpenAI-compatible setup.", - "qwen": "Use when you want Qwen Code CLI with OmniRoute-managed provider settings and multi-provider model access.", "hermes": "Bruk når du trenger en lett terminal-native AI-assistent for raske oppgaver.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Bruk for tilpassede verktøyimplementeringer eller generiske OpenAI-kompatible konfigurasjoner." @@ -2157,7 +2156,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Hermes AI terminalassistent", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/phi.json b/src/i18n/messages/phi.json index ffc09fb9e36..772dfd658c4 100644 --- a/src/i18n/messages/phi.json +++ b/src/i18n/messages/phi.json @@ -2138,7 +2138,6 @@ "antigravity": "Gamitin kapag ang trapiko ng Antigravity/Kiro ay dapat ma-intercept sa MITM at iruta sa OmniRoute.", "copilot": "Gamitin kapag gusto mong Copilot chat style UX habang ipinapatupad ang mga OmniRoute key at mga panuntunan sa pagruruta.", "amp": "Use when you want Amp CLI routed through OmniRoute with stable shorthand aliases and OpenAI-compatible setup.", - "qwen": "Use when you want Qwen Code CLI with OmniRoute-managed provider settings and multi-provider model access.", "hermes": "Gamitin kapag kailangan mo ng magaan na terminal-native AI assistant para sa mga mabilisang gawain.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Gamitin para sa mga custom na pagpapatupad ng tool o generic na mga configuration na tugma sa OpenAI." @@ -2157,7 +2156,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Hermes AI Terminal Assistant", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/pl.json b/src/i18n/messages/pl.json index a75f4b05770..410048a5871 100644 --- a/src/i18n/messages/pl.json +++ b/src/i18n/messages/pl.json @@ -2160,7 +2160,6 @@ "antigravity": "Do stosowania, gdy ruch z Antigravity/Kiro musi być przechwytywany przez MITM i kierowany do OmniRoute.", "copilot": "Do stosowania przy chęci korzystania z interfejsu w stylu Copilot chat z jednoczesnym wymuszaniem kluczy i reguł routingu OmniRoute.", "amp": "Do stosowania przy chęci korzystania ze skróconych procesów roboczych Amp z zachowaniem wymuszania aliasów i reguł routingu OmniRoute.", - "qwen": "Do stosowania, gdy potrzebne jest Alibaba Qwen Code CLI do zadań związanych z kodowaniem.", "hermes": "Do stosowania, gdy potrzebny jest lekki asystent AI działający w terminalu do szybkich zadań.", "hermes-agent": "Do stosowania, gdy potrzebny jest Hermes Agent (od Nousresearch) z models domyślnymi, delegowanymi, wizyjnymi i pomocniczymi kierowanymi przez OmniRoute.", "custom": "Do stosowania przy niestandardowych implementacjach narzędzi lub ogólnych konfiguracjach zgodnych z OpenAI." @@ -2179,7 +2178,6 @@ "kiro": "Amazon Kiro — IDE oparte na AI", "windsurf": "Edytor kodu Windsurf AI", "copilot": "Asystent AI GitHub Copilot", - "qwen": "Alibaba Qwen Code CLI", "amp": "CLI asystenta kodowania Sourcegraph Amp", "hermes": "Asystent terminalowy AI Hermes", "hermes-agent": "Hermes Agent (od Nousresearch) - zaawansowana terminalowa AI ze wsparciem dla wielu models (delegowanie, wizja, kompresja itp.)", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index 3adf48e95e1..8378b873d90 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -2150,7 +2150,6 @@ "antigravity": "Use quando o tráfego Antigravity/Kiro deve ser interceptado através do MITM e roteado para OmniRoute.", "copilot": "Use quando desejar UX no estilo de bate-papo do Copilot enquanto impõe chaves OmniRoute e regras de roteamento.", "amp": "Use when you want Amp CLI routed through OmniRoute with stable shorthand aliases and OpenAI-compatible setup.", - "qwen": "Use quando precisar do Alibaba Qwen Code CLI para tarefas de programação.", "hermes": "Use quando precisar de um assistente leve de IA nativo do terminal para tarefas rápidas.", "hermes-agent": "Use quando precisar do Hermes Agent (pela Nousresearch) com modelos padrão, delegação, visão e auxiliares roteados pelo OmniRoute.", "custom": "Use quando seu CLI ou SDK não estiver hardcoded no OmniRoute, mas ainda aceitar base URL, chave de API e model string compatíveis com OpenAI." @@ -2169,7 +2168,6 @@ "kiro": "Amazon Kiro — IDE com IA", "windsurf": "Windsurf — Editor de Código com IA", "copilot": "GitHub Copilot — Assistente de IA", - "qwen": "Alibaba Qwen Code CLI", "amp": "CLI do assistente de codificação Sourcegraph Amp", "hermes": "Assistente de Terminal Hermes AI", "hermes-agent": "Hermes Agent (by Nousresearch) - IA de terminal avançada com suporte multi-modelo (delegação, visão, compressão, etc.)", @@ -4626,7 +4624,7 @@ "blackboxWebCookiePlaceholder": "Cole o valor de __Secure-authjs.session-token", "t3ChatWebCookieHint": "Abra t3.chat → DevTools → Aplicativo → Armazenamento local → https://t3.chat, copie 'convex-session-id'. Em seguida, abra DevTools → Rede, copie o cabeçalho completo do cookie de qualquer solicitação de chat. Cole os dois valores nos campos abaixo.", "t3ChatWebCookiePlaceholder": "id-de-sessão-convexa=abc123...", - "grokWebCookieHint": "O cookie cf_clearance do grok.com é vinculado ao IP, ao User-Agent e à impressão digital TLS do navegador de onde você o copiou — colá-lo de outra máquina/IP causa um erro 403. Cole sso e sso-rw aqui e depois abra Configurações Avançadas e preencha o Custom User-Agent com o valor EXATO do User-Agent desse mesmo navegador, usando o mesmo IP/proxy para esta conexão.", + "grokWebCookieHint": "Cole o cookie sso do grok.com. Um valor completo `sso=...` também funciona.", "blockClaudeExtraUsageDescription": "Quando habilitado, o OmniRoute marca esta conta do Claude Code como indisponível assim que a API de usage reporta `extra_usage.queued`, para que o fallback troque para outra conta antes de continuar com cobranças extras pay-as-you-go.", "blockClaudeExtraUsageLabel": "Bloquear Claude Extra Usage", "disableCoolingDescription": "__MISSING__:Skip the transient cooldown so this connection stays eligible even after recoverable errors (terminal states like banned/expired still apply).", @@ -4704,7 +4702,6 @@ "tierFast": "Rápido", "antigravityProjectIdLabel": "ID do projeto do Google Cloud", "antigravityProjectIdPlaceholder": "meu-gcp-project-id", - "grokWebCookieHint": "Cole o cookie sso do grok.com. Um valor completo `sso=...` também funciona.", "grokWebCookiePlaceholder": "Cole o valor do cookie sso do grok.com", "herokuBaseUrlHint": "Obrigatório: cole a URL base do Heroku Inference. O app adicionará /v1/chat/completions.", "hideEmail": "Ocultar e-mail", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 252a7f93607..5f02305084a 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -2138,7 +2138,6 @@ "antigravity": "Use quando o tráfego Antigravity/Kiro deve ser interceptado através do MITM e roteado para OmniRoute.", "copilot": "Use quando desejar UX no estilo de bate-papo do Copilot enquanto impõe chaves OmniRoute e regras de roteamento.", "amp": "Use when you want Amp CLI routed through OmniRoute with stable shorthand aliases and OpenAI-compatible setup.", - "qwen": "Use when you want Qwen Code CLI with OmniRoute-managed provider settings and multi-provider model access.", "hermes": "Use quando precisar de um assistente leve de IA nativo do terminal para tarefas rápidas.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Use para implementações de ferramentas personalizadas ou configurações genéricas compatíveis com OpenAI." @@ -2157,7 +2156,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Assistente de Terminal Hermes AI", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/ro.json b/src/i18n/messages/ro.json index 59f766a3fe1..4ef50d640db 100644 --- a/src/i18n/messages/ro.json +++ b/src/i18n/messages/ro.json @@ -2138,7 +2138,6 @@ "antigravity": "Utilizați atunci când traficul Antigravity/Kiro trebuie interceptat prin MITM și direcționat către OmniRoute.", "copilot": "Utilizați atunci când doriți UX în stilul de chat Copilot, în timp ce aplicați cheile și regulile de rutare OmniRoute.", "amp": "Use when you want Amp CLI routed through OmniRoute with stable shorthand aliases and OpenAI-compatible setup.", - "qwen": "Use when you want Qwen Code CLI with OmniRoute-managed provider settings and multi-provider model access.", "hermes": "Utilizați atunci când aveți nevoie de un asistent AI nativ pentru terminale ușor pentru sarcini rapide.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Utilizați pentru implementări de instrumente personalizate sau configurații generice compatibile cu OpenAI." @@ -2157,7 +2156,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Hermes AI Terminal Assistant", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index 391a1ff6e77..8b779520dd9 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -2138,7 +2138,6 @@ "antigravity": "Используйте, когда трафик Антигравитации/Киро необходимо перехватить через MITM и направить в OmniRoute.", "copilot": "Используйте его, если вам нужен пользовательский интерфейс в стиле чата Copilot, одновременно обеспечивая соблюдение ключей OmniRoute и правил маршрутизации.", "amp": "Use when you want Amp CLI routed through OmniRoute with stable shorthand aliases and OpenAI-compatible setup.", - "qwen": "Use when you want Qwen Code CLI with OmniRoute-managed provider settings and multi-provider model access.", "hermes": "Используйте, когда вам нужен легкий помощник с искусственным интеллектом, встроенный в терминал, для быстрых задач.", "hermes-agent": "Используйте, когда вам нужен Hermes Agent (от Nousresearch) с моделями по умолчанию, делегирования, видения и вспомогательными, маршрутизированными через OmniRoute.", "custom": "Используйте для пользовательских реализаций инструментов или общих конфигураций, совместимых с OpenAI." @@ -2157,7 +2156,6 @@ "kiro": "Amazon Kiro - IDE с ИИ", "windsurf": "Редактор кода Windsurf с ИИ", "copilot": "Ассистент GitHub Copilot с ИИ", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Помощник по терминалу Hermes AI", "hermes-agent": "Агент Hermes (от Nousresearch) - Продвинутый терминальный ИИ с поддержкой нескольких моделей (делегирование, зрение, сжатие и т.д.)", diff --git a/src/i18n/messages/sk.json b/src/i18n/messages/sk.json index ff9574d750e..19d7466c249 100644 --- a/src/i18n/messages/sk.json +++ b/src/i18n/messages/sk.json @@ -2138,7 +2138,6 @@ "antigravity": "Použite, keď musí byť premávka Antigravity/Kiro zachytená cez MITM a nasmerovaná na OmniRoute.", "copilot": "Použite, keď chcete UX v štýle chatu Copilot pri presadzovaní kľúčov OmniRoute a pravidiel smerovania.", "amp": "Use when you want Amp CLI routed through OmniRoute with stable shorthand aliases and OpenAI-compatible setup.", - "qwen": "Use when you want Qwen Code CLI with OmniRoute-managed provider settings and multi-provider model access.", "hermes": "Použite, keď potrebujete ľahkého terminálového natívneho asistenta AI na rýchle úlohy.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Používa sa na implementácie vlastných nástrojov alebo všeobecné konfigurácie kompatibilné s OpenAI." @@ -2157,7 +2156,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Hermes AI Terminal Assistant", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/sv.json b/src/i18n/messages/sv.json index 3f6daed9c10..2202a19a9df 100644 --- a/src/i18n/messages/sv.json +++ b/src/i18n/messages/sv.json @@ -2138,7 +2138,6 @@ "antigravity": "Använd när Antigravity/Kiro-trafik måste avlyssnas genom MITM och dirigeras till OmniRoute.", "copilot": "Använd när du vill ha Copilot chattstil UX samtidigt som du upprätthåller OmniRoute-nycklar och routingregler.", "amp": "Use when you want Amp CLI routed through OmniRoute with stable shorthand aliases and OpenAI-compatible setup.", - "qwen": "Use when you want Qwen Code CLI with OmniRoute-managed provider settings and multi-provider model access.", "hermes": "Använd när du behöver en lätt terminalbaserad AI-assistent för snabba uppgifter.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Använd för anpassade verktygsimplementeringar eller generiska OpenAI-kompatibla konfigurationer." @@ -2157,7 +2156,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Hermes AI terminalassistent", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/sw.json b/src/i18n/messages/sw.json index 650e45ee5c1..4c6012d1c5f 100644 --- a/src/i18n/messages/sw.json +++ b/src/i18n/messages/sw.json @@ -2036,7 +2036,6 @@ "antigravity": "Use when Antigravity/Kiro traffic must be intercepted through MITM and routed to OmniRoute.", "copilot": "Use when you want Copilot chat style UX while enforcing OmniRoute keys and routing rules.", "amp": "Use when you want Amp shorthand workflows but still need OmniRoute alias and routing rules enforcement.", - "qwen": "Use when you need Alibaba Qwen Code CLI for coding tasks.", "hermes": "Use when you need a lightweight terminal-native AI assistant for quick tasks.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Use for custom tool implementations or generic OpenAI-compatible configurations." @@ -2055,7 +2054,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Hermes AI Terminal Assistant", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/ta.json b/src/i18n/messages/ta.json index d3a331b7fe8..564a8ceb34d 100644 --- a/src/i18n/messages/ta.json +++ b/src/i18n/messages/ta.json @@ -2036,7 +2036,6 @@ "antigravity": "Use when Antigravity/Kiro traffic must be intercepted through MITM and routed to OmniRoute.", "copilot": "Use when you want Copilot chat style UX while enforcing OmniRoute keys and routing rules.", "amp": "Use when you want Amp shorthand workflows but still need OmniRoute alias and routing rules enforcement.", - "qwen": "Use when you need Alibaba Qwen Code CLI for coding tasks.", "hermes": "Use when you need a lightweight terminal-native AI assistant for quick tasks.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Use for custom tool implementations or generic OpenAI-compatible configurations." @@ -2055,7 +2054,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Hermes AI Terminal Assistant", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/te.json b/src/i18n/messages/te.json index 24735f438c6..a5a16af3e0e 100644 --- a/src/i18n/messages/te.json +++ b/src/i18n/messages/te.json @@ -2036,7 +2036,6 @@ "antigravity": "Use when Antigravity/Kiro traffic must be intercepted through MITM and routed to OmniRoute.", "copilot": "Use when you want Copilot chat style UX while enforcing OmniRoute keys and routing rules.", "amp": "Use when you want Amp shorthand workflows but still need OmniRoute alias and routing rules enforcement.", - "qwen": "Use when you need Alibaba Qwen Code CLI for coding tasks.", "hermes": "Use when you need a lightweight terminal-native AI assistant for quick tasks.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Use for custom tool implementations or generic OpenAI-compatible configurations." @@ -2055,7 +2054,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Hermes AI Terminal Assistant", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/th.json b/src/i18n/messages/th.json index 41f4c703f2b..6b555dcac53 100644 --- a/src/i18n/messages/th.json +++ b/src/i18n/messages/th.json @@ -2036,7 +2036,6 @@ "antigravity": "ใช้เมื่อต้องสกัดกั้นการรับส่งข้อมูล Antigravity/Kiro ผ่าน MITM และกำหนดเส้นทางไปยัง OmniRoute", "copilot": "ใช้เมื่อคุณต้องการ UX รูปแบบการแชทของ Copilot ในขณะที่บังคับใช้คีย์ OmniRoute และกฎการกำหนดเส้นทาง", "amp": "Use when you want Amp CLI routed through OmniRoute with stable shorthand aliases and OpenAI-compatible setup.", - "qwen": "Use when you want Qwen Code CLI with OmniRoute-managed provider settings and multi-provider model access.", "hermes": "ใช้เมื่อคุณต้องการผู้ช่วย AI แบบเนทิฟเทอร์มินัลน้ำหนักเบาสำหรับงานที่รวดเร็ว", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "ใช้สำหรับการใช้เครื่องมือแบบกำหนดเองหรือการกำหนดค่าทั่วไปที่เข้ากันได้กับ OpenAI" @@ -2055,7 +2054,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "ผู้ช่วยเทอร์มินัล Hermes AI", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/tr.json b/src/i18n/messages/tr.json index a4bf9ecdae0..d66c81865e9 100644 --- a/src/i18n/messages/tr.json +++ b/src/i18n/messages/tr.json @@ -2036,7 +2036,6 @@ "antigravity": "Antigravity/Kiro trafiğinin MITM üzerinden yakalanıp OmniRoute'a yönlendirilmesi gerektiğinde kullanın.", "copilot": "OmniRoute anahtarları ve yönlendirme kuralları uygulanırken Copilot sohbet tarzı bir UX istediğinizde kullanın.", "amp": "Use when you want Amp CLI routed through OmniRoute with stable shorthand aliases and OpenAI-compatible setup.", - "qwen": "Use when you want Qwen Code CLI with OmniRoute-managed provider settings and multi-provider model access.", "hermes": "Hızlı görevler için hafif, terminalde yerleşik bir yapay zeka asistanına ihtiyaç duyduğunuzda kullanın.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Özel araç uygulamaları veya genel OpenAI uyumlu yapılandırmalar için kullanın." @@ -2055,7 +2054,6 @@ "kiro": "Amazon Kiro - AI destekli IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Hermes AI Terminal Asistanı", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/uk-UA.json b/src/i18n/messages/uk-UA.json index 970b9101cfa..9087938bb4e 100644 --- a/src/i18n/messages/uk-UA.json +++ b/src/i18n/messages/uk-UA.json @@ -2036,7 +2036,6 @@ "antigravity": "Використовуйте, коли трафік Antigravity/Kiro потрібно перехопити через MITM і направити на OmniRoute.", "copilot": "Використовуйте, коли вам потрібен UX у стилі чату Copilot із застосуванням ключів OmniRoute і правил маршрутизації.", "amp": "Use when you want Amp CLI routed through OmniRoute with stable shorthand aliases and OpenAI-compatible setup.", - "qwen": "Use when you want Qwen Code CLI with OmniRoute-managed provider settings and multi-provider model access.", "hermes": "Використовуйте, коли вам потрібен легкий власний помічник зі штучним інтелектом для швидких завдань.", "hermes-agent": "Використовуйте, коли потрібен Hermes Agent (від Nousresearch) із моделями за замовчуванням, делегування, vision та допоміжними, маршрутизованими через OmniRoute.", "custom": "Використовуйте для реалізації спеціальних інструментів або загальних конфігурацій, сумісних з OpenAI." @@ -2055,7 +2054,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Hermes AI Terminal Assistant", "hermes-agent": "Hermes Agent (від Nousresearch) — просунутий термінальний AI з підтримкою мультимоделей (делегування, vision, стиснення тощо)", diff --git a/src/i18n/messages/ur.json b/src/i18n/messages/ur.json index b4233deb350..dff2ee63b90 100644 --- a/src/i18n/messages/ur.json +++ b/src/i18n/messages/ur.json @@ -2036,7 +2036,6 @@ "antigravity": "Use when Antigravity/Kiro traffic must be intercepted through MITM and routed to OmniRoute.", "copilot": "Use when you want Copilot chat style UX while enforcing OmniRoute keys and routing rules.", "amp": "Use when you want Amp shorthand workflows but still need OmniRoute alias and routing rules enforcement.", - "qwen": "Use when you need Alibaba Qwen Code CLI for coding tasks.", "hermes": "Use when you need a lightweight terminal-native AI assistant for quick tasks.", "hermes-agent": "__MISSING__:Use when you need Hermes Agent (by Nousresearch) with default, delegation, vision and auxiliary models routed through OmniRoute.", "custom": "Use for custom tool implementations or generic OpenAI-compatible configurations." @@ -2055,7 +2054,6 @@ "kiro": "Amazon Kiro — AI-powered IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp coding assistant CLI", "hermes": "Hermes AI Terminal Assistant", "hermes-agent": "__MISSING__:Hermes Agent (by Nousresearch) - Advanced terminal AI with multi-model support (delegation, vision, compression, etc.)", diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json index 3ea2b92f357..72f5f5fc74d 100644 --- a/src/i18n/messages/vi.json +++ b/src/i18n/messages/vi.json @@ -2183,7 +2183,6 @@ "antigravity": "Sử dụng khi lưu lượng truy cập Antigravity/Kiro cần được chặn bắt qua MITM và định tuyến đến OmniRoute.", "copilot": "Sử dụng khi bạn muốn trải nghiệm trò chuyện kiểu Copilot đồng thời áp dụng các khóa OmniRoute và quy tắc định tuyến.", "amp": "Sử dụng khi bạn muốn các quy trình làm việc viết tắt của Amp nhưng vẫn cần áp dụng bí danh và quy tắc định tuyến của OmniRoute.", - "qwen": "Sử dụng khi bạn cần Alibaba Qwen Code CLI cho các tác vụ lập trình.", "hermes": "Sử dụng khi bạn cần một trợ lý AI gọn nhẹ hoạt động trực tiếp trong terminal cho các tác vụ nhanh.", "hermes-agent": "Sử dụng khi bạn cần Hermes Agent (do Nousresearch phát triển) với các mô hình mặc định, ủy quyền, xử lý hình ảnh và phụ trợ được định tuyến qua OmniRoute.", "custom": "Sử dụng để triển khai công cụ tùy chỉnh hoặc cấu hình chung tương thích với OpenAI." @@ -2202,7 +2201,6 @@ "kiro": "Amazon Kiro — IDE tích hợp AI", "windsurf": "Trình chỉnh sửa mã AI Windsurf", "copilot": "Trợ lý AI GitHub Copilot", - "qwen": "Alibaba Qwen Code CLI", "amp": "CLI trợ lý lập trình Sourcegraph Amp", "hermes": "Trợ lý AI terminal Hermes", "hermes-agent": "Hermes Agent (bởi Nousresearch) - AI nâng cao cho terminal, hỗ trợ đa mô hình (ủy quyền, thị giác, nén, v.v.)", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 8ce52b2a77c..6660828d5f9 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -2121,7 +2121,6 @@ "antigravity": "当必须通过 MITM 拦截 Antigravity/Kiro 流量并将其路由到 OmniRoute 时使用。", "copilot": "当您想要 Copilot 聊天风格的 UX 同时强制执行 OmniRoute 键和路由规则时使用。", "amp": "当您想要 Amp 简写工作流,但仍需要 OmniRoute 别名和路由规则支持时使用。", - "qwen": "当您需要使用阿里云 Qwen Code CLI 进行编码任务时使用。", "hermes": "当您需要轻量级终端原生 AI 助手来处理快速任务时使用。", "hermes-agent": "需要使用 Hermes Agent (by Nousresearch) 时使用,默认、委派、视觉与辅助模型均通过 OmniRoute 路由。", "custom": "用于自定义工具实现或通用 OpenAI 兼容配置。" @@ -2140,7 +2139,6 @@ "kiro": "Amazon Kiro - AI 驱动 IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp 编程助手 CLI", "hermes": "Hermes AI 终端助手", "hermes-agent": "Hermes Agent (by Nousresearch) — 支持多模型(委派、视觉、压缩等)的高级终端 AI。", diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index 72045dd9e44..74340dd37f2 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -2144,7 +2144,6 @@ "antigravity": "當必須通過 MITM 攔截 Antigravity/Kiro 流量並將其路由到 OmniRoute 時使用。", "copilot": "當您想要 Copilot 聊天風格的 UX 同時強制執行 OmniRoute 鍵和路由規則時使用。", "amp": "當您想要 Amp 簡寫工作流,但仍需要 OmniRoute 別名和路由規則支援時使用。", - "qwen": "當您需要使用阿里雲 Qwen Code CLI 進行編碼任務時使用。", "hermes": "當您需要輕量級終端原生 AI 助手來處理快速任務時使用。", "hermes-agent": "需要使用 Hermes Agent (by Nousresearch) 時使用,預設、委派、視覺與輔助模型均通過 OmniRoute 路由。", "custom": "用於自定義工具實現或通用 OpenAI 相容配置。" @@ -2163,7 +2162,6 @@ "kiro": "Amazon Kiro - AI 驅動 IDE", "windsurf": "Windsurf AI Code Editor", "copilot": "GitHub Copilot AI Assistant", - "qwen": "Alibaba Qwen Code CLI", "amp": "Sourcegraph Amp 程式設計助手 CLI", "hermes": "Hermes AI 終端助手", "hermes-agent": "Hermes Agent (by Nousresearch) — 支援多模型(委派、視覺、壓縮等)的高階終端 AI。", diff --git a/src/lib/acp/manager.ts b/src/lib/acp/manager.ts index 055fc1e222c..e95f1d0f194 100644 --- a/src/lib/acp/manager.ts +++ b/src/lib/acp/manager.ts @@ -47,7 +47,7 @@ export class AcpManager extends EventEmitter { args: string[] = [], env: Record = {} ): AcpSession { - const ALLOWED_AGENTS = ["claude", "codex", "gemini", "qwen"]; + const ALLOWED_AGENTS = ["claude", "codex", "gemini"]; if (!ALLOWED_AGENTS.includes(agentId)) { throw new Error(`Unknown agent: ${agentId}`); } diff --git a/src/lib/acp/registry.ts b/src/lib/acp/registry.ts index df3a2134cd5..07300475f67 100644 --- a/src/lib/acp/registry.ts +++ b/src/lib/acp/registry.ts @@ -5,7 +5,7 @@ * and running version commands. Used to offer ACP transport as an alternative * to the HTTP proxy method. * - * Supports 14 built-in agents + user-defined custom agents from settings. + * Supports 12 built-in agents + user-defined custom agents from settings. * * Reference: https://github.com/iOfficeAI/AionUi (auto-detects CLI agents) */ @@ -114,15 +114,6 @@ const AGENT_DEFINITIONS: Omit[] = [ spawnArgs: [], protocol: "stdio", }, - { - id: "qwen-code", - name: "Qwen Code", - binary: "qwen", - versionCommand: "qwen --version", - providerAlias: "qwen", - spawnArgs: [], - protocol: "stdio", - }, { id: "forge", name: "ForgeCode", diff --git a/src/lib/cliTools/checkToolConfigStatus.ts b/src/lib/cliTools/checkToolConfigStatus.ts index 482c1a45c66..2425d86c39e 100644 --- a/src/lib/cliTools/checkToolConfigStatus.ts +++ b/src/lib/cliTools/checkToolConfigStatus.ts @@ -67,17 +67,6 @@ export async function checkToolConfigStatus( return (config?.env as Record)?.ANTHROPIC_BASE_URL ? "configured" : "not_configured"; - case "qwen": { - // Check modelProviders for OmniRoute entries - const mp = config?.modelProviders; - if (!mp) return "not_configured"; - const qwenConfigStr = JSON.stringify(mp).toLowerCase(); - return qwenConfigStr.includes("omniroute") || - qwenConfigStr.includes(`localhost:${apiPort}`) || - qwenConfigStr.includes(`127.0.0.1:${apiPort}`) - ? "configured" - : "not_configured"; - } case "droid": case "openclaw": case "cline": diff --git a/src/shared/constants/cliTools.ts b/src/shared/constants/cliTools.ts index 644c8999d2b..57714b6fb56 100644 --- a/src/shared/constants/cliTools.ts +++ b/src/shared/constants/cliTools.ts @@ -399,135 +399,6 @@ export const CLI_TOOLS: Record = { { step: 4, title: "Select Model", type: "modelSelector" }, ], }, - qwen: { - id: "qwen", - name: "Qwen Code", - icon: "psychology", - color: "#10B981", - description: - "Alibaba Qwen Code CLI — supports OpenAI, Anthropic & Gemini providers via OmniRoute", - docsUrl: "https://qwenlm.github.io/qwen-code-docs/en/users/configuration/model-providers/", - configType: "guide", - category: "code", - vendor: "Alibaba", - acpSpawnable: true, - baseUrlSupport: "full", - defaultCommand: "qwen", - notes: [ - { - type: "info", - text: "Qwen Code supports multiple provider types (openai, anthropic, gemini) via modelProviders in settings.json. OmniRoute works as an OpenAI-compatible endpoint.", - }, - { - type: "info", - text: "Any model available in OmniRoute can be used — not just Qwen models. Select from Qwen, Claude, Gemini, GPT, and more.", - }, - { - type: "warning", - text: "Config path: Linux/macOS ~/.qwen/settings.json • Windows %USERPROFILE%\\.qwen\\settings.json", - }, - { - type: "error", - text: "Qwen OAuth free tier was discontinued on 2026-04-15. Use OmniRoute with bailian-coding-plan/alibaba/alibaba-cn/openrouter/anthropic/gemini providers instead.", - }, - ], - modelAliases: [ - "coder-model", - "qwen3-coder-plus", - "qwen3-coder-flash", - "vision-model", - "claude-sonnet-4-6", - "claude-opus-4-6-thinking", - "gemini-3-flash", - "gemini-3.1-pro-high", - ], - defaultModels: [ - { - id: "coder-model", - name: "Coder Model (Qwen 3.6 Plus)", - alias: "coder-model", - envKey: "OPENAI_MODEL", - defaultValue: "coder-model", - isTopLevel: true, - }, - { - id: "qwen3-coder-plus", - name: "Qwen 3 Coder Plus", - alias: "qwen3-coder-plus", - envKey: "OPENAI_MODEL", - defaultValue: "qwen3-coder-plus", - }, - { - id: "qwen3-coder-flash", - name: "Qwen 3 Coder Flash", - alias: "qwen3-coder-flash", - envKey: "OPENAI_MODEL", - defaultValue: "qwen3-coder-flash", - }, - { - id: "vision-model", - name: "Vision Model (Multimodal)", - alias: "vision-model", - envKey: "OPENAI_MODEL", - defaultValue: "vision-model", - }, - { - id: "claude-sonnet-4-6", - name: "Claude Sonnet 4.6", - alias: "claude-sonnet-4-6", - envKey: "OPENAI_MODEL", - defaultValue: "claude-sonnet-4-6", - }, - { - id: "claude-opus-4-6-thinking", - name: "Claude Opus 4.6 Thinking", - alias: "claude-opus-4-6-thinking", - envKey: "OPENAI_MODEL", - defaultValue: "claude-opus-4-6-thinking", - }, - { - id: "gemini-3.1-pro-high", - name: "Gemini 3.1 Pro High", - alias: "gemini-3.1-pro-high", - envKey: "OPENAI_MODEL", - defaultValue: "gemini-3.1-pro-high", - }, - { - id: "gemini-3-flash", - name: "Gemini 3 Flash", - alias: "gemini-3-flash", - envKey: "OPENAI_MODEL", - defaultValue: "gemini-3-flash", - }, - ], - guideSteps: [ - { step: 1, title: "Install Qwen Code", desc: "npm install -g @qwen-code/qwen-code" }, - { step: 2, title: "API Key", type: "apiKeySelector" }, - { step: 3, title: "Base URL", value: "{{baseUrl}}", copyable: true }, - { step: 4, title: "Select Model", type: "modelSelector" }, - { - step: 5, - title: "Save Config", - desc: "Click Save Config below to write your settings.json automatically.", - }, - ], - codeBlock: { - language: "json", - code: `# ~/.qwen/settings.json — OmniRoute via security.auth -{ - "security": { - "auth": { - "selectedType": "openai", - "apiKey": "{{apiKey}}", - "baseUrl": "{{baseUrl}}" - } - }, - "model": { - "name": "{{model}}" - } -}`, - }, - }, custom: { id: "custom", name: "Custom CLI", diff --git a/src/shared/schemas/cliCatalog.ts b/src/shared/schemas/cliCatalog.ts index 27ab19e3c99..2f76b1bbd28 100644 --- a/src/shared/schemas/cliCatalog.ts +++ b/src/shared/schemas/cliCatalog.ts @@ -63,7 +63,7 @@ export const CliCatalogSchema = z.record(CliCatalogEntrySchema); // +1 (2026-07-02): "codewhale" added as a dual entry alongside "deepseek-tui" // (CodeWhale is the actively-maintained successor to DeepSeek TUI). // +1 (grok-build, decolua/9router#2571): xAI Grok Build TUI coding agent. -export const EXPECTED_CODE_COUNT = 21; +export const EXPECTED_CODE_COUNT = 20; // +2 (#6318): "omp" (Oh My Pi) and "letta" (Letta CLI) added as agent entries. // Note: #6318 originally also shipped duplicate "pi"/"jcode"/"codewhale" entries — // those tools were already delivered by a separate PR, so only omp+letta landed here. diff --git a/src/shared/services/cliRuntime.ts b/src/shared/services/cliRuntime.ts index 509bfd18cde..c2471fc65a8 100644 --- a/src/shared/services/cliRuntime.ts +++ b/src/shared/services/cliRuntime.ts @@ -168,16 +168,6 @@ const CLI_TOOLS: Record = { auth: ".qoder/auth.json", }, }, - qwen: { - defaultCommand: "qwen", - envBinKey: "CLI_QWEN_BIN", - requiresBinary: true, - healthcheckTimeoutMs: 12000, - paths: { - settings: ".qwen/settings.json", - env: ".qwen/.env", - }, - }, // ── Plan 14 — new "custom" configType tools ─────────────────────────────── forge: { defaultCommand: "forge", diff --git a/tests/unit/acp-registry.test.ts b/tests/unit/acp-registry.test.ts index 6e5d2f66f1e..75051918ae4 100644 --- a/tests/unit/acp-registry.test.ts +++ b/tests/unit/acp-registry.test.ts @@ -34,7 +34,7 @@ test("resolveVersionProbe rejects shell metacharacters in version commands", () test("shouldUseShellForVersionProbe preserves Windows npm wrapper detection", () => { assert.equal(shouldUseShellForVersionProbe("codex", "win32"), true); assert.equal( - shouldUseShellForVersionProbe("C:\\Users\\dev\\AppData\\Roaming\\npm\\qwen.cmd", "win32"), + shouldUseShellForVersionProbe("C:\\Users\\dev\\AppData\\Roaming\\npm\\codex.cmd", "win32"), true ); assert.equal(shouldUseShellForVersionProbe("C:\\Tools\\claude.exe", "win32"), false); diff --git a/tests/unit/check-tool-config-status.test.ts b/tests/unit/check-tool-config-status.test.ts index ae8db8a5838..93e4e55d76c 100644 --- a/tests/unit/check-tool-config-status.test.ts +++ b/tests/unit/check-tool-config-status.test.ts @@ -96,25 +96,6 @@ test("codex: returns 'not_configured' when TOML does not mention OmniRoute", asy assert.equal(result, "not_configured"); }); -// ── Qwen tests ──────────────────────────────────────────────────────────────── - -test("qwen: returns 'configured' when modelProviders has OmniRoute URL", async () => { - const configPath = await writeTempFile( - "qwen.json", - JSON.stringify({ - modelProviders: [{ apiBase: "http://localhost:20128/v1", name: "omniroute" }], - }) - ); - const result = await checkToolConfigStatus("qwen", configPath); - assert.equal(result, "configured"); -}); - -test("qwen: returns 'not_configured' when modelProviders is missing", async () => { - const configPath = await writeTempFile("qwen.json", JSON.stringify({})); - const result = await checkToolConfigStatus("qwen", configPath); - assert.equal(result, "not_configured"); -}); - // ── Hermes tests ────────────────────────────────────────────────────────────── test("hermes: returns 'configured' when config contains OmniRoute", async () => { diff --git a/tests/unit/cli-catalog-acpspawnable.test.ts b/tests/unit/cli-catalog-acpspawnable.test.ts index 06e79f76ce9..379aaafca86 100644 --- a/tests/unit/cli-catalog-acpspawnable.test.ts +++ b/tests/unit/cli-catalog-acpspawnable.test.ts @@ -16,7 +16,6 @@ const ACP_SPAWNABLE_IDS = [ "aider", "opencode", "cline", - "qwen", "forge", "interpreter", "cursor-cli", diff --git a/tests/unit/cli-catalog-counts.test.ts b/tests/unit/cli-catalog-counts.test.ts index c6f203d9288..d7b2a070cd9 100644 --- a/tests/unit/cli-catalog-counts.test.ts +++ b/tests/unit/cli-catalog-counts.test.ts @@ -30,7 +30,7 @@ test(`CLI_TOOLS has exactly ${EXPECTED_AGENT_COUNT} agent entries`, () => { ); }); -test("CLI_TOOLS total code entries (including none) equals 25 (21 visible + 4 none)", () => { +test("CLI_TOOLS total code entries (including none) equals 24 (20 visible + 4 none)", () => { // code-none entries: antigravity, kiro, cursor (app), hermes (simple guide) const codeNone = codeAll.filter((t) => t.baseUrlSupport === "none"); assert.equal( @@ -38,11 +38,11 @@ test("CLI_TOOLS total code entries (including none) equals 25 (21 visible + 4 no 4, `Expected 4 code entries with baseUrlSupport='none', got ${codeNone.length}: ${codeNone.map((t) => t.id).join(", ")}` ); - assert.equal(codeAll.length, 25, `Expected 25 total code entries, got ${codeAll.length}`); + assert.equal(codeAll.length, 24, `Expected 24 total code entries, got ${codeAll.length}`); }); -test("CLI_TOOLS total (code + agent) = 33", () => { - assert.equal(all.length, 33, `Expected 33 total entries, got ${all.length}`); +test("CLI_TOOLS total (code + agent) = 32", () => { + assert.equal(all.length, 32, `Expected 32 total entries, got ${all.length}`); }); test("All code-none entries have configType mitm OR are legacy excluded entries", () => { @@ -66,7 +66,7 @@ test("All agent entries have baseUrlSupport 'full' or 'partial' (no agent is 'no } }); -test("The 21 visible code entries match D15 list exactly (+ crush + codewhale + grok-build)", () => { +test("The 20 visible code entries match D15 list exactly (+ crush + codewhale + grok-build)", () => { const d15List = new Set([ "claude", "codex", @@ -74,7 +74,6 @@ test("The 21 visible code entries match D15 list exactly (+ crush + codewhale + "kilo", "roo", "continue", - "qwen", "aider", "forge", "jcode", diff --git a/tests/unit/cli-catalog-schema.test.ts b/tests/unit/cli-catalog-schema.test.ts index 2a263e7d227..48425d759e9 100644 --- a/tests/unit/cli-catalog-schema.test.ts +++ b/tests/unit/cli-catalog-schema.test.ts @@ -8,9 +8,8 @@ import assert from "node:assert/strict"; import { z } from "zod"; const { CLI_TOOLS } = await import("../../src/shared/constants/cliTools.ts"); -const { CliCatalogEntrySchema, CliCatalogSchema } = await import( - "../../src/shared/schemas/cliCatalog.ts" -); +const { CliCatalogEntrySchema, CliCatalogSchema } = + await import("../../src/shared/schemas/cliCatalog.ts"); test("Every CLI_TOOLS entry passes CliCatalogEntrySchema.parse() without error", () => { for (const [key, tool] of Object.entries(CLI_TOOLS)) { @@ -62,7 +61,7 @@ test("CliCatalogEntrySchema throws ZodError for invalid baseUrlSupport value", ( }); test("CliCatalogEntrySchema throws ZodError when required string fields are empty", () => { - const base = { ...CLI_TOOLS["qwen"] }; + const base = { ...CLI_TOOLS["opencode"] }; const invalid = { ...base, vendor: "" }; assert.throws( () => CliCatalogEntrySchema.parse(invalid), diff --git a/tests/unit/cli-runtime-detection.test.ts b/tests/unit/cli-runtime-detection.test.ts index bd996616f89..53d12bfbbf6 100644 --- a/tests/unit/cli-runtime-detection.test.ts +++ b/tests/unit/cli-runtime-detection.test.ts @@ -73,7 +73,6 @@ describe("CLI_TOOL_IDS", () => { "continue", "opencode", "qoder", - "qwen", ]; for (const id of expected) { assert.ok(CLI_TOOL_IDS.includes(id), `Missing tool: ${id}`); diff --git a/tests/unit/cli-tools-schema.test.ts b/tests/unit/cli-tools-schema.test.ts index 85841732cbd..801a926a195 100644 --- a/tests/unit/cli-tools-schema.test.ts +++ b/tests/unit/cli-tools-schema.test.ts @@ -1,7 +1,7 @@ import test from "node:test"; import assert from "node:assert/strict"; -test("CLI_TOOLS registry contains all expected tools (plan 14 — 33 total + crush + codewhale + omp + letta + grok-build)", async () => { +test("CLI_TOOLS registry contains all expected tools (plan 14 — 32 total + crush + codewhale + omp + letta + grok-build)", async () => { const { CLI_TOOLS } = await import("../../src/shared/constants/cliTools.ts"); // windsurf and amp removed per plan 14 D17 (MITM backlog plan 11) // New entries added: roo, jcode, deepseek-tui, smelt, pi, aider, forge, @@ -26,7 +26,6 @@ test("CLI_TOOLS registry contains all expected tools (plan 14 — 33 total + cru "hermes", "hermes-agent", "kiro", - "qwen", "custom", "aider", "forge", diff --git a/tests/unit/cli/setup-qwen.test.ts b/tests/unit/cli/setup-qwen.test.ts deleted file mode 100644 index c56cfa1a118..00000000000 --- a/tests/unit/cli/setup-qwen.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import { resolveQwenTarget, buildQwenSettings } from "../../../bin/cli/commands/setup-qwen.mjs"; - -test("resolveQwenTarget ensures /v1", () => { - assert.equal(resolveQwenTarget({ remote: "http://vps:20128" }).baseUrl, "http://vps:20128/v1"); -}); -test("resolveQwenTarget: explicit --api-key wins", () => { - assert.equal(resolveQwenTarget({ remote: "http://x:20128", apiKey: "sk-x" }).apiKey, "sk-x"); -}); -test("buildQwenSettings adds an openai modelProvider (baseUrl /v1, envKey), sets model", () => { - const s = buildQwenSettings({}, { baseUrl: "http://vps:20128/v1", model: "glm/glm-5.2" }); - const p = s.modelProviders.find((x) => x.id === "omniroute"); - assert.equal(p.authType, "openai"); - assert.equal(p.baseUrl, "http://vps:20128/v1"); - assert.equal(p.envKey, "OMNIROUTE_API_KEY"); - assert.equal(s.model, "glm/glm-5.2"); - assert.equal(s.selectedProvider, "omniroute"); -}); -test("buildQwenSettings de-dupes the omniroute provider + preserves others", () => { - const s = buildQwenSettings( - { modelProviders: [{ id: "other" }, { id: "omniroute", baseUrl: "old" }], theme: "dark" }, - { baseUrl: "http://x/v1", model: "m" } - ); - assert.equal(s.modelProviders.filter((p) => p.id === "omniroute").length, 1); - assert.ok(s.modelProviders.some((p) => p.id === "other")); - assert.equal(s.theme, "dark"); -}); diff --git a/tests/unit/guide-settings-route.test.ts b/tests/unit/guide-settings-route.test.ts index b6648d2d797..8537cbc56cd 100644 --- a/tests/unit/guide-settings-route.test.ts +++ b/tests/unit/guide-settings-route.test.ts @@ -9,9 +9,7 @@ import * as yaml from "js-yaml"; const guideSettingsRoute = await import("../../src/app/api/cli-tools/guide-settings/[toolId]/route.ts"); -const DUMMY_HOME = path.join(os.tmpdir(), "omniroute-qwen-test-" + Date.now()); -const QWEN_CONFIG_PATH = path.join(DUMMY_HOME, ".qwen", "settings.json"); -const QWEN_ENV_PATH = path.join(DUMMY_HOME, ".qwen", ".env"); +const DUMMY_HOME = path.join(os.tmpdir(), "omniroute-guide-settings-test-" + Date.now()); const OPENCODE_CONFIG_PATH = path.join(DUMMY_HOME, ".config", "opencode", "opencode.json"); // cliRuntime.ts hermes entry maps to .config/hermes/config.json (not .hermes/config.yaml) const HERMES_CONFIG_PATH = path.join(DUMMY_HOME, ".config", "hermes", "config.json"); @@ -31,18 +29,14 @@ async function createAuthCookie() { return `auth_token=${token}`; } -type QwenProviderEntry = { - id?: string; - baseUrl?: string; - envKey?: string; - generationConfig?: { - contextWindowSize?: number; - }; +type HermesConfig = { + model?: { default?: string; provider?: string; base_url?: string }; + providers?: { omniroute?: { base_url?: string; api_key?: string } }; }; -async function buildRequest(body: any) { +async function buildRequest(toolId: string, body: unknown) { const cookie = await createAuthCookie(); - return new Request("http://localhost/api/cli-tools/guide-settings/qwen", { + return new Request(`http://localhost/api/cli-tools/guide-settings/${toolId}`, { method: "POST", headers: { "Content-Type": "application/json", cookie }, body: JSON.stringify(body), @@ -57,7 +51,6 @@ test.beforeEach(async () => { process.env.XDG_CONFIG_HOME = path.join(DUMMY_HOME, ".config"); process.env.APPDATA = path.join(DUMMY_HOME, ".config"); process.env.API_KEY_SECRET = "test-secret"; - await fs.mkdir(path.dirname(QWEN_CONFIG_PATH), { recursive: true }).catch(() => {}); }); test.afterEach(async () => { @@ -70,28 +63,8 @@ test.afterEach(async () => { else process.env.JWT_SECRET = originalJwtSecret; }); -test("guide-settings POST creates new qwen settings.json if it doesn't exist", async () => { - const req = await buildRequest({ - baseUrl: "http://my-omni", - apiKey: "sk-123", - model: "qwen/qwen3-coder-plus", - }); - const response = (await guideSettingsRoute.POST(req, { params: { toolId: "qwen" } })) as Response; - const data = (await response.json()) as any; - - assert.equal(response.status, 200, "Response should be OK"); - assert.equal(data.success, true); - - const content = JSON.parse(await fs.readFile(QWEN_CONFIG_PATH, "utf-8")); - // Uses security.auth format (not modelProviders) - assert.equal(content.security?.auth?.selectedType, "openai"); - assert.ok(content.security?.auth?.apiKey.startsWith("sk-")); - assert.equal(content.security?.auth?.baseUrl, "http://my-omni"); - assert.equal(content.model?.name, "qwen/qwen3-coder-plus"); -}); - test("guide-settings POST creates new hermes config.yaml if it doesn't exist", async () => { - const req = await buildRequest({ + const req = await buildRequest("hermes", { baseUrl: "http://my-omni", apiKey: "sk-hermes", model: "gpt-5.4-mini", @@ -99,12 +72,12 @@ test("guide-settings POST creates new hermes config.yaml if it doesn't exist", a const response = (await guideSettingsRoute.POST(req, { params: { toolId: "hermes" }, })) as Response; - const data = (await response.json()) as any; + const data = (await response.json()) as { success?: boolean }; assert.equal(response.status, 200, "Response should be OK"); assert.equal(data.success, true); - const content = yaml.load(await fs.readFile(HERMES_CONFIG_PATH, "utf-8")) as any; + const content = yaml.load(await fs.readFile(HERMES_CONFIG_PATH, "utf-8")) as HermesConfig; assert.equal(content.model?.default, "gpt-5.4-mini"); assert.equal(content.model?.provider, "omniroute"); assert.equal(content.model?.base_url, "http://my-omni/v1"); @@ -112,34 +85,6 @@ test("guide-settings POST creates new hermes config.yaml if it doesn't exist", a assert.ok(String(content.providers?.omniroute?.api_key || "").startsWith("sk-")); }); -test("guide-settings POST merges into existing qwen settings.json", async () => { - await fs.mkdir(path.dirname(QWEN_CONFIG_PATH), { recursive: true }); - await fs.writeFile( - QWEN_CONFIG_PATH, - JSON.stringify({ - permissions: { allow: ["Bash(*)"] }, - }), - "utf-8" - ); - - const req = await buildRequest({ - baseUrl: "http://my-omni", - apiKey: "sk-456", - model: "claude-sonnet-4-6", - }); - const response = await guideSettingsRoute.POST(req, { params: { toolId: "qwen" } }); - assert.equal(response.status, 200); - - const content = JSON.parse(await fs.readFile(QWEN_CONFIG_PATH, "utf-8")); - // security.auth format - assert.equal(content.security?.auth?.selectedType, "openai"); - assert.ok(content.security?.auth?.apiKey.startsWith("sk-")); - assert.equal(content.security?.auth?.baseUrl, "http://my-omni"); - assert.equal(content.model?.name, "claude-sonnet-4-6"); - // Preserves other settings - assert.deepEqual(content.permissions?.allow, ["Bash(*)"]); -}); - test("guide-settings POST writes OpenCode config with current schema and multi-model selection", async () => { await fs.mkdir(path.dirname(OPENCODE_CONFIG_PATH), { recursive: true }); await fs.writeFile( diff --git a/tests/unit/qwen-api-key-auto-create.test.ts b/tests/unit/qwen-api-key-auto-create.test.ts deleted file mode 100644 index fa4f8b4816d..00000000000 --- a/tests/unit/qwen-api-key-auto-create.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs/promises"; -import os from "os"; -import path from "path"; -import { SignJWT } from "jose"; -import { getOrCreateApiKey, resolveApiKey } from "../../src/shared/services/apiKeyResolver"; -import { validateApiKey } from "../../src/lib/db/apiKeys"; -import { getDbInstance } from "../../src/lib/db/core"; - -const DUMMY_HOME = path.join(os.tmpdir(), "omniroute-qwen-key-test-" + Date.now()); -const originalJwtSecret = process.env.JWT_SECRET; - -async function createAuthCookie() { - process.env.JWT_SECRET = "test-cli-tools-secret"; - const secret = new TextEncoder().encode(process.env.JWT_SECRET); - const token = await new SignJWT({ sub: "test-user" }) - .setProtectedHeader({ alg: "HS256" }) - .setIssuedAt() - .setExpirationTime("1h") - .sign(secret); - return `auth_token=${token}`; -} - -const originalHomedir = os.homedir; - -test.beforeEach(async () => { - process.env.DATA_DIR = DUMMY_HOME; - process.env.API_KEY_SECRET = "test-secret"; - os.homedir = () => DUMMY_HOME; - await fs.mkdir(DUMMY_HOME, { recursive: true }).catch(() => {}); - // Initialize DB - getDbInstance(); -}); - -test.afterEach(async () => { - await fs.rm(DUMMY_HOME, { recursive: true, force: true }).catch(() => {}); - os.homedir = originalHomedir; - if (originalJwtSecret === undefined) delete process.env.JWT_SECRET; - else process.env.JWT_SECRET = originalJwtSecret; - if (process.env.DATA_DIR?.includes("omniroute-qwen-key-test")) { - delete process.env.DATA_DIR; - } -}); - -test("getOrCreateApiKey() creates DB-backed key when no keyId provided", async () => { - const apiKey = await getOrCreateApiKey(null); - - // Key should NOT be the placeholder "sk_omniroute" - assert.notEqual(apiKey, "sk_omniroute", "Should not return placeholder"); - assert.ok(apiKey.startsWith("sk-"), "Key should start with sk- prefix"); - - // Key should be valid in DB - const valid = await validateApiKey(apiKey); - assert.equal(valid, true, "Auto-created key should validate successfully"); -}); - -test("getOrCreateApiKey() returns existing key when keyId is provided", async () => { - // First create a key with a specific keyId - const firstKey = await getOrCreateApiKey(null); - assert.ok(firstKey.startsWith("sk-")); - - // Create another key and get its ID - const secondKey = await getOrCreateApiKey(null); - - // When we pass the same keyId, we should get the same key back - // (This tests the keyId resolution path) - const resolvedKey = await resolveApiKey(null, firstKey); - assert.equal(resolvedKey, firstKey, "Should return same key when resolved"); -}); - -test("Qwen guide-settings POST creates valid DB-backed key (no keyId)", async () => { - const guideSettingsRoute = - await import("../../src/app/api/cli-tools/guide-settings/[toolId]/route.ts"); - - const cookie = await createAuthCookie(); - const req = new Request("http://localhost/api/cli-tools/guide-settings/qwen", { - method: "POST", - headers: { "Content-Type": "application/json", cookie }, - body: JSON.stringify({ - baseUrl: "http://localhost:20128/v1", - model: "qwen3-coder-flash", - // No keyId provided - should auto-create - }), - }); - - const response = await guideSettingsRoute.POST(req, { params: { toolId: "qwen" } }); - assert.equal(response.status, 200, "Response should be OK"); - - // Verify settings.json was written with security.auth format and a valid DB-backed key - const configPath = path.join(DUMMY_HOME, ".qwen", "settings.json"); - const content = JSON.parse(await fs.readFile(configPath, "utf-8")); - - assert.equal(content.security?.auth?.selectedType, "openai", "Should use openai auth type"); - assert.ok(content.security?.auth?.apiKey, "Should have an API key"); - assert.equal( - content.security?.auth?.baseUrl, - "http://localhost:20128/v1", - "Should have base URL" - ); - assert.equal(content.model?.name, "qwen3-coder-flash", "Should have model name"); - - const apiKey = content.security.auth.apiKey; - assert.notEqual(apiKey, "sk_omniroute", "Should not use placeholder"); - assert.ok(apiKey.startsWith("sk-"), "Key should start with sk- prefix"); - - // Verify the key is valid in DB - const valid = await validateApiKey(apiKey); - assert.equal(valid, true, "Auto-created key should validate in DB"); -}); - -test("Qwen guide-settings POST with keyId uses existing key", async () => { - const guideSettingsRoute = - await import("../../src/app/api/cli-tools/guide-settings/[toolId]/route.ts"); - - // Pre-create a key via getOrCreateApiKey - const existingKey = await getOrCreateApiKey(null); - const keyIdMatch = existingKey.match(/^sk-[^-]+-([^-]+)-/); - assert.ok(keyIdMatch, "Key should have ID portion"); - - // Get key metadata to find the ID - const db = getDbInstance(); - const stmt = db.prepare("SELECT id FROM api_keys WHERE `key` = ?"); - const row = stmt.get(existingKey) as { id: string } | undefined; - assert.ok(row, "Key should exist in DB"); - - const cookie = await createAuthCookie(); - const req = new Request("http://localhost/api/cli-tools/guide-settings/qwen", { - method: "POST", - headers: { "Content-Type": "application/json", cookie }, - body: JSON.stringify({ - baseUrl: "http://localhost:20128/v1", - model: "qwen3-coder-plus", - keyId: row.id, - }), - }); - - const response = await guideSettingsRoute.POST(req, { params: { toolId: "qwen" } }); - assert.equal(response.status, 200, "Response should be OK"); - - // Verify settings.json uses security.auth format with the existing key - const configPath = path.join(DUMMY_HOME, ".qwen", "settings.json"); - const content = JSON.parse(await fs.readFile(configPath, "utf-8")); - - assert.equal(content.security?.auth?.selectedType, "openai"); - assert.equal( - content.security?.auth?.apiKey, - existingKey, - "Should use existing key when keyId provided" - ); - assert.equal(content.security?.auth?.baseUrl, "http://localhost:20128/v1"); - assert.equal(content.model?.name, "qwen3-coder-plus"); -}); From b46633c4ace6022d1da840042fb15a9e4602056d Mon Sep 17 00:00:00 2001 From: backryun Date: Mon, 20 Jul 2026 20:57:06 +0900 Subject: [PATCH 2/6] refactor(qwen): remove deprecated Qwen OAuth provider --- .env.example | 6 - AGENTS.md | 4 +- SECURITY.md | 2 +- bin/cli/commands/oauth.mjs | 1 - .../7690-basered-full-suite-realignment.md | 2 +- .../tokenrefresh-provider-extraction.md | 2 +- config/quality/eslint-suppressions.json | 8 +- config/quality/file-size-baseline.json | 5 +- docs/architecture/ARCHITECTURE.md | 3 +- docs/architecture/CODEBASE_DOCUMENTATION.md | 2 +- docs/architecture/REPOSITORY_MAP.md | 4 +- docs/compression/COMPRESSION_GUIDE.md | 2 +- docs/guides/FEATURES.md | 2 +- docs/guides/USER_GUIDE.md | 6 +- docs/i18n/ar/llm.txt | 4 +- docs/i18n/az/llm.txt | 4 +- docs/i18n/bg/llm.txt | 4 +- docs/i18n/bn/llm.txt | 4 +- docs/i18n/cs/llm.txt | 4 +- docs/i18n/da/llm.txt | 4 +- docs/i18n/de/llm.txt | 4 +- docs/i18n/es/llm.txt | 4 +- docs/i18n/fa/llm.txt | 4 +- docs/i18n/fi/llm.txt | 4 +- docs/i18n/fr/llm.txt | 4 +- docs/i18n/gu/llm.txt | 4 +- docs/i18n/he/llm.txt | 4 +- docs/i18n/hi/llm.txt | 4 +- docs/i18n/hu/llm.txt | 4 +- docs/i18n/id/llm.txt | 4 +- docs/i18n/in/llm.txt | 4 +- docs/i18n/it/llm.txt | 4 +- docs/i18n/ja/llm.txt | 4 +- docs/i18n/ko/llm.txt | 4 +- docs/i18n/mr/llm.txt | 4 +- docs/i18n/ms/llm.txt | 4 +- docs/i18n/nl/llm.txt | 4 +- docs/i18n/no/llm.txt | 4 +- docs/i18n/phi/llm.txt | 4 +- docs/i18n/pl/llm.txt | 4 +- docs/i18n/pt-BR/llm.txt | 4 +- docs/i18n/pt/llm.txt | 4 +- docs/i18n/ro/llm.txt | 4 +- docs/i18n/ru/llm.txt | 4 +- docs/i18n/sk/llm.txt | 4 +- docs/i18n/sv/llm.txt | 4 +- docs/i18n/sw/llm.txt | 4 +- docs/i18n/ta/llm.txt | 4 +- docs/i18n/te/llm.txt | 4 +- docs/i18n/th/llm.txt | 4 +- docs/i18n/tr/llm.txt | 4 +- docs/i18n/uk-UA/llm.txt | 4 +- docs/i18n/ur/llm.txt | 4 +- docs/i18n/vi/llm.txt | 4 +- docs/i18n/zh-CN/llm.txt | 4 +- docs/i18n/zh-TW/llm.txt | 4 +- docs/reference/ENVIRONMENT.md | 3 - docs/reference/FREE_TIERS.md | 4 +- docs/reference/PROVIDER_REFERENCE.md | 1 - docs/security/STEALTH_GUIDE.md | 4 +- llm.txt | 4 +- open-sse/config/cliFingerprints.ts | 39 ------- open-sse/config/constants.ts | 4 - open-sse/config/providerHeaderProfiles.ts | 29 +---- open-sse/config/providers/index.ts | 2 - .../config/providers/registry/qwen/index.ts | 25 ----- .../providers/registry/qwen/web/index.ts | 3 +- open-sse/config/providers/shared.ts | 2 - open-sse/executors/default.ts | 42 +------ open-sse/executors/index.ts | 1 - open-sse/handlers/chatCore.ts | 72 +----------- open-sse/handlers/chatCore/upstreamBody.ts | 39 +++---- open-sse/index.ts | 1 - .../services/__tests__/tierResolver.test.ts | 12 +- open-sse/services/refreshSerializer.ts | 1 - open-sse/services/tierConfig.ts | 1 - open-sse/services/tierDefaults.json | 1 - open-sse/services/tokenRefresh.ts | 8 -- .../services/tokenRefresh/providers/qwen.ts | 81 -------------- open-sse/services/usage.ts | 21 ---- open-sse/utils/cacheControlPolicy.ts | 3 +- open-sse/utils/publicCreds.ts | 5 - src/app/(dashboard)/dashboard/combos/page.tsx | 2 +- .../onboarding/providerOnboardingCatalog.ts | 1 - .../models/discovery/providerModelsConfig.ts | 13 +-- src/app/api/providers/[id]/models/route.ts | 19 ---- .../[id]/sync-models/degradedLocalCatalog.ts | 2 +- .../providers/[id]/test/oauthTestConfig.ts | 11 +- src/app/api/v1/models/catalogProviderMaps.ts | 1 - src/domain/types.ts | 2 +- src/lib/arenaEloSync.ts | 4 +- src/lib/copilot/engine.ts | 2 +- src/lib/copilot/systemPrompt.ts | 2 +- .../129_remove_unregistered_qwen_data.sql | 8 ++ src/lib/oauth/constants/oauth.ts | 12 -- src/lib/oauth/providers/index.ts | 2 - src/lib/oauth/providers/qwen.ts | 82 -------------- src/lib/oauth/utils/cliProxyAuthImport.ts | 1 - src/lib/usage/fetcher.ts | 19 ---- src/shared/components/OAuthModal.tsx | 3 +- src/shared/constants/cliCompatProviders.ts | 6 - .../constants/pricing/oauth-subscriptions.ts | 40 ------- src/shared/constants/providers/oauth.ts | 12 -- src/shared/constants/providers/web-cookie.ts | 2 +- src/shared/utils/requestTimeout.ts | 1 - src/sse/services/tokenRefresh.ts | 6 - tests/snapshots/provider/translate-path.json | 65 ----------- .../cache-control-claude-providers.test.ts | 1 - tests/unit/cache-control-policy.test.ts | 1 - tests/unit/catalog-updates-v3x.test.ts | 54 ++++----- tests/unit/chatcore-translation-paths.test.ts | 104 ------------------ tests/unit/chatcore-upstream-body.test.ts | 37 +------ tests/unit/check-public-creds.test.ts | 6 +- tests/unit/claude-oauth-provider.test.ts | 1 - tests/unit/cliproxy-auth-import-1934.test.ts | 4 +- tests/unit/codex-failover.test.ts | 4 +- tests/unit/compression/cachingAware.test.ts | 2 +- tests/unit/executor-default-base.test.ts | 39 +------ tests/unit/free-model-catalog.test.ts | 2 +- .../unit/oauth-device-code-endpoints.test.ts | 19 +--- tests/unit/oauth-providers-config.test.ts | 54 +-------- .../oauth-providers-error-handling.test.ts | 12 -- tests/unit/provider-alias-uniqueness.test.ts | 8 +- tests/unit/provider-header-profiles.test.ts | 23 ++-- ...ider-limits-rotating-expired-guard.test.ts | 15 ++- tests/unit/provider-models-route.test.ts | 15 --- tests/unit/proxy-connection-test.test.ts | 7 +- tests/unit/public-client-ids-3493.test.ts | 10 +- tests/unit/qoder-oauth-config.test.ts | 1 - ...stream-options-claude-code-port663.test.ts | 100 ----------------- .../qwen-web-models-discovery-3931.test.ts | 4 +- ...s-degraded-local-catalog-5460-5465.test.ts | 2 +- .../unit/token-refresh-route-service.test.ts | 16 +-- tests/unit/token-refresh-service.test.ts | 39 ------- tests/unit/usage-providers.test.ts | 11 -- tests/unit/usage-service-hardening.test.ts | 29 +---- tests/unit/web-cookie-providers-new.test.ts | 1 - 137 files changed, 227 insertions(+), 1337 deletions(-) delete mode 100644 open-sse/config/providers/registry/qwen/index.ts delete mode 100644 open-sse/services/tokenRefresh/providers/qwen.ts create mode 100644 src/lib/db/migrations/129_remove_unregistered_qwen_data.sql delete mode 100644 src/lib/oauth/providers/qwen.ts delete mode 100644 tests/unit/qwen-strip-stream-options-claude-code-port663.test.ts diff --git a/.env.example b/.env.example index cebc3df2f43..41452fb26b6 100644 --- a/.env.example +++ b/.env.example @@ -895,9 +895,6 @@ CODEX_OAUTH_CLIENT_ID=app_EMoamEEZ73f0CkXaXp7hrann # ANTIGRAVITY_OAUTH_CLIENT_SECRET= # WINDSURF_FIREBASE_API_KEY= -# ── Qwen (Alibaba) ── -QWEN_OAUTH_CLIENT_ID=f0304373b74a44d2b584a3fb70ca9e56 - # ── Kimi Coding (Moonshot) ── KIMI_CODING_OAUTH_CLIENT_ID=17e5f671-d194-4dfb-9706-5516cb48c098 @@ -1038,7 +1035,6 @@ KIRO_USER_AGENT="AWS-SDK-JS/3.0.0 kiro-ide/1.0.0" # Used by: open-sse/executors/kiro.ts # KIRO_VERIFY_FULL_CRC=false QODER_USER_AGENT="Qoder-Cli" -QWEN_USER_AGENT="QwenCode/0.19.3 (linux; x64)" CURSOR_USER_AGENT="Cursor/3.4" # Override Codex client version sent in headers independently of the @@ -1068,8 +1064,6 @@ CURSOR_USER_AGENT="Cursor/3.4" # CLI_COMPAT_KIMI_CODING=1 # CLI_COMPAT_KILOCODE=1 # CLI_COMPAT_CLINE=1 -# CLI_COMPAT_QWEN=1 - # Or enable for all providers at once: # CLI_COMPAT_ALL=1 diff --git a/AGENTS.md b/AGENTS.md index d996a87055b..b8af5518dd1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -267,8 +267,8 @@ Zod schemas, and unit tests aligned when editing. ### Provider Categories -- **Free** (3): Qoder AI, Qwen Code, Kiro AI -- **OAuth** (14): Claude Code, Antigravity, Codex, GitHub Copilot, Cursor, Kimi Coding, Kilo Code, Cline, Qwen (⚠️ free tier discontinued 2026-04-15), Kiro, Qoder, Gemini, Windsurf (v3.8), GitLab Duo (v3.8) +- **Free** (2): Qoder AI, Kiro AI +- **OAuth** (13): Claude Code, Antigravity, Codex, GitHub Copilot, Cursor, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf (v3.8), GitLab Duo (v3.8) - **API Key** (120+): OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, Perplexity, Together, Fireworks, Cerebras, Cohere, NVIDIA, Nebius, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare AI, Scaleway, AI/ML API, Pollinations, diff --git a/SECURITY.md b/SECURITY.md index 6b9c5a4f60f..9bafdbd286f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -42,7 +42,7 @@ Request → CORS → Authz pipeline (classify → policies → enforce) | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | **Dashboard Login** | Password-based auth with JWT tokens (HttpOnly cookies) | | **API Key Auth** | HMAC-signed keys with CRC validation | -| **OAuth 2.0 + PKCE** | 14 providers (Claude, Codex, GitHub, Cursor, Antigravity, Gemini, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Windsurf, GitLab Duo) | +| **OAuth 2.0 + PKCE** | 13 providers (Claude, Codex, GitHub, Cursor, Antigravity, Gemini, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Windsurf, GitLab Duo) | | **Token Refresh** | Automatic OAuth token refresh before expiry | | **Secure Cookies** | `AUTH_COOKIE_SECURE=true` for HTTPS environments | | **Authz Pipeline** | Route classification (PUBLIC / CLIENT_API / MANAGEMENT) — see `docs/architecture/AUTHZ_GUIDE.md` | diff --git a/bin/cli/commands/oauth.mjs b/bin/cli/commands/oauth.mjs index 2d907df39f0..9bcbc92d6c7 100644 --- a/bin/cli/commands/oauth.mjs +++ b/bin/cli/commands/oauth.mjs @@ -7,7 +7,6 @@ const PROVIDERS_WITH_OAUTH = [ { id: "gemini", name: "Google Gemini", flow: "browser" }, { id: "antigravity", name: "Antigravity", flow: "browser" }, { id: "windsurf", name: "Windsurf", flow: "browser" }, - { id: "qwen", name: "Qwen Code", flow: "browser" }, { id: "cursor", name: "Cursor", flow: "import" }, { id: "zed", name: "Zed", flow: "import" }, { id: "kiro", name: "Amazon Kiro", flow: "social" }, diff --git a/changelog.d/maintenance/7690-basered-full-suite-realignment.md b/changelog.d/maintenance/7690-basered-full-suite-realignment.md index f746b0dc682..b95e13a7d75 100644 --- a/changelog.d/maintenance/7690-basered-full-suite-realignment.md +++ b/changelog.d/maintenance/7690-basered-full-suite-realignment.md @@ -1 +1 @@ -- Realigned 13 test files that had drifted red on the release tip after the 102-PR merge campaign (full-suite sweep): provider counts/goldens/docs regenerated, qwen OAuth + 1M-beta assertions caught up with the live-validated behavior, and two real fixes — legacy `refresh_token` column healed before its index is created, and `shouldSkipCloudSyncInitialization` no longer swaps its `(env, argv)` arguments. +- Realigned 13 test files that had drifted red on the release tip after the 102-PR merge campaign (full-suite sweep): provider counts/goldens/docs regenerated, 1M-beta assertions caught up with the live-validated behavior, and two real fixes — legacy `refresh_token` column healed before its index is created, and `shouldSkipCloudSyncInitialization` no longer swaps its `(env, argv)` arguments. diff --git a/changelog.d/maintenance/tokenrefresh-provider-extraction.md b/changelog.d/maintenance/tokenrefresh-provider-extraction.md index 74b3fb37b8f..a813551b0c8 100644 --- a/changelog.d/maintenance/tokenrefresh-provider-extraction.md +++ b/changelog.d/maintenance/tokenrefresh-provider-extraction.md @@ -1 +1 @@ -- **chore(quality):** split `open-sse/services/tokenRefresh.ts` (2249 lines, frozen) into per-provider leaves — `open-sse/services/tokenRefresh/providers/{windsurf,codebuddyCn,cline,kimiCoding,gitlabDuo,claudeOAuth,google,qwen,codex,kiro,qoder,github,copilot}.ts` plus a `tokenRefresh/shared.ts` for the cross-provider OAuth-error classifier and form-body helper — pure move, zero behavior change; the file shrinks to ~989 lines and every previously-public export (`refreshCopilotToken`, `getAccessToken`, `extractOAuthErrorCode`, etc.) is re-exported unchanged so no importer needed to change. Idea originally proposed by @KooshaPari in #7338 against a much older base; redone here against the current tip. +- **chore(quality):** split `open-sse/services/tokenRefresh.ts` (2249 lines, frozen) into per-provider leaves — `open-sse/services/tokenRefresh/providers/{windsurf,codebuddyCn,cline,kimiCoding,gitlabDuo,claudeOAuth,google,codex,kiro,qoder,github,copilot}.ts` plus a `tokenRefresh/shared.ts` for the cross-provider OAuth-error classifier and form-body helper — pure move, zero behavior change; the file shrinks to ~989 lines and every previously-public export (`refreshCopilotToken`, `getAccessToken`, `extractOAuthErrorCode`, etc.) is re-exported unchanged so no importer needed to change. Idea originally proposed by @KooshaPari in #7338 against a much older base; redone here against the current tip. diff --git a/config/quality/eslint-suppressions.json b/config/quality/eslint-suppressions.json index e3a4ccbabff..27a834b3772 100644 --- a/config/quality/eslint-suppressions.json +++ b/config/quality/eslint-suppressions.json @@ -704,7 +704,7 @@ }, "tests/unit/chatcore-translation-paths.test.ts": { "@typescript-eslint/no-explicit-any": { - "count": 40 + "count": 34 } }, "tests/unit/chatgpt-web-tools-5240.test.ts": { @@ -1349,7 +1349,7 @@ }, "tests/unit/executor-default-base.test.ts": { "@typescript-eslint/no-explicit-any": { - "count": 43 + "count": 42 } }, "tests/unit/executor-github.test.ts": { @@ -1739,7 +1739,7 @@ }, "tests/unit/provider-models-route.test.ts": { "@typescript-eslint/no-explicit-any": { - "count": 55 + "count": 54 } }, "tests/unit/provider-models-token-limits.test.ts": { @@ -2224,7 +2224,7 @@ }, "tests/unit/usage-service-hardening.test.ts": { "@typescript-eslint/no-explicit-any": { - "count": 59 + "count": 56 } }, "tests/unit/v1-ws-bridge.test.ts": { diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 0672067e333..56c421b099c 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -91,7 +91,7 @@ "_rebaseline_2026_06_18_qg9_chatcore_split_prB": "QG v2 Fase 9 T5 C6-C7: chatCore.ts 5265->5060 (shrink, stacked on prA #4188) — two pure, byte-identical extractions into open-sse/handlers/chatCore/. (1) comboContextCache.ts (5265 (wc -l 5264 + 1). Fase 2 of the chatCore split following #4159 (which created 10 leaf modules). Three more sibling leaves created under open-sse/handlers/chatCore/, all 866 (+21) and EditConnectionModal.tsx 1174->1204 (+30) = the 'import only free models' connection option — a free-models Toggle gated by providerHasFreeModels() plus its form-state wiring (importFreeModelsOnly field + providerSpecificData persistence, explicit-false on edit so the PUT merge doesn't keep a stale true) added to both connection modals, mirroring the prior per-modal toggle bumps #3879 (redact-thinking) and #2997 (disable-cooling). Detection lives in the new shared src/shared/utils/freeModels.ts (112 LOC, 2531 (+19 = one PROVIDER_MODELS_CONFIG entry for `qwen-web` + a 4-line comment). qwen-web was missing from the config map so its model-discovery page returned nothing (the OAuth fallback only fires for provider===qwen). Pure additive config entry pointing at the public chat.qwen.ai/api/v2/models endpoint; standard per-provider addition, not extractable.", + "_rebaseline_2026_06_18_3931_qwen_web_models": "Issue #3931 (Problem #3) own growth: providers/[id]/models/route.ts 2512->2531 (+19 = one PROVIDER_MODELS_CONFIG entry for `qwen-web` + a 4-line comment). qwen-web was missing from the config map so its model-discovery page returned nothing. Pure additive config entry pointing at the public chat.qwen.ai/api/v2/models endpoint; standard per-provider addition, not extractable.", "_rebaseline_2026_06_18_4165_queue_timeout_msg": "Issue #4165 own growth: rateLimitManager.ts 1022->1035 (+13 at the existing withRateLimit catch chokepoint). Bottleneck's raw `This job timed out after ms.` is rewritten into a clear OmniRoute-owned error (names resilienceSettings.requestQueue.maxWaitMs, disclaims upstream, keeps the original as `cause`, tags code=RATE_LIMIT_QUEUE_TIMEOUT) so queue-saturation 502s stop masquerading as provider outages. The branch already existed (it only logged); this adds the error construction at the same point. Not extractable — closes over provider/model/maxWaitMs locals of the single catch.", "_rebaseline_2026_06_18_qg9_combo_split_d4": "QG v2 Fase 9 T5 D4: combo.ts 4740->4589 — shadow routing extracted byte-identically to the new open-sse/services/combo/shadowRouting.ts (4430 — target sorters extracted byte-identically to the new open-sse/services/combo/targetSorters.ts (873 (+3: antigravity projectId assertion + 50ms tick for the now fire-and-forget onboarding, matching the no-PKCE/no-openid flow).", "_rebaseline_2026_07_02_5928_base_red": "web-cookie-providers-new.test.ts 845->850: #5928 (test(security) Kimi Web URL host parse, CodeQL #689) grew the file +5 lines and merged into release/v3.8.44 WITHOUT rebaselining, leaving a fast-gates base-red that blocked every subsequent PR->release. Test growth is legitimate (a security regression test); maintainer absorbs the drift here. Frozen at 850.", "_rebaseline_basered_codebuddy_cn": "Base-red fix (#4664 CodeBuddy CN): oauth-providers-config.test.ts 867->870 (+3) to align the EXPECTED provider list/config with the codebuddy-cn provider that #4664 added to the registry without updating this test (it asserts 'exactly once').", - "_rebaseline_pr4561_qwen_oauth_url": "Reconcile #4561 (port decolua/9router#683) already-merged growth: oauth-providers-config.test.ts 855->867 (+12, qwen.ai URL regression-pin test). Fast-gate PR->release does not run check:file-size, so this surfaced post-merge.", "_rebaseline_pr4613_compatible_provider_groups": "Reconcile #4613 already-merged growth: providers-page-utils.test.ts 1004->1052 (+48, buildCompatibleProviderGroups partition unit test). Fast-gate PR->release does not run check:file-size, so this surfaced post-merge.", "tests/integration/chat-pipeline.test.ts": 1601, "tests/integration/chatcore-compression-integration.test.ts": 1111, @@ -423,4 +422,4 @@ "_rebaseline_2026_07_07_6525_chirag_image_guard": "PR #6525 (@chirag127, #6457) own growth: chat.ts ->1778 (reject image-only models on /v1/chat/completions; stacks on #6515). Owner-approved. Frozen.", "_rebaseline_2026_07_15_7045_perf_instrumentation": "PR #7045 (@oyi77) own growth: open-sse/utils/stream.ts 2796->2814 (+18) from performance.mark/measure instrumentation around the SSE dispatch chokepoint (b48ba21c4), a TextEncoder hoisting fix to avoid a per-chunk allocation on the hot path (c35e8a9b4), and clearing the fixed-name \"omni-request-body-size\" mark immediately after creation (babysit fix, addressing a review-flagged unbounded-growth leak in Node's global performance timeline). Cohesive wiring at the existing stream-dispatch chokepoint; not extractable. Covered by tests/unit/chatcore-streaming-pipeline.test.ts + tests/unit/stream-request-body-size-mark-7045.test.ts.", "_rebaseline_2026_07_18_basereds_test_realignment": "Base-red sweep own growth (post 102-PR campaign, full-suite realignment): tests/unit/combo-routing-engine.test.ts 3209->3243 (+34 = least-used tests now prime usage through real handleComboChat calls so recordComboRequest keys by the resolved executionKey exactly as production does — #7015 keying); tests/unit/db-migration-runner.test.ts 1491->1499 (+8 = withNonTestEnvironment now also strips node --test tokens from process.execArgv, matching the #7359 isAutomatedTestProcess widening); tests/unit/executor-default-base.test.ts 1523->1527 (+4 = 1M-beta assertion updated for claude-sonnet-4-6 GA #7129). All three are test-fidelity realignments, not extractable." -} \ No newline at end of file +} diff --git a/docs/architecture/ARCHITECTURE.md b/docs/architecture/ARCHITECTURE.md index 68415907748..53b11e7e8e8 100644 --- a/docs/architecture/ARCHITECTURE.md +++ b/docs/architecture/ARCHITECTURE.md @@ -175,7 +175,7 @@ flowchart LR end subgraph Upstreams[Upstream Providers] - P1[OAuth Providers\nClaude/Codex/Gemini/Qwen/Qoder/GitHub/Kiro/Cursor/Antigravity] + P1[OAuth Providers\nClaude/Codex/Gemini/Qoder/GitHub/Kiro/Cursor/Antigravity] P2[API Key Providers\nOpenAI/Anthropic/OpenRouter/GLM/Kimi/MiniMax\nDeepSeek/Groq/xAI/Mistral/Perplexity\nTogether/Fireworks/Cerebras/Cohere/NVIDIA] P3[Compatible Nodes\nOpenAI-compatible / Anthropic-compatible] end @@ -948,7 +948,6 @@ All other providers (including custom compatible nodes) use the `DefaultExecutor | GitHub Copilot | openai | OAuth + Copilot Token | ✅ | ✅ | ✅ | ✅ Quota snapshots | | Cursor | cursor | Custom checksum | ✅ | ✅ | ❌ | ❌ | | Kiro | kiro | AWS SSO OIDC | ✅ (EventStream) | ❌ | ✅ | ✅ Usage limits | -| Qwen | openai | OAuth | ✅ | ✅ | ✅ | ⚠️ Per request | | Qoder | openai | OAuth / PAT | ✅ | ✅ | ✅ | ⚠️ Per request | | Kilo Code | openai | OAuth | ✅ | ✅ | ✅ | ❌ | | Cline | openai | OAuth | ✅ | ✅ | ✅ | ❌ | diff --git a/docs/architecture/CODEBASE_DOCUMENTATION.md b/docs/architecture/CODEBASE_DOCUMENTATION.md index fe0e187eb22..91b1c989f6f 100644 --- a/docs/architecture/CODEBASE_DOCUMENTATION.md +++ b/docs/architecture/CODEBASE_DOCUMENTATION.md @@ -293,7 +293,7 @@ table groups the actual directories and notable top-level files. | `jobs/` | Background jobs (`autoUpdate.ts`, …) | | `memory/` | Persistent memory: `store.ts`, `cache.ts`, `retrieval.ts`, `summarization.ts`, `extraction.ts`, `injection.ts`, `qdrant.ts`, `settings.ts`, `verify.ts`, `schemas.ts`, `types.ts` | | `monitoring/` | `observability.ts` | -| `oauth/` | OAuth providers (14): `antigravity`, `claude`, `cline`, `codex`, `cursor`, `gemini`, `github`, `gitlab-duo`, `kilocode`, `kimi-coding`, `kiro`, `qoder`, `qwen`, `windsurf` plus `services/`, `utils/{pkce, server, banner, codexAuthFile, ui}`, `constants/oauth.ts` | +| `oauth/` | OAuth providers (13): `antigravity`, `claude`, `cline`, `codex`, `cursor`, `gemini`, `github`, `gitlab-duo`, `kilocode`, `kimi-coding`, `kiro`, `qoder`, `windsurf` plus `services/`, `utils/{pkce, server, banner, codexAuthFile, ui}`, `constants/oauth.ts` | | `plugins/` | Plugin loader (`index.ts`) | | `promptCache/` | `prefixAnalyzer.ts`, `index.ts` | | `providerModels/` | Managed model lifecycle: `modelDiscovery.ts`, `managedModelImport.ts`, `managedAvailableModels.ts`, `cursorAgent.ts` | diff --git a/docs/architecture/REPOSITORY_MAP.md b/docs/architecture/REPOSITORY_MAP.md index 2b1c5676d01..302fc1e31fb 100644 --- a/docs/architecture/REPOSITORY_MAP.md +++ b/docs/architecture/REPOSITORY_MAP.md @@ -182,7 +182,7 @@ src/ | `compliance/` | Audit log + provider audit — see `docs/security/COMPLIANCE.md` | | `compression/` | Compression engine glue (engines live in `open-sse/services/compression/`) | | `config/` | Runtime config helpers | -| `db/` | 95+ domain DB modules + 110+ migrations (always go through here for SQLite) | +| `db/` | 95+ domain DB modules + 110+ migrations (always go through here for SQLite) | | `quota/` | Quota Sharing Engine: `dimensions.ts` (types/Zod), `types.ts` (QuotaStore interface), `sqliteQuotaStore.ts`, `redisQuotaStore.ts`, `storeFactory.ts`, `fairShare.ts`, `burnRate.ts`, `planResolver.ts`, `planRegistry.ts`, `saturationSignals.ts`, `enforce.ts`, `spendRecorder.ts` — see `docs/routing/QUOTA_SHARE.md` | | `display/` | UI formatting helpers (cost, latency, etc.) | | `embeddings/` | Embeddings service helpers | @@ -195,7 +195,7 @@ src/ | `memory/vectorStore.ts` | sqlite-vec v0.1.9 wrapper — KNN brute-force + hybrid RRF (FTS5 + vector, k=60). Lazy-init, degrades gracefully when sqlite-vec unavailable. (plan 21) | | `memory/reindex.ts` | `runReindexBatch()` — processes memories with `needs_reindex=1` in background; called by `POST /api/memory/reindex` and lazy-backfill path. (plan 21) | | `monitoring/` | Health checks, metrics emission | -| `oauth/` | OAuth flows for 14 providers (claude, codex, antigravity, cursor, github, gemini, kimi-coding, kilocode, cline, qwen, kiro, qoder, gitlab-duo, windsurf) | +| `oauth/` | OAuth flows for 13 providers (claude, codex, antigravity, cursor, github, gemini, kimi-coding, kilocode, cline, kiro, qoder, gitlab-duo, windsurf) | | `plugins/` | Plugin registry | | `promptCache/` | Anthropic-style prompt cache breakpoints | | `skills/` | Skills framework (built-in + marketplace + SkillsSH) — see `docs/frameworks/SKILLS.md` | diff --git a/docs/compression/COMPRESSION_GUIDE.md b/docs/compression/COMPRESSION_GUIDE.md index 314458f2874..1a23310fdee 100644 --- a/docs/compression/COMPRESSION_GUIDE.md +++ b/docs/compression/COMPRESSION_GUIDE.md @@ -184,7 +184,7 @@ Combo: "free-forever" Pipeline: RTK -> Caveman Targets: 1. if/kimi-k2-thinking - 2. qw/qwen3-coder-plus + 2. if/qwen3-coder-plus ``` This lets you use stacked compression on free/coding providers while keeping lite mode on paid diff --git a/docs/guides/FEATURES.md b/docs/guides/FEATURES.md index df540dd1f5f..92f2a9f3883 100644 --- a/docs/guides/FEATURES.md +++ b/docs/guides/FEATURES.md @@ -51,7 +51,7 @@ The v3.7.x → v3.8.0 cycle added zero-config auto routing, new providers, OAuth ## 🔌 Providers -Manage AI provider connections: OAuth providers (Claude Code, Codex), API key providers (Groq, DeepSeek, OpenRouter), and free providers (Qoder, Qwen, Kiro). Kiro accounts include credit balance tracking — remaining credits, total allowance, and renewal date visible in Dashboard → Usage. +Manage AI provider connections: OAuth providers (Claude Code, Codex), API key providers (Groq, DeepSeek, OpenRouter), and free providers (Qoder, Kiro). Kiro accounts include credit balance tracking — remaining credits, total allowance, and renewal date visible in Dashboard → Usage. OpenRouter connections can store a per-connection `preset` in Advanced Settings. When set, OmniRoute sends it as the OpenRouter top-level request field, for example `"preset": "email-copywriter"`, unless the client request already supplied its own `preset`. diff --git a/docs/guides/USER_GUIDE.md b/docs/guides/USER_GUIDE.md index 80505b7c760..2aa7d35f86c 100644 --- a/docs/guides/USER_GUIDE.md +++ b/docs/guides/USER_GUIDE.md @@ -83,7 +83,7 @@ vs. $20 + hitting limits = frustration ``` Combo: "free-forever" 1. if/kimi-k2 (unlimited free) - 2. qw/qwen3-coder-plus (unlimited free) + 2. kr/qwen3-coder-next (free fallback) Monthly cost: $0 Quality: Production-ready models @@ -252,7 +252,7 @@ Use in CLI: premium-coding Name: free-combo Models: 1. if/kimi-k2 (unlimited) - 2. qw/coder-model (unlimited) + 2. kr/qwen3-coder-next (free fallback) Cost: $0 forever! ``` @@ -576,8 +576,6 @@ For the full environment variable reference, see the [README](../README.md). **Qoder (`if/`)** — FREE OAuth: `if/kimi-k2-0905`, `if/kimi-k2`, `if/qwen3-coder-plus`, `if/qwen3-max`, `if/qwen3-max-preview`, `if/qwen3-vl-plus`, `if/qwen3-32b`, `if/qwen3-235b-a22b-thinking-2507`, `if/qwen3-235b-a22b-instruct`, `if/qwen3-235b`, `if/deepseek-v3.2`, `if/deepseek-v3`, `if/deepseek-r1`, `if/qoder-rome-30ba3b` -**Qwen (`qw/`)** — FREE OAuth (chat.qwen.ai): `qw/coder-model`, `qw/vision-model` - **GLM (`glm/`, `glm-cn/`, `zai/`, `glmt/`)** — $0.2–0.6/1M: `glm/glm-5.1`, `glm/glm-5`, `glm/glm-5-turbo`, `glm/glm-4.7`, `glm/glm-4.7-flash`, `glm/glm-4.6`, `glm/glm-4.6v`, `glm/glm-4.5`, `glm/glm-4.5v`, `glm/glm-4.5-air` **MiniMax (`minimax/`, `minimax-cn/`)** — $0.2/1M: `minimax/MiniMax-M2.7`, `minimax/MiniMax-M2.7-highspeed`, `minimax/MiniMax-M2.5`, `minimax/MiniMax-M2.5-highspeed` diff --git a/docs/i18n/ar/llm.txt b/docs/i18n/ar/llm.txt index bbf5c27c788..d10256c0ea9 100644 --- a/docs/i18n/ar/llm.txt +++ b/docs/i18n/ar/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/az/llm.txt b/docs/i18n/az/llm.txt index f687ce262d1..777d36e91bd 100644 --- a/docs/i18n/az/llm.txt +++ b/docs/i18n/az/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/bg/llm.txt b/docs/i18n/bg/llm.txt index f687ce262d1..777d36e91bd 100644 --- a/docs/i18n/bg/llm.txt +++ b/docs/i18n/bg/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/bn/llm.txt b/docs/i18n/bn/llm.txt index 14269bdeaa0..df9132a1c18 100644 --- a/docs/i18n/bn/llm.txt +++ b/docs/i18n/bn/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/cs/llm.txt b/docs/i18n/cs/llm.txt index d26dc026f38..66741f71c44 100644 --- a/docs/i18n/cs/llm.txt +++ b/docs/i18n/cs/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/da/llm.txt b/docs/i18n/da/llm.txt index 8c54b16c7f6..c4958aa5238 100644 --- a/docs/i18n/da/llm.txt +++ b/docs/i18n/da/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/de/llm.txt b/docs/i18n/de/llm.txt index fc82d6b08e6..050cd8bc4e9 100644 --- a/docs/i18n/de/llm.txt +++ b/docs/i18n/de/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/es/llm.txt b/docs/i18n/es/llm.txt index fa6930d790c..54d1c60d56f 100644 --- a/docs/i18n/es/llm.txt +++ b/docs/i18n/es/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/fa/llm.txt b/docs/i18n/fa/llm.txt index 8758d5925b2..70d59867f2a 100644 --- a/docs/i18n/fa/llm.txt +++ b/docs/i18n/fa/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/fi/llm.txt b/docs/i18n/fi/llm.txt index 7b832d7f640..b453fd313ee 100644 --- a/docs/i18n/fi/llm.txt +++ b/docs/i18n/fi/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/fr/llm.txt b/docs/i18n/fr/llm.txt index 7978e1e7223..4d8f1606aea 100644 --- a/docs/i18n/fr/llm.txt +++ b/docs/i18n/fr/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/gu/llm.txt b/docs/i18n/gu/llm.txt index 716a3c1a503..5dc7dfee2d6 100644 --- a/docs/i18n/gu/llm.txt +++ b/docs/i18n/gu/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/he/llm.txt b/docs/i18n/he/llm.txt index 17466a0dce8..40366a4e919 100644 --- a/docs/i18n/he/llm.txt +++ b/docs/i18n/he/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/hi/llm.txt b/docs/i18n/hi/llm.txt index 45f2f5ee0e0..3733c814c48 100644 --- a/docs/i18n/hi/llm.txt +++ b/docs/i18n/hi/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/hu/llm.txt b/docs/i18n/hu/llm.txt index ba6d27cb243..9f974a8ea77 100644 --- a/docs/i18n/hu/llm.txt +++ b/docs/i18n/hu/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/id/llm.txt b/docs/i18n/id/llm.txt index 281a63458bb..abd125d6528 100644 --- a/docs/i18n/id/llm.txt +++ b/docs/i18n/id/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/in/llm.txt b/docs/i18n/in/llm.txt index 3a9d45ef206..a91aec3d081 100644 --- a/docs/i18n/in/llm.txt +++ b/docs/i18n/in/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/it/llm.txt b/docs/i18n/it/llm.txt index c25fa077baa..d4075558638 100644 --- a/docs/i18n/it/llm.txt +++ b/docs/i18n/it/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/ja/llm.txt b/docs/i18n/ja/llm.txt index 04692656455..ff989e9a412 100644 --- a/docs/i18n/ja/llm.txt +++ b/docs/i18n/ja/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/ko/llm.txt b/docs/i18n/ko/llm.txt index f33c1815663..50f4b60ce89 100644 --- a/docs/i18n/ko/llm.txt +++ b/docs/i18n/ko/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/mr/llm.txt b/docs/i18n/mr/llm.txt index f052ca9bd4e..a4f7728f176 100644 --- a/docs/i18n/mr/llm.txt +++ b/docs/i18n/mr/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/ms/llm.txt b/docs/i18n/ms/llm.txt index 8850fb0de77..71c07aec124 100644 --- a/docs/i18n/ms/llm.txt +++ b/docs/i18n/ms/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/nl/llm.txt b/docs/i18n/nl/llm.txt index 6465050541d..5f40de82a89 100644 --- a/docs/i18n/nl/llm.txt +++ b/docs/i18n/nl/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/no/llm.txt b/docs/i18n/no/llm.txt index be667f94f01..2efe3ac90af 100644 --- a/docs/i18n/no/llm.txt +++ b/docs/i18n/no/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/phi/llm.txt b/docs/i18n/phi/llm.txt index fae6a95de1b..02f2f1cf0d1 100644 --- a/docs/i18n/phi/llm.txt +++ b/docs/i18n/phi/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/pl/llm.txt b/docs/i18n/pl/llm.txt index 50a20e3320a..f395947b7a9 100644 --- a/docs/i18n/pl/llm.txt +++ b/docs/i18n/pl/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/pt-BR/llm.txt b/docs/i18n/pt-BR/llm.txt index b40da47c952..4bcf2731c57 100644 --- a/docs/i18n/pt-BR/llm.txt +++ b/docs/i18n/pt-BR/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/pt/llm.txt b/docs/i18n/pt/llm.txt index 9568237c5d9..57718023e24 100644 --- a/docs/i18n/pt/llm.txt +++ b/docs/i18n/pt/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/ro/llm.txt b/docs/i18n/ro/llm.txt index f92f0b2444d..862eb157362 100644 --- a/docs/i18n/ro/llm.txt +++ b/docs/i18n/ro/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/ru/llm.txt b/docs/i18n/ru/llm.txt index ccd8bcecc4c..b71aa5071ca 100644 --- a/docs/i18n/ru/llm.txt +++ b/docs/i18n/ru/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/sk/llm.txt b/docs/i18n/sk/llm.txt index e19bf8803f8..8919c9d5a4f 100644 --- a/docs/i18n/sk/llm.txt +++ b/docs/i18n/sk/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/sv/llm.txt b/docs/i18n/sv/llm.txt index 2d7cc63ed25..27384831baf 100644 --- a/docs/i18n/sv/llm.txt +++ b/docs/i18n/sv/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/sw/llm.txt b/docs/i18n/sw/llm.txt index fc08f7b1428..64e7fb624cc 100644 --- a/docs/i18n/sw/llm.txt +++ b/docs/i18n/sw/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/ta/llm.txt b/docs/i18n/ta/llm.txt index 1015b8850a5..3e6a3693b8d 100644 --- a/docs/i18n/ta/llm.txt +++ b/docs/i18n/ta/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/te/llm.txt b/docs/i18n/te/llm.txt index 375192f15ab..d6dd146c9de 100644 --- a/docs/i18n/te/llm.txt +++ b/docs/i18n/te/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/th/llm.txt b/docs/i18n/th/llm.txt index 64e8b57d816..67678e49e23 100644 --- a/docs/i18n/th/llm.txt +++ b/docs/i18n/th/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/tr/llm.txt b/docs/i18n/tr/llm.txt index 74ef1375dbc..2afb8335009 100644 --- a/docs/i18n/tr/llm.txt +++ b/docs/i18n/tr/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/uk-UA/llm.txt b/docs/i18n/uk-UA/llm.txt index 54e0cafbc0c..004e5fdbc03 100644 --- a/docs/i18n/uk-UA/llm.txt +++ b/docs/i18n/uk-UA/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/ur/llm.txt b/docs/i18n/ur/llm.txt index 105c5cd6992..a940a92b076 100644 --- a/docs/i18n/ur/llm.txt +++ b/docs/i18n/ur/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/vi/llm.txt b/docs/i18n/vi/llm.txt index 24c03f7a2d6..253c184061a 100644 --- a/docs/i18n/vi/llm.txt +++ b/docs/i18n/vi/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/zh-CN/llm.txt b/docs/i18n/zh-CN/llm.txt index 4a21c4fe0db..143a9fd6ec1 100644 --- a/docs/i18n/zh-CN/llm.txt +++ b/docs/i18n/zh-CN/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/i18n/zh-TW/llm.txt b/docs/i18n/zh-TW/llm.txt index 0c39f612a59..449bd4a5d89 100644 --- a/docs/i18n/zh-TW/llm.txt +++ b/docs/i18n/zh-TW/llm.txt @@ -364,9 +364,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index cd2fa040ee0..a21ebd22fbf 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -470,7 +470,6 @@ Built-in credentials for **localhost development**. For remote deployments, regi | `CODEX_OAUTH_CLIENT_ID` | Codex / OpenAI | Public client. | | `GEMINI_OAUTH_CLIENT_ID` | Gemini (Google) | Requires matching `_SECRET`. | | `GEMINI_OAUTH_CLIENT_SECRET` | Gemini (Google) | — | -| `QWEN_OAUTH_CLIENT_ID` | Qwen (Alibaba) | Public client. | | `KIMI_CODING_OAUTH_CLIENT_ID` | Kimi Coding (Moonshot) | Public client. | | `ANTIGRAVITY_OAUTH_CLIENT_ID` | Antigravity (Google) | Requires matching `_SECRET`. | | `ANTIGRAVITY_OAUTH_CLIENT_SECRET` | Antigravity (Google) | — | @@ -529,7 +528,6 @@ process.env[`${PROVIDER_ID}_USER_AGENT`] | `KIRO_OAUTH_CLIENT_ID` | `kiro-cli` | Override the Kiro social device-code `clientId` (public id) | | `KIRO_VERIFY_FULL_CRC` | `false` | Opt-in: full per-frame message CRC validation on the Kiro event stream (debug corrupted streams) | | `QODER_USER_AGENT` | `Qoder-Cli` | When Qoder CLI updates | -| `QWEN_USER_AGENT` | `QwenCode/0.19.3 (linux; x64)` | When Qwen Code updates | | `CURSOR_USER_AGENT` | `Cursor/3.3` | When Cursor updates | > [!TIP] @@ -555,7 +553,6 @@ When enabled, OmniRoute reorders HTTP headers and JSON body fields to match the | `CLI_COMPAT_KIMI_CODING` | `=1` | Mimics Kimi Coding request signature | | `CLI_COMPAT_KILOCODE` | `=1` | Mimics Kilo Code request signature | | `CLI_COMPAT_CLINE` | `=1` | Mimics Cline request signature | -| `CLI_COMPAT_QWEN` | `=1` | Mimics Qwen Code request signature | ### Global diff --git a/docs/reference/FREE_TIERS.md b/docs/reference/FREE_TIERS.md index bac9c0f7254..f57d5b825f2 100644 --- a/docs/reference/FREE_TIERS.md +++ b/docs/reference/FREE_TIERS.md @@ -83,7 +83,7 @@ A 50-agent web-research pass (official docs + last-7-days news, adversarially ve | `muse-spark-web` | Meta ToS explicitly prohibits automated access without prior permission, reverse engineering without written permission, and circumventing technologi… | | `nlpcloud` | ToS explicitly prohibits "setting up a proxy or other device that allows others to access the Service through it" and grants only a non-transferable,… | | `opencode` | ToS (Anomaly Innovations, Inc.) explicitly restricts use to "your own internal use, and not on behalf of or for the benefit of any third party" — ope… | -| `qwen-web` | The free OAuth tier is discontinued; no ToS permits a self-hosted proxy using session tokens against chat.qwen.ai. Even before shutdown, automated/pr… | +| `qwen-web` | No ToS permits a self-hosted proxy using session tokens against chat.qwen.ai; automated/programmatic access remains high-risk. | | `t3-web` | ToS explicitly restricts accounts to personal use only, prohibits credential sharing with third parties, and bans automated/bot/scraping access — a s… | ### ✅ Generally permissive — caution / ambiguous / ok (the rest) @@ -308,7 +308,7 @@ A 50-agent web-research pass (official docs + last-7-days news, adversarially ve - **`publicai`** — The shipped freeNote ("Free community inference tier") is broadly accurate but understates the specificity: the 20 RPM rate limit is now documented. No major tightening found; the service remains fre… - **`puter`** — Partially matches: the "500+ models" count is still accurate. However "users pay via Puter account" understates the reality — free accounts receive an undocumented starting credit that can be exhaust… - **`qoder`** — Our catalog ships freeNote "(none)", but Qoder does have a free tier: a Community Edition with unlimited basic-model completions (daily-capped, unspecified limit) plus a one-time 14-day/300-credit Pr… -- **`qwen-web`** — The shipped freeNote ("Free — Qwen models via chat.qwen.ai with login token") is now stale. The login-token/OAuth free API path was terminated on 2026-04-15. The qwen-web executor will receive 401 er… +- **`qwen-web`** — Session-token access against chat.qwen.ai is not a dependable free-provider path and may be rejected upstream. - **`sambanova`** — Our shipped note only described the one-time $5 credit (30-day validity). The current reality includes a permanent recurring free tier with documented rate limits (20 RPM, 20 RPD, 200k TPD) that pers… - **`sensenova`** — Our shipped freeNote says "Free SenseTime models" which is vague but directionally correct — free access does exist. However, reality is more nuanced: free access is a time-limited public beta (Token… - **`serper-search`** — The shipped freeNote says "(none)" which is partially accurate — there is no recurring free plan — but Serper does offer 2,500 one-time trial credits on signup. The catalog note could be more precise… diff --git a/docs/reference/PROVIDER_REFERENCE.md b/docs/reference/PROVIDER_REFERENCE.md index e51d8e1bdc6..ba05c4497ca 100644 --- a/docs/reference/PROVIDER_REFERENCE.md +++ b/docs/reference/PROVIDER_REFERENCE.md @@ -54,7 +54,6 @@ Use the dashboard at `/dashboard/providers` to enable, configure, and test each | `kimi-coding` | `kmc` | Kimi Code CLI | OAuth | — | Sign in with the same Kimi account used by Kimi Code CLI. OmniRoute uses the CLI OAuth flow and Kimi Coding Plan endpoints. | | `kiro` | `kr` | Kiro AI | OAuth | — | Free tier: 50 credits/month (~25K–100K tokens). ⚠️ Kiro ToS prohibits third-party proxy/harness use. | | `qoder` | `if` | Qoder | OAuth | — | — | -| `qwen` | `qw` | Qwen Code | OAuth | — | ⚠️ **DEPRECATED.** Qwen OAuth free tier was discontinued on 2026-04-15. Use 'bailian-coding-plan', 'alibaba', 'alibaba-cn', or 'openrouter' provider with API key instead. | | `trae` | `tr` | Trae | OAuth | [link](https://trae.ai) | Trae is an AI-native IDE by ByteDance (SOLO remote agent). Authorize via trae.ai in the popup, or sign in at solo.trae.ai and paste the Cloud-IDE-JWT (sent as 'Authorization: Cloud-IDE-JWT ', ~14-day lifetime) as the access token; web_id/biz_user_id/user_unique_id/scope/tenant/region propagate via providerSpecificData. No headless refresh for pasted tokens — re-paste on expiry. | | `windsurf` | `ws` | Windsurf (Devin CLI) | OAuth | [link](https://windsurf.com) | In the Windsurf / VS Code IDE, open the command palette and run `Windsurf: Provide Auth Token` (or click the Jupyter "Get Windsurf Authentication Token" button), then copy the shown token and paste it here. Note: opening windsurf.com/show-auth-token directly only renders a "Redirecting" page — the IDE must initiate the flow (it adds a `?state=...` param) for the token to appear. | | `xai-oauth` | `xao` | xAI OAuth (Grok) | OAuth | [link](https://x.ai) | Sign in with xAI to use api.x.ai models such as Grok 4.5. This is separate from Grok Build JWT sessions, which use cli-chat-proxy.grok.com and grok-build model aliases. | diff --git a/docs/security/STEALTH_GUIDE.md b/docs/security/STEALTH_GUIDE.md index 9d60d7be597..43043ce99bd 100644 --- a/docs/security/STEALTH_GUIDE.md +++ b/docs/security/STEALTH_GUIDE.md @@ -147,7 +147,7 @@ Touch points: ## CLI Fingerprint Registry — `open-sse/config/cliFingerprints.ts` -Per-provider table that pins **exact** header ordering and JSON body field ordering captured from mitmproxy traces of the official CLIs. Currently registered: `codex`, `claude`, plus runtime-derived profiles in `providerHeaderProfiles.ts` for `antigravity`, `qwen`, `github`. +Per-provider table that pins **exact** header ordering and JSON body field ordering captured from mitmproxy traces of the official CLIs. Currently registered: `codex`, `claude`, plus runtime-derived profiles in `providerHeaderProfiles.ts` for `antigravity` and `github`. ```ts interface CliFingerprint { @@ -220,7 +220,6 @@ All MITM endpoints require management auth (`requireCliToolsAuth`). The sudo pas | `ANTIGRAVITY_USER_AGENT` | `antigravity/2.0.1 linux/arm64 google-api-nodejs-client/10.3.0` | | `KIRO_USER_AGENT` | `AWS-SDK-JS/3.0.0 kiro-ide/1.0.0` | | `QODER_USER_AGENT` | `Qoder-Cli` | -| `QWEN_USER_AGENT` | `QwenCode/0.19.3 (linux; x64)` | | `CURSOR_USER_AGENT` | `Cursor/3.4` | Consumed by `open-sse/executors/base.ts::buildHeaders()` via dynamic lookup. **Bump these when providers release new CLI versions** — stale UA strings start getting rejected as outdated clients. @@ -238,7 +237,6 @@ Consumed by `open-sse/executors/base.ts::buildHeaders()` via dynamic lookup. **B | `CLI_COMPAT_KIMI_CODING=1` | Kimi Coding | | `CLI_COMPAT_KILOCODE=1` | KiloCode | | `CLI_COMPAT_CLINE=1` | Cline | -| `CLI_COMPAT_QWEN=1` | Qwen Code | | `CLI_COMPAT_ALL=1` | Enable all of the above | The provider IP is **always preserved** — the toggle only reshapes the request wire image, it does not switch IP egress. diff --git a/llm.txt b/llm.txt index 9e012ae34da..f2e4ffd9de9 100644 --- a/llm.txt +++ b/llm.txt @@ -360,9 +360,9 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **pool**, **notion**, * ### Provider Categories -**Free Providers (4):** Qoder AI, Qwen Code (deprecated), Kiro AI, Windsurf +**Free Providers (3):** Qoder AI, Kiro AI, Windsurf -**OAuth Providers (14):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Qwen, Kiro, Qoder, Gemini, Windsurf, GitLab Duo +**OAuth Providers (13):** Claude Code, Antigravity, OpenAI Codex, GitHub Copilot, Cursor IDE, Kimi Coding, Kilo Code, Cline, Kiro, Qoder, Gemini, Windsurf, GitLab Duo **API Key Providers (48+):** OpenAI, Anthropic, Gemini (Google AI Studio), DeepSeek, Groq, xAI (Grok), Mistral, Perplexity, Together AI, Fireworks AI, Cerebras, Cohere, NVIDIA NIM, Nebius AI, SiliconFlow, Hyperbolic, HuggingFace, OpenRouter, Vertex AI, Cloudflare Workers AI, Scaleway AI, AI/ML API, Pollinations AI, Puter AI, LongCat AI, Alibaba, Alibaba (China), Kimi, Kimi Coding (API Key), Minimax, Minimax (China), Blackbox AI, Synthetic, Kilo Gateway, Z.AI, GLM Coding, Deepgram, AssemblyAI, ElevenLabs, Cartesia, PlayHT, Inworld, NanoBanana, SD WebUI, ComfyUI, Ollama Cloud, Perplexity Search, Serper Search, Brave Search, Exa Search, Tavily Search, OpenCode Zen, OpenCode Go, Alibaba Coding Plan diff --git a/open-sse/config/cliFingerprints.ts b/open-sse/config/cliFingerprints.ts index 7009a89505c..da65fce35bb 100644 --- a/open-sse/config/cliFingerprints.ts +++ b/open-sse/config/cliFingerprints.ts @@ -13,7 +13,6 @@ import { isClaudeCodeCompatible } from "../services/provider.ts"; import { getAntigravityUserAgent, GITHUB_COPILOT_CHAT_USER_AGENT, - getQwenOauthHeaders, } from "./providerHeaderProfiles.ts"; import { normalizeCliCompatProviderId } from "@/shared/utils/cliCompat"; @@ -198,44 +197,6 @@ export const CLI_FINGERPRINTS: Record = { ], userAgent: getAntigravityUserAgent, }, - qwen: { - headerOrder: [ - "Host", - "Content-Type", - "Authorization", - "User-Agent", - "X-Dashscope-AuthType", - "X-Dashscope-CacheControl", - "X-Dashscope-UserAgent", - "X-Stainless-Arch", - "X-Stainless-Lang", - "X-Stainless-Os", - "X-Stainless-Package-Version", - "X-Stainless-Retry-Count", - "X-Stainless-Runtime", - "X-Stainless-Runtime-Version", - "Connection", - "Accept", - "Accept-Language", - "Sec-Fetch-Mode", - "Accept-Encoding", - ], - bodyFieldOrder: [ - "model", - "messages", - "temperature", - "top_p", - "max_tokens", - "stream", - "tools", - "tool_choice", - "response_format", - "n", - "stop", - ], - userAgent: getQwenOauthHeaders()["User-Agent"], - extraHeaders: getQwenOauthHeaders(), - }, }; /** diff --git a/open-sse/config/constants.ts b/open-sse/config/constants.ts index 381dd9fe9b2..894560805db 100644 --- a/open-sse/config/constants.ts +++ b/open-sse/config/constants.ts @@ -76,10 +76,6 @@ export const OAUTH_ENDPOINTS = { token: "https://api.anthropic.com/v1/oauth/token", auth: "https://api.anthropic.com/v1/oauth/authorize", }, - qwen: { - token: "https://chat.qwen.ai/api/v1/oauth2/token", // From CLIProxyAPI - auth: "https://chat.qwen.ai/api/v1/oauth2/device/code", // From CLIProxyAPI - }, qoder: { token: process.env.QODER_OAUTH_TOKEN_URL || "", auth: process.env.QODER_OAUTH_AUTHORIZE_URL || "", diff --git a/open-sse/config/providerHeaderProfiles.ts b/open-sse/config/providerHeaderProfiles.ts index 59dc53aa8ff..47d33a84ad6 100644 --- a/open-sse/config/providerHeaderProfiles.ts +++ b/open-sse/config/providerHeaderProfiles.ts @@ -13,11 +13,6 @@ export const GITHUB_COPILOT_USER_AGENT_LIBRARY = "electron-fetch"; export const QWEN_CLI_VERSION = "0.19.3"; export const QWEN_STAINLESS_LANG = "js"; -export const QWEN_STAINLESS_PACKAGE_VERSION = "5.11.0"; -export const QWEN_STAINLESS_RETRY_COUNT = "1"; -export const QWEN_STAINLESS_RUNTIME = "node"; -export const QWEN_ACCEPT_LANGUAGE = "*"; -export const QWEN_SEC_FETCH_MODE = "cors"; export const QODER_DEFAULT_USER_AGENT = "Qoder-Cli"; @@ -85,8 +80,8 @@ export function normalizeStainlessArch(arch: string = getRuntimeArch()): string } export function getQwenCliUserAgent(version = QWEN_CLI_VERSION): string { - // Qwen Code builds this from the runtime process values. Keep it runtime-derived so - // packaged deployments use their own platform/architecture instead of a maintainer's host. + // Qoder's DashScope-compatible backend expects Qwen Code's runtime-derived wire identity. + // Keep it runtime-derived so packaged deployments use their own platform/architecture. return `QwenCode/${version} (${getRuntimePlatform()}; ${getRuntimeArch()})`; } @@ -111,26 +106,6 @@ export function getGitHubCopilotRefreshHeaders(authorization: string): Record { - const userAgent = getQwenCliUserAgent(); - return { - "User-Agent": userAgent, - "X-Dashscope-AuthType": "qwen-oauth", - "X-Dashscope-CacheControl": "enable", - "X-Dashscope-UserAgent": userAgent, - "X-Stainless-Arch": normalizeStainlessArch(), - "X-Stainless-Lang": QWEN_STAINLESS_LANG, - "X-Stainless-Os": normalizeStainlessPlatform(), - "X-Stainless-Package-Version": QWEN_STAINLESS_PACKAGE_VERSION, - "X-Stainless-Retry-Count": QWEN_STAINLESS_RETRY_COUNT, - "X-Stainless-Runtime": QWEN_STAINLESS_RUNTIME, - "X-Stainless-Runtime-Version": getRuntimeVersion(), - Connection: "keep-alive", - "Accept-Language": QWEN_ACCEPT_LANGUAGE, - "Sec-Fetch-Mode": QWEN_SEC_FETCH_MODE, - }; -} - export function getQoderDefaultHeaders(): Record { return { "User-Agent": QODER_DEFAULT_USER_AGENT, diff --git a/open-sse/config/providers/index.ts b/open-sse/config/providers/index.ts index 7bbc71969bb..ebd6f7ed133 100644 --- a/open-sse/config/providers/index.ts +++ b/open-sse/config/providers/index.ts @@ -83,7 +83,6 @@ import { snowflakeProvider } from "./registry/snowflake/index.ts"; import { huggingfaceProvider } from "./registry/huggingface/index.ts"; import { chipotleProvider } from "./registry/chipotle/index.ts"; import { freeaiapikeyProvider } from "./registry/freeaiapikey/index.ts"; -import { qwenProvider } from "./registry/qwen/index.ts"; import { qwen_webProvider } from "./registry/qwen/web/index.ts"; import { zai_webProvider } from "./registry/zai-web/index.ts"; import { modalProvider } from "./registry/modal/index.ts"; @@ -284,7 +283,6 @@ export const REGISTRY: Record = { huggingface: huggingfaceProvider, chipotle: chipotleProvider, freeaiapikey: freeaiapikeyProvider, - qwen: qwenProvider, "qwen-web": qwen_webProvider, "zai-web": zai_webProvider, modal: modalProvider, diff --git a/open-sse/config/providers/registry/qwen/index.ts b/open-sse/config/providers/registry/qwen/index.ts deleted file mode 100644 index e7aa78f8607..00000000000 --- a/open-sse/config/providers/registry/qwen/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { RegistryEntry } from "../../shared.ts"; -import { getQwenOauthHeaders, resolvePublicCred } from "../../shared.ts"; - -export const qwenProvider: RegistryEntry = { - id: "qwen", - alias: "qw", - format: "openai", - executor: "default", - baseUrl: "https://chat.qwen.ai/api/v1/services/aigc/text-generation/generation", - authType: "oauth", - authHeader: "bearer", - headers: getQwenOauthHeaders(), - oauth: { - clientIdEnv: "QWEN_OAUTH_CLIENT_ID", - clientIdDefault: resolvePublicCred("qwen_id"), - tokenUrl: "https://chat.qwen.ai/api/v1/oauth2/token", - authUrl: "https://chat.qwen.ai/api/v1/oauth2/device/code", - }, - models: [ - { id: "qwen3-coder-plus", name: "Qwen3 Coder Plus" }, - { id: "qwen3-coder-flash", name: "Qwen3 Coder Flash" }, - { id: "vision-model", name: "Qwen3 Vision Model" }, - { id: "coder-model", name: "Qwen3.6 (Coder Model)" }, - ], -}; diff --git a/open-sse/config/providers/registry/qwen/web/index.ts b/open-sse/config/providers/registry/qwen/web/index.ts index 5d5bff7800e..8b86c9806cd 100644 --- a/open-sse/config/providers/registry/qwen/web/index.ts +++ b/open-sse/config/providers/registry/qwen/web/index.ts @@ -2,8 +2,7 @@ import type { RegistryEntry } from "../../../shared.ts"; export const qwen_webProvider: RegistryEntry = { id: "qwen-web", - // Distinct alias: the primary "qwen" provider keeps the short "qw" alias; - // this web/cookie variant is addressed by its own id. + // The web/cookie variant is addressed by its own id. alias: "qwen-web", format: "openai", executor: "qwen-web", diff --git a/open-sse/config/providers/shared.ts b/open-sse/config/providers/shared.ts index fd32da1ad64..5d14e5a454c 100644 --- a/open-sse/config/providers/shared.ts +++ b/open-sse/config/providers/shared.ts @@ -33,7 +33,6 @@ import { getGitHubCopilotChatHeaders, getKiroServiceHeaders, getQoderDefaultHeaders, - getQwenOauthHeaders, getRuntimePlatform, getRuntimeArch, } from "../providerHeaderProfiles.ts"; @@ -691,7 +690,6 @@ export { getGitHubCopilotChatHeaders, getKiroServiceHeaders, getQoderDefaultHeaders, - getQwenOauthHeaders, getRuntimePlatform, getRuntimeArch, resolvePublicCred, diff --git a/open-sse/executors/default.ts b/open-sse/executors/default.ts index bc5685f521b..7f45e03083a 100644 --- a/open-sse/executors/default.ts +++ b/open-sse/executors/default.ts @@ -28,7 +28,6 @@ import { getTargetFormat, isClaudeCodeCompatible, } from "../services/provider.ts"; -import { sanitizeQwenThinkingToolChoice } from "../services/qwenThinking.ts"; import { getSapResourceGroup } from "../config/sap.ts"; import { normalizeBailianMessagesUrl, @@ -257,10 +256,6 @@ export class DefaultExecutor extends BaseExecutor { return `${this.config.baseUrl}?beta=true`; case "gemini": return `${this.config.baseUrl}/${model}:${stream ? "streamGenerateContent?alt=sse" : "generateContent"}`; - case "qwen": { - const resourceUrl = credentials?.providerSpecificData?.resourceUrl; - return `https://${resourceUrl || "portal.qwen.ai"}/v1/chat/completions`; - } default: { // Honor a user-supplied custom base URL (providerSpecificData.baseUrl) for // OpenAI-format providers (e.g. the built-in "openai" provider pointed at a @@ -445,16 +440,6 @@ export class DefaultExecutor extends BaseExecutor { headers["Accept"] = stream ? "text/event-stream" : "application/json"; - // Qwen header cleanup: Remove X-Dashscope-* headers if using an API key (DashScope compatible mode). - // If using OAuth (Qwen Code), we MUST keep them for portal.qwen.ai to accept the request. - if (this.provider === "qwen" && effectiveKey) { - for (const key of Object.keys(headers)) { - if (key.toLowerCase().startsWith("x-dashscope-")) { - delete headers[key]; - } - } - } - const isCompatibleProvider = this.provider?.startsWith?.("openai-compatible-") || this.provider?.startsWith?.("anthropic-compatible-"); @@ -621,21 +606,11 @@ export class DefaultExecutor extends BaseExecutor { withDefaults = withoutStreamOptions; } } else if (stream && targetFormat === "openai" && requestFormat !== "openai-responses") { - // Port of decolua/9router#663 (closes upstream #557): Qwen rejects with - // 400 "'stream_options' only set this when you set stream: true" when the - // outgoing body carries `stream: false` (Claude Code / Claude-Code- - // compatible callers force the executor-level stream flag on via - // `upstreamStream = stream || isClaudeCodeCompatible`, but the body keeps - // the caller's original `stream: false`). Same upstream also rejects the - // injection when `thinking` / `enable_thinking` is set. Skip injection in - // those cases instead of unconditionally adding `stream_options`. + // Do not inject stream_options when the outgoing body explicitly disables streaming. const defaultsRecord = withDefaults as Record; const bodyDisablesStreamOptions = defaultsRecord.stream !== undefined && defaultsRecord.stream !== true; - const qwenBlocksStreamOptions = - this.provider === "qwen" && - (Boolean(defaultsRecord.thinking) || Boolean(defaultsRecord.enable_thinking)); - if (bodyDisablesStreamOptions || qwenBlocksStreamOptions) { + if (bodyDisablesStreamOptions) { if (Object.prototype.hasOwnProperty.call(defaultsRecord, "stream_options")) { const withoutStreamOptions = { ...defaultsRecord }; delete withoutStreamOptions.stream_options; @@ -695,15 +670,10 @@ export class DefaultExecutor extends BaseExecutor { } } - if (this.provider === "qwen" && typeof withDefaults === "object" && withDefaults !== null) { - return sanitizeQwenThinkingToolChoice( - withDefaults as Record, - "QwenExecutor" - ); - } - - // Strip params unsupported by the target provider/model before sending upstream. - // Rules live in ../translator/paramSupport.ts (9router#7ae9fff6; fixes #1748). + // Config-driven strip of params unsupported by the target provider/model + // (e.g. claude-opus-4 deprecated `temperature` → Anthropic 400). Port from + // 9router#7ae9fff6 (fixes upstream #1748). Rules live in + // ../translator/paramSupport.ts so adding one means editing one table. if (typeof withDefaults === "object" && withDefaults !== null) { const bodyRecord = withDefaults as Record; const outboundModel = typeof bodyRecord.model === "string" ? bodyRecord.model : model; diff --git a/open-sse/executors/index.ts b/open-sse/executors/index.ts index ada346d03a8..be70ba0bf52 100644 --- a/open-sse/executors/index.ts +++ b/open-sse/executors/index.ts @@ -161,7 +161,6 @@ const executors = { "doubao-web": new DoubaoWebExecutor(), db: new DoubaoWebExecutor(), // Alias "qwen-web": new QwenWebExecutor(), - qw: new QwenWebExecutor(), // Alias "zai-web": new ZaiWebExecutor(), zw: new ZaiWebExecutor(), // Alias theoldllm: new TheOldLlmExecutor(), diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 871b27d0bcf..b70275ee84b 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -256,12 +256,6 @@ import { } from "./chatCore/serviceTier.ts"; import { cacheReasoningFromAssistantMessage } from "../services/reasoningCache.ts"; import { sanitizeOpenAITool } from "../services/toolSchemaSanitizer.ts"; -import { - setDetectedToolLimit, - parseToolLimitFromError, - shouldDetectLimit, -} from "../services/toolLimitDetector.ts"; - import { isCompactResponsesEndpoint } from "../executors/codex.ts"; import { buildCodexQuotaPersistence } from "./chatCore/codexQuota.ts"; import { invalidateCodexQuotaCache } from "../services/codexQuotaFetcher.ts"; @@ -1741,9 +1735,7 @@ export async function handleChatCore({ finalCompressionBody?.messages || body?.contents || body?.request?.contents || - (body?.input && typeof body.input === "object" && !Array.isArray(body.input) - ? body.input - : []); + (body?.input && typeof body.input === "object" && !Array.isArray(body.input) ? body.input : []); const finalEstimatedInputTokens = estimateTokens(finalMessages) + (Array.isArray(body?.tools) ? estimateTokens(body.tools) : 0) + @@ -2476,7 +2468,7 @@ export async function handleChatCore({ const execute = async () => { // Upstream body preparation extracted to chatCore/upstreamBody.ts (#3501 — first internal // sub-slice of executeProviderRequest); produces the body sent upstream (payload rules + - // tool-limit truncation + qwen oauth user backfill + prompt_cache_key injection). + // tool-limit truncation + prompt_cache_key injection). let bodyToSend = await prepareUpstreamBody({ translatedBody, modelToCall, @@ -2497,13 +2489,7 @@ export async function handleChatCore({ const rawResult = await (async () => { let attempts = 0; const isModelScopeForRequest = isModelScope(); - const maxAttempts = isModelScopeForRequest - ? 3 - : provider === "qwen" - ? 3 - : provider === "codex" - ? 3 - : 1; + const maxAttempts = isModelScopeForRequest ? 3 : provider === "codex" ? 3 : 1; // ── Codex 429 account-rotation state ───────────────────────────────── // Track excluded connection IDs for codex failover across attempts. @@ -2608,26 +2594,6 @@ export async function handleChatCore({ recordKeyHealthStatus(401, execCreds); } - // Qwen 429 strict quota backoff (wait 1.5s, 3s and retry) - if ( - provider === "qwen" && - res.response.status === 429 && - attempts < maxAttempts - 1 - ) { - const bodyPeek = await res.response - .clone() - .text() - .catch(() => ""); - if (bodyPeek.toLowerCase().includes("exceeded your current quota")) { - const delay = 1500 * (attempts + 1); - log?.warn?.("QWEN_RETRY", `Quota 429 hit. Retrying in ${delay}ms...`); - releaseAccountSemaphore(); - await new Promise((r) => setTimeout(r, delay)); - attempts++; - continue; - } - } - if (isModelScope() && res.response.status === 429 && attempts < maxAttempts - 1) { const bodyPeek = await res.response .clone() @@ -3193,39 +3159,12 @@ export async function handleChatCore({ upstreamErrorType ); } - // We need to peek at the error text if it's 400 for Qwen let upstreamErrorParsed = false; let parsedStatusCode = providerResponse.status; let parsedMessage = ""; let parsedRetryAfterMs: number | null = null; let upstreamErrorBody: unknown = null; - if (provider === "qwen" && providerResponse.status === HTTP_STATUS.BAD_REQUEST) { - const errorDetails = await parseUpstreamError(providerResponse, provider); - parsedStatusCode = errorDetails.statusCode; - parsedMessage = errorDetails.message; - parsedRetryAfterMs = errorDetails.retryAfterMs; - upstreamErrorBody = errorDetails.responseBody; - upstreamErrorParsed = true; - } - - const errorMessageForToolDetection = - typeof upstreamErrorBody === "string" - ? upstreamErrorBody - : JSON.stringify(upstreamErrorBody ?? {}); - if (shouldDetectLimit(errorMessageForToolDetection, parsedStatusCode)) { - const detectedLimit = parseToolLimitFromError(errorMessageForToolDetection); - if (detectedLimit) { - setDetectedToolLimit(provider, detectedLimit); - log?.info?.("TOOL_LIMIT", `Detected tool limit ${detectedLimit} for ${provider}`); - } - } - - const isQwenExpiredError = - provider === "qwen" && - parsedStatusCode === HTTP_STATUS.BAD_REQUEST && - parsedMessage?.toLowerCase().includes("session has expired"); - // Track whether stream_options was present and stripped — if so, 401/403 after // that may be from the modification rather than a genuine auth failure, so we // skip the credential refresh attempt in that case. @@ -3235,11 +3174,10 @@ export async function handleChatCore({ delete translatedBody.stream_options; } - // Handle 401/403 (and Qwen explicit expiration) - try token refresh using executor + // Handle 401/403 - try token refresh using executor if ( (providerResponse.status === HTTP_STATUS.UNAUTHORIZED || - providerResponse.status === HTTP_STATUS.FORBIDDEN || - isQwenExpiredError) && + providerResponse.status === HTTP_STATUS.FORBIDDEN) && !hadStreamOptions // Skip refresh if failure may be from stream_options removal, not auth ) { // Fix A: wrap refreshCredentials in runWithOnPersist so the persist callback diff --git a/open-sse/handlers/chatCore/upstreamBody.ts b/open-sse/handlers/chatCore/upstreamBody.ts index 85b0624a6bc..57e3a358b4b 100644 --- a/open-sse/handlers/chatCore/upstreamBody.ts +++ b/open-sse/handlers/chatCore/upstreamBody.ts @@ -4,8 +4,8 @@ * * Extracted from handleChatCore's execute() closure: prepares the body actually sent upstream for a * given target model. Pins the model id, applies the configured payload rules, truncates the tool - * list to the provider's effective limit, backfills a default `user` for Qwen OAuth requests, and - * injects an OpenAI `prompt_cache_key` for caching-capable providers. Pure with respect to handler + * list to the provider's effective limit and injects an OpenAI `prompt_cache_key` for + * caching-capable providers. Pure with respect to handler * state (returns a fresh body, only logs as a side effect); behaviour is byte-identical to the * previous inline block. Split into small private steps so each stays under the complexity cap. */ @@ -26,7 +26,11 @@ import { sanitizeRequestForResolvedTarget } from "../../services/targetRequestSa type LoggerLike = { debug?: (...args: unknown[]) => void } | null | undefined; type Body = Record; type CredentialsLike = - | { apiKey?: unknown; accessToken?: unknown; providerSpecificData?: Record | null } + | { + apiKey?: unknown; + accessToken?: unknown; + providerSpecificData?: Record | null; + } | null | undefined; @@ -83,27 +87,6 @@ function truncateToolList( return bodyToSend; } -// Qwen OAuth rejects requests without a non-empty `user` field. Some minimal OpenAI-compatible -// clients omit it, so we backfill a stable default only for OAuth mode (API key mode is unaffected). -function backfillQwenOAuthUser( - bodyToSend: Body, - provider: string | null | undefined, - credentials: CredentialsLike, - log?: LoggerLike -): Body { - const hasValidQwenUser = typeof bodyToSend.user === "string" && bodyToSend.user.trim().length > 0; - const isQwenOAuthRequest = - provider === "qwen" && - !credentials?.apiKey && - typeof credentials?.accessToken === "string" && - credentials.accessToken.trim().length > 0; - if (isQwenOAuthRequest && !hasValidQwenUser) { - bodyToSend = { ...bodyToSend, user: "omniroute-qwen-oauth" }; - log?.debug?.("QWEN", "Injected fallback user for OAuth request"); - } - return bodyToSend; -} - // Inject prompt_cache_key only for providers that support it. async function injectPromptCacheKey( bodyToSend: Body, @@ -175,9 +158,13 @@ export async function prepareUpstreamBody(opts: { log, }); bodyToSend = truncateToolList(bodyToSend, provider, bypassDefaultToolLimit ?? false, log); - bodyToSend = backfillQwenOAuthUser(bodyToSend, provider, credentials, log); const connectionCacheOverride = resolveConnectionCacheOverride(credentials?.providerSpecificData); - bodyToSend = await injectPromptCacheKey(bodyToSend, provider, targetFormat, connectionCacheOverride); + bodyToSend = await injectPromptCacheKey( + bodyToSend, + provider, + targetFormat, + connectionCacheOverride + ); return bodyToSend; } diff --git a/open-sse/index.ts b/open-sse/index.ts index 2c494106886..245a9567c95 100644 --- a/open-sse/index.ts +++ b/open-sse/index.ts @@ -60,7 +60,6 @@ export { refreshAccessToken, refreshClaudeOAuthToken, refreshGoogleToken, - refreshQwenToken, refreshCodexToken, refreshQoderToken, refreshGitHubToken, diff --git a/open-sse/services/__tests__/tierResolver.test.ts b/open-sse/services/__tests__/tierResolver.test.ts index 458a567627c..2a1fdd2e2ad 100644 --- a/open-sse/services/__tests__/tierResolver.test.ts +++ b/open-sse/services/__tests__/tierResolver.test.ts @@ -49,12 +49,6 @@ describe("TierResolver", () => { assert.equal(result.hasFreeTier, true); }); - it("classifies Qwen as free", () => { - const result = classifyTier("qwen", "qwen3-coder-plus"); - assert.equal(result.tier, PROVIDER_TIER.FREE); - assert.equal(result.hasFreeTier, true); - }); - it("classifies Cloudflare AI as free", () => { const result = classifyTier("cloudflare-ai", "llama-3.3-70b"); assert.equal(result.tier, PROVIDER_TIER.FREE); @@ -188,15 +182,14 @@ describe("TierResolver", () => { { provider: "anthropic", model: "claude-opus-4-7" }, { provider: "groq", model: "llama-3.3-70b" }, { provider: "qoder", model: "kimi-k2-thinking" }, - { provider: "qwen", model: "qwen3-coder-plus" }, { provider: "unknown", model: "unknown-model" }, ]; const results = classifyTiers(targets); - assert.equal(results.length, 10); + assert.equal(results.length, 9); assert.equal(results[0].tier, PROVIDER_TIER.FREE); // kiro assert.equal(results[1].tier, PROVIDER_TIER.PREMIUM); // openai gpt-4o ($2.50/M) assert.equal(results[2].tier, PROVIDER_TIER.CHEAP); // deepseek - assert.equal(results[9].tier, PROVIDER_TIER.PREMIUM); // unknown + assert.equal(results[8].tier, PROVIDER_TIER.PREMIUM); // unknown }); it("uses cache for repeated models", () => { @@ -230,7 +223,6 @@ describe("TierResolver", () => { "pollinations", "longcat", "cloudflare-ai", - "qwen", "nvidia-nim", "cerebras", "groq", diff --git a/open-sse/services/refreshSerializer.ts b/open-sse/services/refreshSerializer.ts index ff08f8982f9..f71d5c41906 100644 --- a/open-sse/services/refreshSerializer.ts +++ b/open-sse/services/refreshSerializer.ts @@ -27,7 +27,6 @@ const ROTATION_LOCK_GROUP: Record = { "gitlab-duo": "gitlab-duo", kiro: "kiro", "kimi-coding": "kimi-coding", - qwen: "qwen", }; // Protective settle gap (ms) between two consecutive sibling refreshes when the diff --git a/open-sse/services/tierConfig.ts b/open-sse/services/tierConfig.ts index 9bd1939aa68..2119029e13b 100644 --- a/open-sse/services/tierConfig.ts +++ b/open-sse/services/tierConfig.ts @@ -51,7 +51,6 @@ export const LEGACY_FREE_PROVIDERS: readonly string[] = [ "pollinations", "longcat", "cloudflare-ai", - "qwen", "nvidia-nim", "cerebras", "groq", diff --git a/open-sse/services/tierDefaults.json b/open-sse/services/tierDefaults.json index 49be7b08d3a..5e1e4a23cff 100644 --- a/open-sse/services/tierDefaults.json +++ b/open-sse/services/tierDefaults.json @@ -18,7 +18,6 @@ "pollinations", "longcat", "cloudflare-ai", - "qwen", "nvidia-nim", "cerebras", "groq" diff --git a/open-sse/services/tokenRefresh.ts b/open-sse/services/tokenRefresh.ts index 7d51aca80ba..33b53a0d967 100755 --- a/open-sse/services/tokenRefresh.ts +++ b/open-sse/services/tokenRefresh.ts @@ -23,7 +23,6 @@ import { refreshKimiCodingToken } from "./tokenRefresh/providers/kimiCoding.ts"; import { refreshGitLabDuoToken } from "./tokenRefresh/providers/gitlabDuo.ts"; import { refreshClaudeOAuthToken } from "./tokenRefresh/providers/claudeOAuth.ts"; import { refreshGoogleToken } from "./tokenRefresh/providers/google.ts"; -import { refreshQwenToken } from "./tokenRefresh/providers/qwen.ts"; import { refreshCodexToken } from "./tokenRefresh/providers/codex.ts"; import { refreshKiroToken } from "./tokenRefresh/providers/kiro.ts"; import { refreshQoderToken } from "./tokenRefresh/providers/qoder.ts"; @@ -38,7 +37,6 @@ export { refreshGitLabDuoToken, refreshClaudeOAuthToken, refreshGoogleToken, - refreshQwenToken, refreshCodexToken, refreshKiroToken, refreshQoderToken, @@ -77,7 +75,6 @@ export const REFRESH_LEAD_MS: Record = { "gitlab-duo": 5 * 60 * 1000, // GitLab token family revocation on misuse kiro: 5 * 60 * 1000, // AWS SSO OIDC issues one-time-use refresh tokens "kimi-coding": 5 * 60 * 1000, // Moonshot rotates per-refresh - qwen: 5 * 60 * 1000, // Alibaba device-code path also rotates // Non-rotating providers — longer lead is safe. iflow: 24 * 60 * 60 * 1000, // 24 hours // Google OAuth refresh_tokens are permanent (non-rotating) — longer lead @@ -391,9 +388,6 @@ async function _getAccessTokenInternal(provider, credentials, log, proxyConfig: case "codex": return await refreshCodexToken(credentials.refreshToken, log, proxyConfig); - case "qwen": - return await refreshQwenToken(credentials.refreshToken, log, proxyConfig); - case "qoder": return await refreshQoderToken(credentials.refreshToken, log, proxyConfig); @@ -457,7 +451,6 @@ export function supportsTokenRefresh(provider) { "agy", "claude", "codex", - "qwen", "qoder", "github", "kiro", @@ -767,7 +760,6 @@ export function formatProviderCredentials(provider, credentials, log) { }; case "codex": - case "qwen": case "qoder": case "openai": case "openrouter": diff --git a/open-sse/services/tokenRefresh/providers/qwen.ts b/open-sse/services/tokenRefresh/providers/qwen.ts deleted file mode 100644 index 6c0df5fde0b..00000000000 --- a/open-sse/services/tokenRefresh/providers/qwen.ts +++ /dev/null @@ -1,81 +0,0 @@ -// @ts-nocheck -// Extracted from open-sse/services/tokenRefresh.ts — see ../shared.ts for -// provenance notes (ported idea from KooshaPari's PR #7338, redone on tip). -import { PROVIDERS, OAUTH_ENDPOINTS } from "../../../config/constants.ts"; -import { runWithProxyContext } from "../../../utils/proxyFetch.ts"; -import { buildFormParams } from "../shared.ts"; - -export async function refreshQwenToken(refreshToken, log, proxyConfig: unknown = null) { - const endpoint = OAUTH_ENDPOINTS.qwen.token; - - try { - const response = await runWithProxyContext(proxyConfig, () => - fetch(endpoint, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json", - }, - body: buildFormParams({ - grant_type: "refresh_token", - refresh_token: refreshToken, - client_id: PROVIDERS.qwen.clientId, - }), - }) - ); - - if (response.status === 200) { - const tokens = await response.json(); - - log?.info?.("TOKEN_REFRESH", "Successfully refreshed Qwen token", { - hasNewAccessToken: !!tokens.access_token, - hasNewRefreshToken: !!tokens.refresh_token, - expiresIn: tokens.expires_in, - }); - - return { - accessToken: tokens.access_token, - refreshToken: tokens.refresh_token || refreshToken, - expiresIn: tokens.expires_in, - providerSpecificData: tokens.resource_url - ? { resourceUrl: tokens.resource_url } - : undefined, - }; - } else { - const errorText = await response.text().catch(() => ""); - - // Detect unrecoverable invalid_request (expired/revoked refresh token or bad client_id) - let errorCode = null; - try { - const parsed = JSON.parse(errorText); - errorCode = parsed?.error; - } catch { - // not JSON, ignore - } - - if (errorCode === "invalid_request" || errorCode === "invalid_grant") { - log?.error?.( - "TOKEN_REFRESH", - "Qwen refresh token is invalid or expired. Re-authentication required.", - { - status: response.status, - errorCode, - } - ); - return { error: "unrecoverable_refresh_error", code: errorCode }; - } - - log?.warn?.("TOKEN_REFRESH", `Error with Qwen endpoint`, { - status: response.status, - error: errorText, - }); - } - } catch (error) { - log?.warn?.("TOKEN_REFRESH", `Network error trying Qwen endpoint`, { - error: error.message, - }); - } - - log?.error?.("TOKEN_REFRESH", "Failed to refresh Qwen token"); - return null; -} diff --git a/open-sse/services/usage.ts b/open-sse/services/usage.ts index 162f40b50e3..c0aa9128ae1 100644 --- a/open-sse/services/usage.ts +++ b/open-sse/services/usage.ts @@ -519,7 +519,6 @@ export const USAGE_FETCHER_PROVIDERS = [ "amazon-q", "kimi-coding", "kimi-coding-apikey", - "qwen", "qoder", "glm", "glm-cn", @@ -584,8 +583,6 @@ export async function getUsageForProvider( case "kimi-coding": case "kimi-coding-apikey": return await getKimiUsage(accessToken, apiKey, providerSpecificData); - case "qwen": - return await getQwenUsage(accessToken, providerSpecificData); case "qoder": // Qoder PATs live in `apiKey` (decrypted) or `providerSpecificData.qoderPat`, // never in `accessToken`. @@ -885,24 +882,6 @@ async function getVertexUsage(connectionId: string, provider: string) { } } -/** - * Qwen Usage - */ -async function getQwenUsage(accessToken?: string, providerSpecificData?: JsonRecord) { - void accessToken; - try { - const resourceUrl = providerSpecificData?.resourceUrl; - if (!resourceUrl) { - return { message: "Qwen connected. No resource URL available." }; - } - - // Qwen may have usage endpoint at resource URL - return { message: "Qwen connected. Usage tracked per request." }; - } catch (error) { - return { message: "Unable to fetch Qwen usage." }; - } -} - /** * Qoder Usage * diff --git a/open-sse/utils/cacheControlPolicy.ts b/open-sse/utils/cacheControlPolicy.ts index 9f28f8f14ad..8e22f8a0fdd 100644 --- a/open-sse/utils/cacheControlPolicy.ts +++ b/open-sse/utils/cacheControlPolicy.ts @@ -7,7 +7,7 @@ * Client-side caching (e.g., Claude Code) should be preserved when: * 1. Client is Claude Code or similar caching-aware client * 2. Request will hit a deterministic target (single model or deterministic combo strategy) - * 3. Provider supports prompt caching (Anthropic, Alibaba Qwen, etc.) + * 3. Provider supports prompt caching (Anthropic, etc.) */ import type { RoutingStrategyValue } from "../../src/shared/constants/routingStrategies"; @@ -76,7 +76,6 @@ const CACHING_PROVIDERS = new Set([ "claude", "anthropic", "zai", - "qwen", "deepseek", // Kimi Code's OpenAI protocol requires prompt_cache_key for Coding Plan // cache affinity. The OAuth card and hidden API-key compatibility ID share diff --git a/open-sse/utils/publicCreds.ts b/open-sse/utils/publicCreds.ts index 799901ca487..1cdeccbb1ab 100644 --- a/open-sse/utils/publicCreds.ts +++ b/open-sse/utils/publicCreds.ts @@ -165,11 +165,6 @@ const EMBEDDED_DEFAULTS = { 14, 29, 30, 54, 55, 34, 26, 21, 8, 104, 53, 47, 85, 95, 15, 83, 110, 29, 105, 14, 53, 30, 94, 26, 29, 20, 26, 11, ], - // Qwen Code CLI — qwen oauth client (public, device flow) - qwen_id: [ - 9, 93, 93, 89, 70, 92, 66, 71, 7, 26, 68, 20, 86, 88, 13, 81, 79, 67, 9, 91, 12, 93, 15, 16, 88, - 69, 23, 4, 20, 21, 64, 84, - ], // Kimi coding CLI — moonshot oauth client (public) kimi_id: [ 94, 90, 11, 92, 20, 89, 66, 69, 72, 73, 65, 76, 86, 65, 93, 7, 75, 20, 28, 86, 90, 94, 95, 95, diff --git a/src/app/(dashboard)/dashboard/combos/page.tsx b/src/app/(dashboard)/dashboard/combos/page.tsx index ea29735d058..733ce313d13 100644 --- a/src/app/(dashboard)/dashboard/combos/page.tsx +++ b/src/app/(dashboard)/dashboard/combos/page.tsx @@ -407,7 +407,7 @@ const COMBO_TEMPLATE_FALLBACK = { balancedDesc: "Least-used routing to spread demand over time.", freeStackTitle: "Free Stack ($0)", freeStackDesc: - "Round-robin across free providers: Kiro, Qoder, Qwen, Antigravity CLI. Zero cost, never stops.", + "Round-robin across free providers: Kiro, Qoder, Antigravity CLI. Zero cost, never stops.", paidPremiumTitle: "Paid Premium", paidPremiumDesc: "Round-robin across paid subscriptions: Cursor, Antigravity. Top-tier models, distributed load.", diff --git a/src/app/(dashboard)/dashboard/providers/components/onboarding/providerOnboardingCatalog.ts b/src/app/(dashboard)/dashboard/providers/components/onboarding/providerOnboardingCatalog.ts index bd91a298614..81684d6a295 100644 --- a/src/app/(dashboard)/dashboard/providers/components/onboarding/providerOnboardingCatalog.ts +++ b/src/app/(dashboard)/dashboard/providers/components/onboarding/providerOnboardingCatalog.ts @@ -40,7 +40,6 @@ export const SUPPORTED_WIZARD_OAUTH_PROVIDER_IDS = new Set([ "codex", "antigravity", "agy", - "qwen", "kimi-coding", "github", "gitlab-duo", diff --git a/src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts b/src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts index 926a1a0fd81..10af061e004 100644 --- a/src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts +++ b/src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts @@ -139,16 +139,8 @@ export const PROVIDER_MODELS_CONFIG: Record = authPrefix: "Bearer ", parseResponse: (data) => normalizeOpenAiLikeModelsResponse(data, "huggingface"), }, - qwen: { - url: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/models", - method: "GET", - headers: { "Content-Type": "application/json" }, - authHeader: "Authorization", - authPrefix: "Bearer ", - parseResponse: (data) => data.data || [], - }, // #3931: qwen-web (cookie provider) was missing here, so its discovery page - // showed nothing (the OAuth fallback above only fires for provider==="qwen"). + // showed nothing. // `chat.qwen.ai/api/v2/models/` is public (no auth header configured/sent); // shape `{ data: { data: [{ id, name, owned_by }] } }`, flatter `{ data: [] }` fallback. "qwen-web": { @@ -226,8 +218,7 @@ export const PROVIDER_MODELS_CONFIG: Record = if (token) out["x-api-key"] = token; return out; }, - parseResponse: (data: any) => - Array.isArray(data) ? data : (data?.data || data?.models || []), + parseResponse: (data: any) => (Array.isArray(data) ? data : data?.data || data?.models || []), }, openai: { url: "https://api.openai.com/v1/models", diff --git a/src/app/api/providers/[id]/models/route.ts b/src/app/api/providers/[id]/models/route.ts index 10eed675e75..49a7d724dc3 100755 --- a/src/app/api/providers/[id]/models/route.ts +++ b/src/app/api/providers/[id]/models/route.ts @@ -1870,25 +1870,6 @@ export async function GET( provider in PROVIDER_MODELS_CONFIG ? PROVIDER_MODELS_CONFIG[provider as keyof typeof PROVIDER_MODELS_CONFIG] : deriveConfigFromRegistryModelsUrl(provider); - // Static model providers (no remote /models API) - // Qwen OAuth Fallback: The Dashscope /models API rejects OAuth tokens with 401 - if (provider === "qwen" && connection.authType === "oauth") { - const qwenModels = getModelsByProviderId("qwen"); - return buildResponse({ - provider, - connectionId, - models: qwenModels.map((m: any) => ({ - id: m.id, - name: m.name || m.id, - owned_by: "qwen", - })), - source: "local_catalog", - // #5460/#5465 — Qwen OAuth has no OAuth-compatible remote /models list; - // the static catalog is intentional, so model-sync should import it. - intentional: true, - }); - } - if (provider === "codex") { // Auto-merge live/GitHub/local (future-proof discovery), then apply explicit // denylist filters (e.g. drop GPT-5.4 family). Do not gate remote-only IDs. diff --git a/src/app/api/providers/[id]/sync-models/degradedLocalCatalog.ts b/src/app/api/providers/[id]/sync-models/degradedLocalCatalog.ts index beff0041e9f..6e521e81ce2 100644 --- a/src/app/api/providers/[id]/sync-models/degradedLocalCatalog.ts +++ b/src/app/api/providers/[id]/sync-models/degradedLocalCatalog.ts @@ -2,7 +2,7 @@ * #5460 (Reka) + #5465 (t3.chat) — Distinguish a genuinely degraded * `local_catalog` models response (remote model discovery failed → the sync * route surfaces a 502) from a provider whose local catalog is its INTENDED and - * only discovery source (reka, qwen-oauth, embedding/rerank providers like + * only discovery source (reka, embedding/rerank providers like * voyage-ai/jina-ai, web-cookie providers like t3-web). * * The models route tags the latter with `intentional: true`. Before this guard, diff --git a/src/app/api/providers/[id]/test/oauthTestConfig.ts b/src/app/api/providers/[id]/test/oauthTestConfig.ts index 0387cdf895f..5792265cd23 100644 --- a/src/app/api/providers/[id]/test/oauthTestConfig.ts +++ b/src/app/api/providers/[id]/test/oauthTestConfig.ts @@ -75,12 +75,6 @@ export const OAUTH_TEST_CONFIG = { authPrefix: "Bearer ", refreshable: true, }, - qwen: { - // DashScope (previously portal.qwen.ai) /v1/models might return 404 or auth issues. - // Use checkExpiry instead — actual connectivity is validated via real requests. - checkExpiry: true, - refreshable: true, - }, cursor: { checkExpiry: true, }, @@ -121,7 +115,7 @@ export const OAUTH_TEST_CONFIG = { // below. Grok Build's cli-chat-proxy endpoint doesn't expose a lightweight // userinfo probe, and it enforces cli-specific headers (see // GrokCliExecutor.buildHeaders) that this shared prober doesn't send — so - // mirror qwen/cline/kilocode's checkExpiry pattern instead of a live probe. + // mirror cline/kilocode's checkExpiry pattern instead of a live probe. // Real connectivity is still validated on every chat/completions request. checkExpiry: true, refreshable: true, @@ -130,8 +124,7 @@ export const OAUTH_TEST_CONFIG = { // GHE Copilot: probe the enterprise user-info endpoint derived from gheUrl // (stored in providerSpecificData). getUrl: (connection: any) => { - const gheUrl = - connection?.providerSpecificData?.gheUrl || connection?.gheUrl || ""; + const gheUrl = connection?.providerSpecificData?.gheUrl || connection?.gheUrl || ""; const base = gheUrl.replace(/\/+$/, ""); return `${base}/api/v3/user`; }, diff --git a/src/app/api/v1/models/catalogProviderMaps.ts b/src/app/api/v1/models/catalogProviderMaps.ts index c9dd6c2c997..6de91e6458a 100644 --- a/src/app/api/v1/models/catalogProviderMaps.ts +++ b/src/app/api/v1/models/catalogProviderMaps.ts @@ -15,7 +15,6 @@ export const FALLBACK_ALIAS_TO_PROVIDER = { kc: "kilocode", kmc: "kimi-coding", kr: "kiro", - qw: "qwen", }; export function buildAliasMaps() { diff --git a/src/domain/types.ts b/src/domain/types.ts index 4c71bf00060..0fad2d26787 100644 --- a/src/domain/types.ts +++ b/src/domain/types.ts @@ -8,7 +8,7 @@ */ /** - * @typedef {'openai'|'claude'|'gemini'|'codex'|'qwen'|'deepseek'|'cohere'|'groq'|'blackbox'|'mistral'|'openrouter'} ProviderId + * @typedef {'openai'|'claude'|'gemini'|'codex'|'deepseek'|'cohere'|'groq'|'blackbox'|'mistral'|'openrouter'} ProviderId */ /** diff --git a/src/lib/arenaEloSync.ts b/src/lib/arenaEloSync.ts index 1e24148716f..75584296b0a 100644 --- a/src/lib/arenaEloSync.ts +++ b/src/lib/arenaEloSync.ts @@ -150,8 +150,8 @@ const MODEL_ALIAS_MAP: Record = { "gpt-5.5": ["openai/gpt-5.5", "gpt-5"], "gemini-3-flash": ["google/gemini-3-flash", "gemini-flash"], "deepseek-r1": ["deepseek/deepseek-r1", "if/deepseek-r1"], - "kimi-k2-thinking": ["moonshot/kimi-k2", "qw/kimi-k2"], - "qwen3-coder-plus": ["qw/qwen3-coder-plus", "alibaba/qwen3-coder"], + "kimi-k2-thinking": ["moonshot/kimi-k2"], + "qwen3-coder-plus": ["alibaba/qwen3-coder"], "llama-4": ["meta/llama-4", "llama4"], }; diff --git a/src/lib/copilot/engine.ts b/src/lib/copilot/engine.ts index 17d7122702f..2f2366cfe64 100644 --- a/src/lib/copilot/engine.ts +++ b/src/lib/copilot/engine.ts @@ -86,7 +86,7 @@ Use \`createCombo\` tool or \`runOmniRouteCli\` to create them.`; return `## Providers (212+) OmniRoute supports 212+ providers across categories: -- **Free**: Qoder AI, Qwen Code, Kiro AI +- **Free**: Qoder AI, Kiro AI - **OAuth** (14): Claude Code, Antigravity, Codex, GitHub Copilot, Cursor, Kimi Coding, Windsurf, etc. - **API Key** (120+): OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, etc. - **Self-Hosted** (8+): LM Studio, vLLM, Ollama, Triton, etc. diff --git a/src/lib/copilot/systemPrompt.ts b/src/lib/copilot/systemPrompt.ts index 99a8c4f18f7..442e7793943 100644 --- a/src/lib/copilot/systemPrompt.ts +++ b/src/lib/copilot/systemPrompt.ts @@ -80,7 +80,7 @@ Client → API Route (/v1/chat/completions) ### 3.1 Providers (212+) Registered in src/shared/constants/providers.ts. Categories: -- **Free** (3): Qoder AI, Qwen Code, Kiro AI +- **Free** (2): Qoder AI, Kiro AI - **OAuth** (14): Claude Code, Antigravity, Codex, GitHub Copilot, Cursor, etc. - **API Key** (120+): OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, etc. - **Self-Hosted** (8+): LM Studio, vLLM, Ollama, etc. diff --git a/src/lib/db/migrations/129_remove_unregistered_qwen_data.sql b/src/lib/db/migrations/129_remove_unregistered_qwen_data.sql new file mode 100644 index 00000000000..f32f694b6f0 --- /dev/null +++ b/src/lib/db/migrations/129_remove_unregistered_qwen_data.sql @@ -0,0 +1,8 @@ +-- Remove saved state for a provider ID that is no longer registered. + +DELETE FROM provider_connections +WHERE provider = 'qwen'; + +DELETE FROM key_value +WHERE namespace IN ('cliToolLastConfig', 'cliToolInitialConfig') + AND key = 'qwen'; diff --git a/src/lib/oauth/constants/oauth.ts b/src/lib/oauth/constants/oauth.ts index 4db9ef2f60c..da064378272 100644 --- a/src/lib/oauth/constants/oauth.ts +++ b/src/lib/oauth/constants/oauth.ts @@ -71,17 +71,6 @@ export const CODEX_CONFIG = { }, }; -// Qwen OAuth Configuration (Device Code Flow with PKCE) -export const QWEN_CONFIG = { - clientId: resolvePublicCred("qwen_id", "QWEN_OAUTH_CLIENT_ID"), - // Host is chat.qwen.ai — the bare qwen.ai host 404s on these paths (verified: the qwen-code - // device flow lives at chat.qwen.ai, returning a valid device_code; qwen.ai returns Not Found). - deviceCodeUrl: "https://chat.qwen.ai/api/v1/oauth2/device/code", - tokenUrl: "https://chat.qwen.ai/api/v1/oauth2/token", - scope: "openid profile email model.completion", - codeChallengeMethod: "S256", -}; - // Qoder OAuth Configuration (Authorization Code) const QODER_OAUTH_AUTHORIZE_URL = process.env.QODER_OAUTH_AUTHORIZE_URL || ""; const QODER_OAUTH_TOKEN_URL = process.env.QODER_OAUTH_TOKEN_URL || ""; @@ -504,7 +493,6 @@ export const PROVIDERS = { CLAUDE: "claude", CODEX: "codex", GEMINI: "gemini", - QWEN: "qwen", QODER: "qoder", ANTIGRAVITY: "antigravity", AGY: "agy", diff --git a/src/lib/oauth/providers/index.ts b/src/lib/oauth/providers/index.ts index 801f78a4248..2ec8a86994d 100644 --- a/src/lib/oauth/providers/index.ts +++ b/src/lib/oauth/providers/index.ts @@ -15,7 +15,6 @@ import { codex } from "./codex"; import { antigravity } from "./antigravity"; import { agy } from "./agy"; import { qoder } from "./qoder"; -import { qwen } from "./qwen"; import { kimiCoding } from "./kimi-coding"; import { github } from "./github"; import { gheCopilot } from "./ghe-copilot"; @@ -38,7 +37,6 @@ export const PROVIDERS = { antigravity, agy, qoder, - qwen, "kimi-coding": kimiCoding, github, "ghe-copilot": gheCopilot, diff --git a/src/lib/oauth/providers/qwen.ts b/src/lib/oauth/providers/qwen.ts deleted file mode 100644 index 8a3503bffe3..00000000000 --- a/src/lib/oauth/providers/qwen.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { QWEN_CONFIG } from "../constants/oauth"; -import { decodeJwt } from "jose"; - -export const qwen = { - config: QWEN_CONFIG, - flowType: "device_code", - requestDeviceCode: async (config, codeChallenge) => { - const response = await fetch(config.deviceCodeUrl, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json", - }, - body: new URLSearchParams({ - client_id: config.clientId, - scope: config.scope, - code_challenge: codeChallenge, - code_challenge_method: config.codeChallengeMethod, - }), - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`Device code request failed: ${error}`); - } - - return await response.json(); - }, - pollToken: async (config, deviceCode, codeVerifier) => { - const response = await fetch(config.tokenUrl, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json", - }, - body: new URLSearchParams({ - grant_type: "urn:ietf:params:oauth:grant-type:device_code", - client_id: config.clientId, - device_code: deviceCode, - code_verifier: codeVerifier, - }), - }); - - return { - ok: response.ok, - data: await response.json(), - }; - }, - mapTokens: (tokens) => { - let email = null; - let displayName = null; - if (tokens.id_token) { - try { - const decoded = decodeJwt(tokens.id_token); - email = decoded.email || decoded.preferred_username || null; - displayName = decoded.name || email; - } catch (e) { - // Ignore - } - } - - if (!email && tokens.access_token) { - try { - const decodedToken = decodeJwt(tokens.access_token); - email = decodedToken.email || decodedToken.preferred_username || decodedToken.sub || null; - displayName = decodedToken.name || email; - } catch (e) { - // Ignore - } - } - - return { - accessToken: tokens.access_token, - refreshToken: tokens.refresh_token, - expiresIn: tokens.expires_in, - idToken: tokens.id_token, - email, - displayName, - providerSpecificData: { resourceUrl: tokens.resource_url }, - }; - }, -}; diff --git a/src/lib/oauth/utils/cliProxyAuthImport.ts b/src/lib/oauth/utils/cliProxyAuthImport.ts index 6ebc2eaddd2..9d416924981 100644 --- a/src/lib/oauth/utils/cliProxyAuthImport.ts +++ b/src/lib/oauth/utils/cliProxyAuthImport.ts @@ -24,7 +24,6 @@ export const CLIPROXY_TYPE_TO_PROVIDER: Record = { claude: "claude", codex: "codex", antigravity: "antigravity", - qwen: "qwen", kimi: "kimi", }; diff --git a/src/lib/usage/fetcher.ts b/src/lib/usage/fetcher.ts index 8a427d381b5..cdac5e81954 100644 --- a/src/lib/usage/fetcher.ts +++ b/src/lib/usage/fetcher.ts @@ -45,8 +45,6 @@ export async function getUsageForProvider(connection) { return await getClaudeUsage(accessToken); case "codex": return await getCodexUsage(accessToken, providerSpecificData); - case "qwen": - return await getQwenUsage(accessToken, providerSpecificData); case "qoder": return await getQoderUsage(accessToken); case "kiro": @@ -402,23 +400,6 @@ async function getCodexUsage(accessToken, providerSpecificData: Record = { claude: 90000, // Claude can be slower for long outputs gemini: 60000, codex: 120000, // Coding tasks often take longer - qwen: 45000, deepseek: 60000, cohere: 45000, groq: 30000, // Groq is fast diff --git a/src/sse/services/tokenRefresh.ts b/src/sse/services/tokenRefresh.ts index 05585ae3018..99f5c533794 100755 --- a/src/sse/services/tokenRefresh.ts +++ b/src/sse/services/tokenRefresh.ts @@ -11,7 +11,6 @@ import { refreshAccessToken as _refreshAccessToken, refreshClaudeOAuthToken as _refreshClaudeOAuthToken, refreshGoogleToken as _refreshGoogleToken, - refreshQwenToken as _refreshQwenToken, refreshCodexToken as _refreshCodexToken, refreshQoderToken as _refreshQoderToken, refreshGitHubToken as _refreshGitHubToken, @@ -68,11 +67,6 @@ export const refreshGoogleToken = async ( return _refreshGoogleToken(refreshToken, clientId, clientSecret, log, proxy); }; -export const refreshQwenToken = async (refreshToken: string, credentials?: any) => { - const proxy = await resolveProxyForCredentials("qwen", credentials); - return _refreshQwenToken(refreshToken, log, proxy); -}; - export const refreshCodexToken = async (refreshToken: string, credentials?: any) => { const proxy = await resolveProxyForCredentials("codex", credentials); return _refreshCodexToken(refreshToken, log, proxy); diff --git a/tests/snapshots/provider/translate-path.json b/tests/snapshots/provider/translate-path.json index b4436f0596f..fac56957d53 100644 --- a/tests/snapshots/provider/translate-path.json +++ b/tests/snapshots/provider/translate-path.json @@ -3903,71 +3903,6 @@ "stream": "https://api.qoder.com/v1/chat/completions" } }, - "qwen": { - "format": "openai", - "headers": { - "apiKey": { - "Accept": "text/event-stream", - "Accept-Language": "*", - "Authorization": "Bearer ", - "Connection": "keep-alive", - "Content-Type": "application/json", - "Sec-Fetch-Mode": "cors", - "User-Agent": "QwenCode/0.19.3 (; )", - "X-Dashscope-AuthType": "qwen-oauth", - "X-Dashscope-CacheControl": "enable", - "X-Dashscope-UserAgent": "QwenCode/0.19.3 (; )", - "X-Stainless-Arch": "", - "X-Stainless-Lang": "js", - "X-Stainless-Os": "", - "X-Stainless-Package-Version": "5.11.0", - "X-Stainless-Retry-Count": "1", - "X-Stainless-Runtime": "node", - "X-Stainless-Runtime-Version": "" - }, - "nonStream": { - "Accept-Language": "*", - "Authorization": "Bearer ", - "Connection": "keep-alive", - "Content-Type": "application/json", - "Sec-Fetch-Mode": "cors", - "User-Agent": "QwenCode/0.19.3 (; )", - "X-Dashscope-AuthType": "qwen-oauth", - "X-Dashscope-CacheControl": "enable", - "X-Dashscope-UserAgent": "QwenCode/0.19.3 (; )", - "X-Stainless-Arch": "", - "X-Stainless-Lang": "js", - "X-Stainless-Os": "", - "X-Stainless-Package-Version": "5.11.0", - "X-Stainless-Retry-Count": "1", - "X-Stainless-Runtime": "node", - "X-Stainless-Runtime-Version": "" - }, - "oauth": { - "Accept": "text/event-stream", - "Accept-Language": "*", - "Authorization": "Bearer ", - "Connection": "keep-alive", - "Content-Type": "application/json", - "Sec-Fetch-Mode": "cors", - "User-Agent": "QwenCode/0.19.3 (; )", - "X-Dashscope-AuthType": "qwen-oauth", - "X-Dashscope-CacheControl": "enable", - "X-Dashscope-UserAgent": "QwenCode/0.19.3 (; )", - "X-Stainless-Arch": "", - "X-Stainless-Lang": "js", - "X-Stainless-Os": "", - "X-Stainless-Package-Version": "5.11.0", - "X-Stainless-Retry-Count": "1", - "X-Stainless-Runtime": "node", - "X-Stainless-Runtime-Version": "" - } - }, - "url": { - "nonStream": "https://chat.qwen.ai/api/v1/services/aigc/text-generation/generation", - "stream": "https://chat.qwen.ai/api/v1/services/aigc/text-generation/generation" - } - }, "qwen-web": { "format": "openai", "headers": { diff --git a/tests/unit/cache-control-claude-providers.test.ts b/tests/unit/cache-control-claude-providers.test.ts index d868c796b39..ed50593e33c 100644 --- a/tests/unit/cache-control-claude-providers.test.ts +++ b/tests/unit/cache-control-claude-providers.test.ts @@ -11,7 +11,6 @@ describe("Cache Control Policy - Claude Protocol Providers", () => { assert.equal(providerSupportsCaching("claude", "claude"), true); assert.equal(providerSupportsCaching("anthropic", "claude"), true); assert.equal(providerSupportsCaching("zai", "claude"), true); - assert.equal(providerSupportsCaching("qwen", "openai"), true); assert.equal(providerSupportsCaching("deepseek", "openai"), true); // Claude-protocol providers NOT in CACHING_PROVIDERS set diff --git a/tests/unit/cache-control-policy.test.ts b/tests/unit/cache-control-policy.test.ts index 0f67de43b56..558e768f288 100644 --- a/tests/unit/cache-control-policy.test.ts +++ b/tests/unit/cache-control-policy.test.ts @@ -37,7 +37,6 @@ describe("Cache Control Policy", () => { assert.equal(providerSupportsCaching("claude"), true); assert.equal(providerSupportsCaching("anthropic"), true); assert.equal(providerSupportsCaching("zai"), true); - assert.equal(providerSupportsCaching("qwen"), true); assert.equal(providerSupportsCaching("deepseek"), true); // #3088 — Xiaomi MiMo supports prompt caching; cache_control breakpoints // sent by Claude Code (via cc-switch) must be preserved, not stripped. diff --git a/tests/unit/catalog-updates-v3x.test.ts b/tests/unit/catalog-updates-v3x.test.ts index 0f3bbc80ee4..9e3c3558fed 100644 --- a/tests/unit/catalog-updates-v3x.test.ts +++ b/tests/unit/catalog-updates-v3x.test.ts @@ -59,7 +59,11 @@ test("Fable 5 catalog exposes claude-fable-5 in cc — but NOT via Kiro (fabrica assert.ok(ccPricing["claude-fable-5"], "cc pricing must include claude-fable-5"); const kiroIds = new Set(getModelsByProviderId("kiro").map((m) => m.id)); - assert.equal(kiroIds.has("claude-fable-5"), false, "kiro must NOT expose claude-fable-5 (fabricated)"); + assert.equal( + kiroIds.has("claude-fable-5"), + false, + "kiro must NOT expose claude-fable-5 (fabricated)" + ); }); test("Sonnet 5 catalog exposes claude-sonnet-5 across cc/kiro/anthropic/blackbox with Sonnet-tier pricing", () => { @@ -91,9 +95,21 @@ test("Kiro catalog does NOT expose Claude Opus (fabricated — Kiro upstream has // 400 "Invalid model. Please select a different model". #6170 removed them. const ids = new Set(getModelsByProviderId("kiro").map((model) => model.id)); - assert.equal(ids.has("claude-opus-4.8"), false, "kiro must NOT expose claude-opus-4.8 (fabricated)"); - assert.equal(ids.has("claude-opus-4.7"), false, "kiro must NOT expose claude-opus-4.7 (fabricated)"); - assert.equal(ids.has("claude-opus-4.6"), false, "kiro must NOT expose claude-opus-4.6 (fabricated)"); + assert.equal( + ids.has("claude-opus-4.8"), + false, + "kiro must NOT expose claude-opus-4.8 (fabricated)" + ); + assert.equal( + ids.has("claude-opus-4.7"), + false, + "kiro must NOT expose claude-opus-4.7 (fabricated)" + ); + assert.equal( + ids.has("claude-opus-4.6"), + false, + "kiro must NOT expose claude-opus-4.6 (fabricated)" + ); }); test("Every Kiro registry model resolves a non-zero pricing row (no $0.00 usage)", async () => { @@ -164,33 +180,3 @@ test("Every Codex registry model resolves a non-zero pricing row (alias: cx)", a ); } }); - -test("Every Qwen registry model resolves a non-zero pricing row (alias: qw)", async () => { - const { getPricingForModel } = await import("../../src/shared/constants/pricing.ts"); - const models = getModelsByProviderId("qwen"); - assert.ok(models.length > 0, "qwen must expose models"); - - for (const model of models) { - // Qwen pricing lives under the "qw" alias (its DEFAULT_PRICING key). - const pricing = getPricingForModel("qw", model.id) as { - input?: number; - output?: number; - } | null; - assert.ok(pricing, `qw pricing must include qwen model "${model.id}"`); - assert.equal( - typeof pricing?.input === "number" && typeof pricing?.output === "number", - true, - `qw pricing for "${model.id}" must have numeric input/output` - ); - } - - // Regression guard: the "coder-model" id (Qwen3.5/3.6 Coder Model, ported from - // upstream 9router PR #156) must be priced like the other Qwen coder tier. - const coderModel = getPricingForModel("qw", "coder-model") as { - input: number; - output: number; - } | null; - assert.ok(coderModel, "qw pricing must include coder-model"); - assert.equal(typeof coderModel?.input, "number"); - assert.equal(typeof coderModel?.output, "number"); -}); diff --git a/tests/unit/chatcore-translation-paths.test.ts b/tests/unit/chatcore-translation-paths.test.ts index d59df9815c1..eb12a8c8f09 100644 --- a/tests/unit/chatcore-translation-paths.test.ts +++ b/tests/unit/chatcore-translation-paths.test.ts @@ -1788,110 +1788,6 @@ test("chatCore redirects background utility tasks to a cheaper mapped model", as assert.equal(call.body.model, "gpt-5-mini"); }); -test("chatCore retries Qwen quota 429 responses before succeeding", async () => { - const originalSetTimeout = globalThis.setTimeout; - try { - (globalThis as any).setTimeout = (callback: any, ms: any, ...args: any[]) => { - // Only make Qwen retry delays (≤5s) synchronous; let longer timeouts (e.g. body read) use real setTimeout - if (typeof ms === "number" && ms > 5000) { - return originalSetTimeout(callback, ms, ...args); - } - callback(...args); - return 0 as any; - }; - - const { calls, result } = await invokeChatCore({ - provider: "qwen", - model: "qwen3-coder", - body: { - model: "qwen3-coder", - stream: false, - messages: [{ role: "user", content: "retry the quota hit" }], - }, - responseFactory(_captured, seenCalls) { - if (seenCalls.length === 1) { - return new Response( - JSON.stringify({ error: { message: "You exceeded your current quota for Qwen." } }), - { - status: 429, - headers: { "Content-Type": "application/json" }, - } - ); - } - return buildOpenAIResponse(false, "qwen recovered"); - }, - }); - - const payload = (await result.response.json()) as any; - assert.equal(result.success, true); - assert.equal(calls.length, 2); - assert.equal(payload.choices[0].message.content, "qwen recovered"); - } finally { - globalThis.setTimeout = originalSetTimeout; - } -}); - -test("chatCore injects fallback user for Qwen OAuth requests without user", async () => { - const { call, result } = await invokeChatCore({ - provider: "qwen", - model: "qwen3-coder", - credentials: { - accessToken: "qwen-oauth-token", - providerSpecificData: { resourceUrl: "portal.qwen.ai" }, - }, - body: { - model: "qwen3-coder", - stream: false, - messages: [{ role: "user", content: "check qwen user fallback" }], - }, - responseFormat: "openai", - }); - - assert.equal(result.success, true); - assert.equal(call.body.user, "omniroute-qwen-oauth"); -}); - -test("chatCore keeps explicit user for Qwen OAuth requests", async () => { - const { call, result } = await invokeChatCore({ - provider: "qwen", - model: "qwen3-coder", - credentials: { - accessToken: "qwen-oauth-token", - providerSpecificData: { resourceUrl: "portal.qwen.ai" }, - }, - body: { - model: "qwen3-coder", - stream: false, - user: "explicit-user", - messages: [{ role: "user", content: "keep my user" }], - }, - responseFormat: "openai", - }); - - assert.equal(result.success, true); - assert.equal(call.body.user, "explicit-user"); -}); - -test("chatCore does not inject fallback user for Qwen API key requests", async () => { - const { call, result } = await invokeChatCore({ - provider: "qwen", - model: "qwen3-coder", - credentials: { - apiKey: "qwen-api-key", - providerSpecificData: { resourceUrl: "dashscope.aliyuncs.com/compatible-mode/v1" }, - }, - body: { - model: "qwen3-coder", - stream: false, - messages: [{ role: "user", content: "api key mode should stay untouched" }], - }, - responseFormat: "openai", - }); - - assert.equal(result.success, true); - assert.equal("user" in call.body, false); -}); - test("chatCore preserves Codex dual-window scope cooldowns on 429 responses", async () => { const connection = await providersDb.createProviderConnection({ provider: "codex", diff --git a/tests/unit/chatcore-upstream-body.test.ts b/tests/unit/chatcore-upstream-body.test.ts index 7d39ee1c4d2..1348300cba1 100644 --- a/tests/unit/chatcore-upstream-body.test.ts +++ b/tests/unit/chatcore-upstream-body.test.ts @@ -1,8 +1,8 @@ // tests/unit/chatcore-upstream-body.test.ts // Characterization of prepareUpstreamBody — the first internal sub-slice of executeProviderRequest // (chatCore god-file decomposition, #3501). Uses a fresh temp DB (no payload rules / no detected -// tool limits → defaults). Locks: target-model pinning, the Qwen OAuth user backfill (and its -// guards), and the prompt_cache_key gating (excluded providers + non-OPENAI format never inject). +// tool limits → defaults). Locks: target-model pinning and the prompt_cache_key gating +// (excluded providers + non-OPENAI format never inject). import { test, before, after } from "node:test"; import assert from "node:assert/strict"; import fs from "node:fs"; @@ -232,39 +232,6 @@ test("preserves the full tool list when within the grok-cli limit", async () => assert.equal(out.tools.length, 150); }); -test("backfills the Qwen OAuth user when missing", async () => { - const out = await prepareUpstreamBody({ - translatedBody: { model: "qwen-max", messages: [] }, - modelToCall: "qwen-max", - provider: "qwen", - targetFormat: "claude", - credentials: { accessToken: "tok-123" }, - }); - assert.equal(out.user, "omniroute-qwen-oauth"); -}); - -test("does not backfill the Qwen user when an apiKey is present (API-key mode)", async () => { - const out = await prepareUpstreamBody({ - translatedBody: { model: "qwen-max", messages: [] }, - modelToCall: "qwen-max", - provider: "qwen", - targetFormat: "claude", - credentials: { apiKey: "k", accessToken: "tok-123" }, - }); - assert.equal(out.user, undefined); -}); - -test("does not backfill the Qwen user when one is already set", async () => { - const out = await prepareUpstreamBody({ - translatedBody: { model: "qwen-max", messages: [], user: "real-user" }, - modelToCall: "qwen-max", - provider: "qwen", - targetFormat: "claude", - credentials: { accessToken: "tok-123" }, - }); - assert.equal(out.user, "real-user"); -}); - test("never injects prompt_cache_key for an excluded provider (codex)", async () => { const out = await prepareUpstreamBody({ translatedBody: { model: "gpt-5-codex", messages: [{ role: "user", content: "hi" }] }, diff --git a/tests/unit/check-public-creds.test.ts b/tests/unit/check-public-creds.test.ts index 2e3acbbcce2..8d25f7dec30 100644 --- a/tests/unit/check-public-creds.test.ts +++ b/tests/unit/check-public-creds.test.ts @@ -46,7 +46,7 @@ test('does NOT flag empty-string fallback (process.env || "")', () => { }); test("does NOT flag an *Env key — it carries the env-var NAME, not the secret", () => { - const src = `clientIdEnv: "QWEN_OAUTH_CLIENT_ID",`; + const src = `clientIdEnv: "EXAMPLE_OAUTH_CLIENT_ID",`; assert.deepEqual(findLiteralCreds(src, new Set(), "x.ts"), []); }); @@ -114,8 +114,8 @@ test("every frozen literal is actually present in a scanned file (no dead allowl }); test("with an empty allowlist the real scanned files surface zero violations (all migrated to resolvePublicCred)", () => { - // All five public client_ids (9 call-sites) were migrated to resolvePublicCred() in - // #3493, so neither anchor file has literal credentials anymore. + // Public client_ids were migrated to resolvePublicCred() in #3493, so neither + // anchor file has literal credentials anymore. const reg = fs.readFileSync( path.join(repoRoot, "open-sse/config/providerRegistry.ts"), "utf8" diff --git a/tests/unit/claude-oauth-provider.test.ts b/tests/unit/claude-oauth-provider.test.ts index 8879bf20e1f..dfcf6212d5d 100644 --- a/tests/unit/claude-oauth-provider.test.ts +++ b/tests/unit/claude-oauth-provider.test.ts @@ -8,7 +8,6 @@ import assert from "node:assert/strict"; Object.assign(process.env, { CLAUDE_OAUTH_CLIENT_ID: "9d1c250a-e61b-44d9-88ed-5944d1962f5e", CODEX_OAUTH_CLIENT_ID: "app_EMoamEEZ73f0CkXaXp7hrann", - QWEN_OAUTH_CLIENT_ID: "f0304373b74a44d2b584a3fb70ca9e56", KIMI_CODING_OAUTH_CLIENT_ID: "17e5f671-d194-4dfb-9706-5516cb48c098", GITHUB_OAUTH_CLIENT_ID: "Iv1.b507a08c87ecfe98", }); diff --git a/tests/unit/cliproxy-auth-import-1934.test.ts b/tests/unit/cliproxy-auth-import-1934.test.ts index 9bd32c845a0..2f664c9330f 100644 --- a/tests/unit/cliproxy-auth-import-1934.test.ts +++ b/tests/unit/cliproxy-auth-import-1934.test.ts @@ -55,10 +55,10 @@ test("parseCliProxyAuthRecord returns null for unknown type or missing access to }); test("every CLIPROXY_TYPE_TO_PROVIDER target is a real OAuth provider id", () => { - // codex/antigravity/claude/qwen/kimi are all OmniRoute providers + // codex/antigravity/claude/kimi are all OmniRoute providers for (const provider of Object.values(CLIPROXY_TYPE_TO_PROVIDER)) { assert.ok( - ["claude", "codex", "antigravity", "qwen", "kimi"].includes(provider), + ["claude", "codex", "antigravity", "kimi"].includes(provider), `unexpected provider mapping: ${provider}` ); } diff --git a/tests/unit/codex-failover.test.ts b/tests/unit/codex-failover.test.ts index 0546ab9980e..9de478411e6 100644 --- a/tests/unit/codex-failover.test.ts +++ b/tests/unit/codex-failover.test.ts @@ -60,11 +60,9 @@ test("extractSessionAffinityKey returns null for empty input", () => { // ── Test 2: maxAttempts logic — codex gets 3, other providers get 1 ────────── test("codex maxAttempts is 3 (validates retry budget for account rotation)", () => { // This mirrors the logic: provider === "codex" ? 3 : 1 - const getMaxAttempts = (provider: string) => - provider === "qwen" ? 3 : provider === "codex" ? 3 : 1; + const getMaxAttempts = (provider: string) => (provider === "codex" ? 3 : 1); assert.equal(getMaxAttempts("codex"), 3, "Codex should have 3 max attempts"); - assert.equal(getMaxAttempts("qwen"), 3, "Qwen should have 3 max attempts (unchanged)"); assert.equal(getMaxAttempts("openai"), 1, "OpenAI should have 1 max attempt (unchanged)"); assert.equal(getMaxAttempts("anthropic"), 1, "Anthropic should have 1 max attempt (unchanged)"); assert.equal(getMaxAttempts("gemini"), 1, "Gemini should have 1 max attempt (unchanged)"); diff --git a/tests/unit/compression/cachingAware.test.ts b/tests/unit/compression/cachingAware.test.ts index cf854f35222..4e69580f5be 100644 --- a/tests/unit/compression/cachingAware.test.ts +++ b/tests/unit/compression/cachingAware.test.ts @@ -83,7 +83,7 @@ describe("detectCachingContext", () => { { tools: [{ name: "lookup", cache_control: { type: "ephemeral" } }], }, - { provider: "qwen", targetFormat: "claude" } + { provider: "bailian-coding-plan", targetFormat: "claude" } ); assert.equal(ctx.hasCacheControl, true); diff --git a/tests/unit/executor-default-base.test.ts b/tests/unit/executor-default-base.test.ts index 7382e932ddc..f73f66be582 100644 --- a/tests/unit/executor-default-base.test.ts +++ b/tests/unit/executor-default-base.test.ts @@ -74,10 +74,9 @@ test("BaseExecutor: legacy openai-compatible providers honor providerSpecificDat assert.equal(url, "https://proxy.example/v1/responses"); }); -test("DefaultExecutor.buildUrl handles Gemini, Claude and Qwen variants", () => { +test("DefaultExecutor.buildUrl handles Gemini and Claude variants", () => { const gemini = new DefaultExecutor("gemini"); const claude = new DefaultExecutor("claude"); - const qwen = new DefaultExecutor("qwen"); assert.equal( gemini.buildUrl("gemini-2.5-flash", false), @@ -88,13 +87,6 @@ test("DefaultExecutor.buildUrl handles Gemini, Claude and Qwen variants", () => "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:streamGenerateContent?alt=sse" ); assert.equal(claude.buildUrl("claude-sonnet-4", true), `${PROVIDERS.claude.baseUrl}?beta=true`); - assert.equal(qwen.buildUrl("qwen3-coder", true), "https://portal.qwen.ai/v1/chat/completions"); - assert.equal( - qwen.buildUrl("qwen3-coder", true, 0, { - providerSpecificData: { resourceUrl: "custom.qwen.ai" }, - }), - "https://custom.qwen.ai/v1/chat/completions" - ); }); test("DefaultExecutor.buildUrl uses full chat endpoints for hosted OpenAI-compatible providers", () => { @@ -530,22 +522,6 @@ test("DefaultExecutor.buildHeaders handles Snowflake PATs and GigaChat access to assert.equal(gigachatHeaders.Authorization, "Bearer gigachat-token"); }); -test("DefaultExecutor.buildHeaders strips DashScope headers for Qwen API keys and preserves them for OAuth", () => { - const executor = new DefaultExecutor("qwen"); - - const apiKeyHeaders = executor.buildHeaders({ apiKey: "dash-key" }, true); - const oauthHeaders = executor.buildHeaders({ accessToken: "oauth-token" }, true); - - assert.equal(apiKeyHeaders.Authorization, "Bearer dash-key"); - assert.equal( - Object.keys(apiKeyHeaders).some((key) => key.toLowerCase().startsWith("x-dashscope-")), - false - ); - assert.equal(oauthHeaders.Authorization, "Bearer oauth-token"); - assert.equal(oauthHeaders["X-Dashscope-AuthType"], "qwen-oauth"); - assert.equal(oauthHeaders["X-Dashscope-CacheControl"], "enable"); -}); - test("DefaultExecutor.buildHeaders rotates extra API keys and builds Claude Code compatible headers", () => { const openai = new DefaultExecutor("openai"); const cc = new DefaultExecutor("anthropic-compatible-cc-test"); @@ -1004,19 +980,6 @@ test("DefaultExecutor.transformRequest strips stream_options from Anthropic-comp assert.equal((ccResult as any).stream_options, undefined); }); -test("DefaultExecutor.transformRequest neutralizes incompatible tool_choice for Qwen thinking", () => { - const executor = new DefaultExecutor("qwen"); - const body = { - messages: [{ role: "user", content: "hi" }], - thinking: { type: "enabled" }, - tool_choice: { type: "function", function: { name: "pwd" } }, - }; - const result = executor.transformRequest("qwen3-coder-plus", body, true, {}); - - assert.notEqual(result, body); - assert.equal((result as any).tool_choice, "auto"); -}); - // Port of decolua/9router#1343: openai-compatible-* providers (DeepSeek / Ollama / // local OpenAI-compatible models) often lack native Structured Output, so a // `json_schema` response_format is downgraded to `json_object` with the schema diff --git a/tests/unit/free-model-catalog.test.ts b/tests/unit/free-model-catalog.test.ts index 96e11e3f3d1..07b4da72bdf 100644 --- a/tests/unit/free-model-catalog.test.ts +++ b/tests/unit/free-model-catalog.test.ts @@ -85,7 +85,7 @@ test("2026-06-17 refresh: discontinued providers dropped, new free providers add for (const dead of ["chutes", "phind", "kluster", "gitlawb", "aimlapi", "theoldllm"]) { assert.ok(!providers.has(dead), `${dead} should be removed (discontinued)`); } - // qwen-web is KEPT on purpose: only its OAuth API tier died — OmniRoute uses the cookie/web path. + // qwen-web is kept because it uses its own cookie/web path. assert.ok(providers.has("qwen-web"), "qwen-web must stay (cookie path still free)"); // discovered in the refresh — must be present for (const fresh of ["kilo-gateway", "opencode-zen", "glm-cn"]) { diff --git a/tests/unit/oauth-device-code-endpoints.test.ts b/tests/unit/oauth-device-code-endpoints.test.ts index 325a1b259d9..13d15e5c89c 100644 --- a/tests/unit/oauth-device-code-endpoints.test.ts +++ b/tests/unit/oauth-device-code-endpoints.test.ts @@ -4,11 +4,8 @@ import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, resolve } from "node:path"; -// Regression guards for the two device-code providers that returned "ошибка сервера OmniRoute": +// Regression guard for the device-code provider that returned "ошибка сервера OmniRoute": // -// qwen — QWEN_CONFIG pointed at the bare host qwen.ai, whose device/token paths 404. -// The working qwen-code device flow lives at chat.qwen.ai (verified live: 200 + -// a valid device_code). Guard: the config uses chat.qwen.ai, never bare qwen.ai. // codebuddy-cn— the Tencent state endpoint reads `platform` from the QUERY string, not the JSON // body; body-only returned 400 "platform is empty" (verified live). The fix passes // it as a query param. Guard: requestDeviceCode builds the URL with ?platform=. @@ -18,15 +15,11 @@ import { dirname, resolve } from "node:path"; const here = dirname(fileURLToPath(import.meta.url)); const read = (p: string) => readFileSync(resolve(here, "../..", p), "utf8"); -test("qwen device-code config uses chat.qwen.ai (not the 404ing bare qwen.ai)", () => { - const oauth = read("src/lib/oauth/constants/oauth.ts"); - const qwenBlock = oauth.slice(oauth.indexOf("QWEN_CONFIG"), oauth.indexOf("QWEN_CONFIG") + 700); - assert.match(qwenBlock, /chat\.qwen\.ai\/api\/v1\/oauth2\/device\/code/, "deviceCodeUrl host"); - assert.match(qwenBlock, /chat\.qwen\.ai\/api\/v1\/oauth2\/token/, "tokenUrl host"); - assert.doesNotMatch(qwenBlock, /"https:\/\/qwen\.ai\//, "must not use the bare qwen.ai host"); -}); - test("codebuddy-cn device-code sends platform as a query param (not body-only)", () => { const cb = read("src/lib/oauth/providers/codebuddy-cn.ts"); - assert.match(cb, /\?platform=\$\{encodeURIComponent\(config\.platform\)\}/, "platform query param"); + assert.match( + cb, + /\?platform=\$\{encodeURIComponent\(config\.platform\)\}/, + "platform query param" + ); }); diff --git a/tests/unit/oauth-providers-config.test.ts b/tests/unit/oauth-providers-config.test.ts index 2208c22b772..9f83711529c 100644 --- a/tests/unit/oauth-providers-config.test.ts +++ b/tests/unit/oauth-providers-config.test.ts @@ -9,7 +9,6 @@ Object.assign(process.env, { CLAUDE_OAUTH_CLIENT_ID: "9d1c250a-e61b-44d9-88ed-5944d1962f5e", CODEX_OAUTH_CLIENT_ID: "app_EMoamEEZ73f0CkXaXp7hrann", GITLAB_DUO_OAUTH_CLIENT_ID: "gitlab-duo-client-id", - QWEN_OAUTH_CLIENT_ID: "f0304373b74a44d2b584a3fb70ca9e56", KIMI_CODING_OAUTH_CLIENT_ID: "17e5f671-d194-4dfb-9706-5516cb48c098", KIMI_CODING_DEVICE_ID: "test-kimi-device-id", GITHUB_OAUTH_CLIENT_ID: "Iv1.b507a08c87ecfe98", @@ -41,7 +40,6 @@ const { OAUTH_TIMEOUT, PROVIDERS: OAUTH_PROVIDER_IDS, QODER_CONFIG, - QWEN_CONFIG, TRAE_CONFIG, WINDSURF_CONFIG, XAI_OAUTH_CONFIG, @@ -57,7 +55,6 @@ const EXPECTED_PROVIDER_KEYS = [ "antigravity", "agy", "qoder", - "qwen", "kimi-coding", "github", "ghe-copilot", @@ -91,7 +88,6 @@ const EXPECTED_CONFIG_BY_PROVIDER = { antigravity: ANTIGRAVITY_CONFIG, agy: AGY_CONFIG, qoder: QODER_CONFIG, - qwen: QWEN_CONFIG, "kimi-coding": KIMI_CODING_CONFIG, github: GITHUB_CONFIG, "ghe-copilot": GHE_COPILOT_CONFIG, @@ -129,7 +125,6 @@ const REQUIRED_FIELDS_BY_PROVIDER = { antigravity: ["authorizeUrl", "tokenUrl", "userInfoUrl", "scopes", "clientId"], agy: ["authorizeUrl", "tokenUrl", "userInfoUrl", "scopes", "clientId"], qoder: ["extraParams"], - qwen: ["deviceCodeUrl", "tokenUrl", "scope", "clientId"], "kimi-coding": ["deviceCodeUrl", "tokenUrl", "clientId"], github: ["deviceCodeUrl", "tokenUrl", "userInfoUrl", "copilotTokenUrl", "clientId"], // GHE Copilot derives its URLs at runtime from the per-connection gheUrl — only static fields. @@ -315,20 +310,6 @@ test("all provider endpoint URLs use HTTPS when a URL is configured", () => { } }); -test("Qwen OAuth uses chat.qwen.ai (not bare qwen.ai) for device/token URLs — #7517 supersedes #683/#572", () => { - // Upstream PR #683 / decolua issue #572 originally pointed these at the bare `qwen.ai` host. - // #7517 (2026-07-18) live-verified that host 404s on both paths ("ошибка сервера OmniRoute" in - // the companion extension) and that the working qwen-code device flow lives at `chat.qwen.ai` - // (verified: 200 + a valid device_code/user_code). See tests/unit/oauth-device-code-endpoints.test.ts - // for the companion regression guard. - const deviceUrl = new URL(QWEN_CONFIG.deviceCodeUrl); - const tokenUrl = new URL(QWEN_CONFIG.tokenUrl); - assert.equal(deviceUrl.hostname, "chat.qwen.ai", "deviceCodeUrl must use chat.qwen.ai"); - assert.equal(tokenUrl.hostname, "chat.qwen.ai", "tokenUrl must use chat.qwen.ai"); - assert.equal(deviceUrl.pathname, "/api/v1/oauth2/device/code"); - assert.equal(tokenUrl.pathname, "/api/v1/oauth2/token"); -}); - test("browser-based providers expose buildAuthUrl and return provider-specific auth URLs", () => { const redirectUri = "http://localhost:43121/callback"; const state = "state-123"; @@ -460,7 +441,7 @@ test("Google OAuth callbacks stay on localhost when no custom credentials are co }); test("device and import-token providers expose the flow-specific fields expected by their configs", () => { - const deviceProviders = ["qwen", "kimi-coding", "github", "kiro", "amazon-q", "kilocode"]; + const deviceProviders = ["kimi-coding", "github", "kiro", "amazon-q", "kilocode"]; for (const providerId of deviceProviders) { const provider = PROVIDERS[providerId]; @@ -672,27 +653,8 @@ test("Qoder enabled mode exchanges tokens and loads profile metadata through moc } }); -test("Qwen and Kimi Coding execute mocked device-code flows and token mapping", async () => { - const qwenIdToken = createJwt({ - email: "qwen@example.com", - name: "Qwen User", - }); - +test("Kimi Coding executes mocked device-code flow and token mapping", async () => { useFetchSequence([ - jsonResponse({ - device_code: "qwen-device", - user_code: "QWEN123", - verification_uri: "https://chat.qwen.ai/activate", - expires_in: 300, - interval: 5, - }), - jsonResponse({ - access_token: createJwt({ sub: "qwen-subject" }), - refresh_token: "qwen-refresh", - expires_in: 3600, - id_token: qwenIdToken, - resource_url: "https://chat.qwen.ai/resource", - }), (url, init) => { const params = init.body; assert.equal(String(url), KIMI_CODING_CONFIG.deviceCodeUrl); @@ -701,10 +663,7 @@ test("Qwen and Kimi Coding execute mocked device-code flows and token mapping", assert.equal(init.headers["X-Msh-Device-Id"], "test-kimi-device-id"); assert.equal(init.headers["X-Msh-Os-Version"], os.release()); if (os.type() === "Windows_NT") { - assert.equal( - init.headers["X-Msh-Device-Model"], - `Windows ${os.release()} ${os.arch()}` - ); + assert.equal(init.headers["X-Msh-Device-Model"], `Windows ${os.release()} ${os.arch()}`); } return jsonResponse({ @@ -735,10 +694,6 @@ test("Qwen and Kimi Coding execute mocked device-code flows and token mapping", }, ]); - const qwenDevice = await PROVIDERS.qwen.requestDeviceCode(QWEN_CONFIG, "challenge-123"); - const qwenPoll = await PROVIDERS.qwen.pollToken(QWEN_CONFIG, qwenDevice.device_code, "verifier"); - const qwenMapped = PROVIDERS.qwen.mapTokens(qwenPoll.data); - const kimiDevice = await PROVIDERS["kimi-coding"].requestDeviceCode(KIMI_CODING_CONFIG); const kimiPoll = await PROVIDERS["kimi-coding"].pollToken( KIMI_CODING_CONFIG, @@ -746,9 +701,6 @@ test("Qwen and Kimi Coding execute mocked device-code flows and token mapping", ); const kimiMapped = PROVIDERS["kimi-coding"].mapTokens(kimiPoll.data); - assert.equal(qwenMapped.email, "qwen@example.com"); - assert.equal(qwenMapped.displayName, "Qwen User"); - assert.equal(qwenMapped.providerSpecificData.resourceUrl, "https://chat.qwen.ai/resource"); assert.equal(kimiMapped.accessToken, "kimi-access"); assert.equal(kimiMapped.tokenType, "Bearer"); assert.equal( diff --git a/tests/unit/oauth-providers-error-handling.test.ts b/tests/unit/oauth-providers-error-handling.test.ts index 2ec4adf0b32..fdc9644e8b2 100644 --- a/tests/unit/oauth-providers-error-handling.test.ts +++ b/tests/unit/oauth-providers-error-handling.test.ts @@ -173,18 +173,6 @@ test("P2: refreshGoogleToken parses invalid_grant as unrecoverable", async () => assert.match(fnMatch[0], /unrecoverable_refresh_error/, "must return unrecoverable sentinel"); }); -// ─── P2: Qwen invalid_grant ─────────────────────────────────────────────────── - -test("P2: refreshQwenToken handles invalid_grant in addition to invalid_request", async () => { - // refreshQwenToken lives in its own co-located provider module since the - // tokenRefresh.ts provider-extraction (#7338, redone on tip). - const src = await read("open-sse/services/tokenRefresh/providers/qwen.ts"); - const fnMatch = src.match(/export\s+async\s+function\s+refreshQwenToken\([\s\S]+?\n\}/); - assert.ok(fnMatch, "refreshQwenToken function body not found"); - assert.match(fnMatch[0], /invalid_grant/, "must detect invalid_grant"); - assert.match(fnMatch[0], /unrecoverable_refresh_error/, "must return unrecoverable sentinel"); -}); - // ─── P2: Kiro AWS InvalidGrantException ────────────────────────────────────── test("P2: refreshKiroToken parses AWS InvalidGrantException", async () => { diff --git a/tests/unit/provider-alias-uniqueness.test.ts b/tests/unit/provider-alias-uniqueness.test.ts index 25628a84cb9..e51f7cc24f5 100644 --- a/tests/unit/provider-alias-uniqueness.test.ts +++ b/tests/unit/provider-alias-uniqueness.test.ts @@ -4,14 +4,13 @@ * Before this guard, three aliases collided in the registry and the LAST entry in * iteration order silently won, emitting a startup warning and shadowing a real * provider: - * - "qw" → qwen-web (shadowed qwen) * - "kimi" → kimi-web (shadowed the kimi provider that gained a dedicated executor) * - "hc" → hackclub (shadowed huggingchat) * * The decision: the primary provider keeps the short alias; the web/secondary * variant takes its own id as alias. This test pins both the global uniqueness * invariant (so future additions can't silently re-collide) and the specific - * resolutions for the six affected providers, across BOTH alias sources + * resolutions for the affected providers, across BOTH alias sources * (open-sse registry + src/shared providers map). */ import test from "node:test"; @@ -43,9 +42,8 @@ test("no two provider IDs share the same alias in the open-sse registry", () => ); }); -test("primary providers keep the short alias; web/secondary variants use their own id", () => { +test("primary providers keep the short alias; web variants use their own id", () => { // open-sse registry (source of the startup warning + chat routing) - assert.equal(PROVIDER_ID_TO_ALIAS.qwen, "qw"); assert.equal(PROVIDER_ID_TO_ALIAS["qwen-web"], "qwen-web"); assert.equal(PROVIDER_ID_TO_ALIAS.kimi, "kimi"); assert.equal(PROVIDER_ID_TO_ALIAS["kimi-web"], "kimi-web"); @@ -55,7 +53,6 @@ test("primary providers keep the short alias; web/secondary variants use their o test("src/shared providers map resolves the same aliases unambiguously", () => { // alias → id - assert.equal(resolveProviderId("qw"), "qwen"); assert.equal(resolveProviderId("kimi"), "kimi"); assert.equal(resolveProviderId("hc"), "hackclub"); // id used as alias for the secondary variants @@ -63,7 +60,6 @@ test("src/shared providers map resolves the same aliases unambiguously", () => { assert.equal(resolveProviderId("kimi-web"), "kimi-web"); assert.equal(resolveProviderId("huggingchat"), "huggingchat"); // id → alias - assert.equal(getProviderAlias("qwen"), "qw"); assert.equal(getProviderAlias("kimi"), "kimi"); assert.equal(getProviderAlias("hackclub"), "hc"); }); diff --git a/tests/unit/provider-header-profiles.test.ts b/tests/unit/provider-header-profiles.test.ts index dfb1c035d40..f5c53c24075 100644 --- a/tests/unit/provider-header-profiles.test.ts +++ b/tests/unit/provider-header-profiles.test.ts @@ -17,7 +17,6 @@ import { getGitHubCopilotRefreshHeaders, getKiroServiceHeaders, getQoderDashscopeCompatHeaders, - getQwenOauthHeaders, } from "../../open-sse/config/providerHeaderProfiles.ts"; test("provider header profiles expose current GitHub chat and internal headers", () => { @@ -37,27 +36,20 @@ test("provider header profiles expose current GitHub chat and internal headers", assert.equal(internalHeaders["X-GitHub-Api-Version"], GITHUB_COPILOT_API_VERSION); }); -test("provider header profiles expose dedicated refresh, qwen, qoder and kiro variants", () => { +test("provider header profiles expose dedicated refresh, qoder and kiro variants", () => { const refreshHeaders = getGitHubCopilotRefreshHeaders("token gh-access"); assert.equal(refreshHeaders.Authorization, "token gh-access"); assert.equal(refreshHeaders["User-Agent"], GITHUB_COPILOT_REFRESH_USER_AGENT); assert.equal(refreshHeaders["Editor-Version"], GITHUB_COPILOT_EDITOR_VERSION); assert.equal(refreshHeaders["Editor-Plugin-Version"], GITHUB_COPILOT_REFRESH_PLUGIN_VERSION); - const qwenHeaders = getQwenOauthHeaders(); - assert.equal(qwenHeaders["User-Agent"], getQwenCliUserAgent()); - assert.equal( - qwenHeaders["User-Agent"], - `QwenCode/${QWEN_CLI_VERSION} (${process.platform}; ${process.arch})` - ); - assert.notEqual(qwenHeaders["User-Agent"], "QwenCode/0.15.11 (linux; x64)"); - assert.equal(qwenHeaders["X-Dashscope-UserAgent"], getQwenCliUserAgent()); - assert.equal(qwenHeaders["X-Stainless-Package-Version"], "5.11.0"); - assert.equal(qwenHeaders["X-Stainless-Runtime-Version"], process.version); - const qoderHeaders = getQoderDashscopeCompatHeaders(); assert.equal(qoderHeaders["user-agent"], getQwenCliUserAgent()); assert.equal(qoderHeaders["x-dashscope-useragent"], getQwenCliUserAgent()); + assert.equal( + qoderHeaders["user-agent"], + `QwenCode/${QWEN_CLI_VERSION} (${process.platform}; ${process.arch})` + ); const kiroHeaders = getKiroServiceHeaders("application/json"); assert.equal(kiroHeaders.Accept, "application/json"); @@ -76,9 +68,8 @@ test("provider header profiles tolerate browser-like process shims", async () => try { assert.equal(getQwenCliUserAgent(), `QwenCode/${QWEN_CLI_VERSION} (unknown; unknown)`); - const qwenHeaders = getQwenOauthHeaders(); - assert.equal(qwenHeaders["User-Agent"], `QwenCode/${QWEN_CLI_VERSION} (unknown; unknown)`); - assert.equal(qwenHeaders["X-Stainless-Runtime-Version"], "unknown"); + const qoderHeaders = getQoderDashscopeCompatHeaders(); + assert.equal(qoderHeaders["user-agent"], `QwenCode/${QWEN_CLI_VERSION} (unknown; unknown)`); } finally { Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true }); Object.defineProperty(process, "arch", { value: originalArch, configurable: true }); diff --git a/tests/unit/provider-limits-rotating-expired-guard.test.ts b/tests/unit/provider-limits-rotating-expired-guard.test.ts index 2168bd47104..daca704d088 100644 --- a/tests/unit/provider-limits-rotating-expired-guard.test.ts +++ b/tests/unit/provider-limits-rotating-expired-guard.test.ts @@ -9,9 +9,8 @@ const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-rotating- process.env.DATA_DIR = TEST_DATA_DIR; process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "rotating-expired-guard-secret"; -const { quotaPathShouldMarkExpired, shouldAttemptRotatingRefresh } = await import( - "../../src/lib/usage/providerLimits.ts" -); +const { quotaPathShouldMarkExpired, shouldAttemptRotatingRefresh } = + await import("../../src/lib/usage/providerLimits.ts"); test.after(() => { fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); @@ -23,7 +22,7 @@ test.after(() => { // flagging it expired hid freshly-added Codex accounts from the quota page even // though a providers-page refresh turned them green. test("rotating providers are NEVER flagged expired from the quota path", () => { - for (const provider of ["codex", "openai", "claude", "kiro", "qwen", "gitlab-duo"]) { + for (const provider of ["codex", "openai", "claude", "kiro", "gitlab-duo"]) { assert.equal( quotaPathShouldMarkExpired(provider, "Token expired, please re-authenticate", "active"), false, @@ -54,9 +53,13 @@ test("an already-expired connection is left untouched (no redundant write)", () // expired token (cascade-safe via serializeRefresh), so its live quota shows; // the bulk scheduler (allowRotatingRefresh falsy) must keep #3019 and never do it. test("bulk path never refreshes rotating providers (preserves #3019)", () => { - for (const provider of ["codex", "openai", "claude", "kiro", "qwen", "gitlab-duo"]) { + for (const provider of ["codex", "openai", "claude", "kiro", "gitlab-duo"]) { assert.equal(shouldAttemptRotatingRefresh(provider, undefined), false, `${provider} bulk`); - assert.equal(shouldAttemptRotatingRefresh(provider, false), false, `${provider} explicit false`); + assert.equal( + shouldAttemptRotatingRefresh(provider, false), + false, + `${provider} explicit false` + ); } }); diff --git a/tests/unit/provider-models-route.test.ts b/tests/unit/provider-models-route.test.ts index fe95a79988f..f82f4abd8ff 100644 --- a/tests/unit/provider-models-route.test.ts +++ b/tests/unit/provider-models-route.test.ts @@ -1002,21 +1002,6 @@ test("provider models route falls back through all Antigravity discovery endpoin assert.ok(body.models.some((model) => model.id === "gemini-3-pro-preview")); }); -test("provider models route returns the local catalog for OAuth-backed Qwen connections", async () => { - const connection = await seedConnection("qwen", { - authType: "oauth", - accessToken: "qwen-access", - apiKey: null, - }); - - const response = await callRoute(connection.id); - const body = (await response.json()) as any; - - assert.equal(response.status, 200); - assert.equal(body.source, "local_catalog"); - assert.ok(Array.isArray(body.models)); -}); - test("provider models route filters hidden models from the static Claude catalog when requested", async () => { const connection = await seedConnection("claude", { authType: "oauth", diff --git a/tests/unit/proxy-connection-test.test.ts b/tests/unit/proxy-connection-test.test.ts index b38858b1ab1..a73e19a77cb 100644 --- a/tests/unit/proxy-connection-test.test.ts +++ b/tests/unit/proxy-connection-test.test.ts @@ -289,7 +289,6 @@ test("OAuth test config covers all expected providers", () => { "github", "gitlab-duo", "qoder", - "qwen", "cursor", "kimi-coding", "kilocode", @@ -307,7 +306,6 @@ test("OAuth test config covers all expected providers", () => { "github", "gitlab-duo", "qoder", - "qwen", "cursor", "kimi-coding", "kilocode", @@ -366,7 +364,6 @@ test("Refreshable OAuth providers are correctly identified", () => { "antigravity", "gitlab-duo", "qoder", - "qwen", "kimi-coding", "cline", "kiro", @@ -376,6 +373,6 @@ test("Refreshable OAuth providers are correctly identified", () => { // Verify these two sets are mutually exclusive and cover all providers const allProviders = [...refreshable, ...nonRefreshable]; - assert.equal(allProviders.length, 13); - assert.equal(new Set(allProviders).size, 13); + assert.equal(allProviders.length, 12); + assert.equal(new Set(allProviders).size, 12); }); diff --git a/tests/unit/public-client-ids-3493.test.ts b/tests/unit/public-client-ids-3493.test.ts index 5141f6abb66..c8e7f26d17c 100644 --- a/tests/unit/public-client-ids-3493.test.ts +++ b/tests/unit/public-client-ids-3493.test.ts @@ -3,7 +3,7 @@ import assert from "node:assert/strict"; import { resolvePublicCred } from "../../open-sse/utils/publicCreds.ts"; -// #3493 — five public OAuth client_ids were migrated from string literals to +// #3493 — public OAuth client_ids were migrated from string literals to // resolvePublicCred() (Hard Rule #11). These assertions guard that the embedded // masked-byte defaults still decode to the exact public client_ids, so the OAuth // flows are byte-for-byte unchanged, and that env overrides still win. @@ -11,7 +11,6 @@ import { resolvePublicCred } from "../../open-sse/utils/publicCreds.ts"; const EXPECTED_CLIENT_IDS: Record = { claude_id: "9d1c250a-e61b-44d9-88ed-5944d1962f5e", codex_id: "app_EMoamEEZ73f0CkXaXp7hrann", - qwen_id: "f0304373b74a44d2b584a3fb70ca9e56", kimi_id: "17e5f671-d194-4dfb-9706-5516cb48c098", github_copilot_id: "Iv1.b507a08c87ecfe98", }; @@ -41,9 +40,7 @@ test("#3493 env override takes priority over the embedded default", () => { }); test("#3493 the migrated OAuth/registry configs resolve to the expected client_ids", async () => { - const { CLAUDE_CONFIG, CODEX_CONFIG, QWEN_CONFIG } = await import( - "../../src/lib/oauth/constants/oauth.ts" - ); + const { CLAUDE_CONFIG, CODEX_CONFIG } = await import("../../src/lib/oauth/constants/oauth.ts"); // Only assert when env doesn't override (CI/dev may set these); skip the assert // for any that are env-overridden so the test stays deterministic. if (!process.env.CLAUDE_OAUTH_CLIENT_ID) { @@ -52,7 +49,4 @@ test("#3493 the migrated OAuth/registry configs resolve to the expected client_i if (!process.env.CODEX_OAUTH_CLIENT_ID) { assert.equal(CODEX_CONFIG.clientId, EXPECTED_CLIENT_IDS.codex_id); } - if (!process.env.QWEN_OAUTH_CLIENT_ID) { - assert.equal(QWEN_CONFIG.clientId, EXPECTED_CLIENT_IDS.qwen_id); - } }); diff --git a/tests/unit/qoder-oauth-config.test.ts b/tests/unit/qoder-oauth-config.test.ts index 194d8a08fc4..16d275a049a 100644 --- a/tests/unit/qoder-oauth-config.test.ts +++ b/tests/unit/qoder-oauth-config.test.ts @@ -6,7 +6,6 @@ import assert from "node:assert/strict"; Object.assign(process.env, { CLAUDE_OAUTH_CLIENT_ID: "9d1c250a-e61b-44d9-88ed-5944d1962f5e", CODEX_OAUTH_CLIENT_ID: "app_EMoamEEZ73f0CkXaXp7hrann", - QWEN_OAUTH_CLIENT_ID: "f0304373b74a44d2b584a3fb70ca9e56", KIMI_CODING_OAUTH_CLIENT_ID: "17e5f671-d194-4dfb-9706-5516cb48c098", GITHUB_OAUTH_CLIENT_ID: "Iv1.b507a08c87ecfe98", }); diff --git a/tests/unit/qwen-strip-stream-options-claude-code-port663.test.ts b/tests/unit/qwen-strip-stream-options-claude-code-port663.test.ts deleted file mode 100644 index f4effdacbad..00000000000 --- a/tests/unit/qwen-strip-stream-options-claude-code-port663.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Port of upstream decolua/9router#663 (closes upstream #557). - * - * Scenario: Claude Code (or any caller) hits a Qwen model with an OpenAI body - * that carries `stream: false`. OmniRoute, however, sets the executor-level - * `stream` flag to `true` for Claude-Code-compatible providers via - * `upstreamStream = stream || isClaudeCodeCompatible` - * (`open-sse/handlers/chatCore.ts`). DefaultExecutor.transformRequest then runs - * its `if (stream && targetFormat === "openai")` branch and injects - * `stream_options: { include_usage: true }` onto a body that still carries - * `stream: false`. Qwen upstream rejects with: - * 400 "'stream_options' only set this when you set stream: true" - * - * Fix mirrors upstream: when the OUTGOING body explicitly says `stream: false`, - * do NOT inject `stream_options` regardless of the executor-level `stream` arg. - * Same defensive treatment when the body carries `thinking` / - * `enable_thinking`, since the upstream PR also exempts those. - */ -import test from "node:test"; -import assert from "node:assert/strict"; -import { DefaultExecutor } from "../../open-sse/executors/default.ts"; - -test("port#663 qwen: body.stream===false → no stream_options even when executor stream=true", () => { - const executor = new DefaultExecutor("qwen"); - const body = { - model: "qwen3-coder-plus", - messages: [{ role: "user", content: "hi" }], - stream: false, - }; - const result = executor.transformRequest( - "qwen3-coder-plus", - body, - /* stream */ true, - {} - ) as Record; - assert.equal( - result.stream_options, - undefined, - "stream_options must not be injected when body.stream === false" - ); -}); - -test("port#663 qwen: body.thinking truthy → no stream_options injection", () => { - const executor = new DefaultExecutor("qwen"); - const body = { - model: "qwen3-coder-plus", - messages: [{ role: "user", content: "hi" }], - thinking: { type: "enabled" }, - }; - const result = executor.transformRequest( - "qwen3-coder-plus", - body, - true, - {} - ) as Record; - assert.equal( - result.stream_options, - undefined, - "stream_options must not be injected when thinking mode is requested" - ); -}); - -test("port#663 qwen: body.enable_thinking truthy → no stream_options injection", () => { - const executor = new DefaultExecutor("qwen"); - const body = { - model: "qwen3-coder-plus", - messages: [{ role: "user", content: "hi" }], - enable_thinking: true, - }; - const result = executor.transformRequest( - "qwen3-coder-plus", - body, - true, - {} - ) as Record; - assert.equal( - result.stream_options, - undefined, - "stream_options must not be injected when enable_thinking is true" - ); -}); - -test("port#663 qwen: normal streaming request still injects stream_options.include_usage", () => { - const executor = new DefaultExecutor("qwen"); - const body = { - model: "qwen3-coder-plus", - messages: [{ role: "user", content: "hi" }], - }; - const result = executor.transformRequest( - "qwen3-coder-plus", - body, - true, - {} - ) as Record; - assert.deepEqual( - result.stream_options, - { include_usage: true }, - "regular qwen streaming requests must keep the include_usage injection" - ); -}); diff --git a/tests/unit/qwen-web-models-discovery-3931.test.ts b/tests/unit/qwen-web-models-discovery-3931.test.ts index 3610311767b..0a714b65e51 100644 --- a/tests/unit/qwen-web-models-discovery-3931.test.ts +++ b/tests/unit/qwen-web-models-discovery-3931.test.ts @@ -2,9 +2,7 @@ * TDD regression for #3931 (Problem #3, diagnosed by @thezukiru in discussion * #3895): the `qwen-web` cookie provider had no entry in PROVIDER_MODELS_CONFIG * (`src/app/api/providers/[id]/models/route.ts`), so the model-discovery page - * returned nothing for it. The OAuth fallback at the top of the handler only - * fires for `provider === "qwen" && authType === "oauth"`, so qwen-web fell - * through to the no-config branch. + * returned nothing for it, so qwen-web fell through to the no-config branch. * * (Problem #1 — the validator bare-token false-positive — was already fixed in * the merged PR #3958; Problem #2 — empty stream from WAF bot-detection on the diff --git a/tests/unit/sync-models-degraded-local-catalog-5460-5465.test.ts b/tests/unit/sync-models-degraded-local-catalog-5460-5465.test.ts index fdec9bb5387..a897e93c073 100644 --- a/tests/unit/sync-models-degraded-local-catalog-5460-5465.test.ts +++ b/tests/unit/sync-models-degraded-local-catalog-5460-5465.test.ts @@ -3,7 +3,7 @@ import assert from "node:assert/strict"; // #5460 (Reka) + #5465 (t3.chat): the model-sync route used to 502 on ANY // `local_catalog` source, so providers whose local catalog is their ONLY -// discovery source (reka, qwen-oauth, embedding/rerank + web-cookie providers) +// discovery source (reka, embedding/rerank + web-cookie providers) // failed Import/Sync every time. The route now imports those (flagged // `intentional: true` by the models route) and only 502s a genuinely degraded // remote-fetch fallback. diff --git a/tests/unit/token-refresh-route-service.test.ts b/tests/unit/token-refresh-route-service.test.ts index b8d50ad509e..0a71e72c81a 100644 --- a/tests/unit/token-refresh-route-service.test.ts +++ b/tests/unit/token-refresh-route-service.test.ts @@ -187,13 +187,6 @@ test("token refresh wrapper delegates provider-specific refresh helpers and form refresh_token: "google-refresh-next", expires_in: 3600, }); - case OAUTH_ENDPOINTS.qwen.token: - return jsonResponse({ - access_token: "qwen-access", - refresh_token: "qwen-refresh-next", - expires_in: 900, - resource_url: "https://resource.qwen.local", - }); case OAUTH_ENDPOINTS.openai.token: return jsonResponse({ access_token: "codex-access", @@ -227,7 +220,6 @@ test("token refresh wrapper delegates provider-specific refresh helpers and form "override-client-id", "override-client-secret" ); - const qwen = await tokenRefresh.refreshQwenToken("refresh-qwen"); const codex = await tokenRefresh.refreshCodexToken("refresh-codex"); const qoder = await tokenRefresh.refreshQoderToken("refresh-qoder"); const github = await tokenRefresh.refreshGitHubToken("refresh-github"); @@ -244,10 +236,7 @@ test("token refresh wrapper delegates provider-specific refresh helpers and form refreshToken: "refresh-github", }); const allTokens = await tokenRefresh.getAllAccessTokens({ - connections: [ - { provider: "github", refreshToken: "refresh-github-all", isActive: true }, - { provider: "qwen", refreshToken: "refresh-qwen-all", isActive: false }, - ], + connections: [{ provider: "github", refreshToken: "refresh-github-all", isActive: true }], }); assert.equal(tokenRefresh.TOKEN_EXPIRY_BUFFER_MS, 5 * 60 * 1000); @@ -258,7 +247,6 @@ test("token refresh wrapper delegates provider-specific refresh helpers and form }); assert.equal(claude.accessToken, "claude-access"); assert.equal(google.accessToken, "google-access"); - assert.equal(qwen.providerSpecificData.resourceUrl, "https://resource.qwen.local"); assert.equal(codex.accessToken, "codex-access"); assert.equal(qoder, null); assert.equal(github.refreshToken, "github-refresh-next"); @@ -274,7 +262,7 @@ test("token refresh wrapper delegates provider-specific refresh helpers and form } ); - assert.equal(calls.length >= 8, true); + assert.equal(calls.length >= 7, true); delete PROVIDERS["custom-oauth-local-608"]; }); diff --git a/tests/unit/token-refresh-service.test.ts b/tests/unit/token-refresh-service.test.ts index f952ae4f06d..eb75adb0d31 100644 --- a/tests/unit/token-refresh-service.test.ts +++ b/tests/unit/token-refresh-service.test.ts @@ -13,7 +13,6 @@ const { refreshKimiCodingToken, refreshClaudeOAuthToken, refreshGoogleToken, - refreshQwenToken, refreshCodexToken, refreshKiroToken, refreshQoderToken, @@ -379,44 +378,6 @@ test("refreshGoogleToken exchanges refresh tokens against the shared google endp ); }); -test("refreshQwenToken maps resource_url into providerSpecificData", async () => { - const log = createLog(); - - await withMockedFetch( - async () => - jsonResponse({ - access_token: "qwen-access", - refresh_token: "qwen-refresh-next", - expires_in: 1500, - resource_url: "https://chat.qwen.ai/workspace/resource", - }), - async () => { - const result = await refreshQwenToken("qwen-refresh", log); - assert.deepEqual(result, { - accessToken: "qwen-access", - refreshToken: "qwen-refresh-next", - expiresIn: 1500, - providerSpecificData: { - resourceUrl: "https://chat.qwen.ai/workspace/resource", - }, - }); - } - ); -}); - -test("refreshQwenToken surfaces invalid_request as unrecoverable", async () => { - const log = createLog(); - - await withMockedFetch( - async () => textResponse(JSON.stringify({ error: "invalid_request" }), 400), - async () => { - const result = await refreshQwenToken("qwen-refresh", log); - // Normalized to unrecoverable_refresh_error sentinel (Fix 4) - assert.deepEqual(result, { error: "unrecoverable_refresh_error", code: "invalid_request" }); - } - ); -}); - test("refreshCodexToken recognizes refresh_token_reused responses", async () => { const log = createLog(); diff --git a/tests/unit/usage-providers.test.ts b/tests/unit/usage-providers.test.ts index 52ffabb5348..17bece1108b 100644 --- a/tests/unit/usage-providers.test.ts +++ b/tests/unit/usage-providers.test.ts @@ -96,17 +96,6 @@ test("getUsageForProvider handles kimi-coding provider", async () => { assert.ok(result); }); -test("getUsageForProvider handles qwen provider", async () => { - globalThis.fetch = async () => new Response(JSON.stringify({}), { status: 200 }); - const result = await usageModule.getUsageForProvider({ - id: "test-qw", - provider: "qwen", - accessToken: "tok", - apiKey: "key", - }); - assert.ok(result); -}); - test("getUsageForProvider handles qoder provider", async () => { globalThis.fetch = async () => new Response(JSON.stringify({}), { status: 200 }); const result = await usageModule.getUsageForProvider({ diff --git a/tests/unit/usage-service-hardening.test.ts b/tests/unit/usage-service-hardening.test.ts index 150b99b283a..dd9a537dee9 100644 --- a/tests/unit/usage-service-hardening.test.ts +++ b/tests/unit/usage-service-hardening.test.ts @@ -798,7 +798,7 @@ test("usage service covers Codex, Kiro and Kimi usage parsing and error branches assert.match(kimiInvalidJson.message, /Invalid JSON response/i); }); -test("usage service covers Codex auth failures, Kiro hard failures, Kimi no-quota fallbacks and Qwen catch branch", async () => { +test("usage service covers Codex auth failures, Kiro hard failures and Kimi no-quota fallbacks", async () => { globalThis.fetch = async (url) => { if (String(url).includes("/backend-api/wham/usage")) { return new Response("nope", { status: 401 }); @@ -865,34 +865,9 @@ test("usage service covers Codex auth failures, Kiro hard failures, Kimi no-quot accessToken: "kimi-offline", }); assert.match(kimiOffline.message, /Unable to fetch usage: kimi offline/i); - - const qwenCatch: any = await usageService.getUsageForProvider({ - provider: "qwen", - accessToken: "qwen-catch", - providerSpecificData: { - get resourceUrl() { - throw new Error("resource lookup failed"); - }, - }, - }); - assert.equal(qwenCatch.message, "Unable to fetch Qwen usage."); }); -test("usage service covers Qwen, Qoder, GLM, Z.AI and GLMT branches", async () => { - const qwenMissingUrl: any = await usageService.getUsageForProvider({ - provider: "qwen", - accessToken: "qwen-token", - providerSpecificData: {}, - }); - assert.match(qwenMissingUrl.message, /No resource URL/i); - - const qwen: any = await usageService.getUsageForProvider({ - provider: "qwen", - accessToken: "qwen-token", - providerSpecificData: { resourceUrl: "https://example.com/resource" }, - }); - assert.match(qwen.message, /Usage tracked per request/i); - +test("usage service covers Qoder, GLM, Z.AI and GLMT branches", async () => { // Qoder now reads its PAT from `apiKey` (not `accessToken`); with no PAT the // usage fetcher returns a friendly prompt instead of hitting the network. const qoder: any = await usageService.getUsageForProvider({ diff --git a/tests/unit/web-cookie-providers-new.test.ts b/tests/unit/web-cookie-providers-new.test.ts index 4d90fe359d2..be1042c0e73 100644 --- a/tests/unit/web-cookie-providers-new.test.ts +++ b/tests/unit/web-cookie-providers-new.test.ts @@ -190,7 +190,6 @@ test("Doubao Web sets correct provider", () => { test("Qwen Web executor is registered", () => { assert.ok(hasSpecializedExecutor("qwen-web")); - assert.ok(hasSpecializedExecutor("qw")); const executor = getExecutor("qwen-web"); assert.ok(executor instanceof QwenWebExecutor); }); From e199e35a70f71b3213f7776193e2f378554d8339 Mon Sep 17 00:00:00 2001 From: backryun Date: Mon, 20 Jul 2026 21:14:05 +0900 Subject: [PATCH 3/6] feat(cli): rebuild Qwen Code integration for upstream V4 --- .env.example | 1 + README.md | 2 +- bin/cli/commands/registry.mjs | 2 + bin/cli/commands/setup-qwen.mjs | 166 ++++++++++ docs/frameworks/ACP.md | 10 +- docs/guides/CLI-INTEGRATIONS.md | 7 +- docs/guides/REMOTE-MODE.md | 1 + docs/guides/SETUP_GUIDE.md | 1 + docs/reference/CLI-TOOLS.md | 11 +- docs/reference/ENVIRONMENT.md | 1 + scripts/check/check-route-guard-membership.ts | 1 + .../cli-code/components/DefaultToolCard.tsx | 20 +- src/app/api/cli-tools/all-statuses/route.ts | 5 + src/app/api/cli-tools/backups/route.ts | 2 +- src/app/api/cli-tools/qwen-settings/route.ts | 222 +++++++++++++ src/app/api/cli-tools/status/route.ts | 1 + src/app/api/skills/collect/detect/route.ts | 1 + src/app/api/skills/collect/install/route.ts | 1 + src/lib/acp/manager.ts | 2 +- src/lib/acp/registry.ts | 11 +- src/lib/cliTools/checkToolConfigStatus.ts | 3 + src/server/authz/routeGuard.ts | 1 + src/shared/constants/cliTools.ts | 53 +++ src/shared/constants/spawnCapablePrefixes.ts | 1 + src/shared/schemas/cliCatalog.ts | 2 +- src/shared/services/cliRuntime.ts | 11 + src/shared/services/qwenCodeConfig.ts | 252 +++++++++++++++ tests/unit/acp-registry.test.ts | 9 + tests/unit/check-tool-config-status.test.ts | 38 +++ tests/unit/cli-catalog-acpspawnable.test.ts | 1 + tests/unit/cli-catalog-counts.test.ts | 11 +- tests/unit/cli-runtime-detection.test.ts | 1 + tests/unit/cli-tools-schema.test.ts | 4 +- tests/unit/cli/setup-qwen.test.ts | 85 +++++ tests/unit/qwen-code-config.test.ts | 303 ++++++++++++++++++ tests/unit/qwen-settings-route.test.ts | 137 ++++++++ ...ute-guard-qwen-settings-local-only.test.ts | 21 ++ 37 files changed, 1377 insertions(+), 24 deletions(-) create mode 100644 bin/cli/commands/setup-qwen.mjs create mode 100644 src/app/api/cli-tools/qwen-settings/route.ts create mode 100644 src/shared/services/qwenCodeConfig.ts create mode 100644 tests/unit/cli/setup-qwen.test.ts create mode 100644 tests/unit/qwen-code-config.test.ts create mode 100644 tests/unit/qwen-settings-route.test.ts create mode 100644 tests/unit/route-guard-qwen-settings-local-only.test.ts diff --git a/.env.example b/.env.example index 41452fb26b6..c0de228dbf6 100644 --- a/.env.example +++ b/.env.example @@ -632,6 +632,7 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true # CLI_CLINE_BIN=cline # CLI_CONTINUE_BIN=cn # CLI_QODER_BIN=qoder +# CLI_QWEN_BIN=qwen # CLI_AUGGIE_BIN=auggie # AUGGIE_BIN=auggie diff --git a/README.md b/README.md index 0661f550d19..aa7a70210d6 100644 --- a/README.md +++ b/README.md @@ -322,7 +322,7 @@ All **18** strategies — mix & match per combo step: + also works with · Kiro · Command Code · Antigravity · Windsurf · AMP · any OpenAI-compatible tool -📖 Per-tool setup for all 32 tools (24 CLI Code's + 8 CLI Agents) → [`docs/reference/CLI-TOOLS.md`](docs/reference/CLI-TOOLS.md) · 🧩 OpenCode plugin → [`@omniroute/opencode-provider`](https://www.npmjs.com/package/@omniroute/opencode-provider) +📖 Per-tool setup for all 33 tools (25 CLI Code's + 8 CLI Agents) → [`docs/reference/CLI-TOOLS.md`](docs/reference/CLI-TOOLS.md) · 🧩 OpenCode plugin → [`@omniroute/opencode-provider`](https://www.npmjs.com/package/@omniroute/opencode-provider) diff --git a/bin/cli/commands/registry.mjs b/bin/cli/commands/registry.mjs index da4ad3bacf0..84c71bdf062 100644 --- a/bin/cli/commands/registry.mjs +++ b/bin/cli/commands/registry.mjs @@ -71,6 +71,7 @@ import { registerSetupRoo } from "./setup-roo.mjs"; import { registerSetupCrush } from "./setup-crush.mjs"; import { registerSetupGoose } from "./setup-goose.mjs"; import { registerSetupAider } from "./setup-aider.mjs"; +import { registerSetupQwen } from "./setup-qwen.mjs"; import { registerConnect } from "./connect.mjs"; import { registerContexts } from "./contexts.mjs"; import { registerTokens } from "./tokens.mjs"; @@ -153,6 +154,7 @@ export function registerCommands(program) { registerSetupCrush(program); registerSetupGoose(program); registerSetupAider(program); + registerSetupQwen(program); registerConnect(program); registerContexts(program); registerTokens(program); diff --git a/bin/cli/commands/setup-qwen.mjs b/bin/cli/commands/setup-qwen.mjs new file mode 100644 index 00000000000..ee5ec6d18d3 --- /dev/null +++ b/bin/cli/commands/setup-qwen.mjs @@ -0,0 +1,166 @@ +/** Configure Qwen Code's OpenAI-compatible provider for OmniRoute. */ + +import { + chmodSync, + existsSync, + mkdirSync, + readFileSync, + renameSync, + unlinkSync, + writeFileSync, +} from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import { + mergeQwenCodeEnv, + mergeQwenCodeSettings, + normalizeQwenCodeBaseUrl, +} from "../../../src/shared/services/qwenCodeConfig.ts"; +import { resolveActiveContext } from "../contexts.mjs"; +import { createPrompt, printError, printHeading, printInfo, printSuccess } from "../io.mjs"; + +/** Resolve base URL and key from flags, active context, then local defaults. */ +export function resolveQwenTarget(opts = {}) { + let root = opts.remote ? String(opts.remote) : ""; + let context; + + if (!root || !(opts.apiKey ?? opts["api-key"])) { + try { + context = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT); + } catch { + // An active context is optional for local setup. + } + } + + if (!root) root = context?.baseUrl || ""; + if (!root) { + const port = Number(opts.port ?? process.env.PORT ?? 20128) || 20128; + root = `http://localhost:${port}`; + } + + const apiKey = + opts.apiKey ?? + opts["api-key"] ?? + context?.accessToken ?? + context?.apiKey ?? + process.env.OMNIROUTE_API_KEY ?? + "sk_omniroute"; + + return { baseUrl: normalizeQwenCodeBaseUrl(root), apiKey }; +} + +const readSettings = (filePath) => { + if (!existsSync(filePath)) return {}; + const parsed = JSON.parse(readFileSync(filePath, "utf8")); + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new Error("Qwen Code settings.json must contain a JSON object"); + } + return parsed; +}; + +const readText = (filePath) => (existsSync(filePath) ? readFileSync(filePath, "utf8") : ""); + +const writeAtomic = (filePath, content, mode) => { + const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`; + try { + writeFileSync(tempPath, content, { encoding: "utf8", mode }); + if (mode !== undefined) chmodSync(tempPath, mode); + renameSync(tempPath, filePath); + } catch (error) { + try { + unlinkSync(tempPath); + } catch { + // The temporary file may not have been created. + } + throw error; + } +}; + +const fetchModelIds = async (baseUrl, apiKey) => { + try { + const response = await fetch(`${baseUrl}/models`, { + headers: { Authorization: `Bearer ${apiKey}` }, + signal: AbortSignal.timeout(8000), + }); + if (!response.ok) return []; + const body = await response.json(); + const models = Array.isArray(body) ? body : (body.data ?? body.models ?? []); + return models.map((entry) => (typeof entry === "string" ? entry : entry?.id)).filter(Boolean); + } catch { + return []; + } +}; + +export async function runSetupQwenCommand(opts = {}) { + const { baseUrl, apiKey } = resolveQwenTarget(opts); + const dryRun = Boolean(opts.dryRun ?? opts["dry-run"]); + const settingsPath = + opts.configPath ?? opts["config-path"] ?? path.join(os.homedir(), ".qwen", "settings.json"); + const envPath = opts.envPath ?? opts["env-path"] ?? path.join(path.dirname(settingsPath), ".env"); + + printHeading("OmniRoute → Qwen Code (OpenAI-compatible)"); + printInfo(`baseUrl: ${baseUrl}`); + + let model = String(opts.model || "").trim(); + if (!model && !opts.yes) { + const modelIds = await fetchModelIds(baseUrl, apiKey); + if (modelIds.length > 0) { + printInfo(`Examples: ${modelIds.slice(0, 20).join(", ")}${modelIds.length > 20 ? " …" : ""}`); + } + const prompt = createPrompt(); + try { + model = String(await prompt.ask("Model id for Qwen Code")).trim(); + } finally { + prompt.close(); + } + } + + if (!model) { + printError("A model is required. Pass --model ."); + return 2; + } + + try { + const settings = mergeQwenCodeSettings(readSettings(settingsPath), { baseUrl, model }); + const envText = mergeQwenCodeEnv(readText(envPath), apiKey); + const settingsText = `${JSON.stringify(settings, null, 2)}\n`; + + if (dryRun) { + console.log(`\n${settingsText}`); + printInfo(`[dry-run] settings → ${settingsPath}`); + printInfo(`[dry-run] credential → ${envPath} (OMNIROUTE_API_KEY)`); + return 0; + } + + mkdirSync(path.dirname(settingsPath), { recursive: true, mode: 0o700 }); + mkdirSync(path.dirname(envPath), { recursive: true, mode: 0o700 }); + writeAtomic(settingsPath, settingsText); + writeAtomic(envPath, envText, 0o600); + printSuccess(`Wrote ${settingsPath}`); + printSuccess(`Updated ${envPath} (OMNIROUTE_API_KEY only)`); + printInfo('Run: qwen (or headless: qwen -p "reply OK")'); + return 0; + } catch (error) { + printError(`Failed to configure Qwen Code: ${error?.message || error}`); + return 1; + } +} + +export function registerSetupQwen(program) { + program + .command("setup-qwen") + .description("Configure Qwen Code's upstream V4 modelProviders format for OmniRoute") + .option("--port ", "Local OmniRoute port (ignored when --remote is set)", "20128") + .option("--remote ", "Remote OmniRoute URL") + .option("--api-key ", "OmniRoute API key") + .option("--model ", "Model id for Qwen Code") + .option("--config-path ", "Qwen Code settings.json path") + .option("--env-path ", "Qwen Code .env path") + .option("--yes", "Non-interactive; requires --model") + .option("--dry-run", "Print settings without writing files or secrets") + .action(async (opts) => { + const code = await runSetupQwenCommand(opts); + if (code !== 0) process.exitCode = code; + }); +} diff --git a/docs/frameworks/ACP.md b/docs/frameworks/ACP.md index 6facda83c87..3a7b3862208 100644 --- a/docs/frameworks/ACP.md +++ b/docs/frameworks/ACP.md @@ -19,7 +19,7 @@ ACP (Agent Client Protocol) is a **"CLI-as-backend" transport** for OmniRoute. I | **No API keys needed** | Uses your existing CLI authentication | | **Native protocol** | Uses each CLI's native input/output format | | **Auto-discovery** | Detects installed CLIs on your system | -| **14 built-in agents** | Pre-configured for popular CLI tools | +| **13 built-in agents** | Pre-configured for popular CLI tools | | **Custom agents** | Add your own CLI tools via settings | | **Process management** | Handles lifecycle (spawn, send, kill) | @@ -27,7 +27,7 @@ ACP (Agent Client Protocol) is a **"CLI-as-backend" transport** for OmniRoute. I ## Supported CLI Agents -ACP supports **12 built-in CLI agents** out of the box: +ACP supports **13 built-in CLI agents** out of the box: | Agent ID | Display Name | Binary | Protocol | | ------------- | ------------------ | ------------- | -------- | @@ -38,6 +38,7 @@ ACP supports **12 built-in CLI agents** out of the box: | `aider` | Aider | `aider` | stdio | | `opencode` | OpenCode | `opencode` | stdio | | `cline` | Cline | `cline` | stdio | +| `qwen` | Qwen Code | `qwen --acp` | stdio | | `forge` | ForgeCode | `forge` | stdio | | `amazon-q` | Amazon Q Developer | `q` | stdio | | `interpreter` | Open Interpreter | `interpreter` | stdio | @@ -197,7 +198,7 @@ const session = acpManager.spawn("claude", "claude", ["--print", "--output-forma // Returns: AcpSession ``` -**Allowed agent IDs**: `["claude", "codex", "gemini"]` +**Allowed agent IDs**: `["claude", "codex", "gemini", "qwen"]` #### `acpManager.sendPrompt(sessionId, prompt, timeoutMs)` @@ -406,11 +407,12 @@ Each ACP session runs in its own child process. The process is killed when the s **Problem**: `acpManager.spawn()` throws `Unknown agent: ` -**Solution**: Only 3 agents are allowed in `spawn()`: +**Solution**: Only these agents are allowed in `spawn()`: - `claude` - `codex` - `gemini` +- `qwen` Other agents must be spawned manually or via custom agent definitions. diff --git a/docs/guides/CLI-INTEGRATIONS.md b/docs/guides/CLI-INTEGRATIONS.md index ba056e61898..cd2a3890b1e 100644 --- a/docs/guides/CLI-INTEGRATIONS.md +++ b/docs/guides/CLI-INTEGRATIONS.md @@ -11,8 +11,8 @@ CLI (Codex, Claude Code, OpenCode, Cline, …) to use OmniRoute as its backend the tool talks to **one** endpoint and OmniRoute routes to the right provider with auto-fallback. Each command reads the **live** model catalog from a running OmniRoute (local or remote) and writes the tool's own config file on **your** -machine. The API key is referenced by env var wherever the tool supports it, so the -secret is never written to disk (the exceptions are noted below). +machine. The API key is referenced by an environment variable wherever the tool +supports it. Commands that persist a tool-local environment file are noted below. There are also two launchers — `omniroute launch` (Claude Code) and `omniroute launch-codex` (Codex) — that spawn the CLI with the right env injected, @@ -48,6 +48,7 @@ server and writes the config locally. | `omniroute setup-crush` | Crush | `~/.config/crush/crush.json` — `openai-compat` provider, key via `$OMNIROUTE_API_KEY` | `--remote` `--api-key` `--only` `--dry-run` `--port` `--config-path` | Both | | `omniroute setup-goose` | Goose | `~/.config/goose/config.yaml` (`GOOSE_PROVIDER`/`OPENAI_HOST`/`GOOSE_MODEL`) + prints env recipe | `--remote` `--api-key` `--model` `--yes` `--dry-run` `--port` `--config-path` | Both | | `omniroute setup-aider` | Aider | `~/.aider.conf.yml` (`openai-api-base` + `model: openai/`) + prints env recipe | `--remote` `--api-key` `--model` `--yes` `--dry-run` `--port` `--config-path` | Both | +| `omniroute setup-qwen` | Qwen Code | `~/.qwen/settings.json` — V4 `modelProviders.openai` array + `OMNIROUTE_API_KEY` in `~/.qwen/.env` | `--remote` `--api-key` `--model` `--yes` `--dry-run` `--port` `--config-path` `--env-path` | Both | | `omniroute launch` | Claude Code | Nothing — spawns `claude` with `ANTHROPIC_BASE_URL`/`ANTHROPIC_AUTH_TOKEN` injected | `--remote` `--api-key` `--token` `--profile` `--port` | Both | | `omniroute launch-codex` | OpenAI Codex CLI | Nothing — spawns `codex` with the `omniroute` provider injected via `-c` flags | `--remote` `--api-key` `--profile` (`-p`) `--port` | Both | @@ -100,6 +101,7 @@ opencode -m omniroute/glm/glm-5.2 "..." # Tools without auto-discovery need an explicit model: omniroute setup-aider --model glm/glm-5.2 +omniroute setup-qwen --model qwen/qwen3.8-max-preview # Preview without writing anything: omniroute setup-continue --dry-run @@ -162,6 +164,7 @@ tool expects (verified in the command source): | `setup-kilo`, `setup-roo`, `setup-continue`, `setup-crush`, `setup-cursor` | with `/v1` | Yes | | `setup-claude` (`ANTHROPIC_BASE_URL`), `launch` | root | No — Claude Code appends `/v1/messages` | | `setup-codex`, `launch-codex` (`model_providers.omniroute.base_url`) | with `/v1` | Yes | +| `setup-qwen` (`modelProviders.openai[].baseUrl`) | with `/v1` | Yes | --- diff --git a/docs/guides/REMOTE-MODE.md b/docs/guides/REMOTE-MODE.md index 05f4d559cd4..e6bdd04e737 100644 --- a/docs/guides/REMOTE-MODE.md +++ b/docs/guides/REMOTE-MODE.md @@ -216,6 +216,7 @@ context, or `--remote --api-key `): | Crush | `omniroute setup-crush` | `~/.config/crush/crush.json` — `openai-compat` provider, `base_url` **with** `/v1`, key via `$OMNIROUTE_API_KEY` | | Goose | `omniroute setup-goose` | `~/.config/goose/config.yaml` (`GOOSE_PROVIDER=openai` + `OPENAI_HOST` **without** `/v1` + `GOOSE_MODEL`) + env recipe | | Aider | `omniroute setup-aider` | `~/.aider.conf.yml` (`openai-api-base` **without** `/v1` + `model: openai/`) + env recipe (`aider --message --yes`) | +| Qwen Code | `omniroute setup-qwen` | `~/.qwen/settings.json` V4 `modelProviders.openai` entry + `OMNIROUTE_API_KEY` in `~/.qwen/.env` | ```bash # OpenCode (openai-compatible provider, all catalog models, remote VPS) diff --git a/docs/guides/SETUP_GUIDE.md b/docs/guides/SETUP_GUIDE.md index 550d62fd159..4f02548857b 100644 --- a/docs/guides/SETUP_GUIDE.md +++ b/docs/guides/SETUP_GUIDE.md @@ -180,6 +180,7 @@ omniroute setup-roo # Roo Code import + autoImport pointer omniroute setup-crush # ~/.config/crush/crush.json omniroute setup-goose # ~/.config/goose/config.yaml omniroute setup-aider # ~/.aider.conf.yml +omniroute setup-qwen # ~/.qwen/settings.json + ~/.qwen/.env ``` Each accepts `--remote --api-key ` to configure a local tool against a diff --git a/docs/reference/CLI-TOOLS.md b/docs/reference/CLI-TOOLS.md index 46ae3be954d..81cbd0c4d76 100644 --- a/docs/reference/CLI-TOOLS.md +++ b/docs/reference/CLI-TOOLS.md @@ -12,7 +12,7 @@ OmniRoute integrates with three categories of CLI tools spread across three dedi | Page | Route | Concept | Count | | -------------- | ----------------------- | ------------------------------------------------------------------------- | ------------ | -| **CLI Code's** | `/dashboard/cli-code` | Coding tools you point at OmniRoute (Client → CLI → OmniRoute → Provider) | 20 | +| **CLI Code's** | `/dashboard/cli-code` | Coding tools you point at OmniRoute (Client → CLI → OmniRoute → Provider) | 21 | | **CLI Agents** | `/dashboard/cli-agents` | Autonomous agents you point at OmniRoute (same flow, broader scope) | 6 | | **ACP Agents** | `/dashboard/acp-agents` | CLIs that OmniRoute spawns as backend via stdio/ACP (reverse flow) | see registry | @@ -55,7 +55,7 @@ OmniRoute (local or remote) and writes the tool's own config on your machine: omniroute setup-codex omniroute setup-claude omniroute setup-opencode omniroute setup-cline omniroute setup-kilo omniroute setup-continue omniroute setup-cursor omniroute setup-roo omniroute setup-crush -omniroute setup-goose omniroute setup-aider +omniroute setup-goose omniroute setup-qwen omniroute setup-aider ``` Each accepts `--remote --api-key ` (configure a local tool against a @@ -90,7 +90,7 @@ Entries with `baseUrlSupport: "none"` are **not shown** in the dashboard pages --- -## 1. CLI Code's Catalog (24 tools) +## 1. CLI Code's Catalog (25 tools) All tools that appear in `/dashboard/cli-code`. Those with `baseUrlSupport: none` are wired through MITM or a manual guide instead of a custom base URL: @@ -115,6 +115,7 @@ All tools that appear in `/dashboard/cli-code`. Those with `baseUrlSupport: none | pi | Pi (pi-coding-agent) | M. Zechner (OSS) | full | custom | false | | grok-build | Grok Build | xAI | full | custom | false | | crush | Crush | OSS (Charm) | full | custom | false | +| qwen | Qwen Code | Alibaba | full | guide | true | | cursor | Cursor | Anysphere | none | guide | false | | antigravity | Antigravity | Google | none | mitm | false | | hermes | Hermes | Nous Research | none | guide | false | @@ -209,6 +210,7 @@ New tools with `configType: "custom"` have dedicated settings API routes: | `POST /api/cli-tools/smelt-settings` | Smelt | | `POST /api/cli-tools/pi-settings` | Pi coding agent | | `POST /api/cli-tools/grok-build-settings` | Grok Build (~/.grok/config.toml, `[model.omniroute]`) | +| `POST /api/cli-tools/qwen-settings` | Qwen Code (`~/.qwen/settings.json` + dedicated `.env` key) | All routes use `sanitizeErrorMessage()` for error responses (Hard Rule #12). @@ -299,6 +301,9 @@ npm install -g cline # KiloCode npm install -g kilocode +# Qwen Code +npm install -g @qwen-code/qwen-code + # Aider pip install aider-chat diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index a21ebd22fbf..84ce7d20cae 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -364,6 +364,7 @@ Controls how OmniRoute discovers and launches CLI sidecars (Claude Code, Codex, | `CLI_CLINE_BIN` | `cline` | `src/shared/services/cliRuntime.ts` | Custom path to Cline CLI binary. | | `CLI_CONTINUE_BIN` | `cn` | `src/shared/services/cliRuntime.ts` | Custom path to Continue CLI binary. | | `CLI_QODER_BIN` | `qoder` | `src/shared/services/cliRuntime.ts` | Custom path to Qoder CLI binary. | +| `CLI_QWEN_BIN` | `qwen` | `src/shared/services/cliRuntime.ts` | Custom path to the Qwen Code CLI binary. | | `CLI_DEVIN_BIN` | `devin` | `open-sse/executors/devin-cli.ts` | Custom path to the Devin CLI binary (v3.8.0). Used by the Windsurf/Devin executor. | | `AUGGIE_BIN` | `auggie` | `open-sse/executors/auggie.ts` | Absolute-path override for the Augment (Auggie) CLI binary used by the local `auggie` provider. Falls back to `CLI_AUGGIE_BIN`, then a PATH lookup. | | `CLI_AUGGIE_BIN` | `auggie` | `open-sse/executors/auggie.ts` | Alias override for the Augment (Auggie) CLI binary path (checked after `AUGGIE_BIN`). | diff --git a/scripts/check/check-route-guard-membership.ts b/scripts/check/check-route-guard-membership.ts index cb5ca34c2d9..14e59682781 100644 --- a/scripts/check/check-route-guard-membership.ts +++ b/scripts/check/check-route-guard-membership.ts @@ -52,6 +52,7 @@ export const SPAWN_CAPABLE_ROUTE_ROOTS: ReadonlyArray = [ "src/app/api/skills/collect", // Skill Collector CLI detection: GET .../detect spawns a child process per CLI_TOOL_IDS entry via getCliRuntimeStatus() (Hard Rules #15 + #17, PR #6294 review) "src/app/api/cli-tools/forge-settings", // GET calls getCliRuntimeStatus() to detect the `forge` CLI install (Hard Rules #15 + #17, #7263) "src/app/api/cli-tools/jcode-settings", // GET calls getCliRuntimeStatus() to detect the `jcode` CLI install (Hard Rules #15 + #17, #7263) + "src/app/api/cli-tools/qwen-settings", // GET calls getCliRuntimeStatus("qwen") and writes local ~/.qwen config files (Hard Rules #15 + #17) ]; // Frozen pre-existing exceptions: spawn-capable routes NOT yet classified diff --git a/src/app/(dashboard)/dashboard/cli-code/components/DefaultToolCard.tsx b/src/app/(dashboard)/dashboard/cli-code/components/DefaultToolCard.tsx index b833b0301f8..dc995416903 100644 --- a/src/app/(dashboard)/dashboard/cli-code/components/DefaultToolCard.tsx +++ b/src/app/(dashboard)/dashboard/cli-code/components/DefaultToolCard.tsx @@ -48,6 +48,7 @@ export default function DefaultToolCard({ ); const isMultiModelTool = tool.modelSelectionMode === "multiple"; const usesOpenCodePreview = tool.previewConfigMode === "opencode"; + const usesQwenCodePreview = tool.previewConfigMode === "qwen"; const selectedKeyObj = apiKeys?.find((k) => k.id === selectedApiKeyId); const resolveApiKeyValue = useCallback( @@ -187,13 +188,16 @@ export default function DefaultToolCard({ }, [isExpanded, runtimeStatus, t, toolId]); const replaceVars = useCallback( - (text) => { + (text, modelOverride = "") => { const keyToUse = resolveApiKeyValue(); return text .replace(/\{\{baseUrl\}\}/g, baseUrlWithV1) .replace(/\{\{apiKey\}\}/g, keyToUse) - .replace(/\{\{model\}\}/g, getSelectedModelLabels()[0] || t("modelPlaceholder")); + .replace( + /\{\{model\}\}/g, + modelOverride || getSelectedModelLabels()[0] || t("modelPlaceholder") + ); }, [baseUrlWithV1, getSelectedModelLabels, resolveApiKeyValue, t] ); @@ -211,6 +215,9 @@ export default function DefaultToolCard({ const getRenderedCodeBlock = useCallback(() => { if (!tool.codeBlock?.code) return ""; + if (usesQwenCodePreview) { + return replaceVars(tool.codeBlock.code, getSelectedModels()[0]); + } if (!usesOpenCodePreview) return replaceVars(tool.codeBlock.code); const keyToUse = resolveApiKeyValue(); @@ -233,6 +240,7 @@ export default function DefaultToolCard({ resolveApiKeyValue, tool.codeBlock?.code, usesOpenCodePreview, + usesQwenCodePreview, ]); const handleSelectModel = (model) => { @@ -265,7 +273,11 @@ export default function DefaultToolCard({ // (#523) Prefer keyId lookup so the backend writes the real key to disk. const selectedKeyId = selectedApiKeyId?.trim() || null; - const res = await fetch(`/api/cli-tools/guide-settings/${toolId}`, { + const saveEndpoint = + toolId === "qwen" + ? "/api/cli-tools/qwen-settings" + : `/api/cli-tools/guide-settings/${toolId}`; + const res = await fetch(saveEndpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -296,7 +308,7 @@ export default function DefaultToolCard({ }; // Check if this tool supports direct config file write - const supportsDirectSave = ["continue", "opencode"].includes(toolId); + const supportsDirectSave = ["continue", "opencode", "qwen"].includes(toolId); const renderApiKeySelector = () => { return ( diff --git a/src/app/api/cli-tools/all-statuses/route.ts b/src/app/api/cli-tools/all-statuses/route.ts index 16a7e0a13a6..3b78a3f00df 100644 --- a/src/app/api/cli-tools/all-statuses/route.ts +++ b/src/app/api/cli-tools/all-statuses/route.ts @@ -11,6 +11,7 @@ import { CLI_TOOLS } from "@/shared/constants/cliTools"; import { getCliRuntimeStatus, getCliPrimaryConfigPath } from "@/shared/services/cliRuntime"; import { getAllCliToolLastConfigured } from "@/lib/db/cliToolState"; import { checkToolConfigStatus } from "@/lib/cliTools/checkToolConfigStatus"; +import { findOmniRouteQwenCodeModel } from "@/shared/services/qwenCodeConfig"; import { getCached, setCached } from "@/lib/cliTools/batchStatusCache"; import type { ToolBatchStatus, ToolBatchStatusMap } from "@/shared/types/cliBatchStatus"; @@ -42,6 +43,10 @@ async function extractEndpointFromConfig( const env = config.env as Record | undefined; return (env?.ANTHROPIC_BASE_URL as string | undefined) ?? null; } + case "qwen": { + const managed = findOmniRouteQwenCodeModel(config); + return typeof managed?.baseUrl === "string" ? managed.baseUrl : null; + } case "cline": return (config.openAiBaseUrl as string | undefined) ?? null; case "droid": diff --git a/src/app/api/cli-tools/backups/route.ts b/src/app/api/cli-tools/backups/route.ts index 71549d8ce69..a183a261ba7 100644 --- a/src/app/api/cli-tools/backups/route.ts +++ b/src/app/api/cli-tools/backups/route.ts @@ -8,7 +8,7 @@ import { cliBackupMutationSchema } from "@/shared/validation/schemas"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error"; -const VALID_TOOLS = ["claude", "codex", "droid", "openclaw", "cline", "kilo"]; +const VALID_TOOLS = ["claude", "codex", "droid", "openclaw", "cline", "kilo", "qwen"]; // GET /api/cli-tools/backups?tool=claude — list backups export async function GET(request) { diff --git a/src/app/api/cli-tools/qwen-settings/route.ts b/src/app/api/cli-tools/qwen-settings/route.ts new file mode 100644 index 00000000000..da8240b8f93 --- /dev/null +++ b/src/app/api/cli-tools/qwen-settings/route.ts @@ -0,0 +1,222 @@ +"use server"; + +import fs from "node:fs/promises"; +import path from "node:path"; +import pino from "pino"; +import { NextResponse } from "next/server"; + +import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error"; + +import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth"; +import { getApiKeyById } from "@/lib/db/apiKeys"; +import { deleteCliToolLastConfigured, saveCliToolLastConfigured } from "@/lib/db/cliToolState"; +import { createMultiBackup } from "@/shared/services/backupService"; +import { + hasOmniRouteQwenCodeConfig, + mergeQwenCodeEnv, + mergeQwenCodeSettings, + removeQwenCodeEnv, + removeQwenCodeSettings, +} from "@/shared/services/qwenCodeConfig"; +import { + ensureCliConfigWriteAllowed, + getCliConfigPaths, + getCliRuntimeStatus, +} from "@/shared/services/cliRuntime"; +import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; +import { cliModelConfigSchema } from "@/shared/validation/schemas"; + +const logger = pino({ name: "qwen-code-settings-api" }); + +const getPaths = (): { settings: string; env: string } => { + const paths = getCliConfigPaths("qwen"); + if (!paths?.settings || !paths.env) throw new Error("Qwen Code config paths are unavailable"); + return { settings: paths.settings, env: paths.env }; +}; + +const readTextIfPresent = async (filePath: string): Promise => { + try { + return await fs.readFile(filePath, "utf8"); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") return ""; + throw error; + } +}; + +const fileExists = async (filePath: string): Promise => { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } +}; + +const readSettings = async (filePath: string): Promise> => { + const text = await readTextIfPresent(filePath); + if (!text.trim()) return {}; + + const parsed: unknown = JSON.parse(text); + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new Error("Qwen Code settings.json must contain a JSON object"); + } + return parsed as Record; +}; + +const writeAtomic = async (filePath: string, content: string, mode?: number): Promise => { + const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`; + try { + await fs.writeFile(tempPath, content, { encoding: "utf8", mode }); + if (mode !== undefined) await fs.chmod(tempPath, mode); + await fs.rename(tempPath, filePath); + } catch (error) { + await fs.unlink(tempPath).catch(() => {}); + throw error; + } +}; + +export async function GET(request: Request): Promise { + const authError = await requireCliToolsAuth(request); + if (authError) return authError; + + try { + const configPaths = getPaths(); + const [runtime, settings] = await Promise.all([ + getCliRuntimeStatus("qwen"), + readSettings(configPaths.settings), + ]); + + return NextResponse.json({ + ...runtime, + settings, + hasOmniRoute: hasOmniRouteQwenCodeConfig(settings), + settingsPath: configPaths.settings, + envPath: configPaths.env, + }); + } catch (error) { + logger.error({ err: error }, "Failed to read Qwen Code settings"); + return NextResponse.json( + { error: sanitizeErrorMessage(error instanceof Error ? error.message : String(error)) }, + { status: 500 } + ); + } +} + +export async function POST(request: Request): Promise { + const authError = await requireCliToolsAuth(request); + if (authError) return authError; + + let rawBody: unknown; + try { + rawBody = await request.json(); + } catch { + return NextResponse.json( + { + error: { + message: "Invalid request", + details: [{ field: "body", message: "Invalid JSON body" }], + }, + }, + { status: 400 } + ); + } + + const validation = validateBody(cliModelConfigSchema, rawBody); + if (isValidationFailure(validation)) { + return NextResponse.json({ error: validation.error }, { status: 400 }); + } + + const writeGuard = ensureCliConfigWriteAllowed(); + if (writeGuard) return NextResponse.json({ error: writeGuard }, { status: 403 }); + + try { + const configPaths = getPaths(); + const bodyRecord = rawBody as Record; + const keyId = typeof bodyRecord.keyId === "string" ? bodyRecord.keyId.trim() : ""; + let apiKey = validation.data.apiKey || ""; + if (keyId) { + const keyRecord = await getApiKeyById(keyId); + if (keyRecord?.key) apiKey = keyRecord.key; + } + if (!apiKey) apiKey = "sk_omniroute"; + + const [existingSettings, existingEnv] = await Promise.all([ + readSettings(configPaths.settings), + readTextIfPresent(configPaths.env), + ]); + const nextSettings = mergeQwenCodeSettings(existingSettings, { + baseUrl: validation.data.baseUrl, + model: validation.data.model, + }); + const nextEnv = mergeQwenCodeEnv(existingEnv, apiKey); + + await fs.mkdir(path.dirname(configPaths.settings), { recursive: true, mode: 0o700 }); + await createMultiBackup("qwen", [configPaths.settings, configPaths.env]); + await writeAtomic(configPaths.settings, `${JSON.stringify(nextSettings, null, 2)}\n`); + await writeAtomic(configPaths.env, nextEnv, 0o600); + + try { + saveCliToolLastConfigured("qwen"); + } catch (error) { + logger.warn({ err: error }, "Failed to save Qwen Code configuration timestamp"); + } + + return NextResponse.json({ + success: true, + message: "Qwen Code now routes through OmniRoute", + settingsPath: configPaths.settings, + envPath: configPaths.env, + }); + } catch (error) { + logger.error({ err: error }, "Failed to update Qwen Code settings"); + return NextResponse.json( + { error: sanitizeErrorMessage(error instanceof Error ? error.message : String(error)) }, + { status: 500 } + ); + } +} + +export async function DELETE(request: Request): Promise { + const authError = await requireCliToolsAuth(request); + if (authError) return authError; + + const writeGuard = ensureCliConfigWriteAllowed(); + if (writeGuard) return NextResponse.json({ error: writeGuard }, { status: 403 }); + + try { + const configPaths = getPaths(); + const [settingsExists, envExists, existingSettings, existingEnv] = await Promise.all([ + fileExists(configPaths.settings), + fileExists(configPaths.env), + readSettings(configPaths.settings), + readTextIfPresent(configPaths.env), + ]); + const nextSettings = removeQwenCodeSettings(existingSettings); + const nextEnv = removeQwenCodeEnv(existingEnv); + + if (settingsExists || envExists) { + await createMultiBackup("qwen", [configPaths.settings, configPaths.env]); + if (settingsExists) { + await writeAtomic(configPaths.settings, `${JSON.stringify(nextSettings, null, 2)}\n`); + } + if (envExists) await writeAtomic(configPaths.env, nextEnv, 0o600); + } + + try { + deleteCliToolLastConfigured("qwen"); + } catch (error) { + logger.warn({ err: error }, "Failed to clear Qwen Code configuration timestamp"); + } + + return NextResponse.json({ + success: true, + message: "OmniRoute settings removed from Qwen Code", + }); + } catch (error) { + logger.error({ err: error }, "Failed to reset Qwen Code settings"); + return NextResponse.json( + { error: sanitizeErrorMessage(error instanceof Error ? error.message : String(error)) }, + { status: 500 } + ); + } +} diff --git a/src/app/api/cli-tools/status/route.ts b/src/app/api/cli-tools/status/route.ts index e5ae1675a44..3a2372f3fce 100644 --- a/src/app/api/cli-tools/status/route.ts +++ b/src/app/api/cli-tools/status/route.ts @@ -61,6 +61,7 @@ export async function GET(request: Request) { "cline", "kilo", "hermes", + "qwen", ]; await Promise.all( diff --git a/src/app/api/skills/collect/detect/route.ts b/src/app/api/skills/collect/detect/route.ts index 6975dd49322..6dffdecd641 100644 --- a/src/app/api/skills/collect/detect/route.ts +++ b/src/app/api/skills/collect/detect/route.ts @@ -31,6 +31,7 @@ const CODING_TOOL_KEYWORDS: Record = { openclaw: ["openclaw"], droid: ["droid", "factory-ai"], continue: ["continue"], + qwen: ["qwen", "qwen-code"], antigravity: ["antigravity"], windsurf: ["windsurf"], devin: ["devin", "cognition"], diff --git a/src/app/api/skills/collect/install/route.ts b/src/app/api/skills/collect/install/route.ts index 33cd417e1ba..da220da4f9a 100644 --- a/src/app/api/skills/collect/install/route.ts +++ b/src/app/api/skills/collect/install/route.ts @@ -43,6 +43,7 @@ const CODING_TOOL_PATHS: Record = { openclaw: "~/.openclaw/skills/{category}", droid: "~/.droid/skills/{category}", continue: "~/.continue/skills/{category}", + qwen: "~/.qwen/skills/{category}", }; function inferCategory(skillName: string, description: string): string { diff --git a/src/lib/acp/manager.ts b/src/lib/acp/manager.ts index e95f1d0f194..055fc1e222c 100644 --- a/src/lib/acp/manager.ts +++ b/src/lib/acp/manager.ts @@ -47,7 +47,7 @@ export class AcpManager extends EventEmitter { args: string[] = [], env: Record = {} ): AcpSession { - const ALLOWED_AGENTS = ["claude", "codex", "gemini"]; + const ALLOWED_AGENTS = ["claude", "codex", "gemini", "qwen"]; if (!ALLOWED_AGENTS.includes(agentId)) { throw new Error(`Unknown agent: ${agentId}`); } diff --git a/src/lib/acp/registry.ts b/src/lib/acp/registry.ts index 07300475f67..6945762409b 100644 --- a/src/lib/acp/registry.ts +++ b/src/lib/acp/registry.ts @@ -5,7 +5,7 @@ * and running version commands. Used to offer ACP transport as an alternative * to the HTTP proxy method. * - * Supports 12 built-in agents + user-defined custom agents from settings. + * Supports built-in agents + user-defined custom agents from settings. * * Reference: https://github.com/iOfficeAI/AionUi (auto-detects CLI agents) */ @@ -114,6 +114,15 @@ const AGENT_DEFINITIONS: Omit[] = [ spawnArgs: [], protocol: "stdio", }, + { + id: "qwen", + name: "Qwen Code", + binary: "qwen", + versionCommand: "qwen --version", + providerAlias: "qwen-code", + spawnArgs: ["--acp"], + protocol: "stdio", + }, { id: "forge", name: "ForgeCode", diff --git a/src/lib/cliTools/checkToolConfigStatus.ts b/src/lib/cliTools/checkToolConfigStatus.ts index 2425d86c39e..75fcc5237a6 100644 --- a/src/lib/cliTools/checkToolConfigStatus.ts +++ b/src/lib/cliTools/checkToolConfigStatus.ts @@ -2,6 +2,7 @@ import fs from "fs/promises"; import { getCliPrimaryConfigPath } from "@/shared/services/cliRuntime"; +import { hasOmniRouteQwenCodeConfig } from "@/shared/services/qwenCodeConfig"; import { getRuntimePorts } from "@/lib/runtime/ports"; const { apiPort } = getRuntimePorts(); @@ -67,6 +68,8 @@ export async function checkToolConfigStatus( return (config?.env as Record)?.ANTHROPIC_BASE_URL ? "configured" : "not_configured"; + case "qwen": + return hasOmniRouteQwenCodeConfig(config) ? "configured" : "not_configured"; case "droid": case "openclaw": case "cline": diff --git a/src/server/authz/routeGuard.ts b/src/server/authz/routeGuard.ts index 66ef33afa5b..54b78d12897 100644 --- a/src/server/authz/routeGuard.ts +++ b/src/server/authz/routeGuard.ts @@ -34,6 +34,7 @@ export const LOCAL_ONLY_API_PREFIXES: ReadonlyArray = [ "/api/cli-tools/grok-build-settings", // GET calls getCliRuntimeStatus("grok-build"), which spawns a child process to locate + healthcheck the `grok` binary — same transitive-spawn surface that classified /api/skills/collect/ (Hard Rules #15 + #17). Writing ~/.grok/config.toml is inherently a local-machine operation, so loopback-only costs no real capability. "/api/cli-tools/forge-settings", // spawns via getCliRuntimeStatus() to detect the `forge` CLI install (Hard Rules #15 + #17, #7263) "/api/cli-tools/jcode-settings", // spawns via getCliRuntimeStatus() to detect the `jcode` CLI install (Hard Rules #15 + #17, #7263) + "/api/cli-tools/qwen-settings", // GET probes the local `qwen` binary; writes target ~/.qwen config files (Hard Rules #15 + #17) "/api/services/", // T-10: embedded service lifecycle (spawn child processes) "/dashboard/providers/services/", // T-07: reverse proxy to embedded service UIs "/api/copilot/", // unauthenticated LLM driver — CLI-only by default; admins can opt-in to remote access via manage-scope bypass diff --git a/src/shared/constants/cliTools.ts b/src/shared/constants/cliTools.ts index 57714b6fb56..d8933f3e708 100644 --- a/src/shared/constants/cliTools.ts +++ b/src/shared/constants/cliTools.ts @@ -399,6 +399,59 @@ export const CLI_TOOLS: Record = { { step: 4, title: "Select Model", type: "modelSelector" }, ], }, + qwen: { + id: "qwen", + name: "Qwen Code", + icon: "psychology", + color: "#10B981", + description: "Qwen Code CLI — current V4 OpenAI-compatible model provider via OmniRoute", + docsUrl: "https://qwenlm.github.io/qwen-code-docs/en/users/configuration/model-providers/", + configType: "guide", + category: "code", + vendor: "Alibaba", + acpSpawnable: true, + baseUrlSupport: "full", + defaultCommand: "qwen", + previewConfigMode: "qwen", + notes: [ + { + type: "info", + text: "OmniRoute is registered under modelProviders.openai using Qwen Code's current bare-array V4 format.", + }, + { + type: "info", + text: "The API key is stored only as OMNIROUTE_API_KEY in ~/.qwen/.env, leaving your existing provider credentials untouched.", + }, + ], + guideSteps: [ + { step: 1, title: "Install Qwen Code", desc: "npm install -g @qwen-code/qwen-code" }, + { step: 2, title: "API Key", type: "apiKeySelector" }, + { step: 3, title: "Base URL", value: "{{baseUrl}}", copyable: true }, + { step: 4, title: "Select Model", type: "modelSelector" }, + { + step: 5, + title: "Save Config", + desc: "Write the modelProviders entry and dedicated .env key without replacing other Qwen Code settings.", + }, + ], + codeBlock: { + language: "json", + code: `{ + "modelProviders": { + "openai": [ + { + "id": "{{model}}", + "name": "{{model}} (OmniRoute)", + "envKey": "OMNIROUTE_API_KEY", + "baseUrl": "{{baseUrl}}" + } + ] + }, + "security": { "auth": { "selectedType": "openai" } }, + "model": { "name": "{{model}}", "baseUrl": "{{baseUrl}}" } +}`, + }, + }, custom: { id: "custom", name: "Custom CLI", diff --git a/src/shared/constants/spawnCapablePrefixes.ts b/src/shared/constants/spawnCapablePrefixes.ts index 87c2186a1da..62ada570aae 100644 --- a/src/shared/constants/spawnCapablePrefixes.ts +++ b/src/shared/constants/spawnCapablePrefixes.ts @@ -25,6 +25,7 @@ */ export const SPAWN_CAPABLE_PREFIXES: ReadonlyArray = [ "/api/cli-tools/runtime/", + "/api/cli-tools/qwen-settings", // GET probes the Qwen Code binary; the route also mutates local ~/.qwen files "/api/services/", // T-10: can run npm install + spawn node processes "/api/tools/agent-bridge/", // start/stop MITM server + DNS edits (Hard Rules #15 + #17) "/api/tools/traffic-inspector/", // http-proxy listener + system proxy (Hard Rules #15 + #17) diff --git a/src/shared/schemas/cliCatalog.ts b/src/shared/schemas/cliCatalog.ts index 2f76b1bbd28..27ab19e3c99 100644 --- a/src/shared/schemas/cliCatalog.ts +++ b/src/shared/schemas/cliCatalog.ts @@ -63,7 +63,7 @@ export const CliCatalogSchema = z.record(CliCatalogEntrySchema); // +1 (2026-07-02): "codewhale" added as a dual entry alongside "deepseek-tui" // (CodeWhale is the actively-maintained successor to DeepSeek TUI). // +1 (grok-build, decolua/9router#2571): xAI Grok Build TUI coding agent. -export const EXPECTED_CODE_COUNT = 20; +export const EXPECTED_CODE_COUNT = 21; // +2 (#6318): "omp" (Oh My Pi) and "letta" (Letta CLI) added as agent entries. // Note: #6318 originally also shipped duplicate "pi"/"jcode"/"codewhale" entries — // those tools were already delivered by a separate PR, so only omp+letta landed here. diff --git a/src/shared/services/cliRuntime.ts b/src/shared/services/cliRuntime.ts index c2471fc65a8..8538ec8878c 100644 --- a/src/shared/services/cliRuntime.ts +++ b/src/shared/services/cliRuntime.ts @@ -168,6 +168,16 @@ const CLI_TOOLS: Record = { auth: ".qoder/auth.json", }, }, + qwen: { + defaultCommand: "qwen", + envBinKey: "CLI_QWEN_BIN", + requiresBinary: true, + healthcheckTimeoutMs: 12000, + paths: { + settings: ".qwen/settings.json", + env: ".qwen/.env", + }, + }, // ── Plan 14 — new "custom" configType tools ─────────────────────────────── forge: { defaultCommand: "forge", @@ -568,6 +578,7 @@ export const getKnownToolPaths = (toolId: string): string[] => { ["qodercli.cmd", "qodercli"], ["qodercli.exe", "qodercli"], ], + qwen: [["qwen.cmd", "qwen"]], devin: [ ["devin.exe", "devin"], ["devin.cmd", "devin"], diff --git a/src/shared/services/qwenCodeConfig.ts b/src/shared/services/qwenCodeConfig.ts new file mode 100644 index 00000000000..e90f6a686b1 --- /dev/null +++ b/src/shared/services/qwenCodeConfig.ts @@ -0,0 +1,252 @@ +type JsonRecord = Record; + +export const QWEN_CODE_ENV_KEY = "OMNIROUTE_API_KEY"; + +const LEGACY_ENV_KEYS = new Set(["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GEMINI_API_KEY"]); + +const isRecord = (value: unknown): value is JsonRecord => + typeof value === "object" && value !== null && !Array.isArray(value); + +const cloneRecord = (value: unknown): JsonRecord => (isRecord(value) ? { ...value } : {}); + +export const normalizeQwenCodeBaseUrl = (value: unknown): string => { + const baseUrl = String(value || "") + .trim() + .replace(/\/+$/, ""); + if (!baseUrl) return ""; + return baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`; +}; + +/** + * Identifies only entries owned by OmniRoute. Generic custom endpoints are not + * considered managed: users can keep any other OpenAI-compatible provider. + */ +export const isManagedQwenCodeModel = (value: unknown): boolean => { + if (!isRecord(value)) return false; + if (value.envKey === QWEN_CODE_ENV_KEY || value.id === "omniroute") return true; + + return ( + typeof value.name === "string" && + value.name.endsWith(" (OmniRoute)") && + typeof value.envKey === "string" && + LEGACY_ENV_KEYS.has(value.envKey) + ); +}; + +const unwrapProviderModels = (value: unknown): unknown => { + if (Array.isArray(value)) return [...value]; + if (isRecord(value) && Array.isArray(value.models)) return [...value.models]; + return value; +}; + +const getProviderMap = (value: unknown): JsonRecord => { + if (Array.isArray(value)) { + const migrated: Record = {}; + for (const entry of value) { + if (!isRecord(entry) || typeof entry.id !== "string") continue; + const providerId = typeof entry.authType === "string" ? entry.authType : "openai"; + const model = { ...entry }; + delete model.authType; + (migrated[providerId] ||= []).push(model); + } + return migrated; + } + if (!isRecord(value)) return {}; + return Object.fromEntries( + Object.entries(value).map(([providerId, models]) => [providerId, unwrapProviderModels(models)]) + ); +}; + +const filterManagedModels = (value: unknown): { value: unknown; removed: JsonRecord[] } => { + if (!Array.isArray(value)) return { value, removed: [] }; + + const removed = value.filter(isManagedQwenCodeModel).filter(isRecord); + return { + value: value.filter((entry) => !isManagedQwenCodeModel(entry)), + removed, + }; +}; + +const removeEmptyObject = (parent: JsonRecord, key: string): void => { + if (isRecord(parent[key]) && Object.keys(parent[key]).length === 0) delete parent[key]; +}; + +export type QwenCodeConfigInput = { + baseUrl: string; + model: string; + modelName?: string; +}; + +export const buildQwenCodeModel = ({ + baseUrl, + model, + modelName, +}: QwenCodeConfigInput): JsonRecord => { + const normalizedBaseUrl = normalizeQwenCodeBaseUrl(baseUrl); + const normalizedModel = String(model || "").trim(); + if (!normalizedBaseUrl || !normalizedModel) { + throw new Error("Qwen Code requires a base URL and model"); + } + + return { + id: normalizedModel, + name: `${String(modelName || normalizedModel).trim()} (OmniRoute)`, + envKey: QWEN_CODE_ENV_KEY, + baseUrl: normalizedBaseUrl, + }; +}; + +/** + * Merge one OmniRoute model into Qwen Code's current V4 settings contract. + * `modelProviders.openai` is a bare ModelConfig[] array. Other settings and + * user-owned provider entries are preserved. + */ +export const mergeQwenCodeSettings = ( + existing: unknown, + input: QwenCodeConfigInput +): JsonRecord => { + const next = cloneRecord(existing); + const modelEntry = buildQwenCodeModel(input); + const modelProviders = getProviderMap(next.modelProviders); + + for (const [providerId, value] of Object.entries(modelProviders)) { + const filtered = filterManagedModels(value); + modelProviders[providerId] = filtered.value; + if (Array.isArray(filtered.value) && filtered.value.length === 0 && providerId !== "openai") { + delete modelProviders[providerId]; + } + } + + const openaiModels = Array.isArray(modelProviders.openai) ? modelProviders.openai : []; + modelProviders.openai = [...openaiModels, modelEntry]; + next.modelProviders = modelProviders; + + const security = cloneRecord(next.security); + const auth = cloneRecord(security.auth); + const previousAuthBaseUrl = normalizeQwenCodeBaseUrl(auth.baseUrl); + const replacingLegacyAuth = + auth.selectedType === "openai" && previousAuthBaseUrl === modelEntry.baseUrl; + + auth.selectedType = "openai"; + if (replacingLegacyAuth) { + delete auth.apiKey; + delete auth.baseUrl; + } + security.auth = auth; + next.security = security; + + next.model = { + ...cloneRecord(next.model), + name: modelEntry.id, + baseUrl: modelEntry.baseUrl, + }; + + if (next.selectedProvider === "omniroute") delete next.selectedProvider; + + return next; +}; + +export const findOmniRouteQwenCodeModel = (settings: unknown): JsonRecord | undefined => { + if (!isRecord(settings)) return undefined; + const providers = settings.modelProviders; + if (Array.isArray(providers)) + return providers.find(isManagedQwenCodeModel) as JsonRecord | undefined; + if (!isRecord(providers)) return undefined; + + for (const value of Object.values(providers)) { + const models = unwrapProviderModels(value); + if (!Array.isArray(models)) continue; + const managed = models.find(isManagedQwenCodeModel); + if (isRecord(managed)) return managed; + } + return undefined; +}; + +export const hasOmniRouteQwenCodeConfig = (settings: unknown): boolean => + findOmniRouteQwenCodeModel(settings) !== undefined; + +/** Remove only OmniRoute-owned Qwen Code entries and selection state. */ +export const removeQwenCodeSettings = (existing: unknown): JsonRecord => { + const next = cloneRecord(existing); + const originalProviders = next.modelProviders; + const removed: JsonRecord[] = []; + + if (Array.isArray(originalProviders)) { + const filtered = filterManagedModels(originalProviders); + next.modelProviders = filtered.value; + removed.push(...filtered.removed); + if ((filtered.value as unknown[]).length === 0) delete next.modelProviders; + } else if (isRecord(originalProviders)) { + const providers = { ...originalProviders }; + for (const [providerId, rawValue] of Object.entries(providers)) { + const unwrapped = unwrapProviderModels(rawValue); + const filtered = filterManagedModels(unwrapped); + removed.push(...filtered.removed); + if (Array.isArray(filtered.value) && filtered.value.length === 0) { + delete providers[providerId]; + } else { + providers[providerId] = filtered.value; + } + } + if (Object.keys(providers).length === 0) delete next.modelProviders; + else next.modelProviders = providers; + } + + const model = cloneRecord(next.model); + const selectedMatchesRemoved = removed.some( + (entry) => + entry.id === model.name && + (!model.baseUrl || + normalizeQwenCodeBaseUrl(entry.baseUrl) === normalizeQwenCodeBaseUrl(model.baseUrl)) + ); + + if (selectedMatchesRemoved) { + delete model.name; + delete model.baseUrl; + if (Object.keys(model).length === 0) delete next.model; + else next.model = model; + + const security = cloneRecord(next.security); + const auth = cloneRecord(security.auth); + const remainingOpenai = isRecord(next.modelProviders) ? next.modelProviders.openai : undefined; + const authBaseUrl = normalizeQwenCodeBaseUrl(auth.baseUrl); + const authMatchesRemoved = removed.some( + (entry) => normalizeQwenCodeBaseUrl(entry.baseUrl) === authBaseUrl + ); + if (authMatchesRemoved) { + delete auth.apiKey; + delete auth.baseUrl; + } + if (auth.selectedType === "openai" && !Array.isArray(remainingOpenai)) { + delete auth.selectedType; + } + if (Object.keys(auth).length === 0) delete security.auth; + else security.auth = auth; + if (Object.keys(security).length === 0) delete next.security; + else next.security = security; + } + + if (next.selectedProvider === "omniroute") delete next.selectedProvider; + removeEmptyObject(next, "modelProviders"); + return next; +}; + +const OWNED_ENV_LINE = /^\s*(?:export\s+)?OMNIROUTE_API_KEY\s*=/; + +export const mergeQwenCodeEnv = (existing: unknown, apiKey: unknown): string => { + const lines = String(existing || "") + .split(/\r?\n/) + .filter((line) => !OWNED_ENV_LINE.test(line)); + + while (lines.length > 0 && lines.at(-1) === "") lines.pop(); + lines.push(`${QWEN_CODE_ENV_KEY}=${JSON.stringify(String(apiKey || "sk_omniroute"))}`); + return `${lines.join("\n")}\n`; +}; + +export const removeQwenCodeEnv = (existing: unknown): string => { + const lines = String(existing || "") + .split(/\r?\n/) + .filter((line) => !OWNED_ENV_LINE.test(line)); + while (lines.length > 0 && lines.at(-1) === "") lines.pop(); + return lines.length > 0 ? `${lines.join("\n")}\n` : ""; +}; diff --git a/tests/unit/acp-registry.test.ts b/tests/unit/acp-registry.test.ts index 75051918ae4..deb14d709b5 100644 --- a/tests/unit/acp-registry.test.ts +++ b/tests/unit/acp-registry.test.ts @@ -3,6 +3,7 @@ import assert from "node:assert/strict"; const { resolveVersionProbe, shouldUseShellForVersionProbe } = await import("../../src/lib/acp/registry.ts"); +const { getAgentById } = await import("../../src/lib/acp/registry.ts"); test("resolveVersionProbe parses quoted binary paths without shell semantics", () => { const probe = resolveVersionProbe( @@ -40,3 +41,11 @@ test("shouldUseShellForVersionProbe preserves Windows npm wrapper detection", () assert.equal(shouldUseShellForVersionProbe("C:\\Tools\\claude.exe", "win32"), false); assert.equal(shouldUseShellForVersionProbe("codex", "linux"), false); }); + +test("Qwen Code is registered with its upstream ACP mode", () => { + const qwen = getAgentById("qwen"); + assert.ok(qwen); + assert.deepEqual(qwen.spawnArgs, ["--acp"]); + assert.equal(qwen.providerAlias, "qwen-code"); + assert.equal(qwen.protocol, "stdio"); +}); diff --git a/tests/unit/check-tool-config-status.test.ts b/tests/unit/check-tool-config-status.test.ts index 93e4e55d76c..598bdd53548 100644 --- a/tests/unit/check-tool-config-status.test.ts +++ b/tests/unit/check-tool-config-status.test.ts @@ -157,6 +157,44 @@ test("kilo: returns 'not_configured' when no OmniRoute markers present", async ( assert.equal(result, "not_configured"); }); +// ── Qwen Code ──────────────────────────────────────────────────────────────── + +test("qwen: returns 'configured' only for an OmniRoute-managed model entry", async () => { + const configPath = await writeTempFile( + "settings.json", + JSON.stringify({ + modelProviders: { + openai: [ + { + id: "model-id", + envKey: "OMNIROUTE_API_KEY", + baseUrl: "http://localhost:20128/v1", + }, + ], + }, + }) + ); + assert.equal(await checkToolConfigStatus("qwen", configPath), "configured"); +}); + +test("qwen: does not misclassify an unrelated custom OpenAI endpoint", async () => { + const configPath = await writeTempFile( + "settings.json", + JSON.stringify({ + modelProviders: { + openai: [ + { + id: "custom-model", + envKey: "CUSTOM_API_KEY", + baseUrl: "https://custom.example/v1", + }, + ], + }, + }) + ); + assert.equal(await checkToolConfigStatus("qwen", configPath), "not_configured"); +}); + // ── Edge cases ──────────────────────────────────────────────────────────────── test("error path: non-existent file returns 'not_configured' (no throw)", async () => { diff --git a/tests/unit/cli-catalog-acpspawnable.test.ts b/tests/unit/cli-catalog-acpspawnable.test.ts index 379aaafca86..43f839bde00 100644 --- a/tests/unit/cli-catalog-acpspawnable.test.ts +++ b/tests/unit/cli-catalog-acpspawnable.test.ts @@ -20,6 +20,7 @@ const ACP_SPAWNABLE_IDS = [ "interpreter", "cursor-cli", "warp", + "qwen", ]; for (const id of ACP_SPAWNABLE_IDS) { diff --git a/tests/unit/cli-catalog-counts.test.ts b/tests/unit/cli-catalog-counts.test.ts index d7b2a070cd9..e6ab4417194 100644 --- a/tests/unit/cli-catalog-counts.test.ts +++ b/tests/unit/cli-catalog-counts.test.ts @@ -30,7 +30,7 @@ test(`CLI_TOOLS has exactly ${EXPECTED_AGENT_COUNT} agent entries`, () => { ); }); -test("CLI_TOOLS total code entries (including none) equals 24 (20 visible + 4 none)", () => { +test("CLI_TOOLS total code entries (including none) equals 25 (21 visible + 4 none)", () => { // code-none entries: antigravity, kiro, cursor (app), hermes (simple guide) const codeNone = codeAll.filter((t) => t.baseUrlSupport === "none"); assert.equal( @@ -38,11 +38,11 @@ test("CLI_TOOLS total code entries (including none) equals 24 (20 visible + 4 no 4, `Expected 4 code entries with baseUrlSupport='none', got ${codeNone.length}: ${codeNone.map((t) => t.id).join(", ")}` ); - assert.equal(codeAll.length, 24, `Expected 24 total code entries, got ${codeAll.length}`); + assert.equal(codeAll.length, 25, `Expected 25 total code entries, got ${codeAll.length}`); }); -test("CLI_TOOLS total (code + agent) = 32", () => { - assert.equal(all.length, 32, `Expected 32 total entries, got ${all.length}`); +test("CLI_TOOLS total (code + agent) = 33", () => { + assert.equal(all.length, 33, `Expected 33 total entries, got ${all.length}`); }); test("All code-none entries have configType mitm OR are legacy excluded entries", () => { @@ -66,7 +66,7 @@ test("All agent entries have baseUrlSupport 'full' or 'partial' (no agent is 'no } }); -test("The 20 visible code entries match D15 list exactly (+ crush + codewhale + grok-build)", () => { +test("The 21 visible code entries include Qwen Code's rebuilt integration", () => { const d15List = new Set([ "claude", "codex", @@ -88,6 +88,7 @@ test("The 20 visible code entries match D15 list exactly (+ crush + codewhale + "custom", "crush", "grok-build", + "qwen", ]); const visibleIds = new Set(codeVisible.map((t) => t.id)); for (const id of d15List) { diff --git a/tests/unit/cli-runtime-detection.test.ts b/tests/unit/cli-runtime-detection.test.ts index 53d12bfbbf6..bd996616f89 100644 --- a/tests/unit/cli-runtime-detection.test.ts +++ b/tests/unit/cli-runtime-detection.test.ts @@ -73,6 +73,7 @@ describe("CLI_TOOL_IDS", () => { "continue", "opencode", "qoder", + "qwen", ]; for (const id of expected) { assert.ok(CLI_TOOL_IDS.includes(id), `Missing tool: ${id}`); diff --git a/tests/unit/cli-tools-schema.test.ts b/tests/unit/cli-tools-schema.test.ts index 801a926a195..92d3944882b 100644 --- a/tests/unit/cli-tools-schema.test.ts +++ b/tests/unit/cli-tools-schema.test.ts @@ -1,7 +1,7 @@ import test from "node:test"; import assert from "node:assert/strict"; -test("CLI_TOOLS registry contains all expected tools (plan 14 — 32 total + crush + codewhale + omp + letta + grok-build)", async () => { +test("CLI_TOOLS registry contains all expected tools including rebuilt Qwen Code", async () => { const { CLI_TOOLS } = await import("../../src/shared/constants/cliTools.ts"); // windsurf and amp removed per plan 14 D17 (MITM backlog plan 11) // New entries added: roo, jcode, deepseek-tui, smelt, pi, aider, forge, @@ -44,11 +44,13 @@ test("CLI_TOOLS registry contains all expected tools (plan 14 — 32 total + cru "agent-deck", "crush", "grok-build", + "qwen", ]; for (const id of expected) { assert.ok(id in CLI_TOOLS, `Missing tool: ${id}`); } assert.equal(Object.keys(CLI_TOOLS).length, expected.length); + assert.equal(CLI_TOOLS.qwen.previewConfigMode, "qwen"); // Confirm removed entries are gone assert.equal((CLI_TOOLS as Record)["windsurf"], undefined); assert.equal((CLI_TOOLS as Record)["amp"], undefined); diff --git a/tests/unit/cli/setup-qwen.test.ts b/tests/unit/cli/setup-qwen.test.ts new file mode 100644 index 00000000000..e8086961de1 --- /dev/null +++ b/tests/unit/cli/setup-qwen.test.ts @@ -0,0 +1,85 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +import { resolveQwenTarget, runSetupQwenCommand } from "../../../bin/cli/commands/setup-qwen.mjs"; + +test("resolveQwenTarget normalizes a remote endpoint to the OpenAI /v1 base", () => { + assert.deepEqual(resolveQwenTarget({ remote: "https://omni.example/", apiKey: "sk-explicit" }), { + baseUrl: "https://omni.example/v1", + apiKey: "sk-explicit", + }); +}); + +test("setup-qwen writes current V4 settings and only its dedicated env key", async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "omniroute-setup-qwen-")); + const settingsPath = path.join(tempDir, "settings.json"); + const envPath = path.join(tempDir, ".env"); + await fs.writeFile( + settingsPath, + JSON.stringify({ + ui: { theme: "dark" }, + modelProviders: { + openai: [ + { + id: "personal", + envKey: "OPENAI_API_KEY", + baseUrl: "https://api.openai.com/v1", + }, + ], + }, + }) + ); + await fs.writeFile(envPath, "OPENAI_API_KEY=keep-me\n"); + + try { + const code = await runSetupQwenCommand({ + remote: "http://router:20128", + apiKey: "sk-qwen-dedicated", + model: "qwen/qwen3.8-max-preview", + configPath: settingsPath, + envPath, + yes: true, + }); + assert.equal(code, 0); + + const settings = JSON.parse(await fs.readFile(settingsPath, "utf8")); + assert.equal(settings.ui.theme, "dark"); + assert.equal(Array.isArray(settings.modelProviders.openai), true); + assert.equal(settings.modelProviders.openai.length, 2); + assert.deepEqual(settings.modelProviders.openai[1], { + id: "qwen/qwen3.8-max-preview", + name: "qwen/qwen3.8-max-preview (OmniRoute)", + envKey: "OMNIROUTE_API_KEY", + baseUrl: "http://router:20128/v1", + }); + assert.equal(JSON.stringify(settings).includes("sk-qwen-dedicated"), false); + + const env = await fs.readFile(envPath, "utf8"); + assert.match(env, /^OPENAI_API_KEY=keep-me$/m); + assert.match(env, /^OMNIROUTE_API_KEY="sk-qwen-dedicated"$/m); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } +}); + +test("setup-qwen does not overwrite an invalid settings file", async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "omniroute-setup-qwen-bad-")); + const settingsPath = path.join(tempDir, "settings.json"); + await fs.writeFile(settingsPath, "{ invalid JSON"); + + try { + const code = await runSetupQwenCommand({ + remote: "http://router:20128", + model: "model-id", + configPath: settingsPath, + yes: true, + }); + assert.equal(code, 1); + assert.equal(await fs.readFile(settingsPath, "utf8"), "{ invalid JSON"); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } +}); diff --git a/tests/unit/qwen-code-config.test.ts b/tests/unit/qwen-code-config.test.ts new file mode 100644 index 00000000000..39e28a26a99 --- /dev/null +++ b/tests/unit/qwen-code-config.test.ts @@ -0,0 +1,303 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { + hasOmniRouteQwenCodeConfig, + mergeQwenCodeEnv, + mergeQwenCodeSettings, + removeQwenCodeEnv, + removeQwenCodeSettings, +} from "../../src/shared/services/qwenCodeConfig.ts"; + +test("writes the upstream V4 bare-array modelProviders contract without a secret", () => { + const settings = mergeQwenCodeSettings( + {}, + { baseUrl: "http://localhost:20128", model: "qwen/qwen3.8-max-preview" } + ); + + assert.deepEqual(settings.modelProviders, { + openai: [ + { + id: "qwen/qwen3.8-max-preview", + name: "qwen/qwen3.8-max-preview (OmniRoute)", + envKey: "OMNIROUTE_API_KEY", + baseUrl: "http://localhost:20128/v1", + }, + ], + }); + assert.deepEqual(settings.security, { auth: { selectedType: "openai" } }); + assert.deepEqual(settings.model, { + name: "qwen/qwen3.8-max-preview", + baseUrl: "http://localhost:20128/v1", + }); + assert.equal(JSON.stringify(settings).includes("sk-secret"), false); +}); + +test("preserves user settings and unrelated provider entries", () => { + const existing = { + $version: 4, + mcpServers: { filesystem: { command: "npx" } }, + security: { folderTrust: { enabled: true } }, + modelProviders: { + openai: [ + { + id: "gpt-4o", + name: "Personal OpenAI", + envKey: "OPENAI_API_KEY", + baseUrl: "https://api.openai.com/v1", + }, + ], + idealab: [ + { + id: "ideal-model", + envKey: "IDEALAB_API_KEY", + baseUrl: "https://idealab.example/v1", + }, + ], + }, + providerProtocol: { idealab: "openai" }, + }; + + const settings = mergeQwenCodeSettings(existing, { + baseUrl: "https://omni.example/v1/", + model: "cx/gpt-5.6-sol", + modelName: "GPT-5.6 Sol", + }); + + assert.equal(settings.$version, 4); + assert.deepEqual(settings.mcpServers, existing.mcpServers); + assert.deepEqual(settings.providerProtocol, existing.providerProtocol); + assert.deepEqual( + (settings.modelProviders as Record).idealab, + existing.modelProviders.idealab + ); + assert.equal( + ((settings.modelProviders as Record).openai as unknown[]).length, + 2 + ); +}); + +test("replaces only the managed entry and unwraps the reverted V5 provider shape", () => { + const settings = mergeQwenCodeSettings( + { + $version: 4, + modelProviders: { + openai: { + protocol: "openai", + models: [ + { + id: "old-model", + name: "old-model (OmniRoute)", + envKey: "OPENAI_API_KEY", + baseUrl: "http://old-host/v1", + }, + { + id: "personal-model", + envKey: "PERSONAL_API_KEY", + baseUrl: "https://personal.example/v1", + }, + ], + }, + }, + }, + { baseUrl: "http://new-host", model: "new-model" } + ); + + assert.deepEqual((settings.modelProviders as Record).openai, [ + { + id: "personal-model", + envKey: "PERSONAL_API_KEY", + baseUrl: "https://personal.example/v1", + }, + { + id: "new-model", + name: "new-model (OmniRoute)", + envKey: "OMNIROUTE_API_KEY", + baseUrl: "http://new-host/v1", + }, + ]); +}); + +test("migrates the removed integration's root-array shape without dropping other entries", () => { + const settings = mergeQwenCodeSettings( + { + modelProviders: [ + { + id: "omniroute", + name: "OmniRoute", + authType: "openai", + envKey: "OMNIROUTE_API_KEY", + baseUrl: "http://old/v1", + }, + { + id: "personal-model", + name: "Personal model", + authType: "openai", + envKey: "PERSONAL_API_KEY", + baseUrl: "https://personal.example/v1", + }, + ], + selectedProvider: "omniroute", + }, + { baseUrl: "http://new", model: "new-model" } + ); + + assert.deepEqual((settings.modelProviders as Record).openai, [ + { + id: "personal-model", + name: "Personal model", + envKey: "PERSONAL_API_KEY", + baseUrl: "https://personal.example/v1", + }, + { + id: "new-model", + name: "new-model (OmniRoute)", + envKey: "OMNIROUTE_API_KEY", + baseUrl: "http://new/v1", + }, + ]); + assert.equal(settings.selectedProvider, undefined); +}); + +test("migrates matching legacy security.auth credentials out of settings.json", () => { + const settings = mergeQwenCodeSettings( + { + security: { + auth: { + selectedType: "openai", + apiKey: "sk-legacy-secret", + baseUrl: "http://omni-host/v1", + other: true, + }, + }, + model: { name: "old-model", temperature: 0.2 }, + }, + { baseUrl: "http://omni-host", model: "new-model" } + ); + + assert.deepEqual(settings.security, { + auth: { selectedType: "openai", other: true }, + }); + assert.deepEqual(settings.model, { + name: "new-model", + baseUrl: "http://omni-host/v1", + temperature: 0.2, + }); + assert.equal(JSON.stringify(settings).includes("sk-legacy-secret"), false); +}); + +test("detection is precise and does not claim arbitrary custom endpoints", () => { + assert.equal( + hasOmniRouteQwenCodeConfig({ + modelProviders: { + openai: [ + { + id: "custom-model", + name: "My custom provider", + envKey: "CUSTOM_API_KEY", + baseUrl: "https://custom.example/v1", + }, + ], + }, + }), + false + ); + + assert.equal( + hasOmniRouteQwenCodeConfig({ + modelProviders: { + openai: [ + { + id: "custom-model", + name: "custom-model (OmniRoute)", + envKey: "OMNIROUTE_API_KEY", + baseUrl: "https://omni.example/v1", + }, + ], + }, + }), + true + ); +}); + +test("env merge owns only OMNIROUTE_API_KEY and preserves all user keys", () => { + const original = [ + "# user credentials", + "OPENAI_API_KEY=sk-openai", + "ANTHROPIC_API_KEY=sk-anthropic", + "GEMINI_API_KEY=sk-gemini", + "export OMNIROUTE_API_KEY=old-value", + "OMNIROUTE_API_KEY_BACKUP=keep-me", + "", + ].join("\n"); + + const merged = mergeQwenCodeEnv(original, 'sk-new "quoted" value'); + assert.match(merged, /^OPENAI_API_KEY=sk-openai$/m); + assert.match(merged, /^ANTHROPIC_API_KEY=sk-anthropic$/m); + assert.match(merged, /^GEMINI_API_KEY=sk-gemini$/m); + assert.match(merged, /^OMNIROUTE_API_KEY_BACKUP=keep-me$/m); + assert.match(merged, /^OMNIROUTE_API_KEY="sk-new \\"quoted\\" value"$/m); + assert.equal((merged.match(/^OMNIROUTE_API_KEY=/gm) || []).length, 1); + + const removed = removeQwenCodeEnv(merged); + assert.doesNotMatch(removed, /^OMNIROUTE_API_KEY=/m); + assert.match(removed, /^OMNIROUTE_API_KEY_BACKUP=keep-me$/m); + assert.match(removed, /^OPENAI_API_KEY=sk-openai$/m); +}); + +test("reset removes only managed models and their active selection", () => { + const existing = mergeQwenCodeSettings( + { + ui: { theme: "dark" }, + modelProviders: { + openai: [ + { + id: "personal-model", + envKey: "PERSONAL_API_KEY", + baseUrl: "https://personal.example/v1", + }, + ], + }, + }, + { baseUrl: "http://omni-host", model: "managed-model" } + ); + + const reset = removeQwenCodeSettings(existing); + assert.deepEqual(reset.ui, { theme: "dark" }); + assert.deepEqual((reset.modelProviders as Record).openai, [ + { + id: "personal-model", + envKey: "PERSONAL_API_KEY", + baseUrl: "https://personal.example/v1", + }, + ]); + assert.equal(reset.model, undefined); + assert.deepEqual(reset.security, { auth: { selectedType: "openai" } }); +}); + +test("reset also removes matching deprecated security.auth credentials", () => { + const reset = removeQwenCodeSettings({ + security: { + auth: { + selectedType: "openai", + apiKey: "sk-old-secret", + baseUrl: "http://omni-host/v1", + }, + }, + model: { name: "managed", baseUrl: "http://omni-host/v1" }, + modelProviders: { + openai: [ + { + id: "managed", + name: "managed (OmniRoute)", + envKey: "OMNIROUTE_API_KEY", + baseUrl: "http://omni-host/v1", + }, + ], + }, + }); + + assert.equal(reset.security, undefined); + assert.equal(reset.model, undefined); + assert.equal(JSON.stringify(reset).includes("sk-old-secret"), false); +}); diff --git a/tests/unit/qwen-settings-route.test.ts b/tests/unit/qwen-settings-route.test.ts new file mode 100644 index 00000000000..0b1f2ab6195 --- /dev/null +++ b/tests/unit/qwen-settings-route.test.ts @@ -0,0 +1,137 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { SignJWT } from "jose"; + +const TEST_HOME = path.join(os.tmpdir(), `omniroute-qwen-route-${process.pid}-${Date.now()}`); +const SETTINGS_PATH = path.join(TEST_HOME, ".qwen", "settings.json"); +const ENV_PATH = path.join(TEST_HOME, ".qwen", ".env"); +const originalHome = os.homedir; +const originalJwtSecret = process.env.JWT_SECRET; +const originalWriteFlag = process.env.CLI_ALLOW_CONFIG_WRITES; + +os.homedir = () => TEST_HOME; +process.env.CLI_ALLOW_CONFIG_WRITES = "true"; + +const route = await import("../../src/app/api/cli-tools/qwen-settings/route.ts"); + +const authCookie = async (): Promise => { + process.env.JWT_SECRET = "qwen-settings-route-test-secret"; + const token = await new SignJWT({ sub: "qwen-route-test" }) + .setProtectedHeader({ alg: "HS256" }) + .setIssuedAt() + .setExpirationTime("1h") + .sign(new TextEncoder().encode(process.env.JWT_SECRET)); + return `auth_token=${token}`; +}; + +const request = async (method: string, body?: unknown): Promise => + new Request("http://localhost/api/cli-tools/qwen-settings", { + method, + headers: { + cookie: await authCookie(), + ...(body === undefined ? {} : { "Content-Type": "application/json" }), + }, + ...(body === undefined ? {} : { body: JSON.stringify(body) }), + }); + +test.beforeEach(async () => { + await fs.rm(TEST_HOME, { recursive: true, force: true }); + await fs.mkdir(path.dirname(SETTINGS_PATH), { recursive: true }); +}); + +test.after(async () => { + os.homedir = originalHome; + await fs.rm(TEST_HOME, { recursive: true, force: true }); + if (originalJwtSecret === undefined) delete process.env.JWT_SECRET; + else process.env.JWT_SECRET = originalJwtSecret; + if (originalWriteFlag === undefined) delete process.env.CLI_ALLOW_CONFIG_WRITES; + else process.env.CLI_ALLOW_CONFIG_WRITES = originalWriteFlag; +}); + +test("POST writes the shared V4 contract and preserves unrelated credentials", async () => { + await fs.writeFile( + SETTINGS_PATH, + JSON.stringify({ + ui: { theme: "dark" }, + modelProviders: { + openai: [ + { + id: "personal-model", + envKey: "OPENAI_API_KEY", + baseUrl: "https://api.openai.com/v1", + }, + ], + }, + }) + ); + await fs.writeFile(ENV_PATH, "OPENAI_API_KEY=keep-openai\nGEMINI_API_KEY=keep-gemini\n"); + + const response = await route.POST( + await request("POST", { + baseUrl: "http://localhost:20128", + apiKey: "sk-route-secret", + model: "qwen/qwen3.8-max-preview", + }) + ); + assert.equal(response.status, 200); + + const settings = JSON.parse(await fs.readFile(SETTINGS_PATH, "utf8")); + assert.equal(settings.ui.theme, "dark"); + assert.equal(settings.modelProviders.openai.length, 2); + assert.deepEqual(settings.modelProviders.openai[1], { + id: "qwen/qwen3.8-max-preview", + name: "qwen/qwen3.8-max-preview (OmniRoute)", + envKey: "OMNIROUTE_API_KEY", + baseUrl: "http://localhost:20128/v1", + }); + assert.equal(JSON.stringify(settings).includes("sk-route-secret"), false); + + const env = await fs.readFile(ENV_PATH, "utf8"); + assert.match(env, /^OPENAI_API_KEY=keep-openai$/m); + assert.match(env, /^GEMINI_API_KEY=keep-gemini$/m); + assert.match(env, /^OMNIROUTE_API_KEY="sk-route-secret"$/m); +}); + +test("DELETE removes only OmniRoute-owned settings and env lines", async () => { + const configured = { + ui: { theme: "dark" }, + security: { auth: { selectedType: "openai" } }, + model: { name: "managed", baseUrl: "http://localhost:20128/v1" }, + modelProviders: { + openai: [ + { + id: "personal", + envKey: "OPENAI_API_KEY", + baseUrl: "https://api.openai.com/v1", + }, + { + id: "managed", + name: "managed (OmniRoute)", + envKey: "OMNIROUTE_API_KEY", + baseUrl: "http://localhost:20128/v1", + }, + ], + }, + }; + await fs.writeFile(SETTINGS_PATH, JSON.stringify(configured)); + await fs.writeFile(ENV_PATH, "OPENAI_API_KEY=keep\nOMNIROUTE_API_KEY=remove\n"); + + const response = await route.DELETE(await request("DELETE")); + assert.equal(response.status, 200); + + const settings = JSON.parse(await fs.readFile(SETTINGS_PATH, "utf8")); + assert.deepEqual(settings.ui, { theme: "dark" }); + assert.equal(settings.model, undefined); + assert.deepEqual(settings.modelProviders.openai, [configured.modelProviders.openai[0]]); + assert.equal(await fs.readFile(ENV_PATH, "utf8"), "OPENAI_API_KEY=keep\n"); +}); + +test("DELETE is a no-op when Qwen Code has no config files", async () => { + const response = await route.DELETE(await request("DELETE")); + assert.equal(response.status, 200); + await assert.rejects(fs.access(SETTINGS_PATH)); + await assert.rejects(fs.access(ENV_PATH)); +}); diff --git a/tests/unit/route-guard-qwen-settings-local-only.test.ts b/tests/unit/route-guard-qwen-settings-local-only.test.ts new file mode 100644 index 00000000000..16466e75d7a --- /dev/null +++ b/tests/unit/route-guard-qwen-settings-local-only.test.ts @@ -0,0 +1,21 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { + isLocalOnlyBypassableByManageScope, + isLocalOnlyPath, +} from "../../src/server/authz/routeGuard.ts"; +import { SPAWN_CAPABLE_ROUTE_ROOTS } from "../../scripts/check/check-route-guard-membership.ts"; + +test("Qwen Code settings are local-only because GET probes a binary and writes touch ~/.qwen", () => { + assert.equal(isLocalOnlyPath("/api/cli-tools/qwen-settings"), true); + assert.equal(isLocalOnlyPath("/api/cli-tools/qwen-settings/"), true); +}); + +test("Qwen Code settings cannot be opened through the manage-scope bypass", () => { + assert.equal(isLocalOnlyBypassableByManageScope("/api/cli-tools/qwen-settings"), false); +}); + +test("the spawn-capable route audit includes Qwen Code settings", () => { + assert.ok(SPAWN_CAPABLE_ROUTE_ROOTS.includes("src/app/api/cli-tools/qwen-settings")); +}); From b774ec2ef89126ee961a86beff21d83cb413f44d Mon Sep 17 00:00:00 2001 From: backryun Date: Mon, 20 Jul 2026 23:20:35 +0900 Subject: [PATCH 4/6] fix(qwen): clear stale CLI auth on reset --- src/shared/services/qwenCodeConfig.ts | 22 ++++++++++---------- tests/unit/qwen-code-config.test.ts | 30 +++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/shared/services/qwenCodeConfig.ts b/src/shared/services/qwenCodeConfig.ts index e90f6a686b1..5504e7b817d 100644 --- a/src/shared/services/qwenCodeConfig.ts +++ b/src/shared/services/qwenCodeConfig.ts @@ -205,18 +205,18 @@ export const removeQwenCodeSettings = (existing: unknown): JsonRecord => { delete model.baseUrl; if (Object.keys(model).length === 0) delete next.model; else next.model = model; + } - const security = cloneRecord(next.security); - const auth = cloneRecord(security.auth); - const remainingOpenai = isRecord(next.modelProviders) ? next.modelProviders.openai : undefined; - const authBaseUrl = normalizeQwenCodeBaseUrl(auth.baseUrl); - const authMatchesRemoved = removed.some( - (entry) => normalizeQwenCodeBaseUrl(entry.baseUrl) === authBaseUrl - ); - if (authMatchesRemoved) { - delete auth.apiKey; - delete auth.baseUrl; - } + const security = cloneRecord(next.security); + const auth = cloneRecord(security.auth); + const remainingOpenai = isRecord(next.modelProviders) ? next.modelProviders.openai : undefined; + const authBaseUrl = normalizeQwenCodeBaseUrl(auth.baseUrl); + const authMatchesRemoved = removed.some( + (entry) => normalizeQwenCodeBaseUrl(entry.baseUrl) === authBaseUrl + ); + if (authMatchesRemoved) { + delete auth.apiKey; + delete auth.baseUrl; if (auth.selectedType === "openai" && !Array.isArray(remainingOpenai)) { delete auth.selectedType; } diff --git a/tests/unit/qwen-code-config.test.ts b/tests/unit/qwen-code-config.test.ts index 39e28a26a99..3e59b4e95dc 100644 --- a/tests/unit/qwen-code-config.test.ts +++ b/tests/unit/qwen-code-config.test.ts @@ -301,3 +301,33 @@ test("reset also removes matching deprecated security.auth credentials", () => { assert.equal(reset.model, undefined); assert.equal(JSON.stringify(reset).includes("sk-old-secret"), false); }); + +test("reset removes matching deprecated auth when the selected model is unrelated", () => { + const reset = removeQwenCodeSettings({ + security: { + auth: { + selectedType: "openai", + apiKey: "sk-old-secret", + baseUrl: "http://omni-host/v1", + }, + }, + model: { name: "some-other-model", baseUrl: "https://other.example/v1" }, + modelProviders: { + openai: [ + { + id: "managed", + name: "managed (OmniRoute)", + envKey: "OMNIROUTE_API_KEY", + baseUrl: "http://omni-host/v1", + }, + ], + }, + }); + + assert.equal(reset.security, undefined); + assert.deepEqual(reset.model, { + name: "some-other-model", + baseUrl: "https://other.example/v1", + }); + assert.equal(JSON.stringify(reset).includes("sk-old-secret"), false); +}); From c16bd5460b9c5e9a4b0b63386ca23e9dd98e9361 Mon Sep 17 00:00:00 2001 From: backryun Date: Tue, 21 Jul 2026 02:08:02 +0900 Subject: [PATCH 5/6] test(qwen): align retired provider coverage --- stryker.conf.json | 1 + .../unit/authz/spawn-capable-prefixes-client-safe.test.ts | 2 +- tests/unit/t28-model-catalog-updates.test.ts | 8 +++----- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/stryker.conf.json b/stryker.conf.json index 6066be870db..56c283c1c37 100644 --- a/stryker.conf.json +++ b/stryker.conf.json @@ -239,6 +239,7 @@ "tests/unit/route-explainability.test.ts", "tests/unit/route-guard-forge-jcode-settings-local-only.test.ts", "tests/unit/route-guard-grok-build-settings-local-only.test.ts", + "tests/unit/route-guard-qwen-settings-local-only.test.ts", "tests/unit/route-guard-middleware-local-only.test.ts", "tests/unit/route-guard-plugins-local-only.test.ts", "tests/unit/route-guard-private-lan.test.ts", diff --git a/tests/unit/authz/spawn-capable-prefixes-client-safe.test.ts b/tests/unit/authz/spawn-capable-prefixes-client-safe.test.ts index 7c64ca0f6ca..2522c659a11 100644 --- a/tests/unit/authz/spawn-capable-prefixes-client-safe.test.ts +++ b/tests/unit/authz/spawn-capable-prefixes-client-safe.test.ts @@ -86,5 +86,5 @@ test("SPAWN_CAPABLE_PREFIXES is defined in the server-free constants leaf with t `SPAWN_CAPABLE_PREFIXES lost the spawn-capable prefix "${prefix}" during extraction` ); } - assert.equal(SPAWN_CAPABLE_PREFIXES.length, 9); + assert.equal(SPAWN_CAPABLE_PREFIXES.length, 10); }); diff --git a/tests/unit/t28-model-catalog-updates.test.ts b/tests/unit/t28-model-catalog-updates.test.ts index 3455bafcca7..848749e1446 100644 --- a/tests/unit/t28-model-catalog-updates.test.ts +++ b/tests/unit/t28-model-catalog-updates.test.ts @@ -51,11 +51,9 @@ test("T28: github registry exposes Gemini 3.1 Pro Preview and keeps legacy alias assert.equal(legacy.model, "gemini-3.1-pro-preview"); }); -test("T28: qwen registry uses native chat.qwen.ai base URL", () => { - assert.equal( - REGISTRY.qwen.baseUrl, - "https://chat.qwen.ai/api/v1/services/aigc/text-generation/generation" - ); +test("T28: qwen OAuth registry entry is retired; qwen-web keeps the native chat.qwen.ai URL", () => { + assert.equal(REGISTRY.qwen, undefined); + assert.equal(REGISTRY["qwen-web"].baseUrl, "https://chat.qwen.ai/api/v2/chat/completions"); }); test("T28: lmarena registry seeds Direct-chat Text/search; image models in IMAGE_PROVIDERS", async () => { From 8f2cce9c3ac86c07e09f33dd862ffc1ebe891f7d Mon Sep 17 00:00:00 2001 From: backryun <24198422+backryun@users.noreply.github.com> Date: Mon, 20 Jul 2026 17:17:18 -0300 Subject: [PATCH 6/6] fix(db): renumber qwen-cleanup migration 129 -> 130 release/v3.8.49 tip took slot 129 via #7843 (usage_history_codex_strong_identity, itself renumbered from 128 during the #7838/#7840 base-red cleanup) after this branch forked; renumber remove_unregistered_qwen_data to 130. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --- ...stered_qwen_data.sql => 130_remove_unregistered_qwen_data.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/lib/db/migrations/{129_remove_unregistered_qwen_data.sql => 130_remove_unregistered_qwen_data.sql} (100%) diff --git a/src/lib/db/migrations/129_remove_unregistered_qwen_data.sql b/src/lib/db/migrations/130_remove_unregistered_qwen_data.sql similarity index 100% rename from src/lib/db/migrations/129_remove_unregistered_qwen_data.sql rename to src/lib/db/migrations/130_remove_unregistered_qwen_data.sql