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)
|
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
|
# Effort values understood by both hermes and agy
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,12 @@ class AntigravityAdapter(BaseProviderAdapter):
|
||||||
message=err_msg,
|
message=err_msg,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ErrorCategory.UNKNOWN не существует — обращение к нему роняло сам
|
||||||
|
# классификатор, то есть отказ происходил ровно там, где обрабатывался
|
||||||
|
# другой отказ, и маршрутизация обрывалась вместо перехода к резерву.
|
||||||
|
# FATAL выбран по образцу codex и opencode: неразобранная ошибка не
|
||||||
|
# должна вызывать повторов на том же профиле.
|
||||||
return ErrorClassification(
|
return ErrorClassification(
|
||||||
category=ErrorCategory.UNKNOWN,
|
category=ErrorCategory.FATAL,
|
||||||
message=err_msg,
|
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