From 62dc5d046e286f5eb6fc3ef9291a3c6e985cc57e Mon Sep 17 00:00:00 2001 From: alexthebuildr <116134064+ztsalexey@users.noreply.github.com> Date: Tue, 24 Feb 2026 11:23:39 -0700 Subject: [PATCH] feat: add OpenRouter preset to setup wizard (#270) * feat: add OpenRouter preset to setup wizard Add OpenRouter as a top-level provider option in the onboarding wizard (Step 3). Selecting it pre-fills the base URL (https://openrouter.ai/api/v1) and prompts for an API key, avoiding manual URL entry. Under the hood it uses the existing openai_compatible backend. Inlines the key collection flow (rather than delegating to setup_api_key_provider) so success messages consistently say "OpenRouter" instead of "openai_compatible", including the early-return env-key path. Closes #178 Co-Authored-By: Claude Opus 4.6 * fix: address serrrfirat review comments on OpenRouter wizard preset - Re-run path now recognizes OpenRouter: display shows "OpenRouter" and keep-current routes to setup_openrouter() when base URL contains openrouter.ai - Refactor setup_openrouter() to delegate to setup_api_key_provider() with a display_name override, eliminating ~40 lines of duplication - Update README: remove false claim about model fetching from OpenRouter API, add footnote explaining shared secret/env var between OpenRouter and OpenAI-compatible - Fix pre-existing clippy warning in settings.rs (field_reassign_with_default) Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- src/setup/README.md | 13 +++++++++- src/setup/wizard.rs | 61 ++++++++++++++++++++++++++++++++++++--------- 2 files changed, 61 insertions(+), 13 deletions(-) diff --git a/src/setup/README.md b/src/setup/README.md index 19b210a2..c956529a 100644 --- a/src/setup/README.md +++ b/src/setup/README.md @@ -172,7 +172,18 @@ env-var mode or skipped secrets. | Anthropic | API key | `anthropic_api_key` | `ANTHROPIC_API_KEY` | | OpenAI | API key | `openai_api_key` | `OPENAI_API_KEY` | | Ollama | None | - | - | -| OpenAI-compatible | Optional API key | `llm_compatible_api_key` | `LLM_API_KEY` | +| OpenRouter¹ | API key | `llm_compatible_api_key` | `LLM_API_KEY` | +| OpenAI-compatible¹ | Optional API key | `llm_compatible_api_key` | `LLM_API_KEY` | + +¹ OpenRouter and OpenAI-compatible share the same secret name and env var because +OpenRouter is stored as `llm_backend = "openai_compatible"` under the hood. +Switching between them overwrites the same credential slot. + +**OpenRouter** (`setup_openrouter`): +- Pre-configured OpenAI-compatible preset with base URL `https://openrouter.ai/api/v1` +- Delegates to `setup_api_key_provider()` with a display name override ("OpenRouter") +- Sets `llm_backend = "openai_compatible"` and `openai_compatible_base_url` automatically +- Clears `selected_model` so Step 4 prompts for a model name (manual text input, no API-based model fetching) **API-key providers** (`setup_api_key_provider`): 1. Check env var → if set, ask to reuse, persist to secrets store diff --git a/src/setup/wizard.rs b/src/setup/wizard.rs index fd745161..3662a653 100644 --- a/src/setup/wizard.rs +++ b/src/setup/wizard.rs @@ -735,13 +735,24 @@ impl SetupWizard { async fn step_inference_provider(&mut self) -> Result<(), SetupError> { // Show current provider if already configured if let Some(ref current) = self.settings.llm_backend { - let display = match current.as_str() { - "nearai" => "NEAR AI", - "anthropic" => "Anthropic (Claude)", - "openai" => "OpenAI", - "ollama" => "Ollama (local)", - "openai_compatible" => "OpenAI-compatible endpoint", - other => other, + let is_openrouter = current == "openai_compatible" + && self + .settings + .openai_compatible_base_url + .as_deref() + .is_some_and(|u| u.contains("openrouter.ai")); + + let display = if is_openrouter { + "OpenRouter" + } else { + match current.as_str() { + "nearai" => "NEAR AI", + "anthropic" => "Anthropic (Claude)", + "openai" => "OpenAI", + "ollama" => "Ollama (local)", + "openai_compatible" => "OpenAI-compatible endpoint", + other => other, + } }; print_info(&format!("Current provider: {}", display)); println!(); @@ -753,6 +764,9 @@ impl SetupWizard { if is_known && confirm("Keep current provider?", true).map_err(SetupError::Io)? { // Still run the auth sub-flow in case they need to update keys + if is_openrouter { + return self.setup_openrouter().await; + } match current.as_str() { "nearai" => return self.setup_nearai().await, "anthropic" => return self.setup_anthropic().await, @@ -784,7 +798,8 @@ impl SetupWizard { "Anthropic - Claude models (direct API key)", "OpenAI - GPT models (direct API key)", "Ollama - local models, no API key needed", - "OpenAI-compatible - custom endpoint (vLLM, LiteLLM, Together, etc.)", + "OpenRouter - 200+ models via single API key", + "OpenAI-compatible - custom endpoint (vLLM, LiteLLM, etc.)", ]; let choice = select_one("Provider:", options).map_err(SetupError::Io)?; @@ -794,7 +809,8 @@ impl SetupWizard { 1 => self.setup_anthropic().await?, 2 => self.setup_openai().await?, 3 => self.setup_ollama()?, - 4 => self.setup_openai_compatible().await?, + 4 => self.setup_openrouter().await?, + 5 => self.setup_openai_compatible().await?, _ => return Err(SetupError::Config("Invalid provider selection".to_string())), } @@ -868,6 +884,7 @@ impl SetupWizard { "llm_anthropic_api_key", "Anthropic API key", "https://console.anthropic.com/settings/keys", + None, ) .await } @@ -880,11 +897,12 @@ impl SetupWizard { "llm_openai_api_key", "OpenAI API key", "https://platform.openai.com/api-keys", + None, ) .await } - /// Shared setup flow for API-key-based providers (Anthropic, OpenAI). + /// Shared setup flow for API-key-based providers (Anthropic, OpenAI, OpenRouter). async fn setup_api_key_provider( &mut self, backend: &str, @@ -892,12 +910,13 @@ impl SetupWizard { secret_name: &str, prompt_label: &str, hint_url: &str, + override_display_name: Option<&str>, ) -> Result<(), SetupError> { - let display_name = match backend { + let display_name = override_display_name.unwrap_or(match backend { "anthropic" => "Anthropic", "openai" => "OpenAI", other => other, - }; + }); self.settings.llm_backend = Some(backend.to_string()); if self.settings.selected_model.is_some() { @@ -977,6 +996,24 @@ impl SetupWizard { Ok(()) } + /// OpenRouter provider setup: pre-configured OpenAI-compatible endpoint. + /// + /// Sets the base URL to `https://openrouter.ai/api/v1` and delegates + /// API key collection to `setup_api_key_provider` with a display name + /// override so messages say "OpenRouter" instead of "openai_compatible". + async fn setup_openrouter(&mut self) -> Result<(), SetupError> { + self.settings.openai_compatible_base_url = Some("https://openrouter.ai/api/v1".to_string()); + self.setup_api_key_provider( + "openai_compatible", + "LLM_API_KEY", + "llm_compatible_api_key", + "OpenRouter API key", + "https://openrouter.ai/settings/keys", + Some("OpenRouter"), + ) + .await + } + /// OpenAI-compatible provider setup: base URL + optional API key. async fn setup_openai_compatible(&mut self) -> Result<(), SetupError> { self.settings.llm_backend = Some("openai_compatible".to_string());