fix(models): tolerate non-mapping when_thinking_enabled.extra_body on the reasoning-contract path (#7039)

* fix(models): tolerate non-mapping when_thinking_enabled.extra_body on the reasoning-contract path

* docs: keep frontend guidance below the hard size limit

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
哈基米 2026-10-18 20:27:17 +08:00 committed by GitHub
parent 05db1d66a0
commit 12e619c233
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 45 additions and 2 deletions

View File

@ -160,7 +160,12 @@ def _infer_dialect(effective_wte: dict) -> str:
Mirrors the historical disable-path heuristics so ``dialect: auto`` behaves
like the legacy factory did for the same template shape.
"""
extra_body = effective_wte.get("extra_body") or {}
extra_body = effective_wte.get("extra_body")
if not isinstance(extra_body, dict):
# ``ModelConfig.when_thinking_enabled`` accepts unvalidated nested values,
# so a truthy non-mapping ``extra_body`` reaches this heuristic; like a
# null value it carries no payload shape to infer from.
extra_body = {}
thinking = extra_body.get("thinking")
if isinstance(thinking, dict) and thinking.get("type"):
return "openai_extra_body"
@ -272,7 +277,11 @@ def _apply_contract_thinking_settings(
dialect = contract.dialect if contract.dialect != "auto" else _infer_dialect(effective_wte)
# Both directions mirror whichever vLLM switch the template declares, so a
# template using the older ``thinking`` key never also grows ``enable_thinking``.
chat_template_kwargs = (effective_wte.get("extra_body") or {}).get("chat_template_kwargs") or {}
# ``when_thinking_enabled`` holds unvalidated nested values: a truthy
# non-mapping ``extra_body`` has no ``chat_template_kwargs`` to mirror and is
# treated as absent, like the null values the ``or {}`` defaults already cover.
extra_body = effective_wte.get("extra_body")
chat_template_kwargs = (extra_body.get("chat_template_kwargs") or {}) if isinstance(extra_body, dict) else {}
if thinking_enabled:
payload = _deep_merge_dicts(_dialect_payload(dialect, enabled=True, chat_template_kwargs=chat_template_kwargs), effective_wte)
if contract.history is not None and dialect == "openai_extra_body":

View File

@ -2362,6 +2362,40 @@ def test_contract_dialect_auto_infers_disable_payload_from_when_thinking_enabled
assert "reasoning_effort" not in captured
@pytest.mark.parametrize(
("reasoning", "thinking_enabled", "expected"),
[
({"thinking": "optional"}, True, {"extra_body": "oops"}),
({"thinking": "optional"}, False, {}),
({"thinking": "optional", "dialect": "anthropic"}, True, {"thinking": {"type": "enabled"}, "extra_body": "oops"}),
({"thinking": "optional", "dialect": "anthropic"}, False, {"thinking": {"type": "disabled"}}),
],
ids=["auto-thinking-on", "auto-thinking-off", "anthropic-thinking-on", "anthropic-thinking-off"],
)
def test_contract_non_mapping_template_values_build_on_both_thinking_paths(monkeypatch, reasoning, thinking_enabled, expected):
"""``ModelConfig.when_thinking_enabled`` is ``dict | None`` with unvalidated nested values,
so a truthy non-mapping ``extra_body`` loads fine from ``config.yaml`` — and the contract
path read it unconditionally: the ``dialect: auto`` inference and the shared
``chat_template_kwargs`` lookup both called ``.get`` on it, so every ``create_chat_model``
for the profile crashed with ``AttributeError`` on both thinking directions. The legacy
path forwards the identical template unchanged
(``test_legacy_non_mapping_template_values_are_forwarded_not_rejected``); the contract
path must likewise build, treating the value as absent for its dialect heuristics (the
way the existing ``isinstance`` guards treat null template values) while the enable
direction still forwards it like any other template value."""
model = _contract_model(reasoning=reasoning, when_thinking_enabled={"extra_body": "oops"})
captured: dict = {}
_patch_factory(monkeypatch, _make_app_config([model]), model_class=_capturing_class(FakeChatModel, captured))
factory_module.create_chat_model(name="contract-model", thinking_enabled=thinking_enabled)
for key in ("extra_body", "thinking"):
if key in expected:
assert captured[key] == expected[key], f"non-mapping template value broke the {key} payload"
else:
assert key not in captured, f"non-mapping template value leaked an unexpected {key} payload"
@pytest.mark.parametrize(("thinking_enabled", "expected"), [(True, True), (False, False)])
def test_contract_dialect_vllm_synthesizes_chat_template_kwargs(monkeypatch, thinking_enabled, expected):
model = _contract_model(reasoning={"thinking": "optional", "dialect": "vllm_chat_template"})