From ccdbc89290e692da2b43a632be2f1c103e01e6c0 Mon Sep 17 00:00:00 2001 From: backryun Date: Tue, 21 Jul 2026 06:48:16 +0900 Subject: [PATCH] feat(qwen): add Qwen3.8 Max Preview catalogs [Part 2/3] (#7874) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebuilt clean on release/v3.8.49 after Part 1 (#7866) squash-merged — applies only the Part-2 delta (Qwen Web / Qoder qwen3.8-max-preview registration + required-thinking allowlist + Qoder client rework) onto the current tip. No migration in this part (that was Part 1). Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --- docs/compression/COMPRESSION_GUIDE.md | 16 +-- docs/getting-started/TROUBLESHOOTING.md | 2 +- docs/guides/SETUP_GUIDE.md | 2 +- docs/guides/TROUBLESHOOTING.md | 2 +- docs/guides/USER_GUIDE.md | 28 ++--- open-sse/config/freeModelCatalog.data.ts | 24 ++--- .../config/providers/registry/qoder/index.ts | 76 ++++++++++--- .../providers/registry/qwen/web/index.ts | 39 ++++++- open-sse/executors/qoder.ts | 2 +- open-sse/executors/qwen-web.ts | 4 +- open-sse/services/qoderCli.ts | 64 +++++------ src/shared/constants/modelSpecs.ts | 7 ++ tests/unit/executor-qwen-web.test.ts | 63 ++++++++++- tests/unit/qoder-cli.test.ts | 100 +++++++++++++++++- tests/unit/qoder-executor.test.ts | 32 +++++- .../unit/t31-t33-t34-t38-model-specs.test.ts | 6 ++ 16 files changed, 360 insertions(+), 107 deletions(-) diff --git a/docs/compression/COMPRESSION_GUIDE.md b/docs/compression/COMPRESSION_GUIDE.md index 1a23310fde..d03d6e361e 100644 --- a/docs/compression/COMPRESSION_GUIDE.md +++ b/docs/compression/COMPRESSION_GUIDE.md @@ -183,8 +183,8 @@ Combo: "free-forever" Compression Combo: "coding-agent-stack" Pipeline: RTK -> Caveman Targets: - 1. if/kimi-k2-thinking - 2. if/qwen3-coder-plus + 1. if/kimi-k2.7-code + 2. if/qwen3.8-max-preview ``` This lets you use stacked compression on free/coding providers while keeping lite mode on paid @@ -283,12 +283,12 @@ Every compressed request includes stats in the server logs: ## Phase Roadmap -| Phase | Modes | Status | -| ------- | -------------------------------------------------------------------- | ---------- | -| Phase 1 | Off, Lite | ✅ Shipped | -| Phase 2 | Standard, Aggressive, Ultra | ✅ Shipped | -| Phase 3 | RTK, Stacked, Compression Combos | ✅ Shipped | -| Phase 4 | Output Styles, SLM-tier Ultra, eval harness | ✅ Shipped | +| Phase | Modes | Status | +| -------- | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- | +| Phase 1 | Off, Lite | ✅ Shipped | +| Phase 2 | Standard, Aggressive, Ultra | ✅ Shipped | +| Phase 3 | RTK, Stacked, Compression Combos | ✅ Shipped | +| Phase 4 | Output Styles, SLM-tier Ultra, eval harness | ✅ Shipped | | Phase 4C | Adaptive context-budget ("dial") — compute engine + API (`contextBudget` on `PUT /api/settings/compression`) | ✅ Shipped (API-configurable; dashboard controls not yet built, #7005) | --- diff --git a/docs/getting-started/TROUBLESHOOTING.md b/docs/getting-started/TROUBLESHOOTING.md index 3f92ce8231..21116be112 100644 --- a/docs/getting-started/TROUBLESHOOTING.md +++ b/docs/getting-started/TROUBLESHOOTING.md @@ -146,7 +146,7 @@ omniroute **Fix:** -- Add fallback: `cc/claude-opus-4-6 → glm/glm-4.7 → if/kimi-k2-thinking` +- Add fallback: `cc/claude-opus-4-6 → glm/glm-4.7 → if/qwen3.8-max-preview` - Use GLM/MiniMax as cheap backup ### OAuth Token Expired diff --git a/docs/guides/SETUP_GUIDE.md b/docs/guides/SETUP_GUIDE.md index 4f02548857..f68480eac3 100644 --- a/docs/guides/SETUP_GUIDE.md +++ b/docs/guides/SETUP_GUIDE.md @@ -149,7 +149,7 @@ omniroute providers validate ```txt Base URL: http://localhost:20128/v1 API Key: [copy from Endpoint page] -Model: if/kimi-k2-thinking (or any provider/model prefix) +Model: if/qwen3.8-max-preview (or any provider/model prefix) ``` If your editor cannot send `Authorization: Bearer ...`, use the tokenized compatibility base instead: diff --git a/docs/guides/TROUBLESHOOTING.md b/docs/guides/TROUBLESHOOTING.md index 5efe9e5be2..24976bfe66 100644 --- a/docs/guides/TROUBLESHOOTING.md +++ b/docs/guides/TROUBLESHOOTING.md @@ -194,7 +194,7 @@ omniroute **Fix:** -- Add fallback: `cc/claude-opus-4-6 → glm/glm-4.7 → if/kimi-k2-thinking` +- Add fallback: `cc/claude-opus-4-6 → glm/glm-4.7 → if/qwen3.8-max-preview` - Use GLM/MiniMax as cheap backup ### OAuth Token Expired diff --git a/docs/guides/USER_GUIDE.md b/docs/guides/USER_GUIDE.md index 2aa7d35f86..4dbca34ece 100644 --- a/docs/guides/USER_GUIDE.md +++ b/docs/guides/USER_GUIDE.md @@ -70,7 +70,7 @@ Complete guide for configuring providers, creating combos, integrating CLI tools Combo: "maximize-claude" 1. cc/claude-opus-4-7 (use subscription fully) 2. glm/glm-4.7 (cheap backup when quota out) - 3. if/kimi-k2 (free emergency fallback) + 3. if/qwen3.8-max-preview (free emergency fallback) Monthly cost: $20 (subscription) + ~$5 (backup) = $25 total vs. $20 + hitting limits = frustration @@ -82,8 +82,8 @@ vs. $20 + hitting limits = frustration ``` Combo: "free-forever" - 1. if/kimi-k2 (unlimited free) - 2. kr/qwen3-coder-next (free fallback) + 1. if/kimi-k2.7-code (unlimited free) + 2. kr/qwen3-coder-next (Kiro free fallback) Monthly cost: $0 Quality: Production-ready models @@ -99,7 +99,7 @@ Combo: "always-on" 2. cx/gpt-5.5 (second subscription) 3. glm/glm-4.7 (cheap, resets daily) 4. minimax/MiniMax-M2.1 (cheapest, 5h reset) - 5. if/kimi-k2 (free unlimited) + 5. if/deepseek-v4-flash (free unlimited) Result: 5 layers of fallback = zero downtime Monthly cost: $20-200 (subscriptions) + $10-20 (backup) @@ -111,9 +111,9 @@ Monthly cost: $20-200 (subscriptions) + $10-20 (backup) ``` Combo: "openclaw-free" - 1. if/qwen3-coder-plus (unlimited free) - 2. if/deepseek-r1 (unlimited free) - 3. if/kimi-k2 (unlimited free) + 1. if/qwen3.8-max-preview (unlimited free) + 2. if/deepseek-v4-flash (unlimited free) + 3. if/kimi-k2.7-code (unlimited free) Monthly cost: $0 Access via: WhatsApp, Telegram, Slack, Discord, iMessage, Signal... @@ -210,12 +210,12 @@ No-auth free providers have a switch beside **No authentication required** on th Turning it off disables that provider, removes it from Providers configured/compact views, and removes its models from `/v1/models`. -#### Qoder (8 FREE models) +#### Qoder (9 FREE models) ```bash Dashboard → Connect Qoder → OAuth login → Unlimited usage -Models: if/kimi-k2, if/qwen3-coder-plus, if/qwen3-max, if/qwen3-235b, if/deepseek-r1, if/deepseek-v3.2 +Models: if/qwen3.8-max-preview, if/qwen3.7-max, if/qwen3.7-plus, if/kimi-k3, if/kimi-k2.7-code, if/glm-5.2, if/deepseek-v4-pro, if/deepseek-v4-flash, if/minimax-m3 ``` #### Kiro (Claude FREE) @@ -251,8 +251,8 @@ Use in CLI: premium-coding ``` Name: free-combo Models: - 1. if/kimi-k2 (unlimited) - 2. kr/qwen3-coder-next (free fallback) + 1. if/kimi-k2.7-code (unlimited) + 2. kr/qwen3-coder-next (Kiro free fallback) Cost: $0 forever! ``` @@ -301,7 +301,7 @@ Edit `~/.openclaw/openclaw.json`: { "agents": { "defaults": { - "model": { "primary": "omniroute/if/kimi-k2" } + "model": { "primary": "omniroute/if/kimi-k2.7-code" } } }, "models": { @@ -310,7 +310,7 @@ Edit `~/.openclaw/openclaw.json`: "baseUrl": "http://localhost:20128/v1", "apiKey": "your-omniroute-api-key", "api": "openai-completions", - "models": [{ "id": "if/kimi-k2", "name": "kimi-k2" }] + "models": [{ "id": "if/kimi-k2.7-code", "name": "Kimi K2.7 Code" }] } } } @@ -574,7 +574,7 @@ For the full environment variable reference, see the [README](../README.md). **Kiro (`kr/`)** — FREE OAuth: `kr/auto-kiro`, `kr/claude-opus-4.7`, `kr/claude-opus-4.6`, `kr/claude-sonnet-4.6`, `kr/claude-sonnet-4.5`, `kr/claude-haiku-4.5`, `kr/deepseek-3.2`, `kr/minimax-m2.5`, `kr/minimax-m2.1`, `kr/glm-5`, `kr/qwen3-coder-next` -**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` +**Qoder (`if/`)** — FREE OAuth: `if/qwen3.8-max-preview`, `if/qwen3.7-max`, `if/qwen3.7-plus`, `if/kimi-k3`, `if/kimi-k2.7-code`, `if/glm-5.2`, `if/deepseek-v4-pro`, `if/deepseek-v4-flash`, `if/minimax-m3` **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` diff --git a/open-sse/config/freeModelCatalog.data.ts b/open-sse/config/freeModelCatalog.data.ts index 0e3c97be11..2ff8afad06 100644 --- a/open-sse/config/freeModelCatalog.data.ts +++ b/open-sse/config/freeModelCatalog.data.ts @@ -406,20 +406,16 @@ export const FREE_MODEL_BUDGETS: FreeModelBudget[] = [ { provider: "puter", modelId: "perplexity/sonar-pro", displayName: "Perplexity Sonar Pro (Puter)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "puter", tos: "caution" }, { provider: "puter", modelId: "perplexity/sonar-reasoning-pro", displayName: "Perplexity Sonar Reasoning Pro (Puter)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "puter", tos: "caution" }, { provider: "puter", modelId: "perplexity/sonar", displayName: "Perplexity Sonar (Puter)", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "puter", tos: "caution" }, - { provider: "qoder", modelId: "qoder-rome-30ba3b", displayName: "Qoder ROME", monthlyTokens: 0, creditTokens: 1000000, freeType: "one-time-initial", poolKey: "qoder", tos: "caution" }, - { provider: "qoder", modelId: "qwen3-coder-plus", displayName: "Qwen3 Coder Plus", monthlyTokens: 0, creditTokens: 1000000, freeType: "one-time-initial", poolKey: "qoder", tos: "caution" }, - { provider: "qoder", modelId: "qwen3-max", displayName: "Qwen3 Max", monthlyTokens: 0, creditTokens: 1000000, freeType: "one-time-initial", poolKey: "qoder", tos: "caution" }, - { provider: "qoder", modelId: "qwen3-vl-plus", displayName: "Qwen3 Vision Plus", monthlyTokens: 0, creditTokens: 1000000, freeType: "one-time-initial", poolKey: "qoder", tos: "caution" }, - { provider: "qoder", modelId: "kimi-k2-0905", displayName: "Kimi K2 0905", monthlyTokens: 0, creditTokens: 1000000, freeType: "one-time-initial", poolKey: "qoder", tos: "caution" }, - { provider: "qoder", modelId: "qwen3-max-preview", displayName: "Qwen3 Max Preview", monthlyTokens: 0, creditTokens: 1000000, freeType: "one-time-initial", poolKey: "qoder", tos: "caution" }, - { provider: "qoder", modelId: "kimi-k2", displayName: "Kimi K2", monthlyTokens: 0, creditTokens: 1000000, freeType: "one-time-initial", poolKey: "qoder", tos: "caution" }, - { provider: "qoder", modelId: "deepseek-v3.2", displayName: "DeepSeek-V3.2-Exp", monthlyTokens: 0, creditTokens: 1000000, freeType: "one-time-initial", poolKey: "qoder", tos: "caution" }, - { provider: "qoder", modelId: "deepseek-r1", displayName: "DeepSeek R1", monthlyTokens: 0, creditTokens: 1000000, freeType: "one-time-initial", poolKey: "qoder", tos: "caution" }, - { provider: "qoder", modelId: "deepseek-v3", displayName: "DeepSeek V3", monthlyTokens: 0, creditTokens: 1000000, freeType: "one-time-initial", poolKey: "qoder", tos: "caution" }, - { provider: "qoder", modelId: "qwen3-32b", displayName: "Qwen3 32B", monthlyTokens: 0, creditTokens: 1000000, freeType: "one-time-initial", poolKey: "qoder", tos: "caution" }, - { provider: "qoder", modelId: "qwen3-235b-a22b-thinking-2507", displayName: "Qwen3 235B A22B Thinking 2507", monthlyTokens: 0, creditTokens: 1000000, freeType: "one-time-initial", poolKey: "qoder", tos: "caution" }, - { provider: "qoder", modelId: "qwen3-235b-a22b-instruct", displayName: "Qwen3 235B A22B Instruct", monthlyTokens: 0, creditTokens: 1000000, freeType: "one-time-initial", poolKey: "qoder", tos: "caution" }, - { provider: "qoder", modelId: "qwen3-235b", displayName: "Qwen3 235B", monthlyTokens: 0, creditTokens: 1000000, freeType: "one-time-initial", poolKey: "qoder", tos: "caution" }, + { provider: "qoder", modelId: "qwen3.8-max-preview", displayName: "Qwen3.8-Max-Preview", monthlyTokens: 0, creditTokens: 1000000, freeType: "one-time-initial", poolKey: "qoder", tos: "caution" }, + { provider: "qoder", modelId: "qwen3.7-max", displayName: "Qwen3.7-Max", monthlyTokens: 0, creditTokens: 1000000, freeType: "one-time-initial", poolKey: "qoder", tos: "caution" }, + { provider: "qoder", modelId: "qwen3.7-plus", displayName: "Qwen3.7-Plus", monthlyTokens: 0, creditTokens: 1000000, freeType: "one-time-initial", poolKey: "qoder", tos: "caution" }, + { provider: "qoder", modelId: "kimi-k3", displayName: "Kimi-K3", monthlyTokens: 0, creditTokens: 1000000, freeType: "one-time-initial", poolKey: "qoder", tos: "caution" }, + { provider: "qoder", modelId: "kimi-k2.7-code", displayName: "Kimi-K2.7-Code", monthlyTokens: 0, creditTokens: 1000000, freeType: "one-time-initial", poolKey: "qoder", tos: "caution" }, + { provider: "qoder", modelId: "glm-5.2", displayName: "GLM-5.2", monthlyTokens: 0, creditTokens: 1000000, freeType: "one-time-initial", poolKey: "qoder", tos: "caution" }, + { provider: "qoder", modelId: "deepseek-v4-pro", displayName: "DeepSeek-V4-Pro", monthlyTokens: 0, creditTokens: 1000000, freeType: "one-time-initial", poolKey: "qoder", tos: "caution" }, + { provider: "qoder", modelId: "deepseek-v4-flash", displayName: "DeepSeek-V4-Flash", monthlyTokens: 0, creditTokens: 1000000, freeType: "one-time-initial", poolKey: "qoder", tos: "caution" }, + { provider: "qoder", modelId: "minimax-m3", displayName: "MiniMax-M3", monthlyTokens: 0, creditTokens: 1000000, freeType: "one-time-initial", poolKey: "qoder", tos: "caution" }, + { provider: "qwen-web", modelId: "qwen3.8-max-preview", displayName: "Qwen3.8 Max Preview", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "qwen-web", tos: "avoid" }, { provider: "qwen-web", modelId: "qwen3.7-max", displayName: "Qwen3.7 Max", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "qwen-web", tos: "avoid" }, { provider: "qwen-web", modelId: "qwen3.7-plus", displayName: "Qwen3.7 Plus", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "qwen-web", tos: "avoid" }, { provider: "qwen-web", modelId: "qwen3.6-plus", displayName: "Qwen3.6 Plus", monthlyTokens: 0, creditTokens: 0, freeType: "keyless", poolKey: "qwen-web", tos: "avoid" }, diff --git a/open-sse/config/providers/registry/qoder/index.ts b/open-sse/config/providers/registry/qoder/index.ts index 844cf5a945..4e2bf66ed7 100644 --- a/open-sse/config/providers/registry/qoder/index.ts +++ b/open-sse/config/providers/registry/qoder/index.ts @@ -17,21 +17,65 @@ export const qoderProvider: RegistryEntry = { authUrl: process.env.QODER_OAUTH_AUTHORIZE_URL || "", }, models: [ - { id: "qoder-rome-30ba3b", name: "Qoder ROME" }, - { id: "glm-5.2", name: "GLM-5.2" }, - { id: "minimax-m3", name: "MiniMax M3", supportsVision: true }, - { id: "qwen3-coder-plus", name: "Qwen3 Coder Plus" }, - { id: "qwen3-max", name: "Qwen3 Max" }, - { id: "qwen3-vl-plus", name: "Qwen3 Vision Plus", supportsVision: true }, - { id: "kimi-k2-0905", name: "Kimi K2 0905" }, - { id: "qwen3-max-preview", name: "Qwen3 Max Preview" }, - { id: "kimi-k2", name: "Kimi K2" }, - { id: "deepseek-v3.2", name: "DeepSeek-V3.2-Exp" }, - { id: "deepseek-r1", name: "DeepSeek R1" }, - { id: "deepseek-v3", name: "DeepSeek V3" }, - { id: "qwen3-32b", name: "Qwen3 32B" }, - { id: "qwen3-235b-a22b-thinking-2507", name: "Qwen3 235B A22B Thinking 2507" }, - { id: "qwen3-235b-a22b-instruct", name: "Qwen3 235B A22B Instruct" }, - { id: "qwen3-235b", name: "Qwen3 235B" }, + { + id: "qwen3.8-max-preview", + name: "Qwen3.8-Max-Preview", + supportsVision: true, + supportsReasoning: true, + contextLength: 1_000_000, + maxInputTokens: 180_000, + }, + { + id: "qwen3.7-max", + name: "Qwen3.7-Max", + supportsVision: true, + contextLength: 1_000_000, + }, + { + id: "qwen3.7-plus", + name: "Qwen3.7-Plus", + supportsVision: true, + contextLength: 1_000_000, + }, + { + id: "kimi-k3", + name: "Kimi-K3", + supportsVision: true, + contextLength: 1_000_000, + maxInputTokens: 180_000, + }, + { + id: "kimi-k2.7-code", + name: "Kimi-K2.7-Code", + supportsVision: true, + contextLength: 256_000, + }, + { + id: "glm-5.2", + name: "GLM-5.2", + supportsVision: true, + supportsReasoning: true, + contextLength: 1_000_000, + }, + { + id: "deepseek-v4-pro", + name: "DeepSeek-V4-Pro", + supportsVision: true, + supportsReasoning: true, + contextLength: 1_000_000, + }, + { + id: "deepseek-v4-flash", + name: "DeepSeek-V4-Flash", + supportsVision: true, + supportsReasoning: true, + contextLength: 1_000_000, + }, + { + id: "minimax-m3", + name: "MiniMax-M3", + supportsVision: true, + contextLength: 1_000_000, + }, ], }; diff --git a/open-sse/config/providers/registry/qwen/web/index.ts b/open-sse/config/providers/registry/qwen/web/index.ts index 8b86c9806c..3e3ae455a3 100644 --- a/open-sse/config/providers/registry/qwen/web/index.ts +++ b/open-sse/config/providers/registry/qwen/web/index.ts @@ -16,8 +16,41 @@ export const qwen_webProvider: RegistryEntry = { // ids (qwen-plus, qwen3-max, ...) still resolve via the executor's // MODEL_ALIASES map for backward compatibility. models: [ - { id: "qwen3.7-max", name: "Qwen3.7 Max" }, - { id: "qwen3.7-plus", name: "Qwen3.7 Plus" }, - { id: "qwen3.6-plus", name: "Qwen3.6 Plus" }, + { + id: "qwen3.8-max-preview", + name: "Qwen3.8 Max Preview", + toolCalling: true, + supportsReasoning: true, + supportsVision: true, + contextLength: 1_000_000, + maxOutputTokens: 65_536, + }, + { + id: "qwen3.7-max", + name: "Qwen3.7 Max", + toolCalling: true, + supportsReasoning: true, + supportsVision: false, + contextLength: 1_000_000, + maxOutputTokens: 65_536, + }, + { + id: "qwen3.7-plus", + name: "Qwen3.7 Plus", + toolCalling: true, + supportsReasoning: true, + supportsVision: true, + contextLength: 1_000_000, + maxOutputTokens: 65_536, + }, + { + id: "qwen3.6-plus", + name: "Qwen3.6 Plus", + toolCalling: true, + supportsReasoning: true, + supportsVision: true, + contextLength: 1_000_000, + maxOutputTokens: 65_536, + }, ], }; diff --git a/open-sse/executors/qoder.ts b/open-sse/executors/qoder.ts index 62ac641e90..7dfa139462 100644 --- a/open-sse/executors/qoder.ts +++ b/open-sse/executors/qoder.ts @@ -208,7 +208,7 @@ export class QoderExecutor extends BaseExecutor { }; } - const resolvedModel = model || "qwen3-coder-plus"; + const resolvedModel = model || "qwen3.8-max-preview"; // Detect token type: PAT (Personal Access Token) starts with "pt-". // PATs are driven through the local qodercli binary (see executeViaQoderCli); diff --git a/open-sse/executors/qwen-web.ts b/open-sse/executors/qwen-web.ts index bb812a10e7..f7b072e174 100644 --- a/open-sse/executors/qwen-web.ts +++ b/open-sse/executors/qwen-web.ts @@ -67,6 +67,7 @@ const MODEL_ALIASES: Record = { }; const DEFAULT_MODEL = "qwen3.7-max"; +const REQUIRED_THINKING_MODELS = new Set(["qwen3.8-max-preview"]); function mapModel(modelId: string): string { return MODEL_ALIASES[modelId] || modelId; @@ -297,7 +298,8 @@ export class QwenWebExecutor extends BaseExecutor { requestedModel: string ): Record { const fid = uuid(); - const enableThinking = /think|reason|r1/i.test(requestedModel); + const enableThinking = + REQUIRED_THINKING_MODELS.has(modelId) || /think|reason|r1/i.test(requestedModel); const featureConfig: Record = { thinking_enabled: enableThinking, output_schema: "phase", diff --git a/open-sse/services/qoderCli.ts b/open-sse/services/qoderCli.ts index bccd50de6d..393f2d4638 100644 --- a/open-sse/services/qoderCli.ts +++ b/open-sse/services/qoderCli.ts @@ -4,31 +4,26 @@ import fs from "fs"; import os from "os"; import path from "path"; import { getLookupEnv } from "@/shared/services/cliRuntime"; +import { qoderProvider } from "../config/providers/registry/qoder/index.ts"; import { buildQoderCliNotFoundHint, resolveQoderCliInvocation } from "./qoderCliResolve"; export { getQoderCliCommand } from "./qoderCliResolve"; // #6263 public entry point const DEFAULT_TIMEOUT_MS = 45_000; const DEFAULT_MODELS_TIMEOUT_MS = 20_000; -const QODER_DEFAULT_MODEL = "qoder-rome-30ba3b"; +const QODER_DEFAULT_MODEL = "qwen3.8-max-preview"; +const QODER_MODEL_LEVELS = { + "qwen3.8-max-preview": "qmodel_preview", + "qwen3.7-max": "qmodel_latest", + "qwen3.7-plus": "qmodel", + "kimi-k3": "kmodel_latest", + "kimi-k2.7-code": "kmodel", + "glm-5.2": "gm51model", + "deepseek-v4-pro": "dmodel", + "deepseek-v4-flash": "dfmodel", + "minimax-m3": "mmodel", +} as const; -export const QODER_STATIC_MODELS = [ - { id: "qoder-rome-30ba3b", name: "Qoder ROME" }, - { id: "glm-5.2", name: "GLM-5.2" }, - { id: "minimax-m3", name: "MiniMax M3" }, - { id: "qwen3-coder-plus", name: "Qwen3 Coder Plus" }, - { id: "qwen3-max", name: "Qwen3 Max" }, - { id: "qwen3-vl-plus", name: "Qwen3 Vision Plus" }, - { id: "kimi-k2-0905", name: "Kimi K2 0905" }, - { id: "qwen3-max-preview", name: "Qwen3 Max Preview" }, - { id: "kimi-k2", name: "Kimi K2" }, - { id: "deepseek-v3.2", name: "DeepSeek V3.2" }, - { id: "deepseek-r1", name: "DeepSeek R1" }, - { id: "deepseek-v3", name: "DeepSeek V3" }, - { id: "qwen3-32b", name: "Qwen3 32B" }, - { id: "qwen3-235b-a22b-thinking-2507", name: "Qwen3 235B A22B Thinking 2507" }, - { id: "qwen3-235b-a22b-instruct", name: "Qwen3 235B A22B Instruct" }, - { id: "qwen3-235b", name: "Qwen3 235B" }, -]; +export const QODER_STATIC_MODELS = qoderProvider.models.map(({ id, name }) => ({ id, name })); type JsonRecord = Record; @@ -455,33 +450,30 @@ export function getStaticQoderModels() { } /** qodercli's `-m` accepts these level keys (see `qodercli --list-models`). */ -const QODER_LEVEL_KEYS = new Set([ - "auto", - "ultimate", - "performance", - "efficient", - "lite", - "q35model_preview", - "qmodel_latest", - "qmodel", - "gm51model", - "kmodel", - "dmodel", - "dfmodel", - "mmodel", -]); +const QODER_LEVEL_KEYS = new Set(["auto", ...Object.values(QODER_MODEL_LEVELS)]); + +function resolveQoderModelLevel(value: string): string | null { + const modelId = value.includes("/") ? value.slice(value.lastIndexOf("/") + 1) : value; + return QODER_MODEL_LEVELS[modelId as keyof typeof QODER_MODEL_LEVELS] ?? null; +} export function mapQoderModelToLevel(model: string | null | undefined): string | null { const normalized = String(model || "") .trim() .toLowerCase(); if (!normalized) return null; + // Keep Qoder's retired pre-3.8 level as an alias for the current preview. + if (normalized === "q35model_preview") return "qmodel_preview"; // A caller may pass a qodercli level key directly (e.g. "gm51model") — honor it. if (QODER_LEVEL_KEYS.has(normalized)) return normalized; - if (normalized.includes("deepseek-r1")) return "ultimate"; + const currentLevel = resolveQoderModelLevel(normalized); + if (currentLevel) return currentLevel; + // Legacy public ids stay accepted without keeping the retired abstract tiers. + if (normalized.includes("deepseek-r1")) return "dmodel"; if (normalized.includes("glm")) return "gm51model"; // GLM-5.2 (`qoder/glm-5.2`) if (normalized.includes("minimax")) return "mmodel"; - if (normalized.includes("qwen3-max")) return "performance"; + if (normalized.includes("qwen3-max-preview")) return "qmodel_preview"; + if (normalized.includes("qwen3-max")) return "qmodel_latest"; if (normalized.includes("kimi-k2")) return "kmodel"; if (normalized.includes("qwen3-coder")) return "qmodel"; if (normalized.includes("qoder-rome")) return "qmodel"; diff --git a/src/shared/constants/modelSpecs.ts b/src/shared/constants/modelSpecs.ts index 98cf09e888..240bba7e77 100644 --- a/src/shared/constants/modelSpecs.ts +++ b/src/shared/constants/modelSpecs.ts @@ -400,6 +400,13 @@ export const MODEL_SPECS: Record = { supportsVision: true, aliases: ["qwen3.7-max", "qwen3-max-2026-01-23"], }, + "qwen3.8-max-preview": { + maxOutputTokens: 65536, + contextWindow: 1000000, + supportsThinking: true, + supportsTools: true, + supportsVision: true, + }, "qwen3.6-plus": { maxOutputTokens: 65536, contextWindow: 1000000, diff --git a/tests/unit/executor-qwen-web.test.ts b/tests/unit/executor-qwen-web.test.ts index e0eaff5272..785b2e7a6e 100644 --- a/tests/unit/executor-qwen-web.test.ts +++ b/tests/unit/executor-qwen-web.test.ts @@ -1,5 +1,6 @@ import { describe, it, beforeEach, afterEach } from "node:test"; import assert from "node:assert/strict"; +import type { RegistryModel } from "../../open-sse/config/providers/shared.ts"; const mod = await import("../../open-sse/executors/qwen-web.ts"); const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts"); @@ -40,7 +41,7 @@ function chatCreatedResponse(id = "chat-abc"): Response { function wafHtmlResponse(status = 504): Response { return new Response( "\n504 Gateway Time-out\n\n" + - '

504 Gateway Time-out

\n
alibaba-ga
\n' + + "

504 Gateway Time-out

\n
alibaba-ga
\n" + '\n\n', { status, headers: { "content-type": "text/html; charset=utf-8" } } ); @@ -96,6 +97,7 @@ describe("QwenWebExecutor (v2 migration)", () => { assert.equal(compBody.model, "qwen3.7-max"); assert.equal(compBody.messages[0].role, "user"); assert.equal(compBody.messages[0].content, "hi"); + assert.equal(compBody.messages[0].feature_config.thinking_enabled, false); const json = (await result.response.json()) as any; assert.equal(json.choices[0].message.content, "Hello world"); @@ -280,14 +282,40 @@ describe("QwenWebExecutor (v2 migration)", () => { it("registry points at the v2 endpoint and the current model catalog", () => { const provider = (REGISTRY as any)["qwen-web"]; assert.ok(provider, "qwen-web must be registered"); - assert.match(provider.baseUrl, /\/api\/v2\/chat\/completions$/, "registry must use v2 endpoint"); + assert.match( + provider.baseUrl, + /\/api\/v2\/chat\/completions$/, + "registry must use v2 endpoint" + ); const ids = provider.models.map((m: any) => m.id); - assert.deepEqual(ids.sort(), ["qwen3.6-plus", "qwen3.7-max", "qwen3.7-plus"]); + assert.deepEqual(ids.sort(), [ + "qwen3.6-plus", + "qwen3.7-max", + "qwen3.7-plus", + "qwen3.8-max-preview", + ]); + + const qwen38 = provider.models.find( + (model: RegistryModel) => model.id === "qwen3.8-max-preview" + ); + assert.deepEqual(qwen38, { + id: "qwen3.8-max-preview", + name: "Qwen3.8 Max Preview", + toolCalling: true, + supportsReasoning: true, + supportsVision: true, + contextLength: 1_000_000, + maxOutputTokens: 65_536, + }); + + const qwen37Max = provider.models.find((model: RegistryModel) => model.id === "qwen3.7-max"); + assert.equal(qwen37Max.supportsVision, false); }); it("free-model catalog lists the current qwen-web ids (not the retired ones)", () => { const qwenModels = (FREE_MODEL_BUDGETS as any[]).filter((m) => m.provider === "qwen-web"); const ids = qwenModels.map((m) => m.modelId); + assert.ok(ids.includes("qwen3.8-max-preview"), "catalog must list qwen3.8-max-preview"); assert.ok(ids.includes("qwen3.7-max"), "catalog must list qwen3.7-max"); assert.ok(!ids.includes("qwen-plus"), "retired qwen-plus must be gone"); assert.ok( @@ -296,6 +324,35 @@ describe("QwenWebExecutor (v2 migration)", () => { ); }); + it("passes qwen3.8-max-preview through unchanged", async () => { + globalThis.fetch = (async (url: string | URL | Request, init: RequestInit = {}) => { + calls.push({ url: String(url), init }); + if (String(url).includes("/api/v2/chats/new")) return chatCreatedResponse(); + return sseResponse([ + { choices: [{ delta: { phase: "answer", content: "ok", status: "finished" } }] }, + ]); + }) as typeof globalThis.fetch; + + const executor = new mod.QwenWebExecutor(); + await executor.execute({ + model: "qwen3.8-max-preview", + body: { + model: "qwen3.8-max-preview", + messages: [{ role: "user", content: "hi" }], + }, + stream: false, + credentials: { apiKey: "token=t; cna=c" }, + signal: null, + }); + + const newBody = JSON.parse(calls[0].init.body); + const completionBody = JSON.parse(calls[1].init.body); + assert.deepEqual(newBody.models, ["qwen3.8-max-preview"]); + assert.equal(completionBody.model, "qwen3.8-max-preview"); + assert.equal(completionBody.messages[0].feature_config.thinking_enabled, true); + assert.equal(completionBody.messages[0].feature_config.auto_thinking, true); + }); + it("maps legacy model ids to the current upstream catalog", async () => { globalThis.fetch = (async (url: any, init: any = {}) => { calls.push({ url: String(url), init }); diff --git a/tests/unit/qoder-cli.test.ts b/tests/unit/qoder-cli.test.ts index 5f781f4baa..12fbdce918 100644 --- a/tests/unit/qoder-cli.test.ts +++ b/tests/unit/qoder-cli.test.ts @@ -5,6 +5,7 @@ import os from "node:os"; import path from "node:path"; const qoderCli = await import("../../open-sse/services/qoderCli.ts"); +const { qoderProvider } = await import("../../open-sse/config/providers/registry/qoder/index.ts"); /** * Write a fake `qodercli` binary and point CLI_QODER_BIN at it (see the twin @@ -115,13 +116,105 @@ test("qoder cli static models are copied and model-to-level mapping covers major models[0].name = "mutated"; assert.notEqual(snapshot[0].name, "mutated"); - assert.equal(qoderCli.mapQoderModelToLevel("deepseek-r1"), "ultimate"); - assert.equal(qoderCli.mapQoderModelToLevel("qwen3-max-preview"), "performance"); + assert.deepEqual( + snapshot.map((model) => model.id), + [ + "qwen3.8-max-preview", + "qwen3.7-max", + "qwen3.7-plus", + "kimi-k3", + "kimi-k2.7-code", + "glm-5.2", + "deepseek-v4-pro", + "deepseek-v4-flash", + "minimax-m3", + ] + ); + assert.deepEqual( + snapshot, + qoderProvider.models.map(({ id, name }) => ({ id, name })) + ); + assert.deepEqual(qoderProvider.models, [ + { + id: "qwen3.8-max-preview", + name: "Qwen3.8-Max-Preview", + supportsVision: true, + supportsReasoning: true, + contextLength: 1_000_000, + maxInputTokens: 180_000, + }, + { + id: "qwen3.7-max", + name: "Qwen3.7-Max", + supportsVision: true, + contextLength: 1_000_000, + }, + { + id: "qwen3.7-plus", + name: "Qwen3.7-Plus", + supportsVision: true, + contextLength: 1_000_000, + }, + { + id: "kimi-k3", + name: "Kimi-K3", + supportsVision: true, + contextLength: 1_000_000, + maxInputTokens: 180_000, + }, + { + id: "kimi-k2.7-code", + name: "Kimi-K2.7-Code", + supportsVision: true, + contextLength: 256_000, + }, + { + id: "glm-5.2", + name: "GLM-5.2", + supportsVision: true, + supportsReasoning: true, + contextLength: 1_000_000, + }, + { + id: "deepseek-v4-pro", + name: "DeepSeek-V4-Pro", + supportsVision: true, + supportsReasoning: true, + contextLength: 1_000_000, + }, + { + id: "deepseek-v4-flash", + name: "DeepSeek-V4-Flash", + supportsVision: true, + supportsReasoning: true, + contextLength: 1_000_000, + }, + { + id: "minimax-m3", + name: "MiniMax-M3", + supportsVision: true, + contextLength: 1_000_000, + }, + ]); + assert.equal(qoderCli.mapQoderModelToLevel("qwen3.8-max-preview"), "qmodel_preview"); + assert.equal(qoderCli.mapQoderModelToLevel("qoder/qwen3.8-max-preview"), "qmodel_preview"); + assert.equal(qoderCli.mapQoderModelToLevel("qwen3.7-max"), "qmodel_latest"); + assert.equal(qoderCli.mapQoderModelToLevel("qwen3.7-plus"), "qmodel"); + assert.equal(qoderCli.mapQoderModelToLevel("kimi-k3"), "kmodel_latest"); + assert.equal(qoderCli.mapQoderModelToLevel("kimi-k2.7-code"), "kmodel"); + assert.equal(qoderCli.mapQoderModelToLevel("deepseek-v4-pro"), "dmodel"); + assert.equal(qoderCli.mapQoderModelToLevel("deepseek-v4-flash"), "dfmodel"); + assert.equal(qoderCli.mapQoderModelToLevel("deepseek-r1"), "dmodel"); + assert.equal(qoderCli.mapQoderModelToLevel("qwen3-max-preview"), "qmodel_preview"); + assert.equal(qoderCli.mapQoderModelToLevel("qwen3-max"), "qmodel_latest"); assert.equal(qoderCli.mapQoderModelToLevel("kimi-k2-0905"), "kmodel"); assert.equal(qoderCli.mapQoderModelToLevel("qwen3-coder-plus"), "qmodel"); assert.equal(qoderCli.mapQoderModelToLevel("qoder-rome-30ba3b"), "qmodel"); assert.equal(qoderCli.mapQoderModelToLevel("glm-5.2"), "gm51model"); assert.equal(qoderCli.mapQoderModelToLevel("minimax-m3"), "mmodel"); + assert.equal(qoderCli.mapQoderModelToLevel("q35model_preview"), "qmodel_preview"); + assert.equal(qoderCli.mapQoderModelToLevel("qmodel_preview"), "qmodel_preview"); + assert.equal(qoderCli.mapQoderModelToLevel("kmodel_latest"), "kmodel_latest"); assert.equal(qoderCli.mapQoderModelToLevel("gm51model"), "gm51model"); assert.equal(qoderCli.mapQoderModelToLevel("totally-unknown"), "auto"); assert.equal(qoderCli.mapQoderModelToLevel(""), null); @@ -437,8 +530,9 @@ test("parseQoderCliModelNames extracts display names, dropping header/noise", () }); test("resolveQoderModelName prefers a live display name, then static, then Auto", () => { - const live = ["Auto", "GLM-5.2", "Kimi-K2.7-Code"]; + const live = ["Auto", "Qwen3.8-Max-Preview", "GLM-5.2", "Kimi-K2.7-Code"]; // punctuation/case-insensitive match against the live list + assert.equal(qoderCli.resolveQoderModelName("qwen3.8-max-preview", live), "Qwen3.8-Max-Preview"); assert.equal(qoderCli.resolveQoderModelName("glm-5.2", live), "GLM-5.2"); assert.equal(qoderCli.resolveQoderModelName("GLM-5.2", live), "GLM-5.2"); assert.equal(qoderCli.resolveQoderModelName("kimi-k2.7-code", live), "Kimi-K2.7-Code"); diff --git a/tests/unit/qoder-executor.test.ts b/tests/unit/qoder-executor.test.ts index bbbdacdb02..e5e1d80235 100644 --- a/tests/unit/qoder-executor.test.ts +++ b/tests/unit/qoder-executor.test.ts @@ -6,6 +6,8 @@ import path from "node:path"; import { QoderExecutor } from "../../open-sse/executors/qoder.ts"; import { getQwenCliUserAgent } from "../../open-sse/config/providerHeaderProfiles.ts"; +import { qoderProvider } from "../../open-sse/config/providers/registry/qoder/index.ts"; +import { FREE_MODEL_BUDGETS } from "../../open-sse/config/freeModelCatalog.data.ts"; import { buildQoderPrompt, getStaticQoderModels, @@ -110,15 +112,35 @@ test("normalizeQoderPatProviderData forces PAT + qodercli transport", () => { test("mapQoderModelToLevel maps static models to qodercli levels", () => { assert.equal(mapQoderModelToLevel("qoder-rome-30ba3b"), "qmodel"); - assert.equal(mapQoderModelToLevel("deepseek-r1"), "ultimate"); - assert.equal(mapQoderModelToLevel("qwen3-max"), "performance"); + assert.equal(mapQoderModelToLevel("deepseek-r1"), "dmodel"); + assert.equal(mapQoderModelToLevel("qwen3-max"), "qmodel_latest"); + assert.equal(mapQoderModelToLevel("qwen3.8-max-preview"), "qmodel_preview"); + assert.equal(mapQoderModelToLevel("kimi-k3"), "kmodel_latest"); assert.equal(mapQoderModelToLevel(""), null); }); -test("getStaticQoderModels exposes the static if/* catalog seed", () => { +test("getStaticQoderModels exposes the current nine-model Qoder catalog", () => { const models = getStaticQoderModels(); - assert.ok(models.some((model) => model.id === "qoder-rome-30ba3b")); - assert.ok(models.some((model) => model.id === "deepseek-r1")); + const ids = models.map((model) => model.id); + const freeCatalogIds = FREE_MODEL_BUDGETS.filter((model) => model.provider === "qoder").map( + (model) => model.modelId + ); + + assert.equal(models.length, 9); + assert.deepEqual( + ids, + qoderProvider.models.map((model) => model.id) + ); + assert.deepEqual(freeCatalogIds, ids); + assert.ok(ids.includes("qwen3.8-max-preview")); + assert.ok(ids.includes("deepseek-v4-flash")); + assert.ok(!ids.includes("qoder-rome-30ba3b")); + + const preview = qoderProvider.models.find((model) => model.id === "qwen3.8-max-preview"); + assert.equal(preview?.supportsVision, true); + assert.equal(preview?.supportsReasoning, true); + assert.equal(preview?.contextLength, 1_000_000); + assert.equal(preview?.maxInputTokens, 180_000); }); test("buildQoderPrompt flattens transcript and warns against local tools", () => { diff --git a/tests/unit/t31-t33-t34-t38-model-specs.test.ts b/tests/unit/t31-t33-t34-t38-model-specs.test.ts index fd35fd2445..c27b301f68 100644 --- a/tests/unit/t31-t33-t34-t38-model-specs.test.ts +++ b/tests/unit/t31-t33-t34-t38-model-specs.test.ts @@ -96,6 +96,11 @@ test("opencode-go family: context/output caps match upstream provider docs", () // Qwen3.x Plus / Max: 1M context, 65K output (Bailian) assert.equal(getModelSpec("qwen3-max").contextWindow, 1000000); assert.equal(getModelSpec("qwen3-max").maxOutputTokens, 65536); + assert.equal(getModelSpec("qwen3.8-max-preview").contextWindow, 1000000); + assert.equal(getModelSpec("qwen3.8-max-preview").maxOutputTokens, 65536); + assert.equal(getModelSpec("qwen3.8-max-preview").supportsThinking, true); + assert.equal(getModelSpec("qwen3.8-max-preview").supportsTools, true); + assert.equal(getModelSpec("qwen3.8-max-preview").supportsVision, true); assert.equal(getModelSpec("qwen3.7-max").contextWindow, 1000000); assert.equal(getModelSpec("qwen3-max-2026-01-23").contextWindow, 1000000); assert.equal(getModelSpec("qwen3.6-plus").contextWindow, 1000000); @@ -132,6 +137,7 @@ test("opencode-go family: capMaxOutputTokens grants full upstream budget", () => // Without explicit specs these models would fall back to __default__ (8192). // Assert they now receive the real upstream cap. assert.equal(capMaxOutputTokens("qwen3-max", 100000), 65536); + assert.equal(capMaxOutputTokens("qwen3.8-max-preview", 100000), 65536); assert.equal(capMaxOutputTokens("qwen3.7-max", 100000), 65536); assert.equal(capMaxOutputTokens("qwen3-max-2026-01-23", 100000), 65536); assert.equal(capMaxOutputTokens("kimi-k2.5", 300000), 262144);