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
203 lines
6.6 KiB
Python
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"]
|