fix(opencode): адаптер не читал сохранённый ключ — аккаунт не работал никогда

Владелец: «стоит опенкод аккаунт, который не подключен, у него кончились
лимиты и аккаунт не работает». Лимиты ни при чём.

Мастер подключения сохраняет ключ через 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 <noreply@anthropic.com>
This commit is contained in:
Hermes Team 2026-08-23 21:45:32 +07:00
parent 04e5d0dcf5
commit fb23bff0b0
2 changed files with 88 additions and 2 deletions

View file

@ -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()

View file

@ -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