chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
"""OpenCode readiness + credential reporting for ``omnigent setup``.
|
||||
|
||||
Like :mod:`omnigent.onboarding.goose_auth`, Omnigent stores **no** OpenCode
|
||||
credentials: OpenCode owns its own provider auth via ``opencode auth login``
|
||||
(stored in ``~/.local/share/opencode/auth.json``) or ambient provider env vars
|
||||
(``OPENAI_API_KEY`` / ``ANTHROPIC_API_KEY`` / …). This module is a thin,
|
||||
read-only reporter so ``omnigent setup`` can show which providers OpenCode can
|
||||
reach and offer to run its native login — without ever touching its secrets.
|
||||
|
||||
It reads ``auth.json`` directly (a JSON object keyed by provider id — see
|
||||
``packages/opencode/src/auth`` in the OpenCode source) rather than scraping
|
||||
``opencode auth list`` output, and checks a curated set of common provider env
|
||||
vars. Both are best-effort: a missing/unreadable file or unknown env var simply
|
||||
reports "nothing configured", never raises.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from omnigent.onboarding.harness_install import OPENCODE_KEY, harness_cli_installed
|
||||
|
||||
# Common OpenCode providers → (provider id, display label, env var). The
|
||||
# provider id matches OpenCode's own id (the ``auth.json`` key and the
|
||||
# ``provider/model`` prefix in ``opencode models``). Not exhaustive (OpenCode
|
||||
# resolves many providers from models.dev); this is the set worth surfacing in
|
||||
# setup, including the ``OPENAI_*`` pair the Databricks-gateway path uses.
|
||||
_ENV_PROVIDER_VARS: tuple[tuple[str, str, str], ...] = (
|
||||
("openai", "OpenAI", "OPENAI_API_KEY"),
|
||||
("anthropic", "Anthropic", "ANTHROPIC_API_KEY"),
|
||||
("google", "Google Gemini", "GEMINI_API_KEY"),
|
||||
("google", "Google Gemini", "GOOGLE_GENERATIVE_AI_API_KEY"),
|
||||
("groq", "Groq", "GROQ_API_KEY"),
|
||||
("openrouter", "OpenRouter", "OPENROUTER_API_KEY"),
|
||||
("xai", "xAI", "XAI_API_KEY"),
|
||||
("mistral", "Mistral", "MISTRAL_API_KEY"),
|
||||
("deepseek", "DeepSeek", "DEEPSEEK_API_KEY"),
|
||||
)
|
||||
|
||||
|
||||
def opencode_auth_path() -> Path:
|
||||
"""Return OpenCode's ``auth.json`` path for this process's HOME.
|
||||
|
||||
Honors ``XDG_DATA_HOME``; defaults to ``~/.local/share/opencode/auth.json``
|
||||
(OpenCode's ``Global.Path.data``).
|
||||
"""
|
||||
xdg = os.environ.get("XDG_DATA_HOME", "").strip()
|
||||
base = Path(xdg) if xdg else Path.home() / ".local" / "share"
|
||||
return base / "opencode" / "auth.json"
|
||||
|
||||
|
||||
def _stored_providers() -> tuple[str, ...]:
|
||||
"""Return provider ids with stored credentials in ``auth.json``.
|
||||
|
||||
Best-effort: a missing/unreadable/non-object file yields ``()``. OpenCode's
|
||||
auth file is a provider-id keyed JSON object; an empty object value is not a
|
||||
credential, so ignore it rather than reporting a false ready state from a
|
||||
structural shell like ``{"openai": {}}``.
|
||||
"""
|
||||
try:
|
||||
data = json.loads(opencode_auth_path().read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
return ()
|
||||
if not isinstance(data, dict):
|
||||
return ()
|
||||
return tuple(str(k) for k, v in data.items() if bool(v))
|
||||
|
||||
|
||||
def _env_providers(environ: dict[str, str] | None = None) -> tuple[str, ...]:
|
||||
"""Return provider labels whose API-key env var is present."""
|
||||
env = os.environ if environ is None else environ
|
||||
seen: list[str] = []
|
||||
for _provider_id, label, var in _ENV_PROVIDER_VARS:
|
||||
if env.get(var, "").strip() and label not in seen:
|
||||
seen.append(label)
|
||||
return tuple(seen)
|
||||
|
||||
|
||||
def reachable_provider_ids(environ: dict[str, str] | None = None) -> frozenset[str]:
|
||||
"""Return OpenCode provider ids reachable from stored auth + env keys.
|
||||
|
||||
Ids match OpenCode's own (the ``provider/model`` prefix), so callers can
|
||||
filter a model list down to what the user can actually authenticate.
|
||||
"""
|
||||
env = os.environ if environ is None else environ
|
||||
ids = set(_stored_providers())
|
||||
for provider_id, _label, var in _ENV_PROVIDER_VARS:
|
||||
if env.get(var, "").strip():
|
||||
ids.add(provider_id)
|
||||
return frozenset(ids)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OpenCodeAuthSummary:
|
||||
"""What setup needs to know about the local OpenCode credentials.
|
||||
|
||||
:param installed: ``opencode`` binary present on ``PATH``.
|
||||
:param stored_providers: Provider ids with credentials in ``auth.json``.
|
||||
:param env_providers: Provider labels whose API-key env var is set.
|
||||
"""
|
||||
|
||||
installed: bool
|
||||
stored_providers: tuple[str, ...]
|
||||
env_providers: tuple[str, ...]
|
||||
|
||||
@property
|
||||
def has_provider(self) -> bool:
|
||||
"""Whether any provider is reachable (stored credential or env key)."""
|
||||
return bool(self.stored_providers or self.env_providers)
|
||||
|
||||
@property
|
||||
def ready(self) -> bool:
|
||||
"""Launchable when the CLI is installed AND a provider is configured."""
|
||||
return self.installed and self.has_provider
|
||||
|
||||
def describe(self) -> str:
|
||||
"""A short human summary of configured providers, e.g.
|
||||
``"2 stored (anthropic, openai) + env: OpenAI"``.
|
||||
"""
|
||||
parts: list[str] = []
|
||||
if self.stored_providers:
|
||||
parts.append(
|
||||
f"{len(self.stored_providers)} stored ({', '.join(sorted(self.stored_providers))})"
|
||||
)
|
||||
if self.env_providers:
|
||||
parts.append(f"env: {', '.join(self.env_providers)}")
|
||||
return " · ".join(parts) if parts else "no provider configured yet"
|
||||
|
||||
|
||||
def opencode_auth_summary() -> OpenCodeAuthSummary:
|
||||
"""Summarize the local OpenCode credential state for setup display."""
|
||||
return OpenCodeAuthSummary(
|
||||
installed=harness_cli_installed(OPENCODE_KEY),
|
||||
stored_providers=_stored_providers(),
|
||||
env_providers=_env_providers(),
|
||||
)
|
||||
Reference in New Issue
Block a user