fix(plugin): отказ роутера больше не подменяет ответ модели в Hermes
Hub подключён к Hermes как middleware llm_execution и срабатывает на
каждом обращении к модели. Но Hermes роль не передаёт: в kwargs есть
model, provider, session_id, task_id — role нет. resolve_role поэтому
сваливается в роль по умолчанию, и КАЖДЫЙ вызов Hermes маршрутизируется
как orchestrator.
Цепочка orchestrator у владельца исчерпана целиком:
ag-orch-fallback skipped_unhealthy
codex-orch 429 «account is not active, check billing»
opengo-3 No API key found
ag-w1/ag-w3 agy authentication failed or timed out
Роутер возвращал «⚠️ Hermes Router Failover Exhausted» как ответ
ассистента, и Hermes показывал это вместо ответа модели, хотя его
собственный провайдер работал. Это и есть «основной оркестратор не
выбрался» из отчёта владельца.
Теперь при router_error вызов уходит дальше по цепочке (next_call),
а отказ пишется в журнал уровнем warning с полным следом. Плагин обязан
быть незаметным при отказе: он может улучшить маршрутизацию, но не имеет
права сделать Hermes хуже, чем без него.
Проверено исполнением: Hermes получает ответ провайдера, а не текст
ошибки. Тесты: 287 passed (падает только известный нестабильный Tk-тест,
воспроизводится на чистом main).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
6494c5198a
commit
2d62d3973e
2 changed files with 84 additions and 0 deletions
|
|
@ -37,6 +37,20 @@ def antigravity_llm_execution(**kwargs: Any) -> Any:
|
||||||
role = kwargs.get("role") or request.get("role")
|
role = kwargs.get("role") or request.get("role")
|
||||||
session_id = kwargs.get("session_id") or request.get("session_id")
|
session_id = kwargs.get("session_id") or request.get("session_id")
|
||||||
completion = engine.route_request(request, role=role, session_id=session_id)
|
completion = engine.route_request(request, role=role, session_id=session_id)
|
||||||
|
# Исчерпанная цепочка — это отказ роутера, а не ответ модели.
|
||||||
|
# Возвращать её текст Гермесу нельзя: он подменит собой настоящий
|
||||||
|
# ответ провайдера, который Гермес выбрал бы сам, и пользователь
|
||||||
|
# получит «Failover Exhausted» вместо работы. Плагин обязан быть
|
||||||
|
# незаметным при отказе: пропускаем вызов дальше по цепочке.
|
||||||
|
if isinstance(completion, dict) and completion.get("router_error"):
|
||||||
|
logger.warning(
|
||||||
|
"Router failover exhausted for role %r; passing the call downstream to Hermes: %s",
|
||||||
|
role,
|
||||||
|
completion.get("failover_trail"),
|
||||||
|
)
|
||||||
|
if callable(next_call):
|
||||||
|
return next_call(request)
|
||||||
|
return openai_completion_object(completion)
|
||||||
if isinstance(completion, dict) and "error" in completion and not completion.get("choices"):
|
if isinstance(completion, dict) and "error" in completion and not completion.get("choices"):
|
||||||
err_text = format_antigravity_error(completion.get("error"))
|
err_text = format_antigravity_error(completion.get("error"))
|
||||||
completion = {
|
completion = {
|
||||||
|
|
|
||||||
70
tests/test_plugin_passthrough.py
Normal file
70
tests/test_plugin_passthrough.py
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
"""Плагин не должен делать Hermes хуже, чем без него.
|
||||||
|
|
||||||
|
Hermes вызывает Hub как middleware `llm_execution` на каждом обращении к
|
||||||
|
модели, но **роль не передаёт** — в kwargs есть model, provider, session_id,
|
||||||
|
task_id, а `role` нет. Роутер поэтому сваливается в роль по умолчанию
|
||||||
|
(`orchestrator`), и если её цепочка исчерпана, раньше он возвращал текст
|
||||||
|
«⚠️ Hermes Router Failover Exhausted» как ответ ассистента. Пользователь
|
||||||
|
видел это вместо ответа модели, хотя собственный провайдер Hermes работал.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from antigravity_provider import hermes_plugin
|
||||||
|
|
||||||
|
|
||||||
|
class _ExhaustedEngine:
|
||||||
|
class config:
|
||||||
|
enabled = True
|
||||||
|
|
||||||
|
def route_request(self, request: Dict[str, Any], role: Any = None, session_id: Any = None) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"id": "router-fail-1",
|
||||||
|
"model": "router-failover",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"message": {"role": "assistant", "content": "⚠️ Hermes Router Failover Exhausted"},
|
||||||
|
"finish_reason": "error",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"router_error": True,
|
||||||
|
"failover_trail": [{"profile_id": "codex-orch", "status": "failed"}],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def exhausted_router(monkeypatch):
|
||||||
|
monkeypatch.setattr(hermes_plugin, "get_router_engine", lambda: _ExhaustedEngine(), raising=False)
|
||||||
|
import antigravity_provider.router as router_pkg
|
||||||
|
|
||||||
|
monkeypatch.setattr(router_pkg, "get_router_engine", lambda: _ExhaustedEngine())
|
||||||
|
|
||||||
|
|
||||||
|
def test_exhausted_failover_passes_call_downstream(exhausted_router):
|
||||||
|
downstream_calls = []
|
||||||
|
|
||||||
|
def next_call(payload=None):
|
||||||
|
downstream_calls.append(payload)
|
||||||
|
return {
|
||||||
|
"choices": [
|
||||||
|
{"index": 0, "message": {"role": "assistant", "content": "настоящий ответ"}, "finish_reason": "stop"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
result = hermes_plugin.antigravity_llm_execution(
|
||||||
|
request={"messages": [{"role": "user", "content": "ping"}]},
|
||||||
|
next_call=next_call,
|
||||||
|
provider="gemini",
|
||||||
|
model="gemini-3.7-flash",
|
||||||
|
session_id="s1",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(downstream_calls) == 1, "отказ роутера обязан уходить вниз по цепочке, а не подменять ответ"
|
||||||
|
content = result["choices"][0]["message"]["content"]
|
||||||
|
assert content == "настоящий ответ"
|
||||||
|
assert "Failover Exhausted" not in content
|
||||||
Loading…
Reference in a new issue