From cc6fc6ce8d36b180df4eb53d9dd1a8fa654e8106 Mon Sep 17 00:00:00 2001 From: argenis de la rosa Date: Fri, 13 Feb 2026 16:32:27 -0500 Subject: [PATCH] feat: BYOP provider + tunnel wizard + SVG architecture diagram MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Custom Provider (Bring Your Own): - Add custom:URL format to provider factory (any OpenAI-compatible API) - Works with LiteLLM, LocalAI, vLLM, text-generation-webui, LM Studio, etc. - Example: default_provider = 'custom:http://localhost:1234' - 4 new tests for custom provider (URL, localhost, no-key, empty-URL error) Setup Wizard (6 steps, 5-year-old friendly): - Add '๐Ÿ”ง Custom' tier to provider selection with guided BYOP flow - Add Step 4: Tunnel setup (Cloudflare, Tailscale, ngrok, Custom, or skip) - Emoji labels on all provider categories for visual clarity - Renumber wizard to 6 steps (was 5) Architecture Diagram: - New SVG diagram at docs/architecture.svg (dark theme, color-coded) - Shows: Chat Apps โ†’ Security โ†’ Agent Loop โ†’ AI Providers - Shows: Tunnel layer, Sandbox, Context, Heartbeat/Cron - Shows: Setup Wizard 6-step flow at bottom - Replace ASCII art in README with SVG embed 657 tests passing, 0 clippy warnings, cargo fmt clean --- README.md | 53 +-------- docs/architecture.svg | 249 ++++++++++++++++++++++++++++++++++++++++++ src/onboard/wizard.rs | 225 ++++++++++++++++++++++++++++++++++++-- src/providers/mod.rs | 49 ++++++++- 4 files changed, 516 insertions(+), 60 deletions(-) create mode 100644 docs/architecture.svg diff --git a/README.md b/README.md index 7cbc822..efc06de 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ The fastest, smallest, fully autonomous AI assistant โ€” deploy anywhere, swap anything. ``` -~3MB binary ยท <10ms startup ยท 649 tests ยท 22 providers ยท Pluggable everything +~3MB binary ยท <10ms startup ยท 657 tests ยท 22+ providers ยท Pluggable everything ``` ## Quick Start @@ -54,54 +54,13 @@ cargo run --release -- tools test memory_recall '{"query": "Rust"}' Every subsystem is a **trait** โ€” swap implementations with a config change, zero code changes. -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ ZeroClaw Architecture โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”‚ โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ Chat Apps โ”‚ โ”‚ Security Layer โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ Telegram โ”€โ”€โ”€โ”ค โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ -โ”‚ โ”‚ Discord โ”€โ”€โ”€โ”ค โ”‚ โ”‚ Auth Gate โ”‚ โ”‚ Rate Limiter โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ Slack โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ–บโ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ iMessage โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ€ข allowed_ โ”‚ โ”‚ โ€ข sliding window โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ Matrix โ”€โ”€โ”€โ”ค โ”‚ โ”‚ users โ”‚ โ”‚ โ€ข max actions/hr โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ CLI โ”€โ”€โ”€โ”ค โ”‚ โ”‚ โ€ข webhook โ”‚ โ”‚ โ€ข max cost/day โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ Webhook โ”€โ”€โ”€โ”ค โ”‚ โ”‚ secret โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ”‚ โ–ผ โ–ผ โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ Agent Loop โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ Message โ”€โ”€โ–บ LLM โ”€โ”€โ–บ Tools โ”€โ”€โ–บ Reply โ”‚ โ”‚ -โ”‚ โ”‚ โ–ฒ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ–ผ โ”‚ โ”‚ -โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ Context โ”‚ โ”‚ Sandbox โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ€ข Memory โ”‚ โ”‚ โ€ข allowlist โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ€ข Skills โ”‚ โ”‚ โ€ข path jail โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ€ข Workspace โ”‚ โ”‚ โ€ข forbidden โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ MD files โ”‚ โ”‚ paths โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ”‚ โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ AI Providers (22) โ”‚ โ”‚ -โ”‚ โ”‚ OpenRouter ยท Anthropic ยท OpenAI ยท Mistral ยท Groq ยท Venice โ”‚ โ”‚ -โ”‚ โ”‚ Ollama ยท xAI ยท DeepSeek ยท Cerebras ยท Fireworks ยท Together โ”‚ โ”‚ -โ”‚ โ”‚ Cloudflare ยท Moonshot ยท GLM ยท MiniMax ยท Qianfan ยท + more โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` +

