diff --git a/src/antigravity_provider/agy_subprocess.py b/src/antigravity_provider/agy_subprocess.py index 7aa46bd..4fd1231 100644 --- a/src/antigravity_provider/agy_subprocess.py +++ b/src/antigravity_provider/agy_subprocess.py @@ -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 diff --git a/src/antigravity_provider/router/adapters/antigravity_adapter.py b/src/antigravity_provider/router/adapters/antigravity_adapter.py index 0316f69..3bf94ff 100644 --- a/src/antigravity_provider/router/adapters/antigravity_adapter.py +++ b/src/antigravity_provider/router/adapters/antigravity_adapter.py @@ -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, ) diff --git a/tests/test_router_error_path.py b/tests/test_router_error_path.py new file mode 100644 index 0000000..e776d0c --- /dev/null +++ b/tests/test_router_error_path.py @@ -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()