hermes-hub/tests/test_local_llm_empty_answer.py
Hermes Team 9a2c341f15 Merge A24 (маршрутизация как центр управления) и A25 (локальная модель)
Обе работы приняты, проверено исполнением.

A24: ровно семь разделов, renderTeam и renderProviders удалены, кнопки
«Изменить цепочку» нет, перетаскивание блоков есть. Перестановка цепочки
проверена вживую: сохраняется в router_profiles.yaml и откатывается.

A25: провайдер local подключён к настоящему серверу владельца через
SSH-туннель к 127.0.0.1:8081. health_check проходит, /v1/models отдаёт
модель, реальный вызов возвращает «ОК» за 3.1 с. Профили local-1 и
local-2 добавлены. Квота отдаётся отдельным состоянием
(source=local_provider, «Без ограничений»), а не как отсутствие данных.
Адрес сервера нигде не зашит.

Разрешение конфликта в action_handler: A25 внёс edit_route и assign_role
в список «просто навигация», где они возвращают заглушку. В A24 это
работающие обработчики — сохранение цепочки и назначение роли. Приняв
версию A25 целиком, мы бы молча сломали перестановку блоков. Оставлены
оба: локальный провайдер в add_account и рабочие обработчики ниже.

Исправлено при слиянии:

1. Адаптер отдавал пустой ответ как успех. У сервера владельца
   --reasoning on --reasoning-budget 4096: при скромном max_tokens весь
   бюджет уходит на рассуждения, llama.cpp возвращает 200, заполняет
   reasoning_content и оставляет content пустым. Проверено на живой
   модели: max_tokens=40 — ответа нет, 200 — приходит «ОК». Роутер
   засчитал бы такой вызов, а пользователь не получил бы ничего.
   Теперь это явный отказ с объяснением, и срабатывает переключение.
   Проверка вынесена из блока перехвата: иначе оборачивалась в
   «Transport Error», хотя транспорт отработал штатно.

2. Заглушка в тесте A25 возвращала "choices": [] — такого настоящий
   сервер не отдаёт. Приведена к реальному виду.

Тесты: 404 passed, ruff чисто.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 14:33:16 +07:00

51 lines
2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Пустой ответ локальной модели — отказ, а не успех.
Сервер владельца поднят с ``--reasoning on --reasoning-budget 4096``. При
скромном ``max_tokens`` весь бюджет уходит на рассуждения: llama.cpp
возвращает 200, заполняет ``reasoning_content`` и оставляет ``content``
пустым. Проверено на живой модели через SSH-туннель: с max_tokens=40
ответа нет, с 200 приходит «ОК».
Если отдать такой ответ дальше как успешный, роутер засчитает вызов, а
пользователь не получит ничего и не узнает почему.
"""
from __future__ import annotations
import pytest
from antigravity_provider.router.adapters.local_adapter import LocalLLMAdapter
def test_answer_with_content_passes():
data = {
"choices": [
{"index": 0, "message": {"role": "assistant", "content": "ОК"}, "finish_reason": "stop"}
]
}
LocalLLMAdapter._reject_empty_answer(data) # не должно бросать
def test_reasoning_ate_the_budget_is_rejected():
data = {
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "", "reasoning_content": "долгие раздумья"},
"finish_reason": "length",
}
]
}
with pytest.raises(RuntimeError, match="рассужд"):
LocalLLMAdapter._reject_empty_answer(data)
def test_empty_choices_is_rejected():
with pytest.raises(RuntimeError, match="choices"):
LocalLLMAdapter._reject_empty_answer({"choices": []})
def test_blank_content_without_reasoning_is_rejected():
data = {"choices": [{"index": 0, "message": {"content": " "}, "finish_reason": "stop"}]}
with pytest.raises(RuntimeError, match="пустой"):
LocalLLMAdapter._reject_empty_answer(data)