Skip to content

Commit bbbe6f3

Browse files
committed
test(llm): verify compatibility through requests
1 parent e90a552 commit bbbe6f3

5 files changed

Lines changed: 66 additions & 31 deletions

File tree

src/pythinker_code/llm.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,9 @@ def capped_chat_provider(llm: LLM, max_output_tokens: int) -> ChatProvider:
7373
usual output budget (e.g. a context-compaction summary) can use this
7474
instead of hand-picking a provider-specific kwarg name.
7575
"""
76-
compatibility = getattr(llm, "compatibility", None)
77-
kwarg = compatibility.output_tokens_kwarg if compatibility is not None else "max_tokens"
78-
return cast(Any, llm.chat_provider).with_generation_kwargs(**{kwarg: max_output_tokens})
76+
return cast(Any, llm.chat_provider).with_generation_kwargs(
77+
**{llm.compatibility.output_tokens_kwarg: max_output_tokens}
78+
)
7979

8080

8181
def supports_deferred_tool_search(llm: LLM | None) -> bool:
@@ -529,11 +529,10 @@ def available_model_thinking_levels(
529529
) -> tuple[ThinkingEffort, ...]:
530530
"""Selectable thinking levels for *model*, scoped to provider-specific support.
531531
532-
Starts from the capability-derived ladder, then narrows to a provider's
533-
actually-accepted set when known (currently the OpenAI GPT-5 family) so the
534-
selector never offers — and :func:`create_llm` never sends — a level the
535-
model rejects. Falls back to the full ladder for models without a known
536-
per-model rule.
532+
Starts from the capability-derived ladder, then narrows to the resolved
533+
provider/model profile's accepted set when known so the selector never offers —
534+
and :func:`create_llm` never sends — a level the model rejects. Falls back to
535+
the full ladder for models without a known per-model rule.
537536
"""
538537
base = available_thinking_levels(capabilities)
539538
scoped_levels = (

tests/core/test_compaction_overflow.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121

2222
from pythinker_code.config import LLMModel, LLMProvider
2323
from pythinker_code.llm import LLM, capped_chat_provider
24-
from pythinker_code.provider_compatibility import resolve_provider_compatibility
24+
from pythinker_code.provider_compatibility import (
25+
default_provider_compatibility,
26+
resolve_provider_compatibility,
27+
)
2528
from pythinker_code.soul.compaction import SimpleCompaction
2629
from pythinker_code.wire.types import TextPart
2730

@@ -50,7 +53,14 @@ def with_generation_kwargs(self, **kwargs: object) -> _FakeChatProvider:
5053

5154

5255
def _fake_llm() -> LLM:
53-
return cast(LLM, SimpleNamespace(chat_provider=_FakeChatProvider(), provider_config=None))
56+
return cast(
57+
LLM,
58+
SimpleNamespace(
59+
chat_provider=_FakeChatProvider(),
60+
provider_config=None,
61+
compatibility=default_provider_compatibility(),
62+
),
63+
)
5464

5565

5666
def _fake_llm_with_provider_type(provider_type: str) -> LLM:

tests/core/test_create_llm.py

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -795,20 +795,14 @@ def test_create_llm_zai_glm52_activates_explicit_profile_policy(
795795
assert llm is not None
796796
assert isinstance(llm.chat_provider, OpenAILegacy)
797797
assert llm.compatibility.profile_id == provider_key.removeprefix("managed:")
798+
assert llm.compatibility.reasoning_replay_mode == "exact"
799+
assert llm.compatibility.auto_reasoning_effort is False
800+
assert llm.compatibility.tool_stream is True
801+
assert llm.compatibility.tool_message_conversion == "extract_text"
802+
assert llm.compatibility.max_output_tokens == 131_072
798803
assert llm.chat_provider.thinking_effort is None
799804
assert llm.thinking is True
800805
assert llm.thinking_effort == "xhigh"
801-
assert llm.chat_provider._reasoning_replay_mode == "exact" # pyright: ignore[reportPrivateUsage]
802-
assert llm.chat_provider._auto_reasoning_effort is False # pyright: ignore[reportPrivateUsage]
803-
assert llm.chat_provider._tool_stream is True # pyright: ignore[reportPrivateUsage]
804-
assert llm.chat_provider._tool_message_conversion == "extract_text" # pyright: ignore[reportPrivateUsage]
805-
assert llm.chat_provider._generation_kwargs == { # pyright: ignore[reportPrivateUsage]
806-
"max_tokens": 131_072,
807-
"extra_body": {
808-
"thinking": {"type": "enabled", "clear_thinking": False},
809-
"reasoning_effort": "max",
810-
},
811-
}
812806

813807

814808
@pytest.mark.parametrize(("thinking", "enabled"), [(False, False), (True, True)])
@@ -831,10 +825,10 @@ def test_create_llm_self_hosted_qwen_uses_chat_template_thinking_toggle(
831825

832826
assert llm is not None
833827
assert isinstance(llm.chat_provider, OpenAILegacy)
828+
assert llm.compatibility.profile_id == "qwen-template"
829+
assert llm.compatibility.thinking_format == "qwen_template"
834830
assert llm.chat_provider.thinking_effort is None
835-
assert llm.chat_provider._generation_kwargs.get("extra_body") == { # pyright: ignore[reportPrivateUsage]
836-
"chat_template_kwargs": {"enable_thinking": enabled}
837-
}
831+
assert llm.thinking is enabled
838832

839833

840834
def test_create_llm_zai_binary_model_maps_minimal_to_disabled() -> None:
@@ -854,13 +848,11 @@ def test_create_llm_zai_binary_model_maps_minimal_to_disabled() -> None:
854848

855849
assert llm is not None
856850
assert isinstance(llm.chat_provider, OpenAILegacy)
851+
assert llm.compatibility.thinking_format == "zai_binary"
852+
assert llm.compatibility.max_output_tokens == 131_072
857853
assert llm.chat_provider.thinking_effort is None
858854
assert llm.thinking is False
859855
assert llm.thinking_effort == "off"
860-
assert llm.chat_provider._generation_kwargs == { # pyright: ignore[reportPrivateUsage]
861-
"max_tokens": 131_072,
862-
"extra_body": {"thinking": {"type": "disabled"}},
863-
}
864856

865857

866858
def test_create_llm_local_glm_name_does_not_activate_zai_request_policy() -> None:
@@ -881,8 +873,9 @@ def test_create_llm_local_glm_name_does_not_activate_zai_request_policy() -> Non
881873
assert llm is not None
882874
assert isinstance(llm.chat_provider, OpenAILegacy)
883875
assert llm.compatibility.profile_id == "openai-compatible"
884-
assert "extra_body" not in llm.chat_provider._generation_kwargs # pyright: ignore[reportPrivateUsage]
885-
assert "max_tokens" not in llm.chat_provider._generation_kwargs # pyright: ignore[reportPrivateUsage]
876+
assert llm.compatibility.thinking_format == "none"
877+
assert llm.compatibility.max_output_tokens is None
878+
assert llm.compatibility.tool_stream is False
886879

887880

888881
def test_clone_llm_with_model_alias_preserves_kimi_thinking_disabled():

tests/core/test_model_switch_carryover.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
from pythinker_code.config import Config
2121
from pythinker_code.llm import LLM
22+
from pythinker_code.provider_compatibility import default_provider_compatibility
2223
from pythinker_code.soul.compaction import SimpleCompaction
2324
from pythinker_code.wire.types import TextPart
2425

@@ -31,7 +32,14 @@ def with_generation_kwargs(self, **kwargs: object) -> _FakeChatProvider:
3132

3233

3334
def _fake_llm() -> LLM:
34-
return cast(LLM, SimpleNamespace(chat_provider=_FakeChatProvider(), provider_config=None))
35+
return cast(
36+
LLM,
37+
SimpleNamespace(
38+
chat_provider=_FakeChatProvider(),
39+
provider_config=None,
40+
compatibility=default_provider_compatibility(),
41+
),
42+
)
3543

3644

3745
def _history(n_pairs: int = 3) -> list[Message]:

tests/core/test_z_ai_provider_requests.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ async def test_glm52_request_effort_mapping(
135135
("effort", "thinking"),
136136
[
137137
("off", {"type": "disabled"}),
138+
("minimal", {"type": "disabled"}),
138139
("high", {"type": "enabled", "clear_thinking": False}),
139140
],
140141
)
@@ -250,6 +251,30 @@ async def test_glm52_exact_replay_does_not_synthesize_missing_reasoning(
250251
assert body["reasoning_effort"] != "medium"
251252

252253

254+
@pytest.mark.parametrize(
255+
("effort", "enabled"),
256+
[("off", False), ("high", True)],
257+
)
258+
async def test_self_hosted_qwen_uses_chat_template_thinking_toggle(
259+
effort: ThinkingEffort,
260+
enabled: bool,
261+
) -> None:
262+
body = await _captured_body(
263+
route=None,
264+
provider_key="local",
265+
base_url="http://localhost:8080/v1",
266+
model_id="Qwen3.6-35B-A3B",
267+
max_context_size=262_144,
268+
capabilities={"thinking"},
269+
effort=effort,
270+
history=[Message(role="user", content="hello")],
271+
tools=[],
272+
)
273+
274+
assert body["chat_template_kwargs"] == {"enable_thinking": enabled}
275+
assert "reasoning_effort" not in body
276+
277+
253278
async def test_local_glm_name_has_no_zai_request_policy() -> None:
254279
body = await _captured_body(
255280
route=None,

0 commit comments

Comments
 (0)