fix(ai): use Qwen thinking controls for token plan reasoning models

fixes #6998 #6951
This commit is contained in:
David Brailovsky 2026-07-29 14:58:48 +00:00
parent 4f0437e2d5
commit 4c1a0b92e5
3 changed files with 254 additions and 27 deletions

View file

@ -265,6 +265,33 @@ const DEEPSEEK_V4_THINKING_LEVEL_MAP = {
high: "high",
max: "max",
} as const;
const QWEN_TOKEN_PLAN_HIGH_MAX_THINKING_LEVEL_MAP = {
minimal: null,
low: null,
medium: null,
high: "high",
xhigh: null,
max: "max",
} as const;
const QWEN_TOKEN_PLAN_QWEN38_THINKING_LEVEL_MAP = {
minimal: null,
low: "low",
medium: "medium",
high: null,
xhigh: "xhigh",
max: null,
} as const;
const QWEN_TOKEN_PLAN_REASONING_EFFORT_UNSUPPORTED_MODEL_IDS = new Set([
"MiniMax-M2.5",
"deepseek-v3.2",
"kimi-k2.5",
"kimi-k2.6",
"kimi-k2.7-code",
"qwen3.6-flash",
"qwen3.6-plus",
"qwen3.7-max",
"qwen3.7-plus",
]);
const KIMI_K3_MAX_TOKENS = 131072;
const KIMI_K3_COST = {
@ -1971,6 +1998,7 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
thinkingFormat: "qwen",
supportsDeveloperRole: false,
supportsStore: false,
supportsReasoningEffort: true,
};
const qwenTokenPlanVariants = [
{
@ -1992,6 +2020,7 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
for (const [modelId, model] of Object.entries(providerModels)) {
const m = model as ModelsDevModel;
if (m.tool_call !== true) continue;
const supportsReasoningEffort = !QWEN_TOKEN_PLAN_REASONING_EFFORT_UNSUPPORTED_MODEL_IDS.has(modelId);
models.push({
id: modelId,
@ -1999,7 +2028,17 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
api: "openai-completions",
provider,
baseUrl,
compat: qwenTokenPlanCompat,
compat: supportsReasoningEffort
? qwenTokenPlanCompat
: { ...qwenTokenPlanCompat, supportsReasoningEffort: false },
...(supportsReasoningEffort
? {
thinkingLevelMap:
modelId === "qwen3.8-max-preview"
? QWEN_TOKEN_PLAN_QWEN38_THINKING_LEVEL_MAP
: QWEN_TOKEN_PLAN_HIGH_MAX_THINKING_LEVEL_MAP,
}
: {}),
reasoning: m.reasoning === true,
input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
cost: {
@ -2274,7 +2313,12 @@ async function generateModels() {
allModels.push(...antLingModels);
for (const candidate of allModels) {
if (candidate.api === "openai-completions" && candidate.id.includes("deepseek-v4")) {
if (
candidate.api === "openai-completions" &&
candidate.id.includes("deepseek-v4") &&
candidate.provider !== "qwen-token-plan" &&
candidate.provider !== "qwen-token-plan-cn"
) {
const preservesNativeReasoningEffort = candidate.provider === "openrouter" || candidate.provider === "opencode";
candidate.compat = {
...candidate.compat,
@ -2417,29 +2461,6 @@ async function generateModels() {
});
}
// Add qwen3.8-max-preview to Qwen Token Plan providers until models.dev includes it
for (const qwenTpProvider of ["qwen-token-plan", "qwen-token-plan-cn"] as const) {
if (!allModels.some((m) => m.provider === qwenTpProvider && m.id === "qwen3.8-max-preview")) {
const baseUrl =
qwenTpProvider === "qwen-token-plan"
? "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1"
: "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1";
allModels.push({
id: "qwen3.8-max-preview",
name: "Qwen3.8 Max Preview",
api: "openai-completions",
provider: qwenTpProvider,
baseUrl,
compat: { thinkingFormat: "qwen", supportsDeveloperRole: false, supportsStore: false } satisfies OpenAICompletionsCompat,
reasoning: true,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 1000000,
maxTokens: 65536,
});
}
}
// Add "auto" alias for openrouter/auto
if (!allModels.some(m => m.provider === "openrouter" && m.id === "auto")) {
allModels.push({

View file

@ -752,6 +752,12 @@ function buildParams(
}
} else if (compat.thinkingFormat === "qwen" && model.reasoning) {
(params as any).enable_thinking = !!options?.reasoningEffort;
if (options?.reasoningEffort && compat.supportsReasoningEffort) {
const effort = model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort;
if (typeof effort === "string") {
(params as any).reasoning_effort = effort;
}
}
} else if (compat.thinkingFormat === "qwen-chat-template" && model.reasoning) {
(params as any).chat_template_kwargs = {
enable_thinking: !!options?.reasoningEffort,

View file

@ -1,5 +1,42 @@
import { describe, expect, it } from "vitest";
import { getModels } from "../src/compat.ts";
import { describe, expect, it, vi } from "vitest";
import { getModels, streamSimple } from "../src/compat.ts";
vi.mock("openai", () => {
class FakeOpenAI {
chat = {
completions: {
create: () => {
const stream = {
async *[Symbol.asyncIterator]() {
yield {
choices: [{ delta: {}, finish_reason: "stop" }],
usage: {
prompt_tokens: 1,
completion_tokens: 1,
prompt_tokens_details: { cached_tokens: 0 },
completion_tokens_details: { reasoning_tokens: 0 },
},
};
},
};
const promise = Promise.resolve(stream) as Promise<typeof stream> & {
withResponse: () => Promise<{
data: typeof stream;
response: { status: number; headers: Headers };
}>;
};
promise.withResponse = async () => ({
data: stream,
response: { status: 200, headers: new Headers() },
});
return promise;
},
},
};
}
return { default: FakeOpenAI };
});
const TEXT_MODELS = [
"MiniMax-M2.5",
@ -21,6 +58,33 @@ const TEXT_MODELS = [
const IMAGE_MODELS = ["qwen-image-2.0", "qwen-image-2.0-pro", "wan2.7-image", "wan2.7-image-pro"];
const QWEN_THINKING_MODELS = [
"deepseek-v3.2",
"deepseek-v4-flash",
"deepseek-v4-pro",
"glm-5",
"glm-5.1",
"glm-5.2",
"kimi-k2.5",
"kimi-k2.6",
"kimi-k2.7-code",
"qwen3.6-flash",
"qwen3.6-plus",
"qwen3.7-max",
"qwen3.7-plus",
"qwen3.8-max-preview",
] as const;
const QWEN_THINKING_MODEL_CASES = (["qwen-token-plan", "qwen-token-plan-cn"] as const).flatMap((provider) =>
QWEN_THINKING_MODELS.map((modelId) => ({ provider, modelId })),
);
const QWEN_REASONING_EFFORT_MODELS = ["deepseek-v4-flash", "deepseek-v4-pro", "glm-5", "glm-5.1", "glm-5.2"] as const;
const QWEN_REASONING_EFFORT_MODEL_CASES = (["qwen-token-plan", "qwen-token-plan-cn"] as const).flatMap((provider) =>
QWEN_REASONING_EFFORT_MODELS.map((modelId) => ({ provider, modelId })),
);
describe("Qwen Token Plan models", () => {
it.each(["qwen-token-plan", "qwen-token-plan-cn"] as const)("exposes all text models on %s", (provider) => {
const modelIds = getModels(provider).map((model) => model.id);
@ -35,4 +99,140 @@ describe("Qwen Token Plan models", () => {
expect(modelIds, `${provider} should not include ${excluded}`).not.toContain(excluded);
}
});
// docs: https://modelstudio.console.alibabacloud.com/ap-southeast-1?tab=api&commonbuy=1#/api/?type=model&url=3016807
it.each(QWEN_THINKING_MODEL_CASES)(
"sends Qwen thinking fields for $provider/$modelId",
async ({ provider, modelId }) => {
const model = getModels(provider).find((candidate) => candidate.id === modelId);
expect(model).toBeDefined();
if (!model) throw new Error(`Missing model: ${provider}/${modelId}`);
let payload: unknown;
await streamSimple(
model,
{
messages: [
{
role: "user",
content: "Hi",
timestamp: Date.now(),
},
],
},
{
apiKey: "test",
reasoning: "high",
onPayload: (params) => {
payload = params;
},
},
).result();
expect(payload).toHaveProperty("enable_thinking", true);
expect(payload).not.toHaveProperty("thinking");
},
);
it.each(QWEN_REASONING_EFFORT_MODEL_CASES)(
"exposes Qwen reasoning_effort levels for $provider/$modelId",
({ provider, modelId }) => {
const model = getModels(provider).find((candidate) => candidate.id === modelId);
expect(model).toBeDefined();
if (!model) throw new Error(`Missing model: ${provider}/${modelId}`);
expect(model.thinkingLevelMap).toMatchObject({
minimal: null,
low: null,
medium: null,
high: "high",
xhigh: null,
max: "max",
});
},
);
it.each(["qwen-token-plan", "qwen-token-plan-cn"] as const)(
"exposes qwen3.8 reasoning_effort levels on %s",
(provider) => {
const model = getModels(provider).find((candidate) => candidate.id === "qwen3.8-max-preview");
expect(model).toBeDefined();
if (!model) throw new Error(`Missing model: ${provider}/qwen3.8-max-preview`);
expect(model.thinkingLevelMap).toMatchObject({
minimal: null,
low: "low",
medium: "medium",
high: null,
xhigh: "xhigh",
max: null,
});
},
);
it.each(QWEN_REASONING_EFFORT_MODEL_CASES)(
"sends Qwen reasoning_effort for $provider/$modelId",
async ({ provider, modelId }) => {
const model = getModels(provider).find((candidate) => candidate.id === modelId);
expect(model).toBeDefined();
if (!model) throw new Error(`Missing model: ${provider}/${modelId}`);
let payload: unknown;
await streamSimple(
model,
{
messages: [
{
role: "user",
content: "Hi",
timestamp: Date.now(),
},
],
},
{
apiKey: "test",
reasoning: "high",
onPayload: (params) => {
payload = params;
},
},
).result();
expect(payload).toHaveProperty("reasoning_effort", "high");
},
);
it.each(["qwen-token-plan", "qwen-token-plan-cn"] as const)(
"sends qwen3.8 max reasoning_effort on %s",
async (provider) => {
const model = getModels(provider).find((candidate) => candidate.id === "qwen3.8-max-preview");
expect(model).toBeDefined();
if (!model) throw new Error(`Missing model: ${provider}/qwen3.8-max-preview`);
let payload: unknown;
await streamSimple(
model,
{
messages: [
{
role: "user",
content: "Hi",
timestamp: Date.now(),
},
],
},
{
apiKey: "test",
reasoning: "xhigh",
onPayload: (params) => {
payload = params;
},
},
).result();
expect(payload).toHaveProperty("enable_thinking", true);
expect(payload).toHaveProperty("reasoning_effort", "xhigh");
expect(payload).not.toHaveProperty("thinking");
},
);
});