fix(text,auth): drop enable_thinking retry and omit the field by default

Pass through model constraint errors instead of auto-retrying, switch token-plan default text model to qwen3.7-plus, and align related e2e expectations.
This commit is contained in:
clh02467605
2026-07-28 17:19:05 +08:00
parent 7ad14a79b9
commit 20e3555b84
10 changed files with 38 additions and 349 deletions
@@ -4,9 +4,6 @@ import {
chatPath,
requestJson,
normalizeModelBaseUrl,
applyChatEnableThinking,
resolveChatEnableThinking,
withEnableThinkingRetry,
type AuthPersistPatch,
type AuthStore,
type Identity,
@@ -61,48 +58,31 @@ export async function validateAndPersistApiKey(
? normalizeModelBaseUrl(profile.persistBaseUrl)
: undefined;
const validationModel = profile.defaultTextModel || "qwen3.7-max";
const body: {
model: string;
messages: Array<{ role: string; content: string }>;
max_tokens: number;
stream: boolean;
enable_thinking?: boolean;
} = {
model: validationModel,
messages: [{ role: "user", content: "hi" }],
max_tokens: 1,
stream: false,
};
const requestOpts = {
url: baseUrl + chatPath(),
method: "POST",
headers: { Authorization: `Bearer ${key}` },
timeout: Math.min(deps.settings.timeout, 30),
body,
body: {
model: validationModel,
messages: [{ role: "user", content: "hi" }],
max_tokens: 1,
stream: false,
},
};
try {
await withEnableThinkingRetry({
// Validation requests are always non-streaming.
initial: resolveChatEnableThinking({ stream: false }),
apply: (value) => applyChatEnableThinking(body, value),
run: async () => {
for (let attempt = 1; attempt <= 3; attempt++) {
try {
await requestJson<unknown>(httpDeps, requestOpts);
return;
} catch (error) {
if (attempt >= 3 || !canRetry(error)) throw error;
const delayMs = RETRY_DELAY_BASE_MS * 2 ** (attempt - 1);
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
},
});
} catch (error) {
process.stderr.write("Failed\n");
throw error;
for (let attempt = 1; attempt <= 3; attempt++) {
try {
await requestJson<unknown>(httpDeps, requestOpts);
break;
} catch (error) {
if (attempt >= 3 || !canRetry(error)) {
process.stderr.write("Failed\n");
throw error;
}
const delayMs = RETRY_DELAY_BASE_MS * 2 ** (attempt - 1);
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
process.stderr.write("Valid\n");
+11 -22
View File
@@ -4,9 +4,6 @@ import {
parseSSE,
detectOutputFormat,
readTextFromPathOrStdin,
applyChatEnableThinkingWithBudget,
resolveChatEnableThinking,
withEnableThinkingRetry,
type ChatMessage,
type ChatRequest,
type ChatResponse,
@@ -127,8 +124,7 @@ export default defineCommand({
const { system, messages } = parseMessages(flags);
const model = flags.model || settings.defaultTextModel || "qwen3.7-max";
// Coerce isTTY (may be undefined) so stream:false is serialized.
const shouldStream = Boolean(flags.stream || process.stdout.isTTY);
const shouldStream = flags.stream || process.stdout.isTTY;
const format = detectOutputFormat(settings.output);
// Build messages array with system prompt
@@ -148,14 +144,12 @@ export default defineCommand({
if (flags.temperature !== undefined) body.temperature = flags.temperature;
if (flags.topP !== undefined) body.top_p = flags.topP;
const enableThinking = resolveChatEnableThinking({
enableThinking: flags.enableThinking,
stream: shouldStream,
});
const applyThinking = (value: boolean | undefined) => {
applyChatEnableThinkingWithBudget(body, value, flags.thinkingBudget);
};
applyThinking(enableThinking);
if (flags.enableThinking) {
body.enable_thinking = true;
if (flags.thinkingBudget !== undefined) {
body.thinking_budget = flags.thinkingBudget;
}
}
if (flags.tool) {
const tools = flags.tool.map((t) => {
@@ -230,15 +224,10 @@ export default defineCommand({
resultOut.write("\n");
}
} else {
const response = await withEnableThinkingRetry({
initial: enableThinking,
apply: applyThinking,
run: () =>
ctx.client.requestJson<ChatResponse>({
path: chatPath(),
method: "POST",
body,
}),
const response = await ctx.client.requestJson<ChatResponse>({
path: chatPath(),
method: "POST",
body,
});
const text = response.choices?.[0]?.message?.content ?? "";
+3 -5
View File
@@ -219,7 +219,6 @@ describe("e2e: auth", () => {
body: {
model: "qwen3.7-max",
stream: false,
enable_thinking: false,
},
});
@@ -267,7 +266,7 @@ describe("e2e: auth", () => {
expect(config["token-plan"]).toMatchObject({
api_key: "sk-sp-e2e-placeholder",
base_url: validationServer.baseUrl,
default_text_model: "qwen3.8-max-preview",
default_text_model: "qwen3.7-plus",
default_video_model: "happyhorse-1.1-t2v",
default_image_to_video_model: "happyhorse-1.1-i2v",
default_reference_to_video_model: "happyhorse-1.1-r2v",
@@ -315,9 +314,8 @@ describe("e2e: auth", () => {
authorization: "Bearer sk-sp-e2e-placeholder",
sourceConfig: expect.any(String),
body: {
model: "qwen3.8-max-preview",
model: "qwen3.7-plus",
stream: false,
enable_thinking: false,
},
});
@@ -330,7 +328,7 @@ describe("e2e: auth", () => {
expect(config["token-plan"]).toMatchObject({
api_key: "sk-sp-e2e-placeholder",
base_url: "https://token-plan.cn-beijing.maas.aliyuncs.com",
default_text_model: "qwen3.8-max-preview",
default_text_model: "qwen3.7-plus",
default_video_model: "happyhorse-1.1-t2v",
default_image_to_video_model: "happyhorse-1.1-i2v",
default_reference_to_video_model: "happyhorse-1.1-r2v",
@@ -34,7 +34,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: text chat(DashScope)", () => {
"--model",
"qwen3.7-max",
"--message",
"dry-run",
"干跑",
"--max-tokens",
"8",
"--output",
@@ -42,17 +42,10 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: text chat(DashScope)", () => {
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
request?: {
model?: string;
messages?: Array<{ content?: string }>;
enable_thinking?: boolean;
stream?: boolean;
};
request?: { model?: string; messages?: Array<{ content?: string }> };
}>(stdout);
expect(data.request?.model).toBe("qwen3.7-max");
expect(data.request?.messages?.some((message) => message.content === "dry-run")).toBe(true);
expect(data.request?.stream).toBe(false);
expect(data.request?.enable_thinking).toBe(false);
expect(data.request?.messages?.some((m) => m.content === "干跑")).toBe(true);
});
test("【qwen3.7-max】文本对话", async () => {
@@ -13,7 +13,7 @@ describe("e2e: vision describe", () => {
"token-plan": {
api_key: "sk-sp-e2e-placeholder",
base_url: "https://token-plan.cn-beijing.maas.aliyuncs.com",
default_text_model: "qwen3.8-max-preview",
default_text_model: "qwen3.7-plus",
},
}),
);
@@ -40,6 +40,6 @@ describe("e2e: vision describe", () => {
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ request?: { model?: string } }>(stdout);
expect(data.request?.model).toBe("qwen3.8-max-preview");
expect(data.request?.model).toBe("qwen3.7-plus");
});
});
+1 -1
View File
@@ -10,7 +10,7 @@ interface ModelProfilePreset {
const MODEL_PROFILE_PRESETS: Readonly<Record<string, ModelProfilePreset>> = {
"token-plan": {
baseUrl: "https://token-plan.cn-beijing.maas.aliyuncs.com",
defaultTextModel: "qwen3.8-max-preview",
defaultTextModel: "qwen3.7-plus",
defaultVideoModel: "happyhorse-1.1-t2v",
defaultImageToVideoModel: "happyhorse-1.1-i2v",
defaultReferenceToVideoModel: "happyhorse-1.1-r2v",
-1
View File
@@ -14,6 +14,5 @@ export * from "./finetune/index.ts";
export * from "./deploy/index.ts";
export * from "./types/index.ts";
export * from "./utils/index.ts";
export * from "./models/index.ts";
export * from "./telemetry/index.ts";
export * from "./advisor/index.ts";
-8
View File
@@ -1,8 +0,0 @@
export {
adjustEnableThinkingAfterError,
applyChatEnableThinking,
applyChatEnableThinkingWithBudget,
resolveChatEnableThinking,
withEnableThinkingRetry,
type EnableThinkingAdjustResult,
} from "./thinking.ts";
-88
View File
@@ -1,88 +0,0 @@
/** resolve / adjust / retry helpers for chat `enable_thinking`. */
/** Resolve the initial `enable_thinking` value (`undefined` omits the field). */
export function resolveChatEnableThinking(options: {
enableThinking?: boolean;
/** Whether the request is streaming. */
stream?: boolean;
}): boolean | undefined {
if (options.enableThinking) return true;
if (options.stream === false) return false;
return undefined;
}
export type EnableThinkingAdjustResult =
| { kind: "retry"; value: boolean | undefined }
| { kind: "none" };
/** Map clear `enable_thinking` constraint errors to a one-shot retry adjustment. */
export function adjustEnableThinkingAfterError(
current: boolean | undefined,
errorMessage: string,
): EnableThinkingAdjustResult {
if (current !== true && /enable_thinking parameter is restricted to\s*true/i.test(errorMessage)) {
return { kind: "retry", value: true };
}
if (current === undefined && /enable_thinking must be set to false/i.test(errorMessage)) {
return { kind: "retry", value: false };
}
if (current !== undefined && /does not support enable_thinking/i.test(errorMessage)) {
return { kind: "retry", value: undefined };
}
return { kind: "none" };
}
/** Set or remove `enable_thinking`; clear `thinking_budget` when disabled or omitted. */
export function applyChatEnableThinking(
body: { enable_thinking?: boolean; thinking_budget?: number },
value: boolean | undefined,
): void {
if (value === undefined) {
delete body.enable_thinking;
delete body.thinking_budget;
return;
}
if (value === false) {
body.enable_thinking = false;
delete body.thinking_budget;
return;
}
body.enable_thinking = true;
}
/** Set `enable_thinking` and optionally write `thinking_budget` when enabled. */
export function applyChatEnableThinkingWithBudget(
body: { enable_thinking?: boolean; thinking_budget?: number },
value: boolean | undefined,
thinkingBudget?: number,
): void {
applyChatEnableThinking(body, value);
if (value === true && thinkingBudget !== undefined) {
body.thinking_budget = thinkingBudget;
}
}
function errorMessageOf(error: unknown): string {
if (error instanceof Error) return error.message;
return String(error);
}
/** Run once, then retry once if the error indicates an `enable_thinking` constraint. */
export async function withEnableThinkingRetry<T>(options: {
initial: boolean | undefined;
apply: (value: boolean | undefined) => void;
run: () => Promise<T>;
}): Promise<T> {
options.apply(options.initial);
try {
return await options.run();
} catch (error) {
const adjusted = adjustEnableThinkingAfterError(options.initial, errorMessageOf(error));
if (adjusted.kind === "none") throw error;
options.apply(adjusted.value);
return await options.run();
}
}
-174
View File
@@ -1,174 +0,0 @@
import { expect, test } from "vite-plus/test";
import {
adjustEnableThinkingAfterError,
applyChatEnableThinking,
applyChatEnableThinkingWithBudget,
resolveChatEnableThinking,
withEnableThinkingRetry,
} from "../src/models/thinking.ts";
test("resolveChatEnableThinking:显式开启为 true,非流式默认 false,流式默认 omit", () => {
expect(resolveChatEnableThinking({ enableThinking: true })).toBe(true);
expect(resolveChatEnableThinking({ enableThinking: true, stream: false })).toBe(true);
expect(resolveChatEnableThinking({ stream: false })).toBe(false);
expect(resolveChatEnableThinking({ enableThinking: false, stream: false })).toBe(false);
expect(resolveChatEnableThinking({ stream: true })).toBeUndefined();
expect(resolveChatEnableThinking({})).toBeUndefined();
});
test("adjustEnableThinkingAfterError:false/omit 被要求 true 时重试为 true", () => {
expect(
adjustEnableThinkingAfterError(
false,
"The value of the enable_thinking parameter is restricted to True.",
),
).toEqual({ kind: "retry", value: true });
expect(
adjustEnableThinkingAfterError(
undefined,
"The value of the enable_thinking parameter is restricted to True.",
),
).toEqual({ kind: "retry", value: true });
});
test("adjustEnableThinkingAfterError:omit 被要求 false 时重试为 false", () => {
expect(
adjustEnableThinkingAfterError(
undefined,
"parameter.enable_thinking must be set to false for non-streaming calls",
),
).toEqual({ kind: "retry", value: false });
});
test("adjustEnableThinkingAfterError:不支持时去掉字段", () => {
expect(
adjustEnableThinkingAfterError(false, "The model qwen-turbo does not support enable_thinking."),
).toEqual({ kind: "retry", value: undefined });
expect(
adjustEnableThinkingAfterError(true, "The model qwen-turbo does not support enable_thinking."),
).toEqual({ kind: "retry", value: undefined });
});
test("adjustEnableThinkingAfterError:无关错误不调整", () => {
expect(adjustEnableThinkingAfterError(undefined, "Access denied")).toEqual({ kind: "none" });
expect(adjustEnableThinkingAfterError(false, "Model not exist")).toEqual({ kind: "none" });
expect(
adjustEnableThinkingAfterError(
true,
"The value of the enable_thinking parameter is restricted to True.",
),
).toEqual({ kind: "none" });
});
test("applyChatEnableThinking:设置 / 删除字段,并在关闭时清 thinking_budget", () => {
const body: { enable_thinking?: boolean; thinking_budget?: number } = {
thinking_budget: 1024,
};
applyChatEnableThinking(body, true);
expect(body.enable_thinking).toBe(true);
expect(body.thinking_budget).toBe(1024);
applyChatEnableThinking(body, false);
expect(body.enable_thinking).toBe(false);
expect(body).not.toHaveProperty("thinking_budget");
body.thinking_budget = 2048;
applyChatEnableThinking(body, undefined);
expect(body).not.toHaveProperty("enable_thinking");
expect(body).not.toHaveProperty("thinking_budget");
});
test("withEnableThinkingRetry:restricted-to-true 时从 false 重试为 true", async () => {
const values: Array<boolean | undefined> = [];
let calls = 0;
const result = await withEnableThinkingRetry({
initial: false,
apply: (value) => {
values.push(value);
},
run: async () => {
calls += 1;
if (calls === 1) {
throw new Error("The value of the enable_thinking parameter is restricted to True.");
}
return "ok";
},
});
expect(result).toBe("ok");
expect(calls).toBe(2);
expect(values).toEqual([false, true]);
});
test("withEnableThinkingRetry:must-be-false 时从 omit 重试为 false", async () => {
const values: Array<boolean | undefined> = [];
let calls = 0;
const result = await withEnableThinkingRetry({
initial: undefined,
apply: (value) => {
values.push(value);
},
run: async () => {
calls += 1;
if (calls === 1) {
throw new Error("parameter.enable_thinking must be set to false for non-streaming calls");
}
return "ok";
},
});
expect(result).toBe("ok");
expect(calls).toBe(2);
expect(values).toEqual([undefined, false]);
});
test("applyChatEnableThinkingWithBudget:仅在 enable_thinking=true 时写入 budget", () => {
const body: { enable_thinking?: boolean; thinking_budget?: number } = {};
applyChatEnableThinkingWithBudget(body, false, 2048);
expect(body.enable_thinking).toBe(false);
expect(body).not.toHaveProperty("thinking_budget");
applyChatEnableThinkingWithBudget(body, undefined, 2048);
expect(body).not.toHaveProperty("enable_thinking");
expect(body).not.toHaveProperty("thinking_budget");
applyChatEnableThinkingWithBudget(body, true, 2048);
expect(body.enable_thinking).toBe(true);
expect(body.thinking_budget).toBe(2048);
});
test("withEnableThinkingRetry:restricted-to-true 重试时保留 thinking_budget", async () => {
const body: { enable_thinking?: boolean; thinking_budget?: number } = {};
let calls = 0;
const result = await withEnableThinkingRetry({
initial: false,
apply: (value) => applyChatEnableThinkingWithBudget(body, value, 2048),
run: async () => {
calls += 1;
if (calls === 1) {
throw new Error("The value of the enable_thinking parameter is restricted to True.");
}
return "ok";
},
});
expect(result).toBe("ok");
expect(calls).toBe(2);
expect(body.enable_thinking).toBe(true);
expect(body.thinking_budget).toBe(2048);
});
test("withEnableThinkingRetry:无关错误原样抛出", async () => {
await expect(
withEnableThinkingRetry({
initial: false,
apply: () => {},
run: async () => {
throw new Error("Access denied");
},
}),
).rejects.toThrow(/Access denied/);
});