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"]:
|
if "api_key" in profile.auth_config and profile.auth_config["api_key"]:
|
||||||
return 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("-", "_")
|
suffix = profile.profile_id.upper().replace("-", "_")
|
||||||
for candidate in (
|
for candidate in (
|
||||||
f"OPENCODE_GO_KEY_{suffix}",
|
f"OPENCODE_GO_KEY_{suffix}",
|
||||||
|
|
@ -56,7 +76,7 @@ class OpenCodeGoAdapter(BaseProviderAdapter):
|
||||||
if candidate in os.environ and os.environ[candidate].strip():
|
if candidate in os.environ and os.environ[candidate].strip():
|
||||||
return 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"):
|
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():
|
if global_env in os.environ and os.environ[global_env].strip():
|
||||||
return 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