From fb23bff0b0fbece9dd0996be79a4d1ec7c3de901 Mon Sep 17 00:00:00 2001 From: Hermes Team Date: Sun, 23 Aug 2026 21:45:32 +0700 Subject: [PATCH] =?UTF-8?q?fix(opencode):=20=D0=B0=D0=B4=D0=B0=D0=BF=D1=82?= =?UTF-8?q?=D0=B5=D1=80=20=D0=BD=D0=B5=20=D1=87=D0=B8=D1=82=D0=B0=D0=BB=20?= =?UTF-8?q?=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D1=91=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D0=B9=20=D0=BA=D0=BB=D1=8E=D1=87=20=E2=80=94=20=D0=B0=D0=BA?= =?UTF-8?q?=D0=BA=D0=B0=D1=83=D0=BD=D1=82=20=D0=BD=D0=B5=20=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D0=B0=D0=BB=20=D0=BD=D0=B8=D0=BA=D0=BE=D0=B3?= =?UTF-8?q?=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Владелец: «стоит опенкод аккаунт, который не подключен, у него кончились лимиты и аккаунт не работает». Лимиты ни при чём. Мастер подключения сохраняет ключ через ProfileAuthManager, а _resolve_api_key смотрел только в auth_config из YAML и в переменные окружения. Хранилище профилей он не читал вовсе — в отличие от grok, claude и codex, где такая проверка есть. Следствие: любой аккаунт OpenCode Go, подключённый через интерфейс, был нерабочим. Маршрутизация падала с «No API key found for OpenCode Go profile», и эта строка уже попадалась в следе отказов оркестратора. Проверено на живом профиле владельца: до правки health_check=False и тест профиля возвращал «локальный runtime недоступен», хотя api_key лежал в auth.json. После — health_check=True, тест проходит. Закрыто тестами, включая проверку, что пустое хранилище по-прежнему даёт отказ, а не ложноположительный результат. Тесты: 331 passed, ruff чисто. Co-Authored-By: Claude Opus 5 --- .../router/adapters/opencode_adapter.py | 24 ++++++- tests/test_opencode_credentials.py | 66 +++++++++++++++++++ 2 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 tests/test_opencode_credentials.py diff --git a/src/antigravity_provider/router/adapters/opencode_adapter.py b/src/antigravity_provider/router/adapters/opencode_adapter.py index 36ab490..628de7c 100644 --- a/src/antigravity_provider/router/adapters/opencode_adapter.py +++ b/src/antigravity_provider/router/adapters/opencode_adapter.py @@ -37,7 +37,27 @@ class OpenCodeGoAdapter(BaseProviderAdapter): if "api_key" in profile.auth_config and profile.auth_config["api_key"]: return profile.auth_config["api_key"] - # 2. Account-specific environment variable (e.g. OPENCODE_GO_KEY_OPENGO_1, OPENCODE_GO_KEY_1) + # 2. Профиль в хранилище учётных данных. + # Мастер подключения сохраняет ключ именно сюда, а адаптер его не читал: + # смотрел только YAML-конфиг и переменные окружения. Из-за этого любой + # аккаунт OpenCode, подключённый через интерфейс, не работал никогда — + # маршрутизация падала с «No API key found», хотя ключ лежал на диске. + # Остальные адаптеры (grok, claude, codex) читают профиль; этот — нет. + try: + from ..profile_manager import ProfileAuthManager + + creds = ProfileAuthManager.load_profile_auth("opencode-go", profile.profile_id) + if creds: + for key in ("api_key", "access_token", "token"): + value = creds.get(key) + if isinstance(value, dict): + value = value.get("api_key") or value.get("access_token") + if isinstance(value, str) and value.strip(): + return value.strip() + except Exception: + pass + + # 3. Account-specific environment variable (e.g. OPENCODE_GO_KEY_OPENGO_1, OPENCODE_GO_KEY_1) suffix = profile.profile_id.upper().replace("-", "_") for candidate in ( f"OPENCODE_GO_KEY_{suffix}", @@ -56,7 +76,7 @@ class OpenCodeGoAdapter(BaseProviderAdapter): if candidate in os.environ and os.environ[candidate].strip(): return os.environ[candidate].strip() - # 3. Global OpenCode Go keys + # 4. Global OpenCode Go keys for global_env in ("OPENCODE_GO_API_KEY", "OPENCODE_ZEN_API_KEY", "OPENCODE_API_KEY"): if global_env in os.environ and os.environ[global_env].strip(): return os.environ[global_env].strip() diff --git a/tests/test_opencode_credentials.py b/tests/test_opencode_credentials.py new file mode 100644 index 0000000..30ca557 --- /dev/null +++ b/tests/test_opencode_credentials.py @@ -0,0 +1,66 @@ +"""Адаптер OpenCode Go обязан читать ключ из хранилища профилей. + +Дефект: мастер подключения сохраняет ключ через ProfileAuthManager, а +_resolve_api_key смотрел только в auth_config из YAML и в переменные +окружения. Аккаунт, подключённый через интерфейс, не работал никогда — +маршрутизация падала с «No API key found», хотя ключ лежал на диске, а +карточка показывала «Работает». + +Остальные адаптеры (grok, claude, codex) хранилище читают; этот — нет. +""" + +from __future__ import annotations + +from types import SimpleNamespace + +import pytest + +from antigravity_provider.router.adapters.opencode_adapter import OpenCodeGoAdapter + + +@pytest.fixture +def profile(): + return SimpleNamespace(profile_id="opengo-1", account_id="acc-1", auth_config={}) + + +def test_api_key_is_read_from_profile_store(monkeypatch, profile): + import antigravity_provider.router.profile_manager as pm + + monkeypatch.setattr( + pm.ProfileAuthManager, + "load_profile_auth", + staticmethod(lambda provider, pid: {"api_key": "stored-key-123", "auth_mode": "api_key"}), + ) + for var in ("OPENCODE_GO_API_KEY", "OPENCODE_ZEN_API_KEY", "OPENCODE_API_KEY"): + monkeypatch.delenv(var, raising=False) + + assert OpenCodeGoAdapter()._resolve_api_key(profile) == "stored-key-123" + + +def test_health_check_passes_when_only_profile_store_has_key(monkeypatch, profile): + """Именно это расхождение показывало «Работает» на нерабочем аккаунте.""" + import antigravity_provider.router.profile_manager as pm + + monkeypatch.setattr( + pm.ProfileAuthManager, + "load_profile_auth", + staticmethod(lambda provider, pid: {"api_key": "stored-key-123"}), + ) + for var in ("OPENCODE_GO_API_KEY", "OPENCODE_ZEN_API_KEY", "OPENCODE_API_KEY"): + monkeypatch.delenv(var, raising=False) + + assert OpenCodeGoAdapter().health_check(profile) is True + + +def test_missing_credentials_still_report_no_key(monkeypatch, profile): + """Пустое хранилище не должно давать ложноположительный результат.""" + import antigravity_provider.router.profile_manager as pm + + monkeypatch.setattr( + pm.ProfileAuthManager, "load_profile_auth", staticmethod(lambda provider, pid: {}) + ) + for var in ("OPENCODE_GO_API_KEY", "OPENCODE_ZEN_API_KEY", "OPENCODE_API_KEY"): + monkeypatch.delenv(var, raising=False) + + assert OpenCodeGoAdapter()._resolve_api_key(profile) is None + assert OpenCodeGoAdapter().health_check(profile) is False