diff --git a/nanobot/cli/commands.py b/nanobot/cli/commands.py index 0732d48..855023a 100644 --- a/nanobot/cli/commands.py +++ b/nanobot/cli/commands.py @@ -173,20 +173,33 @@ This file stores important information that should persist across sessions. def _make_provider(config): - """Create LiteLLMProvider from config. Exits if no API key found.""" + """Create provider from config. Exits if no credentials found.""" from nanobot.providers.litellm_provider import LiteLLMProvider from nanobot.providers.openai_codex_provider import OpenAICodexProvider - from oauth_cli_kit import get_token as get_codex_token + from nanobot.providers.registry import PROVIDERS + from oauth_cli_kit import get_token as get_oauth_token - p = config.get_provider() model = config.agents.defaults.model - if model.startswith("openai-codex/"): - try: - _ = get_codex_token() - except Exception: - console.print("Please run: [cyan]nanobot login --provider openai-codex[/cyan]") + model_lower = model.lower() + + # Check for OAuth-based providers first (registry-driven) + for spec in PROVIDERS: + if spec.is_oauth and any(kw in model_lower for kw in spec.keywords): + # OAuth provider matched + try: + _ = get_oauth_token(spec.oauth_provider or spec.name) + except Exception: + console.print(f"Please run: [cyan]nanobot login --provider {spec.name}[/cyan]") + raise typer.Exit(1) + # Return appropriate OAuth provider class + if spec.name == "openai_codex": + return OpenAICodexProvider(default_model=model) + # Future OAuth providers can be added here + console.print(f"[red]Error: OAuth provider '{spec.name}' not fully implemented.[/red]") raise typer.Exit(1) - return OpenAICodexProvider(default_model=model) + + # Standard API key-based providers + p = config.get_provider() if not (p and p.api_key) and not model.startswith("bedrock/"): console.print("[red]Error: No API key configured.[/red]") console.print("Set one in ~/.nanobot/config.json under providers section") diff --git a/nanobot/config/schema.py b/nanobot/config/schema.py index 1707797..cde73f2 100644 --- a/nanobot/config/schema.py +++ b/nanobot/config/schema.py @@ -132,15 +132,19 @@ class Config(BaseSettings): model_lower = (model or self.agents.defaults.model).lower() # Match by keyword (order follows PROVIDERS registry) + # Note: OAuth providers don't require api_key, so we check is_oauth flag for spec in PROVIDERS: p = getattr(self.providers, spec.name, None) - if p and any(kw in model_lower for kw in spec.keywords) and p.api_key: - return p + if p and any(kw in model_lower for kw in spec.keywords): + # OAuth providers don't need api_key + if spec.is_oauth or p.api_key: + return p # Fallback: gateways first, then others (follows registry order) + # OAuth providers are also valid fallbacks for spec in PROVIDERS: p = getattr(self.providers, spec.name, None) - if p and p.api_key: + if p and (spec.is_oauth or p.api_key): return p return None diff --git a/nanobot/providers/registry.py b/nanobot/providers/registry.py index d7226e7..4ccf5da 100644 --- a/nanobot/providers/registry.py +++ b/nanobot/providers/registry.py @@ -51,6 +51,10 @@ class ProviderSpec: # per-model param overrides, e.g. (("kimi-k2.5", {"temperature": 1.0}),) model_overrides: tuple[tuple[str, dict[str, Any]], ...] = () + # OAuth-based providers (e.g., OpenAI Codex) don't use API keys + is_oauth: bool = False # if True, uses OAuth flow instead of API key + oauth_provider: str = "" # OAuth provider name for token retrieval + @property def label(self) -> str: return self.display_name or self.name.title() @@ -157,6 +161,8 @@ PROVIDERS: tuple[ProviderSpec, ...] = ( default_api_base="https://chatgpt.com/backend-api", strip_model_prefix=False, model_overrides=(), + is_oauth=True, # OAuth-based authentication + oauth_provider="openai-codex", # OAuth provider identifier ), # DeepSeek: needs "deepseek/" prefix for LiteLLM routing.