feat(onboarding): remove OpenRouter default, require explicit provider selection
Breaking changes: - Quick setup now requires --provider flag (no default) - TUI wizard requires provider selection (no pre-selection) - Docker compose requires PROVIDER env var - .env.example no longer defaults to openrouter Changes: - wizard.rs: Remove hardcoded "openrouter" defaults, require explicit provider - tui.rs: Add provider placeholder, require selection before proceeding - .env.example: Use provider-neutral placeholders - docker-compose.yml: Require PROVIDER to be explicitly set - docs: Update examples to be provider-agnostic This makes ZeroClaw truly provider-agnostic - users must choose their preferred LLM provider rather than being pushed toward OpenRouter. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1c0a3de4c0
commit
b313ce9bcb
@ -12,10 +12,11 @@
|
||||
API_KEY=your-api-key-here
|
||||
# ZEROCLAW_API_KEY=your-api-key-here
|
||||
|
||||
# Default provider/model (can be overridden by CLI flags)
|
||||
PROVIDER=openrouter
|
||||
# ZEROCLAW_PROVIDER=openrouter
|
||||
# ZEROCLAW_MODEL=anthropic/claude-sonnet-4-6
|
||||
# Default provider/model (required - choose one)
|
||||
# Options: openrouter, openai, anthropic, gemini, ollama, groq, mistral, deepseek, xai, and more
|
||||
# PROVIDER=your-provider-here
|
||||
# ZEROCLAW_PROVIDER=your-provider-here
|
||||
# ZEROCLAW_MODEL=your-model-here
|
||||
# ZEROCLAW_TEMPERATURE=0.7
|
||||
|
||||
# Workspace directory override
|
||||
|
||||
@ -21,9 +21,9 @@ services:
|
||||
# Or use the prefixed version:
|
||||
# - ZEROCLAW_API_KEY=${ZEROCLAW_API_KEY:-}
|
||||
|
||||
# Optional: LLM provider (default: openrouter)
|
||||
# Options: openrouter, openai, anthropic, ollama
|
||||
- PROVIDER=${PROVIDER:-openrouter}
|
||||
# Required: LLM provider (must be set)
|
||||
# Options: openrouter, openai, anthropic, gemini, ollama, groq, mistral, deepseek, xai
|
||||
- PROVIDER=${PROVIDER:?PROVIDER is required - set to openrouter, openai, anthropic, gemini, ollama, etc}
|
||||
|
||||
# Allow public bind inside Docker (required for container networking)
|
||||
- ZEROCLAW_ALLOW_PUBLIC_BIND=true
|
||||
|
||||
@ -14,16 +14,18 @@ For first-time setup and quick orientation.
|
||||
|
||||
| Scenario | Command |
|
||||
|----------|---------|
|
||||
| I have an API key, want fastest setup | `zeroclaw onboard --api-key sk-... --provider openrouter` |
|
||||
| I want guided prompts | `zeroclaw onboard --interactive` |
|
||||
| I have an API key, want fastest setup | `zeroclaw onboard --api-key sk-... --provider <provider>` |
|
||||
| I want guided prompts (TUI wizard) | `zeroclaw onboard --interactive` |
|
||||
| Config exists, just fix channels | `zeroclaw onboard --channels-only` |
|
||||
| Config exists, I intentionally want full overwrite | `zeroclaw onboard --force` |
|
||||
| Using OpenAI Codex subscription auth | See [OpenAI Codex OAuth Quick Setup](#openai-codex-oauth-quick-setup) |
|
||||
|
||||
**Supported providers:** openrouter, openai, anthropic, gemini, ollama, groq, mistral, deepseek, xai, together-ai, fireworks, perplexity, cohere, and more.
|
||||
|
||||
## Onboarding and Validation
|
||||
|
||||
- Quick onboarding: `zeroclaw onboard --api-key "sk-..." --provider openrouter`
|
||||
- Interactive onboarding: `zeroclaw onboard --interactive`
|
||||
- Quick onboarding: `zeroclaw onboard --api-key "sk-..." --provider <provider>`
|
||||
- Interactive onboarding (recommended): `zeroclaw onboard --interactive`
|
||||
- Existing config protection: reruns require explicit confirmation (or `--force` in non-interactive flows)
|
||||
- Ollama cloud models (`:cloud`) require a remote `api_url` and API key (for example `api_url = "https://ollama.com"`).
|
||||
- Validate environment: `zeroclaw status` + `zeroclaw doctor`
|
||||
|
||||
@ -119,22 +119,24 @@ it pulls `ghcr.io/zeroclaw-labs/zeroclaw:latest` and tags it locally before runn
|
||||
### Quick onboarding (non-interactive)
|
||||
|
||||
```bash
|
||||
./bootstrap.sh --onboard --api-key "sk-..." --provider openrouter
|
||||
# Replace <provider> with your choice: openrouter, openai, anthropic, gemini, ollama, etc.
|
||||
./install.sh --onboard --api-key "sk-..." --provider <provider>
|
||||
```
|
||||
|
||||
Or with environment variables:
|
||||
|
||||
```bash
|
||||
ZEROCLAW_API_KEY="sk-..." ZEROCLAW_PROVIDER="openrouter" ./bootstrap.sh --onboard
|
||||
# Replace <provider> with your choice
|
||||
ZEROCLAW_API_KEY="sk-..." ZEROCLAW_PROVIDER="<provider>" ./install.sh --onboard
|
||||
```
|
||||
|
||||
### Interactive onboarding
|
||||
|
||||
```bash
|
||||
./bootstrap.sh --interactive-onboard
|
||||
./install.sh --interactive-onboard
|
||||
```
|
||||
|
||||
This launches the full-screen TUI onboarding flow (`zeroclaw onboard --interactive-ui`).
|
||||
This launches the full-screen TUI onboarding flow (`zeroclaw onboard --interactive`) where you can select your provider interactively.
|
||||
|
||||
## Useful flags
|
||||
|
||||
|
||||
@ -26,7 +26,7 @@ use std::io::{self, IsTerminal};
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
const PROVIDER_OPTIONS: [&str; 5] = ["openrouter", "openai", "anthropic", "gemini", "ollama"];
|
||||
const PROVIDER_OPTIONS: [&str; 6] = ["(select provider)", "openrouter", "openai", "anthropic", "gemini", "ollama"];
|
||||
const MEMORY_OPTIONS: [&str; 4] = ["sqlite", "lucid", "markdown", "none"];
|
||||
const TUNNEL_OPTIONS: [&str; 3] = ["none", "cloudflare", "ngrok"];
|
||||
|
||||
@ -228,13 +228,12 @@ struct TuiOnboardPlan {
|
||||
|
||||
impl TuiOnboardPlan {
|
||||
fn new(default_workspace: PathBuf, force: bool) -> Self {
|
||||
let provider = PROVIDER_OPTIONS[0];
|
||||
Self {
|
||||
workspace_path: default_workspace.display().to_string(),
|
||||
force_overwrite: force,
|
||||
provider_idx: 0,
|
||||
provider_idx: 0, // Start with placeholder - user must select
|
||||
api_key: String::new(),
|
||||
model: provider_default_model(provider),
|
||||
model: String::new(), // No default model until provider is selected
|
||||
memory_idx: 0,
|
||||
disable_totp: false,
|
||||
enable_telegram: false,
|
||||
@ -254,7 +253,16 @@ impl TuiOnboardPlan {
|
||||
}
|
||||
|
||||
fn provider(&self) -> &str {
|
||||
PROVIDER_OPTIONS[self.provider_idx]
|
||||
let idx = self.provider_idx;
|
||||
if idx == 0 {
|
||||
"(select provider)"
|
||||
} else {
|
||||
PROVIDER_OPTIONS[idx]
|
||||
}
|
||||
}
|
||||
|
||||
fn provider_selected(&self) -> bool {
|
||||
self.provider_idx > 0
|
||||
}
|
||||
|
||||
fn memory_backend(&self) -> &str {
|
||||
@ -765,6 +773,9 @@ impl TuiState {
|
||||
Ok(())
|
||||
}
|
||||
Step::Provider => {
|
||||
if !self.plan.provider_selected() {
|
||||
bail!("Provider selection is required. Use arrow keys to select a provider.")
|
||||
}
|
||||
if self.plan.model.trim().is_empty() {
|
||||
bail!("Default model is required")
|
||||
}
|
||||
|
||||
@ -477,8 +477,8 @@ fn apply_provider_update(
|
||||
// ── Quick setup (zero prompts) ───────────────────────────────────
|
||||
|
||||
/// Non-interactive setup: generates a sensible default config instantly.
|
||||
/// Use `zeroclaw onboard` or `zeroclaw onboard --api-key sk-... --provider openrouter --memory sqlite|lucid|cortex-mem`.
|
||||
/// Use `zeroclaw onboard --interactive` for the full wizard.
|
||||
/// Use `zeroclaw onboard --api-key sk-... --provider <provider> --memory sqlite|lucid|cortex-mem`.
|
||||
/// Use `zeroclaw onboard --interactive` for the full wizard with provider selection.
|
||||
fn backend_key_from_choice(choice: usize) -> &'static str {
|
||||
selectable_memory_backends()
|
||||
.get(choice)
|
||||
@ -741,7 +741,12 @@ async fn run_quick_setup_with_home(
|
||||
.await
|
||||
.context("Failed to create workspace directory")?;
|
||||
|
||||
let provider_name = provider.unwrap_or("openrouter").to_string();
|
||||
let provider_name = provider
|
||||
.ok_or_else(|| anyhow::anyhow!(
|
||||
"Provider is required. Use --provider <name> or run with --interactive for the TUI wizard.\n\
|
||||
\nSupported providers: openrouter, openai, anthropic, gemini, ollama, groq, mistral, deepseek, xai, and more."
|
||||
))?
|
||||
.to_string();
|
||||
let model = model_override
|
||||
.map(str::to_string)
|
||||
.unwrap_or_else(|| default_model_for_provider(&provider_name));
|
||||
@ -2200,11 +2205,15 @@ pub async fn run_models_refresh(
|
||||
provider_override: Option<&str>,
|
||||
force: bool,
|
||||
) -> Result<()> {
|
||||
let provider_name = provider_override
|
||||
let provider_name = match provider_override
|
||||
.or(config.default_provider.as_deref())
|
||||
.unwrap_or("openrouter")
|
||||
.trim()
|
||||
.to_string();
|
||||
{
|
||||
Some(p) => p.trim().to_string(),
|
||||
None => anyhow::bail!(
|
||||
"No provider configured. Run `zeroclaw onboard --interactive` or set a provider with `--provider <name>`.\n\
|
||||
\nSupported providers: openrouter, openai, anthropic, gemini, ollama, groq, mistral, deepseek, xai, and more."
|
||||
),
|
||||
};
|
||||
|
||||
if provider_name.is_empty() {
|
||||
anyhow::bail!("Provider name cannot be empty");
|
||||
@ -2284,11 +2293,17 @@ pub async fn run_models_refresh(
|
||||
}
|
||||
|
||||
pub async fn run_models_list(config: &Config, provider_override: Option<&str>) -> Result<()> {
|
||||
let provider_name = provider_override
|
||||
let provider_name = match provider_override
|
||||
.or(config.default_provider.as_deref())
|
||||
.unwrap_or("openrouter");
|
||||
{
|
||||
Some(p) => p.to_string(),
|
||||
None => anyhow::bail!(
|
||||
"No provider configured. Run `zeroclaw onboard --interactive` or specify `--provider <name>`.\n\
|
||||
\nSupported providers: openrouter, openai, anthropic, gemini, ollama, groq, mistral, deepseek, xai, and more."
|
||||
),
|
||||
};
|
||||
|
||||
let cached = load_any_cached_models_for_provider(&config.workspace_dir, provider_name).await?;
|
||||
let cached = load_any_cached_models_for_provider(&config.workspace_dir, &provider_name).await?;
|
||||
|
||||
let Some(cached) = cached else {
|
||||
println!();
|
||||
@ -2336,7 +2351,7 @@ pub async fn run_models_set(config: &Config, model: &str) -> Result<()> {
|
||||
}
|
||||
|
||||
pub async fn run_models_status(config: &Config) -> Result<()> {
|
||||
let provider = config.default_provider.as_deref().unwrap_or("openrouter");
|
||||
let provider = config.default_provider.as_deref().unwrap_or("(not set)");
|
||||
let model = config.default_model.as_deref().unwrap_or("(not set)");
|
||||
|
||||
println!();
|
||||
@ -6794,7 +6809,7 @@ fn print_summary(config: &Config) {
|
||||
println!(
|
||||
" {} Provider: {}",
|
||||
style("🤖").cyan(),
|
||||
config.default_provider.as_deref().unwrap_or("openrouter")
|
||||
config.default_provider.as_deref().unwrap_or("(not set)")
|
||||
);
|
||||
println!(
|
||||
" {} Model: {}",
|
||||
@ -6914,57 +6929,59 @@ fn print_summary(config: &Config) {
|
||||
|
||||
let mut step = 1u8;
|
||||
|
||||
let provider = config.default_provider.as_deref().unwrap_or("openrouter");
|
||||
let canonical_provider = canonical_provider_name(provider);
|
||||
if config.api_key.is_none() && !provider_supports_keyless_local_usage(provider) {
|
||||
if canonical_provider == "copilot" {
|
||||
println!(
|
||||
" {} Authenticate GitHub Copilot:",
|
||||
style(format!("{step}.")).cyan().bold()
|
||||
);
|
||||
println!(" {}", style("zeroclaw agent -m \"Hello!\"").yellow());
|
||||
println!(
|
||||
" {}",
|
||||
style("(device/OAuth prompt appears automatically on first run)").dim()
|
||||
);
|
||||
} else if canonical_provider == "openai-codex" {
|
||||
println!(
|
||||
" {} Authenticate OpenAI Codex:",
|
||||
style(format!("{step}.")).cyan().bold()
|
||||
);
|
||||
println!(
|
||||
" {}",
|
||||
style("zeroclaw auth login --provider openai-codex --device-code").yellow()
|
||||
);
|
||||
} else if canonical_provider == "anthropic" {
|
||||
println!(
|
||||
" {} Configure Anthropic auth:",
|
||||
style(format!("{step}.")).cyan().bold()
|
||||
);
|
||||
println!(
|
||||
" {}",
|
||||
style("export ANTHROPIC_API_KEY=\"sk-ant-...\"").yellow()
|
||||
);
|
||||
println!(
|
||||
" {}",
|
||||
style(
|
||||
"or: zeroclaw auth paste-token --provider anthropic --auth-kind authorization"
|
||||
)
|
||||
.yellow()
|
||||
);
|
||||
} else {
|
||||
let env_var = provider_env_var(provider);
|
||||
println!(
|
||||
" {} Set your API key:",
|
||||
style(format!("{step}.")).cyan().bold()
|
||||
);
|
||||
println!(
|
||||
" {}",
|
||||
style(format!("export {env_var}=\"sk-...\"")).yellow()
|
||||
);
|
||||
let provider = config.default_provider.as_deref();
|
||||
let canonical_provider = provider.map(canonical_provider_name);
|
||||
if let (Some(p), Some(cp)) = (provider, canonical_provider) {
|
||||
if config.api_key.is_none() && !provider_supports_keyless_local_usage(p) {
|
||||
if cp == "copilot" {
|
||||
println!(
|
||||
" {} Authenticate GitHub Copilot:",
|
||||
style(format!("{step}.")).cyan().bold()
|
||||
);
|
||||
println!(" {}", style("zeroclaw agent -m \"Hello!\"").yellow());
|
||||
println!(
|
||||
" {}",
|
||||
style("(device/OAuth prompt appears automatically on first run)").dim()
|
||||
);
|
||||
} else if cp == "openai-codex" {
|
||||
println!(
|
||||
" {} Authenticate OpenAI Codex:",
|
||||
style(format!("{step}.")).cyan().bold()
|
||||
);
|
||||
println!(
|
||||
" {}",
|
||||
style("zeroclaw auth login --provider openai-codex --device-code").yellow()
|
||||
);
|
||||
} else if cp == "anthropic" {
|
||||
println!(
|
||||
" {} Configure Anthropic auth:",
|
||||
style(format!("{step}.")).cyan().bold()
|
||||
);
|
||||
println!(
|
||||
" {}",
|
||||
style("export ANTHROPIC_API_KEY=\"sk-ant-...\"").yellow()
|
||||
);
|
||||
println!(
|
||||
" {}",
|
||||
style(
|
||||
"or: zeroclaw auth paste-token --provider anthropic --auth-kind authorization"
|
||||
)
|
||||
.yellow()
|
||||
);
|
||||
} else {
|
||||
let env_var = provider_env_var(p);
|
||||
println!(
|
||||
" {} Set your API key:",
|
||||
style(format!("{step}.")).cyan().bold()
|
||||
);
|
||||
println!(
|
||||
" {}",
|
||||
style(format!("export {env_var}=\"sk-...\"")).yellow()
|
||||
);
|
||||
}
|
||||
println!();
|
||||
step += 1;
|
||||
}
|
||||
println!();
|
||||
step += 1;
|
||||
}
|
||||
|
||||
// If channels are configured, show channel start as the primary next step
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { LogOut, Menu, PanelLeftClose, PanelLeftOpen } from 'lucide-react';
|
||||
import { LogOut, Menu } from 'lucide-react';
|
||||
import { t, LANGUAGE_BUTTON_LABELS, LANGUAGE_SWITCH_ORDER } from '@/lib/i18n';
|
||||
import { useLocaleContext } from '@/App';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
@ -21,16 +21,10 @@ const routeTitles: Record<string, string> = {
|
||||
const languageSummary = 'English · 简体中文 · 日本語 · Русский · Français · Tiếng Việt · Ελληνικά';
|
||||
|
||||
interface HeaderProps {
|
||||
isSidebarCollapsed: boolean;
|
||||
onToggleSidebar: () => void;
|
||||
onToggleSidebarCollapse: () => void;
|
||||
}
|
||||
|
||||
export default function Header({
|
||||
isSidebarCollapsed,
|
||||
onToggleSidebar,
|
||||
onToggleSidebarCollapse,
|
||||
}: HeaderProps) {
|
||||
export default function Header({ onToggleSidebar }: HeaderProps) {
|
||||
const location = useLocation();
|
||||
const { logout } = useAuth();
|
||||
const { locale, setAppLocale } = useLocaleContext();
|
||||
@ -64,22 +58,12 @@ export default function Header({
|
||||
{pageTitle}
|
||||
</h1>
|
||||
<p className="hidden text-[10px] uppercase tracking-[0.16em] text-[#7ea5eb] sm:block">
|
||||
Electric dashboard
|
||||
ZeroClaw dashboard
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative flex w-full items-center justify-end gap-1.5 sm:gap-2 md:w-auto md:gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleSidebarCollapse}
|
||||
className="hidden items-center gap-1 rounded-lg border border-[#2b4f97] bg-[#091937]/75 px-2.5 py-1.5 text-xs text-[#c4d8ff] transition hover:border-[#4f83ff] hover:text-white md:flex md:text-sm"
|
||||
title={isSidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||
>
|
||||
{isSidebarCollapsed ? <PanelLeftOpen className="h-4 w-4" /> : <PanelLeftClose className="h-4 w-4" />}
|
||||
<span>{isSidebarCollapsed ? 'Expand' : 'Collapse'}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleLanguage}
|
||||
|
||||
@ -39,11 +39,7 @@ export default function Layout() {
|
||||
sidebarCollapsed ? 'md:ml-[6.25rem]' : 'md:ml-[17.5rem]',
|
||||
].join(' ')}
|
||||
>
|
||||
<Header
|
||||
isSidebarCollapsed={sidebarCollapsed}
|
||||
onToggleSidebar={() => setSidebarOpen((open) => !open)}
|
||||
onToggleSidebarCollapse={toggleSidebarCollapsed}
|
||||
/>
|
||||
<Header onToggleSidebar={() => setSidebarOpen((open) => !open)} />
|
||||
|
||||
<main className="flex-1 overflow-y-auto px-4 pb-8 pt-5 md:px-8 md:pt-8">
|
||||
<Outlet />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user