diff --git a/tests/test_llm_core_temperature_anthropic.py b/tests/test_llm_core_temperature_anthropic.py new file mode 100644 index 000000000..5146db286 --- /dev/null +++ b/tests/test_llm_core_temperature_anthropic.py @@ -0,0 +1,34 @@ +"""Regression tests: Anthropic temperature clamping. + +Anthropic rejects temperature values outside [0.0, 1.0]. The payload builder +must clamp the value to that range before sending rather than letting the API +return HTTP 400. +""" +from src import llm_core + + +def _anthropic_payload(temperature): + return llm_core._build_anthropic_payload( + "claude-3-5-sonnet", + [{"role": "user", "content": "Hi"}], + temperature, + max_tokens=5, + ) + + +def test_anthropic_payload_clamps_above_one(): + # Anthropic rejects temperature > 1.0 (e.g. the Nietzsche preset's 1.2). + assert _anthropic_payload(1.2)["temperature"] == 1.0 + + +def test_anthropic_payload_keeps_in_range(): + assert _anthropic_payload(0.7)["temperature"] == 0.7 + + +def test_anthropic_payload_clamps_negative(): + assert _anthropic_payload(-0.5)["temperature"] == 0.0 + + +def test_anthropic_payload_none_temperature_does_not_crash(): + payload = _anthropic_payload(None) + assert payload["temperature"] is None diff --git a/tests/test_llm_core_temperature_moonshot.py b/tests/test_llm_core_temperature_moonshot.py new file mode 100644 index 000000000..936e159ea --- /dev/null +++ b/tests/test_llm_core_temperature_moonshot.py @@ -0,0 +1,100 @@ +"""Regression tests: Moonshot/Kimi temperature detection and payload behavior. + +Moonshot kimi-k2.5+ models reject custom temperature values; the payload +builder must detect the Moonshot provider and omit temperature for the affected +model family. Self-hosted Kimi deployments (non-Moonshot URL) must keep the +caller-specified temperature unchanged. +""" +import httpx +import pytest + +from src import llm_core + + +@pytest.mark.parametrize( + "model", + [ + "kimi-k2.5", + "kimi-k2.6", + "moonshot/kimi-k2.6", + "kimi-k2.6-preview", + ], +) +def test_moonshot_k2_5_plus_uses_fixed_temperature(model): + assert llm_core._moonshot_rejects_custom_temperature("moonshot", model) + + +@pytest.mark.parametrize( + "provider,model", + [ + ("openai", "kimi-k2.6"), + ("moonshot", "kimi-k2-0905-preview"), + ("moonshot", "kimi-k2-thinking"), + ("moonshot", "kimi-k2.50"), + ("moonshot", None), + ], +) +def test_other_models_keep_temperature(provider, model): + assert not llm_core._moonshot_rejects_custom_temperature(provider, model) + + +@pytest.mark.parametrize( + "url", + [ + "https://api.moonshot.ai/v1/chat/completions", + "https://api.moonshot.cn/v1/chat/completions", + ], +) +def test_moonshot_provider_detection(url): + assert llm_core._detect_provider(url) == "moonshot" + + +def _capture_openai_payload( + monkeypatch, + model, + temperature, + url="https://api.openai.com/v1/chat/completions", +): + """Run a synchronous OpenAI-compatible call and return the posted JSON body.""" + llm_core._response_cache.clear() + seen = {} + + def fake_post(url, headers=None, json=None, timeout=None): + seen["json"] = json + request = httpx.Request("POST", url) + return httpx.Response( + 200, + request=request, + json={"choices": [{"message": {"content": "OK"}}]}, + ) + + monkeypatch.setattr(llm_core.httpx, "post", fake_post) + result = llm_core.llm_call( + url, + model, + [{"role": "user", "content": "Say OK"}], + temperature=temperature, + max_tokens=5, + ) + assert result == "OK" + return seen["json"] + + +def test_moonshot_k2_6_payload_omits_temperature(monkeypatch): + payload = _capture_openai_payload( + monkeypatch, + "kimi-k2.6", + 0.7, + url="https://api.moonshot.ai/v1/chat/completions", + ) + assert "temperature" not in payload + + +def test_self_hosted_kimi_k2_6_payload_keeps_temperature(monkeypatch): + payload = _capture_openai_payload( + monkeypatch, + "kimi-k2.6", + 0.7, + url="http://localhost:8000/v1/chat/completions", + ) + assert payload["temperature"] == 0.7 diff --git a/tests/test_llm_core_temperature.py b/tests/test_llm_core_temperature_reasoning.py similarity index 62% rename from tests/test_llm_core_temperature.py rename to tests/test_llm_core_temperature_reasoning.py index ab6334f36..55fd6182a 100644 --- a/tests/test_llm_core_temperature.py +++ b/tests/test_llm_core_temperature_reasoning.py @@ -109,88 +109,3 @@ def test_chatgpt_subscription_payload_omits_max_output_tokens_when_zero(): ) assert "max_output_tokens" not in payload - - -def _anthropic_payload(temperature): - return llm_core._build_anthropic_payload( - "claude-3-5-sonnet", - [{"role": "user", "content": "Hi"}], - temperature, - max_tokens=5, - ) - - -def test_anthropic_payload_clamps_above_one(): - # Anthropic rejects temperature > 1.0 (e.g. the Nietzsche preset's 1.2). - assert _anthropic_payload(1.2)["temperature"] == 1.0 - - -def test_anthropic_payload_keeps_in_range(): - assert _anthropic_payload(0.7)["temperature"] == 0.7 - - -def test_anthropic_payload_clamps_negative(): - assert _anthropic_payload(-0.5)["temperature"] == 0.0 - - -def test_anthropic_payload_none_temperature_does_not_crash(): - payload = _anthropic_payload(None) - assert payload["temperature"] is None - - -@pytest.mark.parametrize( - "model", - [ - "kimi-k2.5", - "kimi-k2.6", - "moonshot/kimi-k2.6", - "kimi-k2.6-preview", - ], -) -def test_moonshot_k2_5_plus_uses_fixed_temperature(model): - assert llm_core._moonshot_rejects_custom_temperature("moonshot", model) - - -@pytest.mark.parametrize( - "provider,model", - [ - ("openai", "kimi-k2.6"), - ("moonshot", "kimi-k2-0905-preview"), - ("moonshot", "kimi-k2-thinking"), - ("moonshot", "kimi-k2.50"), - ("moonshot", None), - ], -) -def test_other_models_keep_temperature(provider, model): - assert not llm_core._moonshot_rejects_custom_temperature(provider, model) - - -@pytest.mark.parametrize( - "url", - [ - "https://api.moonshot.ai/v1/chat/completions", - "https://api.moonshot.cn/v1/chat/completions", - ], -) -def test_moonshot_provider_detection(url): - assert llm_core._detect_provider(url) == "moonshot" - - -def test_moonshot_k2_6_payload_omits_temperature(monkeypatch): - payload = _capture_openai_payload( - monkeypatch, - "kimi-k2.6", - 0.7, - url="https://api.moonshot.ai/v1/chat/completions", - ) - assert "temperature" not in payload - - -def test_self_hosted_kimi_k2_6_payload_keeps_temperature(monkeypatch): - payload = _capture_openai_payload( - monkeypatch, - "kimi-k2.6", - 0.7, - url="http://localhost:8000/v1/chat/completions", - ) - assert payload["temperature"] == 0.7