Skip to content

Add explicit qwen_chat thinking_mode wire policy (server_default/enabled/disabled) - #240

Merged
Yifan Yang (Yif-Yang) merged 1 commit into
microsoft:mainfrom
Yif-Yang:fix/qwen-thinking-mode
Aug 20, 2026
Merged

Add explicit qwen_chat thinking_mode wire policy (server_default/enabled/disabled)#240
Yifan Yang (Yif-Yang) merged 1 commit into
microsoft:mainfrom
Yif-Yang:fix/qwen-thinking-mode

Conversation

@Yif-Yang

@Yif-Yang Yifan Yang (Yif-Yang) commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Resolves the tension between #28 and #90.

The problem

chat_template_kwargs is a vLLM/SGLang extension, not part of the OpenAI chat-completions API. OpenAI, Azure, and strict OpenAI-compatible gateways reject the unknown top-level body field with HTTP 400, and non-Qwen vLLM models served with it can emit <think> output with no <answer> tag — the acc=0.000 reported in #28.

c31c50b fixed that by only emitting the field when thinking was enabled. That closed #28, but it also means enable_thinking=false omits the field rather than sending false, so the client has no supported way to explicitly disable thinking — the request in #90 (and #109).

Both issues are legitimate, and neither is satisfiable by a boolean: the protocol has three states, and which one is correct depends on the serving stack, not on a user preference.

The change

A three-state, role-scoped thinking_mode:

mode on the wire
server_default (default) chat_template_kwargs not sent — the server's chat template decides
enabled chat_template_kwargs: {"enable_thinking": true}
disabled chat_template_kwargs: {"enable_thinking": false}
  • use local llm train ,acc is 0 #28 stays fixed: server_default sends exactly the bytes every existing deployment sends today.
  • Clarification request: Qwen enable_thinking setting used for reported ALFWorld results #90 is satisfied: disabled sends the explicit false.
  • Zero config regressions: the legacy enable_thinking boolean keeps its historical wire meaning (true → send true, false → omit). No existing YAML, env var, or programmatic call changes behavior.
  • Setting both keys to conflicting values raises ValueError rather than silently picking a winner, and unknown tokens raise too — a typo must never silently flip a reproducibility control.
  • None still means "do not modify" in configure_qwen_chat, so a missing YAML key cannot clear env-derived configuration.

Because server_default delegates a result-affecting choice to the server's chat template, the backend warns once per role when a request goes out under it, and the resolved per-role mode is recorded in the run's config.json under resolved_qwen_thinking_modes — so a run's thinking policy is always recoverable from its artifacts.

Rejected alternatives

  • Default to disabled — would re-break use local llm train ,acc is 0 #28 for strict gateways with no config change.
  • Sniff base_url for localhost — deployment topology is not a capability signal; self-hosted vLLM often sits behind a public domain.
  • Match on model name — capability belongs to the serving stack and template, not the model ID; aliases are common and hosted Qwen gateways may still reject vLLM extensions.
  • Catch the 400 and retry without the field — silently abandons the reproducibility guarantee the user explicitly asked for.

Surface

model.qwen_chat_thinking_mode plus optimizer_/target_ variants, wired through YAML, --cfg-options, scripts/train.py CLI, eval_only.py, and QWEN_CHAT_THINKING_MODE env (role-scoped variants take precedence). Docs updated in docs/reference/config.md, docs/guide/configuration.md, and .env.example — including the contradiction between "local vLLM endpoint" and "OpenAI-compatible" wording, which is precisely why the wire policy cannot be inferred and must be explicit.

Tests

20 new tests covering all three payload states, the one-time warning, role/shared env precedence, legacy-key mapping, conflict detection, and invalid-token rejection. Full suite: 1134 passed, 10 skipped.

Supersedes #109 — credit to dylan (@LESdylan) for identifying the gap; the landed fix is the three-state enum rather than an unconditional emit, so #28 does not regress.

chat_template_kwargs is a vLLM/SGLang extension. OpenAI, Azure, and strict
OpenAI-compatible gateways reject the unknown body field with HTTP 400, and
non-Qwen vLLM models served with it can emit <think> output with no <answer>
tag (acc=0.000). c31c50b fixed that by only emitting the field when thinking
was enabled, which closed microsoft#28 but left no supported way to send an explicit
enable_thinking: false -- the request in microsoft#90/microsoft#109.

The protocol has three states, so make the setting three-state:

  server_default (default) -> omit chat_template_kwargs
  enabled                  -> send enable_thinking: true
  disabled                 -> send enable_thinking: false

server_default keeps every existing deployment on exactly the bytes it sends
today, so microsoft#28 stays fixed, while disabled gives microsoft#90 the explicit false it asks
for. The legacy enable_thinking boolean keeps its historical wire meaning
(true -> send true, false -> omit), so no config changes behavior; setting
both keys to conflicting values raises rather than silently picking a winner.
Unknown tokens raise too -- a typo must not silently flip a reproducibility
control.

Because server_default delegates a result-affecting choice to the server's
chat template, the backend warns once per role when it is used, and the
resolved per-role mode is recorded in the run's config.json under
resolved_qwen_thinking_modes.

Also settles the docs contradiction between "local vLLM endpoint" and
"OpenAI-compatible": qwen_chat speaks the OpenAI protocol and reaches both
self-hosted servers and hosted gateways, which is exactly why the wire policy
cannot be inferred and must be explicit.

Closes microsoft#90
@Yif-Yang
Yifan Yang (Yif-Yang) merged commit 601f5f7 into microsoft:main Aug 20, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

use local llm train ,acc is 0

1 participant