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:
parent
04e5d0dcf5
commit
fb23bff0b0
2 changed files with 88 additions and 2 deletions
|
|
@ -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()
|
||||
|
|
|
|||
66
tests/test_opencode_credentials.py
Normal file
66
tests/test_opencode_credentials.py
Normal 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
|
||||
Loading…
Reference in a new issue