Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

203 lines
6.6 KiB
Python

"""CLI commands for LLM provider authentication."""
from __future__ import annotations
import sys
import webbrowser
from collections.abc import Iterable
import click
from surfaces.cli.llm_auth.providers import (
ProviderAuthProfile,
iter_auth_profiles,
resolve_auth_profile,
)
from surfaces.cli.llm_auth.service import (
AuthSetupError,
configure_api_key_provider,
configure_cli_subscription_provider,
logout_provider,
provider_status,
verify_provider,
)
def _provider_choices() -> str:
return ", ".join(profile.name for profile in iter_auth_profiles())
def _resolve_or_raise(provider: str) -> ProviderAuthProfile:
try:
return resolve_auth_profile(provider)
except KeyError as exc:
raise click.UsageError(
f"Unknown auth provider '{provider}'. Choose one of: {_provider_choices()}."
) from exc
def _prompt_provider() -> ProviderAuthProfile:
click.echo("Subscription logins:")
for profile in iter_auth_profiles():
if profile.kind == "cli_subscription":
click.echo(f" {profile.name:<10} {profile.label}")
click.echo("API-key providers:")
api_key_names = [profile.name for profile in iter_auth_profiles() if profile.kind == "api_key"]
click.echo(f" {', '.join(api_key_names)}")
provider = click.prompt("Provider", type=str).strip()
return _resolve_or_raise(provider)
def _maybe_open_setup_page(profile: ProviderAuthProfile, *, enabled: bool) -> None:
if not enabled or not profile.setup_url:
return
if not sys.stdin.isatty():
click.echo(f"Setup page: {profile.setup_url}")
return
if click.confirm(f"Open {profile.label} setup page in your browser?", default=True):
webbrowser.open(profile.setup_url)
def _status_lines(providers: Iterable[ProviderAuthProfile]) -> list[str]:
lines = [f"{'Provider':<14} {'Status':<8} {'Source':<11} Detail"]
for profile in providers:
status = provider_status(profile.name)
state = "stale" if status.stale else "ok" if status.authenticated else "missing"
lines.append(f"{profile.name:<14} {state:<8} {status.source:<11} {status.detail}")
return lines
@click.group(name="auth", invoke_without_command=True)
@click.pass_context
def auth_command(ctx: click.Context) -> None:
"""Log in to LLM providers and inspect local auth state."""
if ctx.invoked_subcommand is None:
for line in _status_lines(iter_auth_profiles()):
click.echo(line)
@auth_command.command(name="login")
@click.argument("provider", required=False)
@click.option("--api-key", help="API key to store for API-key providers.")
@click.option("--model", help="Reasoning model to persist with the provider selection.")
@click.option(
"--set-provider/--no-set-provider",
default=True,
show_default=True,
help="Persist LLM_PROVIDER and provider model settings after login.",
)
@click.option(
"--validate/--no-validate",
default=True,
show_default=True,
help="Run a tiny live validation request before storing API keys.",
)
@click.option(
"--open-browser/--no-open-browser",
default=True,
show_default=True,
help="Offer to open the provider setup page for API-key flows.",
)
@click.option(
"--launch-login/--no-launch-login",
default=True,
show_default=True,
help="Launch the vendor CLI login command for subscription flows when needed.",
)
def auth_login(
provider: str | None,
api_key: str | None,
model: str | None,
set_provider: bool,
validate: bool,
open_browser: bool,
launch_login: bool,
) -> None:
"""Configure one LLM provider auth path."""
profile = _resolve_or_raise(provider) if provider else _prompt_provider()
try:
if profile.kind == "api_key":
_maybe_open_setup_page(profile, enabled=open_browser)
resolved_key = api_key
if resolved_key is None:
resolved_key = click.prompt(
f"{profile.label} key",
hide_input=True,
confirmation_prompt=False,
type=str,
)
result = configure_api_key_provider(
profile=profile,
api_key=resolved_key,
model=model,
set_provider=set_provider,
validate=validate,
)
else:
_maybe_open_setup_page(profile, enabled=open_browser)
result = configure_cli_subscription_provider(
profile=profile,
model=model,
set_provider=set_provider,
launch_login=launch_login,
)
except AuthSetupError as exc:
raise click.ClickException(str(exc)) from exc
click.echo(f"Authenticated: {profile.label}")
click.echo(f"Provider : {result.provider}")
click.echo(f"Credential : {result.source}")
if result.model:
click.echo(f"Model : {result.model}")
if result.env_path:
click.echo(f"Env : {result.env_path}")
click.echo(f"Status : {result.detail}")
click.echo(f"Verify : opensre auth verify {result.provider}")
@auth_command.command(name="status")
@click.argument("provider", required=False)
def auth_status(provider: str | None) -> None:
"""Show provider auth status."""
profiles = (_resolve_or_raise(provider),) if provider else iter_auth_profiles()
for line in _status_lines(profiles):
click.echo(line)
@auth_command.command(name="verify")
@click.argument("provider")
def auth_verify(provider: str) -> None:
"""Intentionally verify one provider's request-time credentials."""
_resolve_or_raise(provider)
try:
status = verify_provider(provider)
except AuthSetupError as exc:
raise click.ClickException(str(exc)) from exc
state = "ok" if status.authenticated else "missing"
if status.stale:
state = "stale"
click.echo(f"Provider : {status.provider}")
click.echo(f"Status : {state}")
click.echo(f"Source : {status.source}")
click.echo(f"Detail : {status.detail}")
@auth_command.command(name="logout")
@click.argument("provider")
@click.option(
"--vendor",
is_flag=True,
help="Also run the vendor CLI logout command for subscription providers.",
)
def auth_logout(provider: str, vendor: bool) -> None:
"""Clear OpenSRE-managed auth for a provider."""
_resolve_or_raise(provider)
try:
detail = logout_provider(provider, vendor=vendor)
except AuthSetupError as exc:
raise click.ClickException(str(exc)) from exc
click.echo(detail)
__all__ = ["auth_command"]