mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
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:
@@ -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");
|
||||
|
||||
@@ -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 ?? "";
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
export {
|
||||
adjustEnableThinkingAfterError,
|
||||
applyChatEnableThinking,
|
||||
applyChatEnableThinkingWithBudget,
|
||||
resolveChatEnableThinking,
|
||||
withEnableThinkingRetry,
|
||||
type EnableThinkingAdjustResult,
|
||||
} from "./thinking.ts";
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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/);
|
||||
});
|
||||
Reference in New Issue
Block a user