c870589831
urlparse raises ValueError on non-integer / out-of-range ports, and http://myhost:99999/v1 passes OpenAI-client construction (only httpx rejects it later), so the crash was reachable from build_kwargs on every request for such a URL. Wrap the parsed.port check in the same try/except ValueError guard hermes_cli.models already uses around its 11434 check, and pin it with parametrized tests.
183 lines
7.3 KiB
Python
183 lines
7.3 KiB
Python
"""Unit tests for the custom provider profile's reasoning wiring.
|
|
|
|
``provider=custom`` covers any OpenAI-compatible endpoint the user points
|
|
Hermes at — local Ollama, vLLM, llama.cpp, and hosted reasoning APIs like
|
|
GLM-5.2 on Volcengine ARK. Before #57601's salvage, ``CustomProfile`` emitted
|
|
nothing when reasoning was *enabled*, so a configured ``reasoning_effort``
|
|
was silently dropped for every custom endpoint.
|
|
|
|
These tests pin the wire-shape contract:
|
|
- disabled on Ollama → extra_body.think = False + reasoning_effort=none
|
|
- disabled elsewhere → reasoning_effort=none, no think (strict APIs 422)
|
|
- enabled + effort → top-level reasoning_effort (native OpenAI-compat
|
|
format GLM/ARK expect), passed through verbatim
|
|
including ``max``/``xhigh``
|
|
- enabled + no effort → nothing emitted (endpoint's server default applies)
|
|
- ollama_num_ctx → extra_body.options.num_ctx, orthogonal to reasoning
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture
|
|
def custom_profile():
|
|
"""Resolve the registered custom profile via the global registry.
|
|
|
|
Importing ``model_tools`` triggers plugin discovery, which registers the
|
|
``custom`` profile. Going through ``get_provider_profile`` keeps the test
|
|
honest — if the registered class is ever downgraded to a plain
|
|
``ProviderProfile``, the assertions below collapse.
|
|
"""
|
|
import model_tools # noqa: F401
|
|
import providers
|
|
|
|
profile = providers.get_provider_profile("custom")
|
|
assert profile is not None, "custom provider profile must be registered"
|
|
return profile
|
|
|
|
|
|
class TestCustomReasoningWireShape:
|
|
"""``build_api_kwargs_extras`` produces the correct wire format."""
|
|
|
|
def test_no_reasoning_config_emits_nothing(self, custom_profile):
|
|
"""Unset reasoning → omit everything so the endpoint's default applies."""
|
|
eb, tl = custom_profile.build_api_kwargs_extras(
|
|
reasoning_config=None, model="glm-5.2"
|
|
)
|
|
assert eb == {}
|
|
assert tl == {}
|
|
|
|
def test_disabled_sends_think_false(self, custom_profile):
|
|
"""enabled=False on an Ollama URL → reasoning_effort='none' + think=False.
|
|
|
|
Both fields are required on Ollama: /v1/chat/completions silently
|
|
ignores extra_body.think (only /api/chat honours it — ollama#14820)
|
|
but respects top-level reasoning_effort (#25758). think=False stays
|
|
for proxies and the native /api/chat path.
|
|
"""
|
|
eb, tl = custom_profile.build_api_kwargs_extras(
|
|
reasoning_config={"enabled": False},
|
|
model="qwen3",
|
|
base_url="http://127.0.0.1:11434/v1",
|
|
)
|
|
assert eb == {"think": False}
|
|
assert tl == {"reasoning_effort": "none"}
|
|
|
|
def test_effort_none_sends_think_false(self, custom_profile):
|
|
"""effort='none' is the disable alias → same dual emission on Ollama."""
|
|
eb, tl = custom_profile.build_api_kwargs_extras(
|
|
reasoning_config={"enabled": True, "effort": "none"},
|
|
model="qwen3",
|
|
base_url="http://localhost:11434/v1",
|
|
)
|
|
assert eb == {"think": False}
|
|
assert tl == {"reasoning_effort": "none"}
|
|
|
|
def test_disabled_omits_think_on_mistral(self, custom_profile):
|
|
"""Strict OpenAI-compat hosts forbid extra ``think`` (HTTP 422)."""
|
|
eb, tl = custom_profile.build_api_kwargs_extras(
|
|
reasoning_config={"enabled": True, "effort": "none"},
|
|
model="mistral-small-latest",
|
|
base_url="https://api.mistral.ai/v1",
|
|
)
|
|
assert "think" not in eb
|
|
assert tl == {"reasoning_effort": "none"}
|
|
|
|
def test_disabled_omits_think_without_base_url(self, custom_profile):
|
|
"""Unknown custom endpoint — do not send the Ollama-only flag."""
|
|
eb, tl = custom_profile.build_api_kwargs_extras(
|
|
reasoning_config={"enabled": False}, model="glm-5.2"
|
|
)
|
|
assert "think" not in eb
|
|
assert tl == {"reasoning_effort": "none"}
|
|
|
|
@pytest.mark.parametrize(
|
|
"base_url",
|
|
[
|
|
"http://127.0.0.1:8080/v1",
|
|
"http://localhost:1234/v1",
|
|
"https://api.groq.com/openai/v1",
|
|
],
|
|
)
|
|
def test_disabled_omits_think_on_non_ollama_relays(self, custom_profile, base_url):
|
|
eb, tl = custom_profile.build_api_kwargs_extras(
|
|
reasoning_config={"effort": "none"},
|
|
model="llama3",
|
|
base_url=base_url,
|
|
)
|
|
assert "think" not in eb
|
|
assert tl == {"reasoning_effort": "none"}
|
|
|
|
def test_disabled_sends_think_false_on_ollama_cloud_host(self, custom_profile):
|
|
eb, tl = custom_profile.build_api_kwargs_extras(
|
|
reasoning_config={"enabled": False},
|
|
model="qwen3",
|
|
base_url="https://ollama.com/v1",
|
|
)
|
|
assert eb == {"think": False}
|
|
assert tl == {"reasoning_effort": "none"}
|
|
|
|
@pytest.mark.parametrize(
|
|
"base_url",
|
|
[
|
|
"http://myhost:99999/v1", # out-of-range port: OpenAI client accepts it
|
|
"http://localhost:80a/v1", # non-integer port
|
|
"http://localhost:11434./v1", # trailing-dot port
|
|
],
|
|
)
|
|
def test_malformed_port_does_not_raise(self, custom_profile, base_url):
|
|
"""Malformed ports must not raise — urlparse's ``port`` is ValueError-happy.
|
|
|
|
The OpenAI client accepts ``http://myhost:99999/v1`` at construction
|
|
(only httpx fails later), so these URLs reach ``build_api_kwargs_extras``
|
|
in production. The heuristic must treat them as non-Ollama rather than
|
|
killing the kwargs build.
|
|
"""
|
|
eb, tl = custom_profile.build_api_kwargs_extras(
|
|
reasoning_config={"enabled": False},
|
|
model="qwen3",
|
|
base_url=base_url,
|
|
)
|
|
assert "think" not in eb
|
|
assert tl == {"reasoning_effort": "none"}
|
|
|
|
@pytest.mark.parametrize(
|
|
"effort", ["minimal", "low", "medium", "high", "xhigh", "max"]
|
|
)
|
|
def test_enabled_effort_goes_top_level(self, custom_profile, effort):
|
|
"""enabled + effort → TOP-LEVEL reasoning_effort, passed through verbatim.
|
|
|
|
GLM-5.2/ARK and OpenAI-compatible reasoning APIs read reasoning_effort
|
|
as a top-level string, not nested in extra_body. ``max`` is GLM's
|
|
native deep-reasoning level and must survive.
|
|
"""
|
|
eb, tl = custom_profile.build_api_kwargs_extras(
|
|
reasoning_config={"enabled": True, "effort": effort}, model="glm-5.2"
|
|
)
|
|
assert tl == {"reasoning_effort": effort}
|
|
assert "reasoning_effort" not in eb
|
|
assert "think" not in eb
|
|
|
|
|
|
def test_does_not_force_think_true_on_enable(self, custom_profile):
|
|
"""We must never send think=True on enable — it's Ollama-only and
|
|
would 400 on GLM/vLLM endpoints that don't recognize it."""
|
|
eb, _ = custom_profile.build_api_kwargs_extras(
|
|
reasoning_config={"enabled": True, "effort": "high"}, model="glm-5.2"
|
|
)
|
|
assert eb.get("think") is not True
|
|
|
|
|
|
class TestCustomReasoningWithNumCtx:
|
|
"""Ollama num_ctx and reasoning are independent and compose."""
|
|
|
|
def test_num_ctx_alone(self, custom_profile):
|
|
eb, tl = custom_profile.build_api_kwargs_extras(
|
|
reasoning_config=None, ollama_num_ctx=8192, model="qwen3"
|
|
)
|
|
assert eb == {"options": {"num_ctx": 8192}}
|
|
assert tl == {}
|
|
|