import asyncio from types import SimpleNamespace from pnp_lab.config import Settings from pnp_lab.model_router import ModelRouter class FakeCompletions: def __init__(self, responses): self.responses = list(responses) self.calls = [] async def create(self, **kwargs): self.calls.append(kwargs) item = self.responses.pop(0) if isinstance(item, Exception): raise item return item class FakeClient: def __init__(self, responses): self.chat = SimpleNamespace(completions=FakeCompletions(responses)) def response(content, *, reasoning="", finish="stop", model="moonshotai/Kimi-K3", prompt_tokens=10, completion_tokens=20): msg = SimpleNamespace(content=content, reasoning_content=reasoning, model_extra={}) choice = SimpleNamespace(message=msg, finish_reason=finish) usage = SimpleNamespace(prompt_tokens=prompt_tokens, completion_tokens=completion_tokens) return SimpleNamespace(choices=[choice], usage=usage, model=model, id="req_test", _request_id="req_test") def make_router(responses): s = Settings() s.hf_token = "hf_test_token" s.kimi_reasoning_effort = "high" router = object.__new__(ModelRouter) router.settings = s router._sem = asyncio.Semaphore(1) router._client = FakeClient(responses) router._prices = {"moonshotai/Kimi-K3": (3.0, 15.0), "zai-org/GLM-5.2": (0.75, 2.4)} router._models_cache = [] return router def test_empty_kimi_content_is_retried_with_lower_reasoning_effort(): router = make_router([ response("", reasoning="x" * 4000, finish="length"), response('{"ok": true}', reasoning="x" * 100, finish="stop"), ]) diagnostics = [] result = asyncio.run(router.chat( "moonshotai/Kimi-K3", "system", "user", max_tokens=1000, retries=2, diagnostic_cb=diagnostics.append, )) assert result.text == '{"ok": true}' assert [d["status"] for d in diagnostics] == ["empty_content", "success"] assert diagnostics[0]["reasoning_effort"] == "high" assert diagnostics[1]["reasoning_effort"] == "low" assert diagnostics[0]["reasoning_chars"] == 4000 def test_router_falls_back_to_second_model_after_empty_primary(): router = make_router([ response("", reasoning="x" * 20, finish="length"), response('{"ok": true}', reasoning="", finish="stop", model="zai-org/GLM-5.2"), ]) diagnostics = [] result = asyncio.run(router.chat( "moonshotai/Kimi-K3", "system", "user", max_tokens=1000, retries=1, fallback_models=["zai-org/GLM-5.2"], diagnostic_cb=diagnostics.append, )) assert result.model_used == "zai-org/GLM-5.2" assert [d["status"] for d in diagnostics] == ["empty_content", "success"] assert diagnostics[-1]["model"] == "zai-org/GLM-5.2"