feat: BYOP provider + tunnel wizard + SVG architecture diagram

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
This commit is contained in:
argenis de la rosa 2026-02-13 16:32:27 -05:00
parent 390cbc0a6c
commit cc6fc6ce8d
4 changed files with 516 additions and 60 deletions

View file

@ -88,8 +88,24 @@ pub fn create_provider(name: &str, api_key: Option<&str>) -> anyhow::Result<Box<
"Cohere", "https://api.cohere.com/compatibility", api_key, AuthStyle::Bearer,
))),
// ── Bring Your Own Provider (custom URL) ───────────
// Format: "custom:https://your-api.com" or "custom:http://localhost:1234"
name if name.starts_with("custom:") => {
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]