fix(router): обработчик ошибок падал сам, уровень усилия не подставлялся
Найдено проверкой всех шести ролей на живой машине владельца. До правок работали три роли из шести. 1. antigravity_adapter.classify_error возвращал ErrorCategory.UNKNOWN — значения с таким именем не существует, есть AUTH_REQUIRED, FATAL, INVALID_REQUEST, QUOTA_EXHAUSTED, RATE_LIMITED, TRANSIENT. Обращение роняло сам классификатор с AttributeError, то есть отказ происходил ровно там, где обрабатывался другой отказ, и маршрутизация обрывалась вместо перехода к резервному профилю. Заменено на FATAL по образцу codex и opencode: неразобранная ошибка не должна давать повторов. 2. Уровень усилия не подставлялся, если у конкретного профиля не выполнен вход agy: карта усилий строится обнаружением ЧЕРЕЗ этот профиль, и при неудаче оставалась пустой. Уровни же — свойство модели, а не аккаунта. Добавлен запасной источник: сохранённый на диске список моделей со склеенными идентификаторами вида gemini-3.7-flash-high, из которых уровни выводятся напрямую и переживают перезапуск. После правок работают все шесть ролей, включая живое переключение 'fast': opengo-1 -> ag-w4. Закрыто тестами, включая защиту от возврата несуществующей категории. Тесты: 373 passed, ruff чисто. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
c626d5dd8d
commit
e15f12a3bd
3 changed files with 91 additions and 2 deletions
|
|
@ -349,7 +349,31 @@ def _model_supported_efforts(agy_model: str, profile_id: str | None = None) -> s
|
|||
сама модель совершенно настоящая.
|
||||
"""
|
||||
discover_models(profile_id=profile_id)
|
||||
return _AGY_EFFORT_MAP.get(agy_model, set())
|
||||
efforts = _AGY_EFFORT_MAP.get(agy_model, set())
|
||||
if efforts:
|
||||
return efforts
|
||||
|
||||
# Запасной источник: сохранённый на диске список моделей. Обнаружение
|
||||
# ходит через КОНКРЕТНЫЙ профиль, и если именно у него вход не сделан,
|
||||
# карта усилий остаётся пустой — тогда уровень не подставляется и agy
|
||||
# отвергает вызов, хотя модель настоящая. Уровни усилия — свойство
|
||||
# модели, а не аккаунта, поэтому их можно взять из склеенных
|
||||
# идентификаторов вида "gemini-3.7-flash-high", уже лежащих в кэше.
|
||||
try:
|
||||
from antigravity_provider.router.model_discovery import ModelDiscoveryService
|
||||
|
||||
cached = ModelDiscoveryService.get().get_models("antigravity") or []
|
||||
except Exception:
|
||||
return efforts
|
||||
|
||||
known = {"low", "medium", "high"}
|
||||
derived = {
|
||||
suffix
|
||||
for model_id in cached
|
||||
for base, _, suffix in [model_id.rpartition("-")]
|
||||
if base == agy_model and suffix in known
|
||||
}
|
||||
return derived
|
||||
|
||||
|
||||
# Effort values understood by both hermes and agy
|
||||
|
|
|
|||
|
|
@ -195,7 +195,12 @@ class AntigravityAdapter(BaseProviderAdapter):
|
|||
message=err_msg,
|
||||
)
|
||||
|
||||
# ErrorCategory.UNKNOWN не существует — обращение к нему роняло сам
|
||||
# классификатор, то есть отказ происходил ровно там, где обрабатывался
|
||||
# другой отказ, и маршрутизация обрывалась вместо перехода к резерву.
|
||||
# FATAL выбран по образцу codex и opencode: неразобранная ошибка не
|
||||
# должна вызывать повторов на том же профиле.
|
||||
return ErrorClassification(
|
||||
category=ErrorCategory.UNKNOWN,
|
||||
category=ErrorCategory.FATAL,
|
||||
message=err_msg,
|
||||
)
|
||||
|
|
|
|||
60
tests/test_router_error_path.py
Normal file
60
tests/test_router_error_path.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
"""Путь обработки ошибок не должен падать сам.
|
||||
|
||||
Два дефекта, найденные проверкой всех шести ролей на живой машине:
|
||||
|
||||
1. antigravity_adapter.classify_error возвращал ErrorCategory.UNKNOWN —
|
||||
значения с таким именем не существует. Обращение к нему роняло сам
|
||||
классификатор, то есть отказ случался ровно там, где обрабатывался
|
||||
другой отказ, и маршрутизация обрывалась вместо перехода к резерву.
|
||||
|
||||
2. Уровень усилия не подставлялся, если у конкретного профиля не выполнен
|
||||
вход agy: карта усилий строится обнаружением через этот профиль. Уровни
|
||||
же — свойство модели, а не аккаунта, и берутся из сохранённого списка.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from antigravity_provider.router.adapters import get_adapter
|
||||
from antigravity_provider.router.adapters.base_adapter import ErrorCategory
|
||||
|
||||
|
||||
def test_error_category_has_no_phantom_members():
|
||||
"""Классификаторы обязаны ссылаться только на существующие категории."""
|
||||
valid = {m for m in dir(ErrorCategory) if not m.startswith("_")}
|
||||
assert "UNKNOWN" not in valid, (
|
||||
"если UNKNOWN добавили — обновите классификаторы, раньше его не было"
|
||||
)
|
||||
for name in ("AUTH_REQUIRED", "FATAL", "QUOTA_EXHAUSTED", "RATE_LIMITED", "TRANSIENT"):
|
||||
assert name in valid
|
||||
|
||||
|
||||
def test_unclassified_error_does_not_crash_classifier():
|
||||
"""Неразобранная ошибка должна классифицироваться, а не ронять обработчик."""
|
||||
for provider in ("antigravity", "openai-codex", "opencode-go", "grok", "claude"):
|
||||
adapter = get_adapter(provider)
|
||||
result = adapter.classify_error(Exception("нечто совершенно неразобранное"))
|
||||
assert result.category, f"{provider}: классификатор вернул пустую категорию"
|
||||
assert isinstance(result.category, str)
|
||||
|
||||
|
||||
def test_effort_levels_derived_from_cached_model_ids(monkeypatch):
|
||||
"""Уровни усилия берутся из склеенных идентификаторов, без обращения к профилю."""
|
||||
import antigravity_provider.agy_subprocess as agy
|
||||
from antigravity_provider.router import model_discovery
|
||||
|
||||
monkeypatch.setattr(agy, "discover_models", lambda profile_id=None: {})
|
||||
monkeypatch.setattr(agy, "_AGY_EFFORT_MAP", {}, raising=False)
|
||||
|
||||
class _Svc:
|
||||
@staticmethod
|
||||
def get():
|
||||
return _Svc()
|
||||
|
||||
def get_models(self, provider):
|
||||
return ["gemini-3.7-flash-low", "gemini-3.7-flash-high", "claude-sonnet-4-6"]
|
||||
|
||||
monkeypatch.setattr(model_discovery, "ModelDiscoveryService", _Svc)
|
||||
|
||||
assert agy._model_supported_efforts("gemini-3.7-flash") == {"low", "high"}
|
||||
# У модели без суффиксов уровней быть не должно — подставлять нечего.
|
||||
assert agy._model_supported_efforts("claude-sonnet-4-6") == set()
|
||||
Loading…
Reference in a new issue