diff --git a/app/main.py b/app/main.py index 7ed40ed..02a7026 100644 --- a/app/main.py +++ b/app/main.py @@ -638,6 +638,7 @@ def api_check(model_id: str): "model": model_id, "messages": [{"role": "user", "content": "What is 2+2? Reply with just the number."}], "max_tokens": 5, + "reasoning_effort": "none", }).encode() req = urllib.request.Request( f"{OMNIROUTE_URL}/v1/chat/completions", @@ -652,8 +653,11 @@ def api_check(model_id: str): # Usage фиксируем ДО валидации контента: даже если парсинг choices # упадёт, реально потраченные токены сохраняются в checks. usage = extract_usage(resp) - content = resp["choices"][0]["message"]["content"] - ok = bool(content.strip()) + choice = resp["choices"][0] + content = choice["message"].get("content") + ok = isinstance(content, str) and bool(content.strip()) + if not ok: + error = f"Empty text response (finish_reason={choice.get('finish_reason')})" except Exception as e: error = f"{type(e).__name__}: {e}" print(f"[check] {model_id}: {error}") diff --git a/app/probe.py b/app/probe.py index 6cbc86e..b322be8 100644 --- a/app/probe.py +++ b/app/probe.py @@ -37,6 +37,7 @@ def check_model(model_id: str) -> tuple[bool, int | None, str | None, dict]: "model": model_id, "messages": [{"role": "user", "content": "2+2=?"}], "max_tokens": 5, + "reasoning_effort": "none", }).encode() req = urllib.request.Request( f"{OMNIROUTE_URL}/v1/chat/completions", @@ -50,8 +51,13 @@ def check_model(model_id: str) -> tuple[bool, int | None, str | None, dict]: resp = json.loads(r.read()) # Usage — до любой валидации контента. usage = extract_usage(resp) - content = resp["choices"][0]["message"]["content"] - ok = bool(content.strip()) + # HTTP 200 может содержать только reasoning и content=null. + # Это не успешный текстовый ответ и не ошибка Python. + choice = resp["choices"][0] + content = choice["message"].get("content") + ok = isinstance(content, str) and bool(content.strip()) + if not ok: + error = f"Empty text response (finish_reason={choice.get('finish_reason')})" except Exception as e: error = f"{type(e).__name__}: {e}" print(f" ✗ {model_id}: {error}", file=sys.stderr) diff --git a/tests/conftest.py b/tests/conftest.py index a767b9b..7a53b36 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -81,9 +81,10 @@ class OmniMock: if not beh.get("no_usage"): resp["usage"] = beh.get("usage") if not beh.get("omit_choices"): - resp["choices"] = [{"index": 0, "finish_reason": "stop", + resp["choices"] = [{"index": 0, "finish_reason": beh.get("finish_reason", "stop"), "message": {"role": "assistant", - "content": beh.get("content", "4")}}] + "content": beh.get("content", "4"), + "reasoning_content": beh.get("reasoning_content")}}] self._send_json(200, resp) def do_GET(self): diff --git a/tests/test_reasoning_probe.py b/tests/test_reasoning_probe.py new file mode 100644 index 0000000..c024950 --- /dev/null +++ b/tests/test_reasoning_probe.py @@ -0,0 +1,34 @@ +"""Оба пути пробы запрашивают none и не падают на reasoning-only ответе.""" +import json +import sqlite3 + + +def test_cron_disables_reasoning_and_reports_null_content(core, omni_server): + omni_server.set_completions(content=None, reasoning_content="thinking", finish_reason="length", + usage={"prompt_tokens": 12, "completion_tokens": 5, "total_tokens": 17}) + ok, latency, error, usage = core.probe.check_model("zai/test-model-a") + assert ok is False + assert error == "Empty text response (finish_reason=length)" + assert usage["total_tokens"] == 17 + request = omni_server.requests[-1]["body"] + assert request["reasoning_effort"] == "none" + assert request["max_tokens"] == 5 + + +def test_manual_disables_reasoning_and_persists_empty_text(core, omni_server): + omni_server.set_completions(content=None, reasoning_content="thinking", finish_reason="length", + usage={"prompt_tokens": 12, "completion_tokens": 5, "total_tokens": 17}) + response = core.client.get("/api/check/zai/test-model-a") + assert response.status_code == 200 + assert response.json()["ok"] is False + assert omni_server.requests[-1]["body"]["reasoning_effort"] == "none" + db = sqlite3.connect(core.db_path) + row = db.execute("SELECT ok,error,total_tokens FROM checks ORDER BY id DESC LIMIT 1").fetchone() + db.close() + assert row == (0, "Empty text response (finish_reason=length)", 17) + + +def test_text_response_still_succeeds(core, omni_server): + omni_server.set_completions(content=" 4 ") + assert core.probe.check_model("zai/test-model-a")[0] is True + assert core.client.get("/api/check/zai/test-model-a").json()["ok"] is True