+ ZeroClaw Architecture +

| Subsystem | Trait | Ships with | Extend | |-----------|-------|------------|--------| -| **AI Models** | `Provider` | 22 providers (OpenRouter, Anthropic, OpenAI, Venice, Groq, Mistral, etc.) | Any OpenAI-compatible API | +| **AI Models** | `Provider` | 22+ providers (OpenRouter, Anthropic, OpenAI, Venice, Groq, Mistral, etc.) | `custom:https://your-api.com` โ€” any OpenAI-compatible API | | **Channels** | `Channel` | CLI, Telegram, Discord, Slack, iMessage, Matrix, Webhook | Any messaging API | | **Memory** | `Memory` | SQLite (default), Markdown | Any persistence | | **Tools** | `Tool` | shell, file_read, file_write, memory_store, memory_recall, memory_forget | Any capability | @@ -342,7 +301,7 @@ interval_minutes = 30 ```bash cargo build # Dev build cargo build --release # Release build (~3MB) -cargo test # 649 tests +cargo test # 657 tests cargo clippy # Lint (0 warnings) # Run the SQLite vs Markdown benchmark diff --git a/docs/architecture.svg b/docs/architecture.svg new file mode 100644 index 0000000..8dcfb77 --- /dev/null +++ b/docs/architecture.svg @@ -0,0 +1,249 @@ + + + + + + + + + + + + + + + + + + + + ZeroClaw Architecture + Zero overhead. Zero compromise. 100% Rust. 100% Agnostic. + + + + Chat Apps + + + + Telegram + + + Discord + + + Slack + + + iMessage + + + Matrix + + + Webhook + + + CLI + + + any Channel trait + + + + + + + + Security Layer + + + + Auth Gate + allowed_users + webhook_secret + + + + Rate Limiter + sliding window + max actions/hr & cost/day + + + + + + + + Agent Loop + + + + Message + ๐Ÿ’ฌ + + + + LLM + ๐Ÿค– + + + + Tools + ๐Ÿ› ๏ธ + + + + Response + ๐Ÿ’ฌ + + + + + + + + + + Context + Memory (SQLite/MD) + Skills & Workspace + IDENTITY.md / AGENTS.md + + + + Sandbox + Command allowlist + Path jail & traversal block + Autonomy levels + + + + Heartbeat & Cron + HEARTBEAT.md tasks + Scheduled actions + Periodic check-ins + + + + Agnostic Tunnel + Bring Your Own + + + Cloudflare + + + Tailscale + + + ngrok + + + Custom (bore, frp, ssh...) + + + any Tunnel trait + + + + + + + + AI Providers (22+) + Any OpenAI-compatible API + + + + OpenRouter + + + Anthropic + + + OpenAI + + + Venice + + + Groq + + + Mistral + + + DeepSeek + + + xAI / Grok + + + Ollama + + + Together AI + + + Fireworks + + + Perplexity + + + Cloudflare AI + + + Cohere + + + Custom + + + any Provider trait / custom:URL + + + + + + + Setup Wizard โ€” zeroclaw onboard + 6 steps, under 60 seconds, so easy a 5-year-old can do it + + + + 1. Workspace + ~/.zeroclaw/ + + + 2. AI Provider + 22+ or custom URL + + + 3. Channels + 7 channels + test + + + 4. Tunnel + 5 providers or skip + + + 5. Personalize + name, style, timezone + + + 6. Scaffold + workspace MD files + + + + + + + + + + + + โœ… Ready โ€” zeroclaw agent + + + ~3MB binary ยท <10ms startup ยท 657 tests ยท 22+ providers ยท 7 channels ยท 5 tunnels ยท Pluggable everything + diff --git a/src/onboard/wizard.rs b/src/onboard/wizard.rs index eaef76b..679957c 100644 --- a/src/onboard/wizard.rs +++ b/src/onboard/wizard.rs @@ -55,19 +55,22 @@ pub fn run_wizard() -> Result { ); println!(); - print_step(1, 5, "Workspace Setup"); + print_step(1, 6, "Workspace Setup"); let (workspace_dir, config_path) = setup_workspace()?; - print_step(2, 5, "AI Provider & API Key"); + print_step(2, 6, "AI Provider & API Key"); let (provider, api_key, model) = setup_provider()?; - print_step(3, 5, "Channels (How You Talk to ZeroClaw)"); + print_step(3, 6, "Channels (How You Talk to ZeroClaw)"); let channels_config = setup_channels()?; - print_step(4, 5, "Project Context (Personalize Your Agent)"); + print_step(4, 6, "Tunnel (Expose to Internet)"); + let tunnel_config = setup_tunnel()?; + + print_step(5, 6, "Project Context (Personalize Your Agent)"); let project_ctx = setup_project_context()?; - print_step(5, 5, "Workspace Files"); + print_step(6, 6, "Workspace Files"); scaffold_workspace(&workspace_dir, &project_ctx)?; // โ”€โ”€ Build config โ”€โ”€ @@ -93,7 +96,7 @@ pub fn run_wizard() -> Result { heartbeat: HeartbeatConfig::default(), channels_config, memory: MemoryConfig::default(), // SQLite + auto-save by default - tunnel: crate::config::TunnelConfig::default(), + tunnel: tunnel_config, }; println!( @@ -208,11 +211,12 @@ fn setup_workspace() -> Result<(PathBuf, PathBuf)> { fn setup_provider() -> Result<(String, String, String)> { // โ”€โ”€ Tier selection โ”€โ”€ let tiers = vec![ - "Recommended (OpenRouter, Venice, Anthropic, OpenAI)", - "Fast inference (Groq, Fireworks, Together AI)", - "Gateway / proxy (Vercel AI, Cloudflare AI, Amazon Bedrock)", - "Specialized (Moonshot/Kimi, GLM/Zhipu, MiniMax, Qianfan, Z.AI, Synthetic, OpenCode Zen, Cohere)", - "Local / private (Ollama โ€” no API key needed)", + "โญ Recommended (OpenRouter, Venice, Anthropic, OpenAI)", + "โšก Fast inference (Groq, Fireworks, Together AI)", + "๐ŸŒ Gateway / proxy (Vercel AI, Cloudflare AI, Amazon Bedrock)", + "๐Ÿ”ฌ Specialized (Moonshot/Kimi, GLM/Zhipu, MiniMax, Qianfan, Z.AI, Synthetic, OpenCode Zen, Cohere)", + "๐Ÿ  Local / private (Ollama โ€” no API key needed)", + "๐Ÿ”ง Custom โ€” bring your own OpenAI-compatible API", ]; let tier_idx = Select::new() @@ -255,9 +259,53 @@ fn setup_provider() -> Result<(String, String, String)> { ("opencode", "OpenCode Zen โ€” code-focused AI"), ("cohere", "Cohere โ€” Command R+ & embeddings"), ], - _ => vec![("ollama", "Ollama โ€” local models (Llama, Mistral, Phi)")], + 4 => vec![("ollama", "Ollama โ€” local models (Llama, Mistral, Phi)")], + _ => vec![], // Custom โ€” handled below }; + // โ”€โ”€ Custom / BYOP flow โ”€โ”€ + if providers.is_empty() { + println!(); + println!( + " {} {}", + style("Custom Provider Setup").white().bold(), + style("โ€” any OpenAI-compatible API").dim() + ); + print_bullet("ZeroClaw works with ANY API that speaks the OpenAI chat completions format."); + print_bullet("Examples: LiteLLM, LocalAI, vLLM, text-generation-webui, LM Studio, etc."); + println!(); + + let base_url: String = Input::new() + .with_prompt(" API base URL (e.g. http://localhost:1234 or https://my-api.com)") + .interact_text()?; + + let base_url = base_url.trim().trim_end_matches('/').to_string(); + if base_url.is_empty() { + anyhow::bail!("Custom provider requires a base URL."); + } + + let api_key: String = Input::new() + .with_prompt(" API key (or Enter to skip if not needed)") + .allow_empty(true) + .interact_text()?; + + let model: String = Input::new() + .with_prompt(" Model name (e.g. llama3, gpt-4o, mistral)") + .default("default".into()) + .interact_text()?; + + let provider_name = format!("custom:{base_url}"); + + println!( + " {} Provider: {} | Model: {}", + style("โœ“").green().bold(), + style(&provider_name).green(), + style(&model).green() + ); + + return Ok((provider_name, api_key, model)); + } + let provider_labels: Vec<&str> = providers.iter().map(|(_, label)| *label).collect(); let provider_idx = Select::new() @@ -1055,6 +1103,159 @@ fn setup_channels() -> Result { Ok(config) } +// โ”€โ”€ Step 4: Tunnel โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +#[allow(clippy::too_many_lines)] +fn setup_tunnel() -> Result { + use crate::config::schema::{ + CloudflareTunnelConfig, CustomTunnelConfig, NgrokTunnelConfig, TailscaleTunnelConfig, + TunnelConfig, + }; + + print_bullet("A tunnel exposes your gateway to the internet securely."); + print_bullet("Skip this if you only use CLI or local channels."); + println!(); + + let options = vec![ + "Skip โ€” local only (default)", + "Cloudflare Tunnel โ€” Zero Trust, free tier", + "Tailscale โ€” private tailnet or public Funnel", + "ngrok โ€” instant public URLs", + "Custom โ€” bring your own (bore, frp, ssh, etc.)", + ]; + + let choice = Select::new() + .with_prompt(" Select tunnel provider") + .items(&options) + .default(0) + .interact()?; + + let config = match choice { + 1 => { + println!(); + print_bullet("Get your tunnel token from the Cloudflare Zero Trust dashboard."); + let token: String = Input::new() + .with_prompt(" Cloudflare tunnel token") + .interact_text()?; + if token.trim().is_empty() { + println!(" {} Skipped", style("โ†’").dim()); + TunnelConfig::default() + } else { + println!( + " {} Tunnel: {}", + style("โœ“").green().bold(), + style("Cloudflare").green() + ); + TunnelConfig { + provider: "cloudflare".into(), + cloudflare: Some(CloudflareTunnelConfig { token }), + ..TunnelConfig::default() + } + } + } + 2 => { + println!(); + print_bullet("Tailscale must be installed and authenticated (tailscale up)."); + let funnel = Confirm::new() + .with_prompt(" Use Funnel (public internet)? No = tailnet only") + .default(false) + .interact()?; + println!( + " {} Tunnel: {} ({})", + style("โœ“").green().bold(), + style("Tailscale").green(), + if funnel { + "Funnel โ€” public" + } else { + "Serve โ€” tailnet only" + } + ); + TunnelConfig { + provider: "tailscale".into(), + tailscale: Some(TailscaleTunnelConfig { + funnel, + hostname: None, + }), + ..TunnelConfig::default() + } + } + 3 => { + println!(); + print_bullet( + "Get your auth token at https://dashboard.ngrok.com/get-started/your-authtoken", + ); + let auth_token: String = Input::new() + .with_prompt(" ngrok auth token") + .interact_text()?; + if auth_token.trim().is_empty() { + println!(" {} Skipped", style("โ†’").dim()); + TunnelConfig::default() + } else { + let domain: String = Input::new() + .with_prompt(" Custom domain (optional, Enter to skip)") + .allow_empty(true) + .interact_text()?; + println!( + " {} Tunnel: {}", + style("โœ“").green().bold(), + style("ngrok").green() + ); + TunnelConfig { + provider: "ngrok".into(), + ngrok: Some(NgrokTunnelConfig { + auth_token, + domain: if domain.is_empty() { + None + } else { + Some(domain) + }, + }), + ..TunnelConfig::default() + } + } + } + 4 => { + println!(); + print_bullet("Enter the command to start your tunnel."); + print_bullet("Use {port} and {host} as placeholders."); + print_bullet("Example: bore local {port} --to bore.pub"); + let cmd: String = Input::new() + .with_prompt(" Start command") + .interact_text()?; + if cmd.trim().is_empty() { + println!(" {} Skipped", style("โ†’").dim()); + TunnelConfig::default() + } else { + println!( + " {} Tunnel: {} ({})", + style("โœ“").green().bold(), + style("Custom").green(), + style(&cmd).dim() + ); + TunnelConfig { + provider: "custom".into(), + custom: Some(CustomTunnelConfig { + start_command: cmd, + health_url: None, + url_pattern: None, + }), + ..TunnelConfig::default() + } + } + } + _ => { + println!( + " {} Tunnel: {}", + style("โœ“").green().bold(), + style("none (local only)").dim() + ); + TunnelConfig::default() + } + }; + + Ok(config) +} + // โ”€โ”€ Step 6: Scaffold workspace files โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ #[allow(clippy::too_many_lines)] diff --git a/src/providers/mod.rs b/src/providers/mod.rs index 436984e..8828a18 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -88,8 +88,24 @@ pub fn create_provider(name: &str, api_key: Option<&str>) -> anyhow::Result { + let base_url = name.strip_prefix("custom:").unwrap_or(""); + if base_url.is_empty() { + anyhow::bail!("Custom provider requires a URL. Format: custom:https://your-api.com"); + } + Ok(Box::new(OpenAiCompatibleProvider::new( + "Custom", + base_url, + api_key, + AuthStyle::Bearer, + ))) + } + _ => anyhow::bail!( - "Unknown provider: {name}. Run `zeroclaw integrations list -c ai` to see all available providers." + "Unknown provider: {name}. Run `zeroclaw integrations list -c ai` to see all available providers.\n\ + Tip: Use \"custom:https://your-api.com\" for any OpenAI-compatible endpoint." ), } } @@ -231,6 +247,37 @@ mod tests { assert!(create_provider("cohere", Some("key")).is_ok()); } + // โ”€โ”€ Custom / BYOP provider โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + #[test] + fn factory_custom_url() { + let p = create_provider("custom:https://my-llm.example.com", Some("key")); + assert!(p.is_ok()); + } + + #[test] + fn factory_custom_localhost() { + let p = create_provider("custom:http://localhost:1234", Some("key")); + assert!(p.is_ok()); + } + + #[test] + fn factory_custom_no_key() { + let p = create_provider("custom:https://my-llm.example.com", None); + assert!(p.is_ok()); + } + + #[test] + fn factory_custom_empty_url_errors() { + match create_provider("custom:", None) { + Err(e) => assert!( + e.to_string().contains("requires a URL"), + "Expected 'requires a URL', got: {e}" + ), + Ok(_) => panic!("Expected error for empty custom URL"), + } + } + // โ”€โ”€ Error cases โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ #[test]