6666)
6767
6868
69- def _detect_enable_thinking_default (tokenizer : PreTrainedTokenizer ) -> bool :
70- """Probe the tokenizer's chat template to learn its ``enable_thinking``
71- default polarity at the generation-prompt boundary.
72-
73- The Qwen3.5 family ships two template variants that differ only in the
74- polarity of the gated branch:
75-
76- * Big sizes (4B / 9B / 35B-A3B / 122B-A10B / 397B-A17B) emit an open
77- ``<think>\\ n`` by default and the empty ``<think>\\ n\\ n</think>\\ n\\ n``
78- block when ``enable_thinking`` is explicitly false.
79- * Small sizes (0.8B / 2B) flip the polarity — they emit the empty
80- block by default and the open ``<think>\\ n`` only when
81- ``enable_thinking`` is explicitly true.
82-
83- A one-shot ``apply_chat_template`` call with no flag and a minimal
84- user message reveals which variant is in use: the empty-block tail
85- ends with ``</think>``, the open-think tail does not. Failing the
86- probe (no chat_template, exotic config) falls back to the big-model
87- default of True, which matches every entry in
88- ``MODEL_RENDERER_MAP`` that routes to ``qwen3.5`` without explicit
89- polarity awareness.
69+ # Per-model ``enable_thinking`` default, applied when the renderer config
70+ # leaves it ``None``. The Qwen3.5 family ships two chat-template variants
71+ # that differ only in the polarity of the gated thinking branch:
72+ #
73+ # * Big sizes (4B / 9B / 35B-A3B / 122B-A10B / 397B-A17B) default
74+ # ``enable_thinking=true`` — an open ``<think>\n`` at the gen prompt.
75+ # * Small sizes (0.8B / 2B) flip it — default ``false``, emitting the
76+ # empty ``<think>\n\n</think>\n\n`` block.
77+ #
78+ # These are hard-coded (keyed by ``tokenizer.name_or_path``) rather than
79+ # probed from the live ``chat_template``: probing meant calling
80+ # ``apply_chat_template`` at construction, which pulls ``transformers`` onto
81+ # the hot path and breaks bring-your-own-tokenizer use. The values are the
82+ # ground truth pinned by ``tests/test_qwen35_size_coverage.py`` — both the
83+ # polarity assertions and byte-parity against each size's own
84+ # ``apply_chat_template``.
85+ _ENABLE_THINKING_DEFAULTS : dict [str , bool ] = {
86+ "Qwen/Qwen3.5-0.8B" : False ,
87+ "Qwen/Qwen3.5-2B" : False ,
88+ "Qwen/Qwen3.5-4B" : True ,
89+ "Qwen/Qwen3.5-9B" : True ,
90+ "Qwen/Qwen3.5-35B-A3B" : True ,
91+ "Qwen/Qwen3.5-122B-A10B" : True ,
92+ "Qwen/Qwen3.5-397B-A17B" : True ,
93+ # Qwen3.6 extends the Qwen3.5 template; same big-size polarity.
94+ "Qwen/Qwen3.6-35B-A3B" : True ,
95+ }
96+
97+
98+ def _default_enable_thinking (tokenizer ) -> bool :
99+ """Hard-coded ``enable_thinking`` default for ``tokenizer``'s model.
100+
101+ Falls back to ``True`` (the big-model default, and the majority of the
102+ family) for unknown / fine-tuned checkpoints whose ``name_or_path`` isn't
103+ in ``_ENABLE_THINKING_DEFAULTS``; pass an explicit ``enable_thinking=`` to
104+ a small-size fine-tune that needs ``False``.
90105 """
91- try :
92- out = tokenizer .apply_chat_template (
93- [{"role" : "user" , "content" : "x" }],
94- tokenize = False ,
95- add_generation_prompt = True ,
96- )
97- except Exception :
98- return True
99- if not isinstance (out , str ):
100- return True
101- return not out .rstrip ().endswith ("</think>" )
106+ return _ENABLE_THINKING_DEFAULTS .get (getattr (tokenizer , "name_or_path" , "" ), True )
102107
103108
104109class Qwen35Renderer :
@@ -116,13 +121,13 @@ def __init__(
116121 self ._tokenizer = tokenizer
117122 self ._processor = processor
118123 cfg = config or type (self )._config_cls ()
119- # ``enable_thinking=None`` defers to the tokenizer 's chat-template
120- # default (Instruct → off, Thinking → on ). Materialise here so
121- # downstream reads see a concrete bool; rebind the config with
122- # the resolved value so introspection sees the same.
124+ # ``enable_thinking=None`` defers to the model 's known default (see
125+ # ``_ENABLE_THINKING_DEFAULTS`` ). Materialise here so downstream reads
126+ # see a concrete bool; rebind the config with the resolved value so
127+ # introspection sees the same.
123128 if cfg .enable_thinking is None :
124129 cfg = cfg .model_copy (
125- update = {"enable_thinking" : _detect_enable_thinking_default (tokenizer )}
130+ update = {"enable_thinking" : _default_enable_thinking (tokenizer )}
126131 )
127132 self .config = cfg
128133
0 commit comments