From 2d62d3973ec2fd498fd3fdabd5289b7b2d0af07e Mon Sep 17 00:00:00 2001 From: Hermes Team Date: Sun, 23 Aug 2026 12:25:02 +0700 Subject: [PATCH] =?UTF-8?q?fix(plugin):=20=D0=BE=D1=82=D0=BA=D0=B0=D0=B7?= =?UTF-8?q?=20=D1=80=D0=BE=D1=83=D1=82=D0=B5=D1=80=D0=B0=20=D0=B1=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D1=88=D0=B5=20=D0=BD=D0=B5=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D1=8F=D0=B5=D1=82=20=D0=BE=D1=82=D0=B2=D0=B5?= =?UTF-8?q?=D1=82=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D0=B8=20=D0=B2=20Hermes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/antigravity_provider/hermes_plugin.py | 14 +++++ tests/test_plugin_passthrough.py | 70 +++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 tests/test_plugin_passthrough.py diff --git a/src/antigravity_provider/hermes_plugin.py b/src/antigravity_provider/hermes_plugin.py index 3649a3e..c403622 100644 --- a/src/antigravity_provider/hermes_plugin.py +++ b/src/antigravity_provider/hermes_plugin.py @@ -37,6 +37,20 @@ def antigravity_llm_execution(**kwargs: Any) -> Any: role = kwargs.get("role") or request.get("role") session_id = kwargs.get("session_id") or request.get("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"): err_text = format_antigravity_error(completion.get("error")) completion = { diff --git a/tests/test_plugin_passthrough.py b/tests/test_plugin_passthrough.py new file mode 100644 index 0000000..d3ffe4e --- /dev/null +++ b/tests/test_plugin_passthrough.py @@ -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