feat(qwen): add Qwen3.8 Max Preview catalogs [Part 2/3] (#7874)

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>
This commit is contained in:
backryun
2026-07-21 06:48:16 +09:00
committed by GitHub
parent 74f5f8377d
commit ccdbc89290
16 changed files with 360 additions and 107 deletions

View File

@@ -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) |
---

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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.20.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`

View File

@@ -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" },

View File

@@ -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,
},
],
};

View File

@@ -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,
},
],
};

View File

@@ -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);

View File

@@ -67,6 +67,7 @@ const MODEL_ALIASES: Record<string, string> = {
};
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<string, unknown> {
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<string, unknown> = {
thinking_enabled: enableThinking,
output_schema: "phase",

View File

@@ -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<string, unknown>;
@@ -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";

View File

@@ -400,6 +400,13 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
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,

View File

@@ -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(
"<html>\n<head><title>504 Gateway Time-out</title></head>\n<body>\n" +
'<center><h1>504 Gateway Time-out</h1></center>\n<hr><center>alibaba-ga</center>\n' +
"<center><h1>504 Gateway Time-out</h1></center>\n<hr><center>alibaba-ga</center>\n" +
'<meta name="aliyun_waf_aa" content="ff926c7f07e45e2e487a29a6197d3460">\n</body>\n</html>',
{ 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 });

View File

@@ -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");

View File

@@ -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", () => {

View File

@@ -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);