Skip to content

Commit 031d0a8

Browse files
committed
fix(text): omit enable_thinking by default and retry when API requires false
Non-streaming chat no longer forces enable_thinking=false, which breaks thinking-only models. Retry once with false only when the server demands it.
1 parent 97b7155 commit 031d0a8

8 files changed

Lines changed: 290 additions & 38 deletions

File tree

packages/commands/src/commands/auth/login-api-key.ts

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import {
44
chatPath,
55
requestJson,
66
normalizeModelBaseUrl,
7+
applyChatEnableThinking,
8+
resolveChatEnableThinking,
9+
withEnableThinkingRetry,
710
type AuthPersistPatch,
811
type AuthStore,
912
type Identity,
@@ -58,32 +61,48 @@ export async function validateAndPersistApiKey(
5861
? normalizeModelBaseUrl(profile.persistBaseUrl)
5962
: undefined;
6063
const validationModel = profile.defaultTextModel || "qwen3.7-max";
64+
const body: {
65+
model: string;
66+
messages: Array<{ role: string; content: string }>;
67+
max_tokens: number;
68+
stream: boolean;
69+
enable_thinking?: boolean;
70+
} = {
71+
model: validationModel,
72+
messages: [{ role: "user", content: "hi" }],
73+
max_tokens: 1,
74+
stream: false,
75+
};
76+
6177
const requestOpts = {
6278
url: baseUrl + chatPath(),
6379
method: "POST",
6480
headers: { Authorization: `Bearer ${key}` },
6581
timeout: Math.min(deps.settings.timeout, 30),
66-
body: {
67-
model: validationModel,
68-
messages: [{ role: "user", content: "hi" }],
69-
max_tokens: 1,
70-
stream: false,
71-
enable_thinking: validationModel === "qwen3.8-max-preview",
72-
},
82+
body,
7383
};
7484

75-
for (let attempt = 1; attempt <= 3; attempt++) {
76-
try {
77-
await requestJson<unknown>(httpDeps, requestOpts);
78-
break;
79-
} catch (error) {
80-
if (attempt >= 3 || !canRetry(error)) {
81-
process.stderr.write("Failed\n");
82-
throw error;
83-
}
84-
const delayMs = RETRY_DELAY_BASE_MS * 2 ** (attempt - 1);
85-
await new Promise((resolve) => setTimeout(resolve, delayMs));
86-
}
85+
try {
86+
await withEnableThinkingRetry({
87+
// Validation requests are always non-streaming.
88+
initial: resolveChatEnableThinking({ stream: false }),
89+
apply: (value) => applyChatEnableThinking(body, value),
90+
run: async () => {
91+
for (let attempt = 1; attempt <= 3; attempt++) {
92+
try {
93+
await requestJson<unknown>(httpDeps, requestOpts);
94+
return;
95+
} catch (error) {
96+
if (attempt >= 3 || !canRetry(error)) throw error;
97+
const delayMs = RETRY_DELAY_BASE_MS * 2 ** (attempt - 1);
98+
await new Promise((resolve) => setTimeout(resolve, delayMs));
99+
}
100+
}
101+
},
102+
});
103+
} catch (error) {
104+
process.stderr.write("Failed\n");
105+
throw error;
87106
}
88107

89108
process.stderr.write("Valid\n");

packages/commands/src/commands/text/chat.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import {
44
parseSSE,
55
detectOutputFormat,
66
readTextFromPathOrStdin,
7+
applyChatEnableThinking,
8+
resolveChatEnableThinking,
9+
withEnableThinkingRetry,
710
type ChatMessage,
811
type ChatRequest,
912
type ChatResponse,
@@ -124,7 +127,8 @@ export default defineCommand({
124127
const { system, messages } = parseMessages(flags);
125128

126129
const model = flags.model || settings.defaultTextModel || "qwen3.7-max";
127-
const shouldStream = flags.stream || process.stdout.isTTY;
130+
// Coerce isTTY (may be undefined) so stream:false is serialized.
131+
const shouldStream = Boolean(flags.stream || process.stdout.isTTY);
128132
const format = detectOutputFormat(settings.output);
129133

130134
// Build messages array with system prompt
@@ -144,16 +148,13 @@ export default defineCommand({
144148
if (flags.temperature !== undefined) body.temperature = flags.temperature;
145149
if (flags.topP !== undefined) body.top_p = flags.topP;
146150

147-
if (flags.enableThinking) {
148-
body.enable_thinking = true;
149-
if (flags.thinkingBudget !== undefined) {
150-
body.thinking_budget = flags.thinkingBudget;
151-
}
152-
} else if (!shouldStream) {
153-
// DashScope qwen3 models default to enable_thinking=true server-side, but
154-
// non-streaming calls require it to be explicitly false. Stream calls
155-
// support thinking, so leave the field unset there (server handles it).
156-
body.enable_thinking = false;
151+
const enableThinking = resolveChatEnableThinking({
152+
enableThinking: flags.enableThinking,
153+
stream: shouldStream,
154+
});
155+
applyChatEnableThinking(body, enableThinking);
156+
if (enableThinking === true && flags.thinkingBudget !== undefined) {
157+
body.thinking_budget = flags.thinkingBudget;
157158
}
158159

159160
if (flags.tool) {
@@ -229,10 +230,15 @@ export default defineCommand({
229230
resultOut.write("\n");
230231
}
231232
} else {
232-
const response = await ctx.client.requestJson<ChatResponse>({
233-
path: chatPath(),
234-
method: "POST",
235-
body,
233+
const response = await withEnableThinkingRetry({
234+
initial: enableThinking,
235+
apply: (value) => applyChatEnableThinking(body, value),
236+
run: () =>
237+
ctx.client.requestJson<ChatResponse>({
238+
path: chatPath(),
239+
method: "POST",
240+
body,
241+
}),
236242
});
237243

238244
const text = response.choices?.[0]?.message?.content ?? "";

packages/commands/tests/e2e/auth.e2e.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ describe("e2e: auth", () => {
317317
body: {
318318
model: "qwen3.8-max-preview",
319319
stream: false,
320-
enable_thinking: true,
320+
enable_thinking: false,
321321
},
322322
});
323323

packages/commands/tests/e2e/text-chat.e2e.test.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,25 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: text chat(DashScope)", () => {
3434
"--model",
3535
"qwen3.7-max",
3636
"--message",
37-
"干跑",
37+
"dry-run",
3838
"--max-tokens",
3939
"8",
4040
"--output",
4141
"json",
4242
]);
4343
expect(exitCode, stderr).toBe(0);
4444
const data = parseStdoutJson<{
45-
request?: { model?: string; messages?: Array<{ content?: string }> };
45+
request?: {
46+
model?: string;
47+
messages?: Array<{ content?: string }>;
48+
enable_thinking?: boolean;
49+
stream?: boolean;
50+
};
4651
}>(stdout);
4752
expect(data.request?.model).toBe("qwen3.7-max");
48-
expect(data.request?.messages?.some((m) => m.content === "干跑")).toBe(true);
53+
expect(data.request?.messages?.some((message) => message.content === "dry-run")).toBe(true);
54+
expect(data.request?.stream).toBe(false);
55+
expect(data.request?.enable_thinking).toBe(false);
4956
});
5057

5158
test("【qwen3.7-max】文本对话", async () => {

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ export * from "./finetune/index.ts";
1414
export * from "./deploy/index.ts";
1515
export * from "./types/index.ts";
1616
export * from "./utils/index.ts";
17+
export * from "./models/index.ts";
1718
export * from "./telemetry/index.ts";
1819
export * from "./advisor/index.ts";

packages/core/src/models/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export {
2+
adjustEnableThinkingAfterError,
3+
applyChatEnableThinking,
4+
resolveChatEnableThinking,
5+
withEnableThinkingRetry,
6+
type EnableThinkingAdjustResult,
7+
} from "./thinking.ts";
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/** resolve / adjust / retry helpers for chat `enable_thinking`. */
2+
3+
/** Resolve the initial `enable_thinking` value (`undefined` omits the field). */
4+
export function resolveChatEnableThinking(options: {
5+
enableThinking?: boolean;
6+
/** Whether the request is streaming. */
7+
stream?: boolean;
8+
}): boolean | undefined {
9+
if (options.enableThinking) return true;
10+
if (options.stream === false) return false;
11+
return undefined;
12+
}
13+
14+
export type EnableThinkingAdjustResult =
15+
| { kind: "retry"; value: boolean | undefined }
16+
| { kind: "none" };
17+
18+
/** Map clear `enable_thinking` constraint errors to a one-shot retry adjustment. */
19+
export function adjustEnableThinkingAfterError(
20+
current: boolean | undefined,
21+
errorMessage: string,
22+
): EnableThinkingAdjustResult {
23+
if (current !== true && /enable_thinking parameter is restricted to\s*true/i.test(errorMessage)) {
24+
return { kind: "retry", value: true };
25+
}
26+
27+
if (current === undefined && /enable_thinking must be set to false/i.test(errorMessage)) {
28+
return { kind: "retry", value: false };
29+
}
30+
31+
if (current !== undefined && /does not support enable_thinking/i.test(errorMessage)) {
32+
return { kind: "retry", value: undefined };
33+
}
34+
35+
return { kind: "none" };
36+
}
37+
38+
/** Set or remove `enable_thinking`; clear `thinking_budget` when disabled or omitted. */
39+
export function applyChatEnableThinking(
40+
body: { enable_thinking?: boolean; thinking_budget?: number },
41+
value: boolean | undefined,
42+
): void {
43+
if (value === undefined) {
44+
delete body.enable_thinking;
45+
delete body.thinking_budget;
46+
return;
47+
}
48+
if (value === false) {
49+
body.enable_thinking = false;
50+
delete body.thinking_budget;
51+
return;
52+
}
53+
body.enable_thinking = true;
54+
}
55+
56+
function errorMessageOf(error: unknown): string {
57+
if (error instanceof Error) return error.message;
58+
return String(error);
59+
}
60+
61+
/** Run once, then retry once if the error indicates an `enable_thinking` constraint. */
62+
export async function withEnableThinkingRetry<T>(options: {
63+
initial: boolean | undefined;
64+
apply: (value: boolean | undefined) => void;
65+
run: () => Promise<T>;
66+
}): Promise<T> {
67+
options.apply(options.initial);
68+
try {
69+
return await options.run();
70+
} catch (error) {
71+
const adjusted = adjustEnableThinkingAfterError(options.initial, errorMessageOf(error));
72+
if (adjusted.kind === "none") throw error;
73+
options.apply(adjusted.value);
74+
return await options.run();
75+
}
76+
}

0 commit comments

Comments
 (0)