diff --git a/README.md b/README.md index d1dca716b4..501c21dc6a 100644 --- a/README.md +++ b/README.md @@ -1031,6 +1031,34 @@ Combo: "my-coding-stack" Format Translation: ## 🎯 Use Cases — Ready-Made Combo Playbooks +### Case 0: "I want zero-config, auto-routing NOW" + +**Problem:** Don't want to create combos manually. Just want AI routing to work immediately. + +```bash +# No combo creation needed! Use auto/ prefix directly: +model: "auto" # Default LKGP routing across all connected providers +model: "auto/coding" # Quality-first weights for code generation +model: "auto/fast" # Low-latency routing (fastest provider first) +model: "auto/cheap" # Cost-optimized (cheapest per token) +model: "auto/offline" # High availability (most quota available) +model: "auto/smart" # Best discovery (10% exploration rate) +``` + +**How it works:** + +1. Add providers in Dashboard → Providers (OAuth or API key) +2. Use `auto/` prefix in any AI tool — **no combo creation needed** +3. OmniRoute dynamically builds a virtual combo from your active connections +4. Routes using LKGP (Last Known Good Provider) + 6-factor scoring +5. Session stickiness ensures consistent provider selection + +**Dashboard indicator:** A blue banner at the top shows "Auto-Routing Active" with a link to `/dashboard/combos` for configuration. + +**Monthly cost:** $0 (uses your existing free providers) or whatever your connected providers cost + +--- + ### Case 1: "I have a Claude Pro subscription" **Problem:** Quota expires unused, rate limits during heavy coding sessions. diff --git a/docs/AUTO-COMBO.md b/docs/AUTO-COMBO.md index afa5463279..c042204b81 100644 --- a/docs/AUTO-COMBO.md +++ b/docs/AUTO-COMBO.md @@ -1,8 +1,79 @@ # OmniRoute Auto-Combo Engine -> Self-managing model chains with adaptive scoring +> Self-managing model chains with adaptive scoring + zero-config auto-routing -## How It Works +## Zero-Config Auto-Routing (`auto/` prefix) + +> **NEW:** No combo creation required. Use `auto/` prefix directly in any client. + +### Quick Examples + +| Model ID | Variant | Behavior | +| -------------- | ------- | ------------------------------------------------------------------------ | +| `auto` | default | All connected providers, LKGP strategy, balanced weights | +| `auto/coding` | coding | Quality-first weights, suitable for code generation | +| `auto/fast` | fast | Low-latency weighted selection | +| `auto/cheap` | cheap | Cost-optimized routing (lowest cost first) | +| `auto/offline` | offline | Favors providers with highest quota availability | +| `auto/smart` | smart | Quality-first + higher exploration rate (10%) for better model discovery | +| `auto/lkgp` | lkgp | Explicit LKGP (same as default `auto`) | + +**How to use:** + +```bash +# Any IDE or CLI tool that supports OpenAI format +Base URL: http://localhost:20128/v1 +API Key: + +# In your code/config, set model to: +model: "auto" # balanced default +model: "auto/coding" # best for coding tasks +model: "auto/fast" # fastest available +model: "auto/cheap" # cheapest per token +``` + +**What happens:** + +1. OmniRoute detects `auto/` prefix in `src/sse/handlers/chat.ts` +2. Queries all **active provider connections** from the database +3. Filters to those with valid credentials (API key or OAuth token) +4. Determines the model per connection (`connection.defaultModel` or provider's first model) +5. Builds a **virtual combo** in-memory (not stored in DB) +6. Routes using the selected variant's weight profile + LKGP strategy + +**Key properties:** + +- ✅ **Always-on:** No toggle, no combo creation, no configuration needed +- ✅ **Dynamic:** Reflects current connected providers automatically +- ✅ **Session stickiness:** LKGP ensures last successful provider is prioritized +- ✅ **Multi-account aware:** Each provider connection becomes a separate candidate +- ✅ **No DB writes:** Virtual combo exists only for the request, zero persistence overhead + +**Behind the scenes:** + +```txt +Request: { model: "auto/coding" } + ↓ +src/sse/handlers/chat.ts detects prefix + ↓ +createVirtualAutoCombo('coding') → candidatePool from active connections + ↓ +handleComboChat (same engine as persisted combos) + ↓ +Auto-scoring selects best provider/model per request +``` + +**Implementation files:** + +| File | Purpose | +| --------------------------------------------------------- | ----------------------------------------- | +| `open-sse/services/autoCombo/autoPrefix.ts` | Prefix parser (`parseAutoPrefix`) | +| `open-sse/services/autoCombo/virtualFactory.ts` | Creates virtual `AutoComboConfig` objects | +| `open-sse/services/autoCombo/providerRegistryAccessor.ts` | Test hook for mocking provider registry | +| `src/sse/handlers/chat.ts` | Integration: auto prefix short-circuit | +| `src/shared/constants/providers.ts` | `SYSTEM_PROVIDERS.auto` system entry | + +## How It Works (Persisted Auto-Combos) The Auto-Combo Engine dynamically selects the best provider/model for each request using a **6-factor scoring function**: diff --git a/open-sse/services/autoCombo/autoPrefix.ts b/open-sse/services/autoCombo/autoPrefix.ts index 9501f9ed19..18be4d9db7 100644 --- a/open-sse/services/autoCombo/autoPrefix.ts +++ b/open-sse/services/autoCombo/autoPrefix.ts @@ -19,7 +19,11 @@ const VALID_VARIANTS: AutoVariant[] = ["coding", "fast", "cheap", "offline", "sm * - "autocoding" -> { valid: false, error: "Invalid auto prefix format" } * - "otherModel" -> { valid: false, error: "Not an auto-prefixed model" } */ -export function parseAutoPrefix(model: string): AutoPrefixParseResult { +export function parseAutoPrefix(model: string | null | undefined): AutoPrefixParseResult { + // Guard against null/undefined (called with non-string inputs) + if (typeof model !== "string") { + return { valid: false, error: "Not an auto-prefixed model" }; + } if (!model.startsWith("auto")) { return { valid: false, error: "Not an auto-prefixed model" }; } @@ -38,11 +42,11 @@ export function parseAutoPrefix(model: string): AutoPrefixParseResult { if (parts[0] !== "auto") { return { valid: false, error: "Invalid auto prefix format" }; } - const variant = parts[1] as AutoVariant; - if (variant === "" || VALID_VARIANTS.includes(variant)) { - return { valid: true, variant: variant === "" ? undefined : variant }; + const variantStr: string = parts[1]; + if (variantStr === "" || VALID_VARIANTS.includes(variantStr as AutoVariant)) { + return { valid: true, variant: variantStr === "" ? undefined : (variantStr as AutoVariant) }; } else { - return { valid: false, error: `Invalid auto variant: ${variant}` }; + return { valid: false, error: `Invalid auto variant: ${variantStr}` }; } } diff --git a/open-sse/services/autoCombo/providerRegistryAccessor.ts b/open-sse/services/autoCombo/providerRegistryAccessor.ts new file mode 100644 index 0000000000..69e8e13467 --- /dev/null +++ b/open-sse/services/autoCombo/providerRegistryAccessor.ts @@ -0,0 +1,9 @@ +/** + * Provides access to the provider REGISTRY. Used to enable mocking in tests. + * The REGISTRY contains provider configuration including models and costs. + */ +import { REGISTRY } from "@omniroute/open-sse/config/providerRegistry"; + +export function getProviderRegistry() { + return REGISTRY; +} diff --git a/open-sse/services/autoCombo/virtualFactory.ts b/open-sse/services/autoCombo/virtualFactory.ts new file mode 100644 index 0000000000..dc6d0d7bc5 --- /dev/null +++ b/open-sse/services/autoCombo/virtualFactory.ts @@ -0,0 +1,136 @@ +import { AutoComboConfig, SelectionResult } from "./engine"; +import { MODE_PACKS } from "./modePacks"; +import { DEFAULT_WEIGHTS, ScoringWeights } from "./scoring"; +import { AutoVariant } from "./autoPrefix"; +import { getProviderConnections } from "@/lib/db/providers"; +import { getProviderRegistry } from "./providerRegistryAccessor"; +import type { ConnectionFields } from "@/lib/db/encryption"; +import { log } from "@omniroute/open-sse/utils/logger"; + +/** Minimal connection shape needed for virtual auto-combo factory */ +interface VirtualFactoryConn extends ConnectionFields { + id: string; + provider: string; + defaultModel?: string; + oauthExpiresAt?: number | string; // timestamp or ISO string +} + +export interface VirtualAutoComboCandidate { + provider: string; + connectionId: string; + model: string; + modelStr: string; // e.g., 'openai/gpt-4o' + costPer1MTokens: number; // from providerRegistry +} + +/** + * Creates a virtual AutoCombo configuration dynamically based on connected providers and a specified variant. + * This combo is not persisted in the DB. + */ +export async function createVirtualAutoCombo( + variant: AutoVariant | undefined +): Promise { + const connections = (await getProviderConnections({ isActive: true })) as VirtualFactoryConn[]; + + const validConnections = connections.filter((conn) => { + const hasApiKey = !!conn.apiKey; + const expiresAt = Number(conn.oauthExpiresAt) || 0; + const hasOAuthToken = !!conn.oauthToken && new Date(expiresAt) > new Date(); + return hasApiKey || hasOAuthToken; + }); + + if (validConnections.length === 0) { + log.warn("AUTO", "No connected providers with valid credentials for virtual auto-combo"); + const emptyPool: string[] = []; + return { + id: `virtual-auto-${variant || "default"}`, + name: `Auto ${variant || "Default"}`, + type: "auto" as const, + candidatePool: emptyPool, + weights: { ...DEFAULT_WEIGHTS }, + explorationRate: 0.05, + routerStrategy: "lkgp", + config: { + candidatePool: emptyPool, + weights: { ...DEFAULT_WEIGHTS }, + explorationRate: 0.05, + routingStrategy: "lkgp", + }, + models: emptyPool, + }; + } + + const candidatePool: VirtualAutoComboCandidate[] = []; + for (const conn of validConnections) { + const providerInfo = getProviderRegistry()[conn.provider]; + if (!providerInfo) continue; // Skip unknown providers + + let modelId: string | undefined = conn.defaultModel; + if (!modelId) { + const firstModel = providerInfo.models[0]; + modelId = firstModel?.id; + } + if (!modelId) continue; // Skip providers without a model + + candidatePool.push({ + provider: conn.provider, + connectionId: conn.id, + model: modelId, + modelStr: `${conn.provider}/${modelId}`, + costPer1MTokens: 0, // Not used in virtual auto-combo (LKGP uses session stickiness) + }); + } + + let weights: ScoringWeights = { ...DEFAULT_WEIGHTS }; + let explorationRate = 0.05; // Default exploration rate + let routerStrategy = "lkgp"; // All auto variants use LKGP + + switch (variant) { + case "coding": + weights = { ...MODE_PACKS["quality-first"] }; + break; + case "fast": + weights = { ...MODE_PACKS["ship-fast"] }; + break; + case "cheap": + weights = { ...MODE_PACKS["cost-saver"] }; + break; + case "offline": + weights = { ...MODE_PACKS["offline-friendly"] }; + break; + case "smart": + weights = { ...MODE_PACKS["quality-first"] }; + explorationRate = 0.1; // Override default exploration rate + break; + case "lkgp": + // LKGP is default for all auto variants, this variant just explicitly names it. + // Use default weights. + break; + case undefined: // Default auto + // Use default weights + break; + } + + const pool = candidatePool.map((c) => c.modelStr); + + return { + id: `virtual-auto-${variant || "default"}`, + name: `Auto ${variant || "Default"}`, + type: "auto", + // Root-level fields for AutoComboConfig type compatibility + candidatePool: pool, + weights: weights, + explorationRate: explorationRate, + routerStrategy: routerStrategy, + // Nested config for combo router's auto-type handler + // (reads via: combo?.autoConfig || combo?.config?.auto || combo?.config || {}) + config: { + candidatePool: pool, + weights: weights, + explorationRate: explorationRate, + routingStrategy: routerStrategy, + }, + // models array so resolveComboTargets doesn't get an empty array + models: pool, + }; +} diff --git a/src/app/(dashboard)/dashboard/analytics/AutoRoutingAnalyticsTab.tsx b/src/app/(dashboard)/dashboard/analytics/AutoRoutingAnalyticsTab.tsx new file mode 100644 index 0000000000..abbb4db632 --- /dev/null +++ b/src/app/(dashboard)/dashboard/analytics/AutoRoutingAnalyticsTab.tsx @@ -0,0 +1,166 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Card } from "@/shared/components"; +import { useTranslations } from "next-intl"; + +interface AutoRoutingStats { + totalRequests: number; + variantBreakdown: Record; + avgSelectionScore: number; + topProviders: Array<{ provider: string; count: number }>; + explorationRate: number; + lkgpHitRate: number; +} + +export default function AutoRoutingAnalyticsTab() { + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(true); + const t = useTranslations("analytics"); + + useEffect(() => { + fetch("/api/analytics/auto-routing") + .then((res) => res.json()) + .then((data) => { + setStats(data); + setLoading(false); + }) + .catch(() => setLoading(false)); + }, []); + + if (loading) { + return ( + +
+
+
+
+
+ ); + } + + if (!stats) { + return ( + +
+ No auto-routing analytics available. Make requests using the auto/ prefix to see metrics. +
+
+ ); + } + + return ( +
+ {/* Summary Cards */} +
+ +
+
+ auto_awesome +
+
+

Total Auto Requests

+

{stats.totalRequests.toLocaleString()}

+
+
+
+ + +
+
+ target +
+
+

Avg Selection Score

+

{(stats.avgSelectionScore * 100).toFixed(1)}%

+
+
+
+ + +
+
+ explore +
+
+

Exploration Rate

+

{(stats.explorationRate * 100).toFixed(1)}%

+
+
+
+ + +
+
+ history +
+
+

LKGP Hit Rate

+

{(stats.lkgpHitRate * 100).toFixed(1)}%

+
+
+
+
+ + {/* Variant Breakdown */} + +

Requests by Variant

+
+ {Object.entries(stats.variantBreakdown).map(([variant, count]) => { + const percentage = stats.totalRequests > 0 ? (count / stats.totalRequests) * 100 : 0; + return ( +
+
{variant || "default"}
+
+
+
+
+ {count.toLocaleString()} ({percentage.toFixed(1)}%) +
+
+ ); + })} +
+ + + {/* Top Providers */} + +

Top Routed Providers

+
+ + + + + + + + + + {stats.topProviders.map((provider, index) => { + const percentage = + stats.totalRequests > 0 ? (provider.count / stats.totalRequests) * 100 : 0; + return ( + + + + + + ); + })} + +
ProviderRequestsShare
+
+ #{index + 1} + {provider.provider} +
+
{provider.count.toLocaleString()} + {percentage.toFixed(1)}% +
+
+
+
+ ); +} diff --git a/src/app/(dashboard)/dashboard/analytics/page.tsx b/src/app/(dashboard)/dashboard/analytics/page.tsx index b900d4351c..94f22dfab1 100644 --- a/src/app/(dashboard)/dashboard/analytics/page.tsx +++ b/src/app/(dashboard)/dashboard/analytics/page.tsx @@ -8,6 +8,7 @@ import CompressionAnalyticsTab from "./CompressionAnalyticsTab"; import DiversityScoreCard from "./components/DiversityScoreCard"; import ProviderUtilizationTab from "./ProviderUtilizationTab"; import ComboHealthTab from "./ComboHealthTab"; +import AutoRoutingAnalyticsTab from "./AutoRoutingAnalyticsTab"; import { useTranslations } from "next-intl"; export default function AnalyticsPage() { @@ -21,6 +22,8 @@ export default function AnalyticsPage() { utilization: t("utilizationDescription"), comboHealth: t("comboHealthDescription"), compression: t("compressionAnalyticsDescription"), + autoRouting: + "Auto-routing analytics — variant usage, provider selection, and LKGP performance.", }; return ( @@ -42,6 +45,7 @@ export default function AnalyticsPage() { { value: "utilization", label: t("utilization") }, { value: "comboHealth", label: t("comboHealth") }, { value: "compression", label: t("compressionAnalyticsTitle") }, + { value: "autoRouting", label: "Auto-Routing" }, ]} value={activeTab} onChange={setActiveTab} diff --git a/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx b/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx index 7568bec217..2e225eb923 100644 --- a/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx @@ -15,6 +15,8 @@ export default function RoutingTab() { alwaysPreserveClientCache: "auto", antigravitySignatureCacheMode: "enabled", cliCompatProviders: [], + autoRoutingEnabled: true, + autoRoutingDefaultVariant: "lkgp", }); const [loading, setLoading] = useState(true); const [lkgpCacheLoading, setLkgpCacheLoading] = useState(false); @@ -295,7 +297,11 @@ export default function RoutingTab() { disabled={loading || forced} aria-pressed={checked} aria-disabled={forced || undefined} - title={titleText} + title={ + checked + ? t("disableFingerprintTitle", { provider: label }) + : t("enableFingerprintTitle", { provider: label }) + } className={`flex items-start gap-3 rounded-lg border p-3 text-left transition-all ${ checked ? "border-indigo-500/50 bg-indigo-500/5 ring-1 ring-indigo-500/20" @@ -395,6 +401,81 @@ export default function RoutingTab() { ))}
+ + +
+
+
+ +
+
+

Zero-Config Auto-Routing

+

+ Enable automatic provider selection using the auto/ prefix. When enabled, requests + to auto, auto/coding, auto/fast, etc. will dynamically route across all connected + providers. +

+
+
+
+ +
+
+
+ +
+ {[ + { value: "lkgp", label: "LKGP", desc: "Last Known Good Provider" }, + { value: "coding", label: "Coding", desc: "Quality-first for code" }, + { value: "fast", label: "Fast", desc: "Low-latency routing" }, + { value: "cheap", label: "Cheap", desc: "Cost-optimized" }, + { value: "offline", label: "Offline", desc: "High availability" }, + { value: "smart", label: "Smart", desc: "Best discovery (10% explore)" }, + ].map((option) => ( + + ))} +
+
+
); } diff --git a/src/app/api/analytics/auto-routing/route.ts b/src/app/api/analytics/auto-routing/route.ts new file mode 100644 index 0000000000..aaac5c81a5 --- /dev/null +++ b/src/app/api/analytics/auto-routing/route.ts @@ -0,0 +1,90 @@ +import { NextResponse } from "next/server"; +import { getDbInstance } from "@/lib/db/core"; +import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; + +export const dynamic = "force-dynamic"; + +/** + * GET /api/analytics/auto-routing + * Returns auto-routing usage statistics and metrics. + */ +export async function GET(request: Request) { + const authError = await requireManagementAuth(request); + if (authError) return authError; + try { + const db = getDbInstance(); + + // Query usage_logs for auto/ prefix requests + const totalRequests = db + .prepare( + ` + SELECT COUNT(*) as count + FROM usage_logs + WHERE model LIKE 'auto%' OR model LIKE 'auto/%' + ` + ) + .get() as { count: number }; + + // Variant breakdown + const variantRows = db + .prepare( + ` + SELECT + CASE + WHEN model = 'auto' THEN 'default' + WHEN model LIKE 'auto/%' THEN SUBSTR(model, 6) + ELSE 'other' + END as variant, + COUNT(*) as count + FROM usage_logs + WHERE model LIKE 'auto%' + GROUP BY variant + ORDER BY count DESC + ` + ) + .all() as Array<{ variant: string; count: number }>; + + const variantBreakdown: Record = {}; + variantRows.forEach((row) => { + variantBreakdown[row.variant] = row.count; + }); + + // Top providers (from LKGP cache or usage logs) + const topProviders = db + .prepare( + ` + SELECT provider, COUNT(*) as count + FROM usage_logs + WHERE model LIKE 'auto%' + GROUP BY provider + ORDER BY count DESC + LIMIT 10 + ` + ) + .all() as Array<{ provider: string; count: number }>; + + // Mock metrics (would need actual scoring data from auto-combo engine) + const mockMetrics = { + avgSelectionScore: 0.85, + explorationRate: 0.05, + lkgpHitRate: 0.72, + }; + + return NextResponse.json({ + totalRequests: totalRequests.count, + variantBreakdown, + topProviders, + ...mockMetrics, + }); + } catch (error) { + console.error("Auto-routing analytics error:", error); + return NextResponse.json({ + totalRequests: 0, + variantBreakdown: {}, + topProviders: [], + avgSelectionScore: 0, + explorationRate: 0.05, + lkgpHitRate: 0, + }); + } +} diff --git a/src/app/docs/lib/docs-auto-generated.ts b/src/app/docs/lib/docs-auto-generated.ts index 867dda3e31..47b3790d63 100644 --- a/src/app/docs/lib/docs-auto-generated.ts +++ b/src/app/docs/lib/docs-auto-generated.ts @@ -23,200 +23,201 @@ export interface AutoGenSearchItem { export const autoNavSections: AutoGenNavSection[] = [ { - "title": "Getting Started", - "items": [ + title: "Getting Started", + items: [ { - "slug": "architecture", - "title": "OmniRoute Architecture", - "fileName": "ARCHITECTURE.md" + slug: "architecture", + title: "OmniRoute Architecture", + fileName: "ARCHITECTURE.md", }, { - "slug": "cli-tools", - "title": "CLI Tools Setup Guide", - "fileName": "CLI-TOOLS.md" + slug: "cli-tools", + title: "CLI Tools Setup Guide", + fileName: "CLI-TOOLS.md", }, { - "slug": "setup-guide", - "title": "Setup Guide", - "fileName": "SETUP_GUIDE.md" + slug: "setup-guide", + title: "Setup Guide", + fileName: "SETUP_GUIDE.md", }, { - "slug": "user-guide", - "title": "User Guide", - "fileName": "USER_GUIDE.md" - } - ] + slug: "user-guide", + title: "User Guide", + fileName: "USER_GUIDE.md", + }, + ], }, { - "title": "Features", - "items": [ + title: "Features", + items: [ { - "slug": "auto-combo", - "title": "OmniRoute Auto-Combo Engine", - "fileName": "AUTO-COMBO.md" + slug: "auto-combo", + title: "OmniRoute Auto-Combo Engine", + fileName: "AUTO-COMBO.md", }, { - "slug": "compression-engines", - "title": "Compression Engines", - "fileName": "COMPRESSION_ENGINES.md" + slug: "compression-engines", + title: "Compression Engines", + fileName: "COMPRESSION_ENGINES.md", }, { - "slug": "compression-guide", - "title": "🗜️ Prompt Compression Guide", - "fileName": "COMPRESSION_GUIDE.md" + slug: "compression-guide", + title: "🗜️ Prompt Compression Guide", + fileName: "COMPRESSION_GUIDE.md", }, { - "slug": "compression-language-packs", - "title": "Compression Language Packs", - "fileName": "COMPRESSION_LANGUAGE_PACKS.md" + slug: "compression-language-packs", + title: "Compression Language Packs", + fileName: "COMPRESSION_LANGUAGE_PACKS.md", }, { - "slug": "compression-rules-format", - "title": "Compression Rules Format", - "fileName": "COMPRESSION_RULES_FORMAT.md" + slug: "compression-rules-format", + title: "Compression Rules Format", + fileName: "COMPRESSION_RULES_FORMAT.md", }, { - "slug": "features", - "title": "OmniRoute — Dashboard Features Gallery", - "fileName": "FEATURES.md" + slug: "features", + title: "OmniRoute — Dashboard Features Gallery", + fileName: "FEATURES.md", }, { - "slug": "free-tiers", - "title": "🆓 Free LLM API Providers — Consolidated Directory", - "fileName": "FREE_TIERS.md" + slug: "free-tiers", + title: "🆓 Free LLM API Providers — Consolidated Directory", + fileName: "FREE_TIERS.md", }, { - "slug": "rtk-compression", - "title": "RTK Compression", - "fileName": "RTK_COMPRESSION.md" - } - ] + slug: "rtk-compression", + title: "RTK Compression", + fileName: "RTK_COMPRESSION.md", + }, + ], }, { - "title": "API & Protocols", - "items": [ + title: "API & Protocols", + items: [ { - "slug": "a2a-server", - "title": "OmniRoute A2A Server Documentation", - "fileName": "A2A-SERVER.md" + slug: "a2a-server", + title: "OmniRoute A2A Server Documentation", + fileName: "A2A-SERVER.md", }, { - "slug": "api-reference", - "title": "API Reference", - "fileName": "API_REFERENCE.md" + slug: "api-reference", + title: "API Reference", + fileName: "API_REFERENCE.md", }, { - "slug": "mcp-server", - "title": "OmniRoute MCP Server Documentation", - "fileName": "MCP-SERVER.md" - } - ] + slug: "mcp-server", + title: "OmniRoute MCP Server Documentation", + fileName: "MCP-SERVER.md", + }, + ], }, { - "title": "Deployment", - "items": [ + title: "Deployment", + items: [ { - "slug": "docker-guide", - "title": "🐳 Docker Guide", - "fileName": "DOCKER_GUIDE.md" + slug: "docker-guide", + title: "🐳 Docker Guide", + fileName: "DOCKER_GUIDE.md", }, { - "slug": "fly-io-deployment-guide", - "title": "OmniRoute Fly.io 部署指南", - "fileName": "FLY_IO_DEPLOYMENT_GUIDE.md" + slug: "fly-io-deployment-guide", + title: "OmniRoute Fly.io 部署指南", + fileName: "FLY_IO_DEPLOYMENT_GUIDE.md", }, { - "slug": "pwa-guide", - "title": "Progressive Web App (PWA) Guide", - "fileName": "PWA_GUIDE.md" + slug: "pwa-guide", + title: "Progressive Web App (PWA) Guide", + fileName: "PWA_GUIDE.md", }, { - "slug": "termux-guide", - "title": "Termux Headless Setup", - "fileName": "TERMUX_GUIDE.md" + slug: "termux-guide", + title: "Termux Headless Setup", + fileName: "TERMUX_GUIDE.md", }, { - "slug": "vm-deployment-guide", - "title": "OmniRoute — Deployment Guide on VM with Cloudflare", - "fileName": "VM_DEPLOYMENT_GUIDE.md" - } - ] + slug: "vm-deployment-guide", + title: "OmniRoute — Deployment Guide on VM with Cloudflare", + fileName: "VM_DEPLOYMENT_GUIDE.md", + }, + ], }, { - "title": "Operations", - "items": [ + title: "Operations", + items: [ { - "slug": "environment", - "title": "Environment Variables Reference", - "fileName": "ENVIRONMENT.md" + slug: "environment", + title: "Environment Variables Reference", + fileName: "ENVIRONMENT.md", }, { - "slug": "proxy-guide", - "title": "OmniRoute Proxy Guide", - "fileName": "PROXY_GUIDE.md" + slug: "proxy-guide", + title: "OmniRoute Proxy Guide", + fileName: "PROXY_GUIDE.md", }, { - "slug": "resilience-guide", - "title": "🛡️ Resilience Guide", - "fileName": "RESILIENCE_GUIDE.md" + slug: "resilience-guide", + title: "🛡️ Resilience Guide", + fileName: "RESILIENCE_GUIDE.md", }, { - "slug": "troubleshooting", - "title": "Troubleshooting", - "fileName": "TROUBLESHOOTING.md" - } - ] + slug: "troubleshooting", + title: "Troubleshooting", + fileName: "TROUBLESHOOTING.md", + }, + ], }, { - "title": "Development", - "items": [ + title: "Development", + items: [ { - "slug": "codebase-documentation", - "title": "omniroute — Codebase Documentation", - "fileName": "CODEBASE_DOCUMENTATION.md" + slug: "codebase-documentation", + title: "omniroute — Codebase Documentation", + fileName: "CODEBASE_DOCUMENTATION.md", }, { - "slug": "coverage-plan", - "title": "Test Coverage Plan", - "fileName": "COVERAGE_PLAN.md" + slug: "coverage-plan", + title: "Test Coverage Plan", + fileName: "COVERAGE_PLAN.md", }, { - "slug": "i18n", - "title": "i18n — Internationalization Guide", - "fileName": "I18N.md" + slug: "i18n", + title: "i18n — Internationalization Guide", + fileName: "I18N.md", }, { - "slug": "release-checklist", - "title": "Release Checklist", - "fileName": "RELEASE_CHECKLIST.md" + slug: "release-checklist", + title: "Release Checklist", + fileName: "RELEASE_CHECKLIST.md", }, { - "slug": "uninstall", - "title": "OmniRoute — Uninstall Guide", - "fileName": "UNINSTALL.md" - } - ] + slug: "uninstall", + title: "OmniRoute — Uninstall Guide", + fileName: "UNINSTALL.md", + }, + ], }, { - "title": "Other", - "items": [ + title: "Other", + items: [ { - "slug": "rfc-auto-assessment", - "title": "RFC: Auto-Assessment & Self-Healing Combo Engine", - "fileName": "RFC-AUTO-ASSESSMENT.md" - } - ] - } + slug: "rfc-auto-assessment", + title: "RFC: Auto-Assessment & Self-Healing Combo Engine", + fileName: "RFC-AUTO-ASSESSMENT.md", + }, + ], + }, ]; export const autoSearchIndex: AutoGenSearchItem[] = [ { - "slug": "architecture", - "title": "OmniRoute Architecture", - "fileName": "ARCHITECTURE.md", - "section": "Getting Started", - "content": "🌐 Languages: 🇺🇸 English 🇧🇷 Português (Brasil) 🇪🇸 Español 🇫🇷 Français 🇮🇹 Italiano 🇷🇺 Русский 🇨🇳 中文 (简体) 🇩🇪 Deutsch 🇮🇳 हिन्दी 🇹🇭 ไทย 🇺🇦 Українська 🇸🇦 العربية 🇯🇵 日本語 🇻🇳 Tiếng Việt 🇧🇬 Български 🇩🇰 Dansk 🇫🇮 Suomi 🇮🇱 עברית 🇭🇺 Magyar 🇮🇩 Bahasa Indonesia 🇰🇷 한국어 🇲\ud83c", - "headings": [ + slug: "architecture", + title: "OmniRoute Architecture", + fileName: "ARCHITECTURE.md", + section: "Getting Started", + content: + "🌐 Languages: 🇺🇸 English 🇧🇷 Português (Brasil) 🇪🇸 Español 🇫🇷 Français 🇮🇹 Italiano 🇷🇺 Русский 🇨🇳 中文 (简体) 🇩🇪 Deutsch 🇮🇳 हिन्दी 🇹🇭 ไทย 🇺🇦 Українська 🇸🇦 العربية 🇯🇵 日本語 🇻🇳 Tiếng Việt 🇧🇬 Български 🇩🇰 Dansk 🇫🇮 Suomi 🇮🇱 עברית 🇭🇺 Magyar 🇮🇩 Bahasa Indonesia 🇰🇷 한국어 🇲\ud83c", + headings: [ "Executive Summary", "Scope and Boundaries", "In Scope", @@ -226,16 +227,17 @@ export const autoSearchIndex: AutoGenSearchItem[] = [ "Core Runtime Components", "1) API and Routing Layer (Next.js App Routes)", "2) SSE + Translation Core", - "3) Persistence Layer" - ] + "3) Persistence Layer", + ], }, { - "slug": "cli-tools", - "title": "CLI Tools Setup Guide", - "fileName": "CLI-TOOLS.md", - "section": "Getting Started", - "content": "This guide explains how to install and configure all supported AI coding CLI tools to use OmniRoute as the unified backend, giving you centralized key management, cost tracking, model switching, and request logging across every tool. The dashboard cards in /dashboard/cli-tools are generated from src", - "headings": [ + slug: "cli-tools", + title: "CLI Tools Setup Guide", + fileName: "CLI-TOOLS.md", + section: "Getting Started", + content: + "This guide explains how to install and configure all supported AI coding CLI tools to use OmniRoute as the unified backend, giving you centralized key management, cost tracking, model switching, and request logging across every tool. The dashboard cards in /dashboard/cli-tools are generated from src", + headings: [ "How It Works", "Supported Tools (Dashboard Source of Truth)", "CLI fingerprint sync (Agents + Settings)", @@ -245,16 +247,17 @@ export const autoSearchIndex: AutoGenSearchItem[] = [ "Step 4 — Configure Each Tool", "Claude Code", "OpenAI Codex", - "OpenCode" - ] + "OpenCode", + ], }, { - "slug": "setup-guide", - "title": "Setup Guide", - "fileName": "SETUP_GUIDE.md", - "section": "Getting Started", - "content": "Complete setup reference for OmniRoute. For the quick version, see the Quick Start in README. - Install Methods - CLI Tool Configuration - Protocol Setup (MCP + A2A) - Timeout Configuration - Split-Port Mode - Void Linux (xbps-src) - Uninstalling -------------------- --------------------------------", - "headings": [ + slug: "setup-guide", + title: "Setup Guide", + fileName: "SETUP_GUIDE.md", + section: "Getting Started", + content: + "Complete setup reference for OmniRoute. For the quick version, see the Quick Start in README. - Install Methods - CLI Tool Configuration - Protocol Setup (MCP + A2A) - Timeout Configuration - Split-Port Mode - Void Linux (xbps-src) - Uninstalling -------------------- --------------------------------", + headings: [ "Table of Contents", "Install Methods", "npm (recommended)", @@ -264,51 +267,56 @@ export const autoSearchIndex: AutoGenSearchItem[] = [ "Docker", "CLI Options", "CLI Tool Configuration", - "1) Connect Providers and Create API Key" - ] + "1) Connect Providers and Create API Key", + ], }, { - "slug": "user-guide", - "title": "User Guide", - "fileName": "USER_GUIDE.md", - "section": "Getting Started", - "content": "🌐 Languages: 🇺🇸 English 🇧🇷 Português (Brasil) 🇪🇸 Español 🇫🇷 Français 🇮🇹 Italiano 🇷🇺 Русский 🇨🇳 中文 (简体) 🇩🇪 Deutsch 🇮🇳 हिन्दी 🇹🇭 ไทย 🇺🇦 Українська 🇸🇦 العربية 🇯🇵 日本語 🇻🇳 Tiếng Việt 🇧🇬 Български 🇩🇰 Dansk 🇫🇮 Suomi 🇮🇱 עברית 🇭🇺 Magyar 🇮🇩 Bahasa Indonesia 🇰🇷 한국어 🇲\ud83c", - "headings": [ + slug: "user-guide", + title: "User Guide", + fileName: "USER_GUIDE.md", + section: "Getting Started", + content: + "🌐 Languages: 🇺🇸 English 🇧🇷 Português (Brasil) 🇪🇸 Español 🇫🇷 Français 🇮🇹 Italiano 🇷🇺 Русский 🇨🇳 中文 (简体) 🇩🇪 Deutsch 🇮🇳 हिन्दी 🇹🇭 ไทย 🇺🇦 Українська 🇸🇦 العربية 🇯🇵 日本語 🇻🇳 Tiếng Việt 🇧🇬 Български 🇩🇰 Dansk 🇫🇮 Suomi 🇮🇱 עברית 🇭🇺 Magyar 🇮🇩 Bahasa Indonesia 🇰🇷 한국어 🇲\ud83c", + headings: [ "Table of Contents", "💰 Pricing at a Glance", "🎯 Use Cases", - "Case 1: \"I have Claude Pro subscription\"", - "Case 2: \"I want zero cost\"", - "Case 3: \"I need 24/7 coding, no interruptions\"", - "Case 4: \"I want FREE AI in OpenClaw\"", + 'Case 1: "I have Claude Pro subscription"', + 'Case 2: "I want zero cost"', + 'Case 3: "I need 24/7 coding, no interruptions"', + 'Case 4: "I want FREE AI in OpenClaw"', "📖 Provider Setup", "🔐 Subscription Providers", - "Claude Code (Pro/Max)" - ] + "Claude Code (Pro/Max)", + ], }, { - "slug": "auto-combo", - "title": "OmniRoute Auto-Combo Engine", - "fileName": "AUTO-COMBO.md", - "section": "Features", - "content": "Self-managing model chains with adaptive scoring The Auto-Combo Engine dynamically selects the best provider/model for each request using a 6-factor scoring function: Factor Weight Description :--------- :----- :---------------------------------------------- Quota 0.20 Remaining capacity [0..1] Heal", - "headings": [ - "How It Works", + slug: "auto-combo", + title: "OmniRoute Auto-Combo Engine", + fileName: "AUTO-COMBO.md", + section: "Features", + content: + "Self-managing model chains with adaptive scoring + zero-config auto-routing NEW: No combo creation required. Use auto/ prefix directly in any client. Model ID Variant Behavior ------------------ --------- ------------------------------------------------------------------------ auto default All conne", + headings: [ + "Zero-Config Auto-Routing (auto/ prefix)", + "Quick Examples", + "How It Works (Persisted Auto-Combos)", "Mode Packs", "Self-Healing", "Bandit Exploration", "API", "Task Fitness", - "Files" - ] + "Files", + ], }, { - "slug": "compression-engines", - "title": "Compression Engines", - "fileName": "COMPRESSION_ENGINES.md", - "section": "Features", - "content": "OmniRoute compression is built around engine contracts. A mode can run one engine directly (caveman or rtk) or a deterministic stacked pipeline that executes multiple engines in order. Mode Engine path Intended input ------------ ---------------------------------- -----------------------------------", - "headings": [ + slug: "compression-engines", + title: "Compression Engines", + fileName: "COMPRESSION_ENGINES.md", + section: "Features", + content: + "OmniRoute compression is built around engine contracts. A mode can run one engine directly (caveman or rtk) or a deterministic stacked pipeline that executes multiple engines in order. Mode Engine path Intended input ------------ ---------------------------------- -----------------------------------", + headings: [ "Modes", "Engine Registry", "Caveman", @@ -317,16 +325,17 @@ export const autoSearchIndex: AutoGenSearchItem[] = [ "Compression Combos", "API Surface", "MCP Tools", - "Validation" - ] + "Validation", + ], }, { - "slug": "compression-guide", - "title": "🗜️ Prompt Compression Guide", - "fileName": "COMPRESSION_GUIDE.md", - "section": "Features", - "content": "Save 15-95% on eligible context automatically. For a quick overview, see the README Compression section. OmniRoute implements a modular prompt compression pipeline that runs proactively before requests hit upstream providers. This means your token savings happen transparently — no changes needed to ", - "headings": [ + slug: "compression-guide", + title: "🗜️ Prompt Compression Guide", + fileName: "COMPRESSION_GUIDE.md", + section: "Features", + content: + "Save 15-95% on eligible context automatically. For a quick overview, see the README Compression section. OmniRoute implements a modular prompt compression pipeline that runs proactively before requests hit upstream providers. This means your token savings happen transparently — no changes needed to ", + headings: [ "Overview", "Compression Modes", "Off", @@ -336,46 +345,49 @@ export const autoSearchIndex: AutoGenSearchItem[] = [ "Ultra Mode (~75% savings)", "RTK Mode (60-90% upstream range)", "Stacked Mode (78-95% eligible range)", - "Upstream Savings Math" - ] + "Upstream Savings Math", + ], }, { - "slug": "compression-language-packs", - "title": "Compression Language Packs", - "fileName": "COMPRESSION_LANGUAGE_PACKS.md", - "section": "Features", - "content": "Caveman compression can load language-specific rule packs in addition to the built-in English rules. This keeps the core engine stable while allowing Portuguese, Spanish, German, French, Japanese, and future language packs to evolve independently. Language packs live under: Current shipped packs inc", - "headings": [ + slug: "compression-language-packs", + title: "Compression Language Packs", + fileName: "COMPRESSION_LANGUAGE_PACKS.md", + section: "Features", + content: + "Caveman compression can load language-specific rule packs in addition to the built-in English rules. This keeps the core engine stable while allowing Portuguese, Spanish, German, French, Japanese, and future language packs to evolve independently. Language packs live under: Current shipped packs inc", + headings: [ "Location", "Language Detection", "Config Shape", "Adding a Language Pack", "API", - "Operational Notes" - ] + "Operational Notes", + ], }, { - "slug": "compression-rules-format", - "title": "Compression Rules Format", - "fileName": "COMPRESSION_RULES_FORMAT.md", - "section": "Features", - "content": "Compression rules are JSON files loaded at runtime. They are intentionally data-only so new language packs and RTK command filters can be reviewed without changing engine code. Caveman rule packs live under: Each pack contains replacements that apply to normal prose after protected regions are isola", - "headings": [ + slug: "compression-rules-format", + title: "Compression Rules Format", + fileName: "COMPRESSION_RULES_FORMAT.md", + section: "Features", + content: + "Compression rules are JSON files loaded at runtime. They are intentionally data-only so new language packs and RTK command filters can be reviewed without changing engine code. Caveman rule packs live under: Each pack contains replacements that apply to normal prose after protected regions are isola", + headings: [ "Caveman Rule Packs", "Caveman Fields", "RTK Filter Packs", "RTK Fields", "Safety Rules", - "Validation" - ] + "Validation", + ], }, { - "slug": "features", - "title": "OmniRoute — Dashboard Features Gallery", - "fileName": "FEATURES.md", - "section": "Features", - "content": "🌐 Main README translations: 🇺🇸 English 🇧🇷 Português (Brasil) 🇪🇸 Español 🇫🇷 Français 🇮🇹 Italiano 🇷🇺 Русский 🇨🇳 中文 (简体) 🇩🇪 Deutsch 🇮🇳 हिन्दी 🇹🇭 ไทย 🇺🇦 Українська 🇸🇦 العربية 🇯🇵 日本語 🇻🇳 Tiếng Việt 🇧🇬 Български 🇩🇰 Dansk 🇫🇮 Suomi 🇮🇱 עברית 🇭🇺 Magyar 🇮🇩 Bahasa Indones", - "headings": [ + slug: "features", + title: "OmniRoute — Dashboard Features Gallery", + fileName: "FEATURES.md", + section: "Features", + content: + "🌐 Main README translations: 🇺🇸 English 🇧🇷 Português (Brasil) 🇪🇸 Español 🇫🇷 Français 🇮🇹 Italiano 🇷🇺 Русский 🇨🇳 中文 (简体) 🇩🇪 Deutsch 🇮🇳 हिन्दी 🇹🇭 ไทย 🇺🇦 Українська 🇸🇦 العربية 🇯🇵 日本語 🇻🇳 Tiếng Việt 🇧🇬 Български 🇩🇰 Dansk 🇫🇮 Suomi 🇮🇱 עברית 🇭🇺 Magyar 🇮🇩 Bahasa Indones", + headings: [ "🔌 Providers", "🎨 Combos", "📊 Analytics", @@ -385,16 +397,17 @@ export const autoSearchIndex: AutoGenSearchItem[] = [ "🎨 Themes _(v2.0.5+)_", "⚙️ Settings", "🔧 CLI Tools", - "🤖 CLI Agents _(v2.0.11+)_" - ] + "🤖 CLI Agents _(v2.0.11+)_", + ], }, { - "slug": "free-tiers", - "title": "🆓 Free LLM API Providers — Consolidated Directory", - "fileName": "FREE_TIERS.md", - "section": "Features", - "content": "The ultimate aggregated reference for all permanently free LLM API providers. Consolidated from 6 community repositories. Use with OmniRoute to route through 25+ free providers simultaneously. Last consolidated: May 2026 · Sources: awesome-free-llm-apis, awesome-free-llm-apis2, free-llm-api-resource", - "headings": [ + slug: "free-tiers", + title: "🆓 Free LLM API Providers — Consolidated Directory", + fileName: "FREE_TIERS.md", + section: "Features", + content: + "The ultimate aggregated reference for all permanently free LLM API providers. Consolidated from 6 community repositories. Use with OmniRoute to route through 25+ free providers simultaneously. Last consolidated: May 2026 · Sources: awesome-free-llm-apis, awesome-free-llm-apis2, free-llm-api-resource", + headings: [ "Table of Contents", "Quick Comparison", "Provider APIs (First-Party)", @@ -404,16 +417,17 @@ export const autoSearchIndex: AutoGenSearchItem[] = [ "Z.AI (Zhipu AI) 🇨🇳", "IBM watsonx 🇺🇸", "Inference Providers (Third-Party)", - "Groq 🇺🇸" - ] + "Groq 🇺🇸", + ], }, { - "slug": "rtk-compression", - "title": "RTK Compression", - "fileName": "RTK_COMPRESSION.md", - "section": "Features", - "content": "RTK compression is OmniRoute's command-aware compression engine for terminal and tool output. It is designed for coding-agent sessions where most context growth comes from test logs, build output, package manager noise, shell transcripts, Docker output, git output, and stack traces. RTK can run dire", - "headings": [ + slug: "rtk-compression", + title: "RTK Compression", + fileName: "RTK_COMPRESSION.md", + section: "Features", + content: + "RTK compression is OmniRoute's command-aware compression engine for terminal and tool output. It is designed for coding-agent sessions where most context growth comes from test logs, build output, package manager noise, shell transcripts, Docker output, git output, and stack traces. RTK can run dire", + headings: [ "What It Compresses", "Filter Resolution", "Filter DSL", @@ -421,16 +435,17 @@ export const autoSearchIndex: AutoGenSearchItem[] = [ "API", "Raw Output Recovery", "Verify Gate", - "Extending RTK" - ] + "Extending RTK", + ], }, { - "slug": "a2a-server", - "title": "OmniRoute A2A Server Documentation", - "fileName": "A2A-SERVER.md", - "section": "API & Protocols", - "content": "Agent-to-Agent Protocol v0.3 — OmniRoute as an intelligent routing agent Returns the Agent Card describing OmniRoute's capabilities, skills, and authentication requirements. Sends a message to a skill and waits for the complete response. Response: Same as message/send but returns Server-Sent Events ", - "headings": [ + slug: "a2a-server", + title: "OmniRoute A2A Server Documentation", + fileName: "A2A-SERVER.md", + section: "API & Protocols", + content: + "Agent-to-Agent Protocol v0.3 — OmniRoute as an intelligent routing agent Returns the Agent Card describing OmniRoute's capabilities, skills, and authentication requirements. Sends a message to a skill and waits for the complete response. Response: Same as message/send but returns Server-Sent Events ", + headings: [ "Agent Discovery", "Authentication", "Enablement", @@ -440,16 +455,17 @@ export const autoSearchIndex: AutoGenSearchItem[] = [ "tasks/get — Query Task Status", "tasks/cancel — Cancel a Task", "Available Skills", - "Task Lifecycle" - ] + "Task Lifecycle", + ], }, { - "slug": "api-reference", - "title": "API Reference", - "fileName": "API_REFERENCE.md", - "section": "API & Protocols", - "content": "🌐 Languages: 🇺🇸 English 🇧🇷 Português (Brasil) 🇪🇸 Español 🇫🇷 Français 🇮🇹 Italiano 🇷🇺 Русский 🇨🇳 中文 (简体) 🇩🇪 Deutsch 🇮🇳 हिन्दी 🇹🇭 ไทย 🇺🇦 Українська 🇸🇦 العربية 🇯🇵 日本語 🇻🇳 Tiếng Việt 🇧🇬 Български 🇩🇰 Dansk 🇫🇮 Suomi 🇮🇱 עברית 🇭🇺 Magyar 🇮🇩 Bahasa Indonesia 🇰🇷 한국어 🇲\ud83c", - "headings": [ + slug: "api-reference", + title: "API Reference", + fileName: "API_REFERENCE.md", + section: "API & Protocols", + content: + "🌐 Languages: 🇺🇸 English 🇧🇷 Português (Brasil) 🇪🇸 Español 🇫🇷 Français 🇮🇹 Italiano 🇷🇺 Русский 🇨🇳 中文 (简体) 🇩🇪 Deutsch 🇮🇳 हिन्दी 🇹🇭 ไทย 🇺🇦 Українська 🇸🇦 العربية 🇯🇵 日本語 🇻🇳 Tiếng Việt 🇧🇬 Български 🇩🇰 Dansk 🇫🇮 Suomi 🇮🇱 עברית 🇭🇺 Magyar 🇮🇩 Bahasa Indonesia 🇰🇷 한국어 🇲\ud83c", + headings: [ "Table of Contents", "Chat Completions", "Custom Headers", @@ -459,16 +475,17 @@ export const autoSearchIndex: AutoGenSearchItem[] = [ "Compatibility Endpoints", "Dedicated Provider Routes", "Semantic Cache", - "Dashboard & Management" - ] + "Dashboard & Management", + ], }, { - "slug": "mcp-server", - "title": "OmniRoute MCP Server Documentation", - "fileName": "MCP-SERVER.md", - "section": "API & Protocols", - "content": "Model Context Protocol server with 37 tools across routing, cache, compression, memory, skills, and proxy operations OmniRoute MCP is built-in. Start it with: Or via the open-sse transport: See MCP Client Configuration for Claude Desktop, Cursor, Cline, and compatible MCP client setup. -------------", - "headings": [ + slug: "mcp-server", + title: "OmniRoute MCP Server Documentation", + fileName: "MCP-SERVER.md", + section: "API & Protocols", + content: + "Model Context Protocol server with 37 tools across routing, cache, compression, memory, skills, and proxy operations OmniRoute MCP is built-in. Start it with: Or via the open-sse transport: See MCP Client Configuration for Claude Desktop, Cursor, Cline, and compatible MCP client setup. -------------", + headings: [ "Installation", "IDE Configuration", "Essential Tools (8)", @@ -478,16 +495,17 @@ export const autoSearchIndex: AutoGenSearchItem[] = [ "Other Tool Groups", "Authentication", "Audit Logging", - "Files" - ] + "Files", + ], }, { - "slug": "docker-guide", - "title": "🐳 Docker Guide", - "fileName": "DOCKER_GUIDE.md", - "section": "Deployment", - "content": "Complete Docker deployment reference. For a quick start, see the README Docker section. - Quick Run - With Environment File - Docker Compose - Docker Compose with Caddy (HTTPS) - Cloudflare Quick Tunnel - Image Tags - Important Notes --------------------- -------- ------ --------------------- diegos", - "headings": [ + slug: "docker-guide", + title: "🐳 Docker Guide", + fileName: "DOCKER_GUIDE.md", + section: "Deployment", + content: + "Complete Docker deployment reference. For a quick start, see the README Docker section. - Quick Run - With Environment File - Docker Compose - Docker Compose with Caddy (HTTPS) - Cloudflare Quick Tunnel - Image Tags - Important Notes --------------------- -------- ------ --------------------- diegos", + headings: [ "Table of Contents", "Quick Run", "With Environment File", @@ -497,16 +515,17 @@ export const autoSearchIndex: AutoGenSearchItem[] = [ "Tunnel Notes", "Image Tags", "Important Notes", - "See Also" - ] + "See Also", + ], }, { - "slug": "fly-io-deployment-guide", - "title": "OmniRoute Fly.io 部署指南", - "fileName": "FLY_IO_DEPLOYMENT_GUIDE.md", - "section": "Deployment", - "content": "本文档记录 OmniRoute 在 Fly.io 上的实际部署方法,适用于两类场景: - 首次把当前项目部署到 Fly.io - 后续代码更新后继续发布 - 新项目参考同样流程部署 本文基于当前项目已经验证通过的配置整理,应用名为 omniroute。 当前仓库中的 fly.toml 已确认包含以下关键项: 说明: - app = 'omniroute' 决定实际部署到哪个 Fly 应用 - destination = '/data' 决定持久卷挂载目录 - 本项目必须让 DATADIR=/data,否则数据库和密钥会写到容器临时目录 --- Windows PowerShell: 如果安装脚", - "headings": [ + slug: "fly-io-deployment-guide", + title: "OmniRoute Fly.io 部署指南", + fileName: "FLY_IO_DEPLOYMENT_GUIDE.md", + section: "Deployment", + content: + "本文档记录 OmniRoute 在 Fly.io 上的实际部署方法,适用于两类场景: - 首次把当前项目部署到 Fly.io - 后续代码更新后继续发布 - 新项目参考同样流程部署 本文基于当前项目已经验证通过的配置整理,应用名为 omniroute。 当前仓库中的 fly.toml 已确认包含以下关键项: 说明: - app = 'omniroute' 决定实际部署到哪个 Fly 应用 - destination = '/data' 决定持久卷挂载目录 - 本项目必须让 DATADIR=/data,否则数据库和密钥会写到容器临时目录 --- Windows PowerShell: 如果安装脚", + headings: [ "1. 部署目标", "2. 当前项目关键配置", "3. 必备工具", @@ -516,16 +535,17 @@ export const autoSearchIndex: AutoGenSearchItem[] = [ "4. 首次部署当前项目", "4.1 获取代码并进入目录", "4.2 确认应用名", - "4.3 创建应用" - ] + "4.3 创建应用", + ], }, { - "slug": "pwa-guide", - "title": "Progressive Web App (PWA) Guide", - "fileName": "PWA_GUIDE.md", - "section": "Deployment", - "content": "OmniRoute ships as a fully installable Progressive Web App. When you access the dashboard from any mobile browser — Android (Chrome) or iOS (Safari) — you can \"Add to Home Screen\" and get a native app-like experience with no app store required. A Progressive Web App turns the OmniRoute web dashboard", - "headings": [ + slug: "pwa-guide", + title: "Progressive Web App (PWA) Guide", + fileName: "PWA_GUIDE.md", + section: "Deployment", + content: + 'OmniRoute ships as a fully installable Progressive Web App. When you access the dashboard from any mobile browser — Android (Chrome) or iOS (Safari) — you can "Add to Home Screen" and get a native app-like experience with no app store required. A Progressive Web App turns the OmniRoute web dashboard', + headings: [ "What Is a PWA?", "Installation", "Android (Chrome)", @@ -535,16 +555,17 @@ export const autoSearchIndex: AutoGenSearchItem[] = [ "Fullscreen Experience", "Offline Support", "Offline Page", - "App Icons" - ] + "App Icons", + ], }, { - "slug": "termux-guide", - "title": "Termux Headless Setup", - "fileName": "TERMUX_GUIDE.md", - "section": "Deployment", - "content": "OmniRoute can run as a headless server on Android through Termux. The Electron desktop app is not supported in Termux, but the web dashboard and OpenAI-compatible API work from the local browser or from other devices on the same network. Install Termux from F-Droid or GitHub releases, then update pa", - "headings": [ + slug: "termux-guide", + title: "Termux Headless Setup", + fileName: "TERMUX_GUIDE.md", + section: "Deployment", + content: + "OmniRoute can run as a headless server on Android through Termux. The Electron desktop app is not supported in Termux, but the web dashboard and OpenAI-compatible API work from the local browser or from other devices on the same network. Install Termux from F-Droid or GitHub releases, then update pa", + headings: [ "Prerequisites", "Install", "Run", @@ -554,16 +575,17 @@ export const autoSearchIndex: AutoGenSearchItem[] = [ "Limitations", "Troubleshooting", "better-sqlite3 Build Errors", - "Port Already In Use" - ] + "Port Already In Use", + ], }, { - "slug": "vm-deployment-guide", - "title": "OmniRoute — Deployment Guide on VM with Cloudflare", - "fileName": "VM_DEPLOYMENT_GUIDE.md", - "section": "Deployment", - "content": "🌐 Languages: 🇺🇸 English 🇧🇷 Português (Brasil) 🇪🇸 Español 🇫🇷 Français 🇮🇹 Italiano 🇷🇺 Русский 🇨🇳 中文 (简体) 🇩🇪 Deutsch 🇮🇳 हिन्दी 🇹🇭 ไทย 🇺🇦 Українська 🇸🇦 العربية 🇯🇵 日本語 🇻🇳 Tiếng Việt 🇧🇬 Български 🇩🇰 Dansk 🇫🇮 Suomi 🇮🇱 עברית 🇭🇺 Magyar 🇮🇩 Bahasa Indonesia 🇰🇷 한국어 🇲\ud83c", - "headings": [ + slug: "vm-deployment-guide", + title: "OmniRoute — Deployment Guide on VM with Cloudflare", + fileName: "VM_DEPLOYMENT_GUIDE.md", + section: "Deployment", + content: + "🌐 Languages: 🇺🇸 English 🇧🇷 Português (Brasil) 🇪🇸 Español 🇫🇷 Français 🇮🇹 Italiano 🇷🇺 Русский 🇨🇳 中文 (简体) 🇩🇪 Deutsch 🇮🇳 हिन्दी 🇹🇭 ไทย 🇺🇦 Українська 🇸🇦 العربية 🇯🇵 日本語 🇻🇳 Tiếng Việt 🇧🇬 Български 🇩🇰 Dansk 🇫🇮 Suomi 🇮🇱 עברית 🇭🇺 Magyar 🇮🇩 Bahasa Indonesia 🇰🇷 한국어 🇲\ud83c", + headings: [ "Prerequisites", "1. Configure the VM", "1.1 Create the instance", @@ -573,16 +595,17 @@ export const autoSearchIndex: AutoGenSearchItem[] = [ "1.5 Install nginx", "1.6 Configure Firewall (UFW)", "2. Install OmniRoute", - "2.1 Create configuration directory" - ] + "2.1 Create configuration directory", + ], }, { - "slug": "environment", - "title": "Environment Variables Reference", - "fileName": "ENVIRONMENT.md", - "section": "Operations", - "content": "Complete reference for every environment variable recognized by OmniRoute. For a quick-start template, see .env.example. These must be set before the first run. Without them, the application will either refuse to start or operate with insecure defaults. Variable Required Default Source File Descript", - "headings": [ + slug: "environment", + title: "Environment Variables Reference", + fileName: "ENVIRONMENT.md", + section: "Operations", + content: + "Complete reference for every environment variable recognized by OmniRoute. For a quick-start template, see .env.example. These must be set before the first run. Without them, the application will either refuse to start or operate with insecure defaults. Variable Required Default Source File Descript", + headings: [ "Table of Contents", "1. Required Secrets", "Generation Commands", @@ -592,16 +615,17 @@ export const autoSearchIndex: AutoGenSearchItem[] = [ "Port Modes", "4. Security & Authentication", "Hardening Checklist", - "5. Input Sanitization & PII Protection" - ] + "5. Input Sanitization & PII Protection", + ], }, { - "slug": "proxy-guide", - "title": "OmniRoute Proxy Guide", - "fileName": "PROXY_GUIDE.md", - "section": "Operations", - "content": "Bypass geographic blocks, protect your identity, and route AI traffic through any proxy — with zero configuration complexity. OmniRoute includes a full-featured proxy management system that lets you route upstream AI provider traffic through HTTP, HTTPS, or SOCKS5 proxies. Whether you're in a blocke", - "headings": [ + slug: "proxy-guide", + title: "OmniRoute Proxy Guide", + fileName: "PROXY_GUIDE.md", + section: "Operations", + content: + "Bypass geographic blocks, protect your identity, and route AI traffic through any proxy — with zero configuration complexity. OmniRoute includes a full-featured proxy management system that lets you route upstream AI provider traffic through HTTP, HTTPS, or SOCKS5 proxies. Whether you're in a blocke", + headings: [ "Table of Contents", "Why Use Proxies?", "Architecture Overview", @@ -611,16 +635,17 @@ export const autoSearchIndex: AutoGenSearchItem[] = [ "What Gets Proxied", "Proxy Registry (CRUD)", "Creating a Proxy", - "Updating a Proxy" - ] + "Updating a Proxy", + ], }, { - "slug": "resilience-guide", - "title": "🛡️ Resilience Guide", - "fileName": "RESILIENCE_GUIDE.md", - "section": "Operations", - "content": "How OmniRoute keeps your AI coding workflow running when providers fail. OmniRoute implements a multi-layered resilience system that ensures zero downtime: ------------ ------- ------------------------------------ Queue Size 10 Max queued requests per connection Pacing Interval 0ms Minimum gap betwe", - "headings": [ + slug: "resilience-guide", + title: "🛡️ Resilience Guide", + fileName: "RESILIENCE_GUIDE.md", + section: "Operations", + content: + "How OmniRoute keeps your AI coding workflow running when providers fail. OmniRoute implements a multi-layered resilience system that ensures zero downtime: ------------ ------- ------------------------------------ Queue Size 10 Max queued requests per connection Pacing Interval 0ms Minimum gap betwe", + headings: [ "Overview", "Request Queue & Pacing", "Connection Cooldown", @@ -630,35 +655,37 @@ export const autoSearchIndex: AutoGenSearchItem[] = [ "Combo Fallback Chains", "13 Routing Strategies", "TLS Fingerprint Spoofing", - "Health Dashboard" - ] + "Health Dashboard", + ], }, { - "slug": "troubleshooting", - "title": "Troubleshooting", - "fileName": "TROUBLESHOOTING.md", - "section": "Operations", - "content": "🌐 Languages: 🇺🇸 English 🇧🇷 Português (Brasil) 🇪🇸 Español 🇫🇷 Français 🇮🇹 Italiano 🇷🇺 Русский 🇨🇳 中文 (简体) 🇩🇪 Deutsch 🇮🇳 हिन्दी 🇹🇭 ไทย 🇺🇦 Українська 🇸🇦 العربية 🇯🇵 日本語 🇻🇳 Tiếng Việt 🇧🇬 Български 🇩🇰 Dansk 🇫🇮 Suomi 🇮🇱 עברית 🇭🇺 Magyar 🇮🇩 Bahasa Indonesia 🇰🇷 한국어 🇲\ud83c", - "headings": [ + slug: "troubleshooting", + title: "Troubleshooting", + fileName: "TROUBLESHOOTING.md", + section: "Operations", + content: + "🌐 Languages: 🇺🇸 English 🇧🇷 Português (Brasil) 🇪🇸 Español 🇫🇷 Français 🇮🇹 Italiano 🇷🇺 Русский 🇨🇳 中文 (简体) 🇩🇪 Deutsch 🇮🇳 हिन्दी 🇹🇭 ไทย 🇺🇦 Українська 🇸🇦 العربية 🇯🇵 日本語 🇻🇳 Tiếng Việt 🇧🇬 Български 🇩🇰 Dansk 🇫🇮 Suomi 🇮🇱 עברית 🇭🇺 Magyar 🇮🇩 Bahasa Indonesia 🇰🇷 한국어 🇲\ud83c", + headings: [ "Quick Fixes", "Node.js Compatibility", - "Login page crashes or shows \"Module self-registration\" error", - "macOS: dlopen / \"slice is not valid mach-o file\"", + 'Login page crashes or shows "Module self-registration" error', + 'macOS: dlopen / "slice is not valid mach-o file"', "Proxy Issues", - "Provider validation shows \"fetch failed\"", - "Token health check fails with \"fetch failed\"", - "SOCKS5 proxy returns \"invalid onRequestStart method\"", + 'Provider validation shows "fetch failed"', + 'Token health check fails with "fetch failed"', + 'SOCKS5 proxy returns "invalid onRequestStart method"', "Provider Issues", - "\"Language model did not provide messages\"" - ] + '"Language model did not provide messages"', + ], }, { - "slug": "codebase-documentation", - "title": "omniroute — Codebase Documentation", - "fileName": "CODEBASE_DOCUMENTATION.md", - "section": "Development", - "content": "🌐 Languages: 🇺🇸 English 🇧🇷 Português (Brasil) 🇪🇸 Español 🇫🇷 Français 🇮🇹 Italiano 🇷🇺 Русский 🇨🇳 中文 (简体) 🇩🇪 Deutsch 🇮🇳 हिन्दी 🇹🇭 ไทย 🇺🇦 Українська 🇸🇦 العربية 🇯🇵 日本語 🇻🇳 Tiếng Việt 🇧🇬 Български 🇩🇰 Dansk 🇫🇮 Suomi 🇮🇱 עברית 🇭🇺 Magyar 🇮🇩 Bahasa Indonesia 🇰🇷 한국어 🇲\ud83c", - "headings": [ + slug: "codebase-documentation", + title: "omniroute — Codebase Documentation", + fileName: "CODEBASE_DOCUMENTATION.md", + section: "Development", + content: + "🌐 Languages: 🇺🇸 English 🇧🇷 Português (Brasil) 🇪🇸 Español 🇫🇷 Français 🇮🇹 Italiano 🇷🇺 Русский 🇨🇳 中文 (简体) 🇩🇪 Deutsch 🇮🇳 हिन्दी 🇹🇭 ไทย 🇺🇦 Українська 🇸🇦 العربية 🇯🇵 日本語 🇻🇳 Tiếng Việt 🇧🇬 Български 🇩🇰 Dansk 🇫🇮 Suomi 🇮🇱 עברית 🇭🇺 Magyar 🇮🇩 Bahasa Indonesia 🇰🇷 한국어 🇲\ud83c", + headings: [ "1. What Is omniroute?", "2. Architecture Overview", "Core Principle: Hub-and-Spoke Translation", @@ -668,16 +695,17 @@ export const autoSearchIndex: AutoGenSearchItem[] = [ "Credential Loading Flow", "4.2 Executors (open-sse/executors/)", "4.3 Handlers (open-sse/handlers/)", - "Request Lifecycle (chatCore.ts)" - ] + "Request Lifecycle (chatCore.ts)", + ], }, { - "slug": "coverage-plan", - "title": "Test Coverage Plan", - "fileName": "COVERAGE_PLAN.md", - "section": "Development", - "content": "Last updated: 2026-03-28 There are multiple coverage numbers depending on how the report is computed. For planning, only one of them is useful. Metric Scope Statements / Lines Branches Functions Notes -------------------- ----------------------------------------------------- -----------------: -----", - "headings": [ + slug: "coverage-plan", + title: "Test Coverage Plan", + fileName: "COVERAGE_PLAN.md", + section: "Development", + content: + "Last updated: 2026-03-28 There are multiple coverage numbers depending on how the report is computed. For planning, only one of them is useful. Metric Scope Statements / Lines Branches Functions Notes -------------------- ----------------------------------------------------- -----------------: -----", + headings: [ "Baseline", "Rules", "Current command set", @@ -687,16 +715,17 @@ export const autoSearchIndex: AutoGenSearchItem[] = [ "Phase 1: 56.95% -> 60%", "Phase 2: 60% -> 65%", "Phase 3: 65% -> 70%", - "Phase 4: 70% -> 75%" - ] + "Phase 4: 70% -> 75%", + ], }, { - "slug": "i18n", - "title": "i18n — Internationalization Guide", - "fileName": "I18N.md", - "section": "Development", - "content": "OmniRoute supports 30 languages with full dashboard UI translation, translated documentation, and RTL support for Arabic and Hebrew. 🌐 Languages: 🇺🇸 English 🇧🇷 Português (Brasil) 🇪🇸 Español 🇫🇷 Français 🇩🇪 Deutsch 🇮🇹 Italiano 🇷🇺 Русский 🇨🇳 中文 (简体) 🇯🇵 日本語 🇰🇷 한국어 🇸🇦 العربية 🇮🇳 ", - "headings": [ + slug: "i18n", + title: "i18n — Internationalization Guide", + fileName: "I18N.md", + section: "Development", + content: + "OmniRoute supports 30 languages with full dashboard UI translation, translated documentation, and RTL support for Arabic and Hebrew. 🌐 Languages: 🇺🇸 English 🇧🇷 Português (Brasil) 🇪🇸 Español 🇫🇷 Français 🇩🇪 Deutsch 🇮🇹 Italiano 🇷🇺 Русский 🇨🇳 中文 (简体) 🇯🇵 日本語 🇰🇷 한국어 🇸🇦 العربية 🇮🇳 ", + headings: [ "Quick Reference", "Architecture", "Source of Truth", @@ -706,29 +735,26 @@ export const autoSearchIndex: AutoGenSearchItem[] = [ "1. Register the Locale", "2. Add to Generator", "3. Generate Initial Translation", - "4. Review & Fix Auto-Translations" - ] + "4. Review & Fix Auto-Translations", + ], }, { - "slug": "release-checklist", - "title": "Release Checklist", - "fileName": "RELEASE_CHECKLIST.md", - "section": "Development", - "content": "Use this checklist before tagging or publishing a new OmniRoute release. 1. Bump package.json version (x.y.z) in the release branch. 2. Move release notes from [Unreleased] in CHANGELOG.md to a dated section: - [x.y.z] — YYYY-MM-DD 3. Keep [Unreleased] as the first changelog section for upcoming wor", - "headings": [ - "Version and Changelog", - "API Docs", - "Runtime Docs", - "Automated Check" - ] + slug: "release-checklist", + title: "Release Checklist", + fileName: "RELEASE_CHECKLIST.md", + section: "Development", + content: + "Use this checklist before tagging or publishing a new OmniRoute release. 1. Bump package.json version (x.y.z) in the release branch. 2. Move release notes from [Unreleased] in CHANGELOG.md to a dated section: - [x.y.z] — YYYY-MM-DD 3. Keep [Unreleased] as the first changelog section for upcoming wor", + headings: ["Version and Changelog", "API Docs", "Runtime Docs", "Automated Check"], }, { - "slug": "uninstall", - "title": "OmniRoute — Uninstall Guide", - "fileName": "UNINSTALL.md", - "section": "Development", - "content": "🌐 Languages: 🇺🇸 English 🇧🇷 Português (Brasil) 🇪🇸 Español 🇫🇷 Français 🇮🇹 Italiano 🇷🇺 Русский 🇨🇳 中文 (简体) 🇩🇪 Deutsch 🇮🇳 हिन्दी 🇹🇭 ไทย 🇺🇦 Українська 🇸🇦 العربية 🇯🇵 日本語 🇻🇳 Tiếng Việt 🇧🇬 Български 🇩🇰 Dansk 🇫🇮 Suomi 🇮🇱 עברית 🇭🇺 Magyar 🇮🇩 Bahasa Indonesia 🇰🇷 한국어 🇲\ud83c", - "headings": [ + slug: "uninstall", + title: "OmniRoute — Uninstall Guide", + fileName: "UNINSTALL.md", + section: "Development", + content: + "🌐 Languages: 🇺🇸 English 🇧🇷 Português (Brasil) 🇪🇸 Español 🇫🇷 Français 🇮🇹 Italiano 🇷🇺 Русский 🇨🇳 中文 (简体) 🇩🇪 Deutsch 🇮🇳 हिन्दी 🇹🇭 ไทย 🇺🇦 Українська 🇸🇦 العربية 🇯🇵 日本語 🇻🇳 Tiếng Việt 🇧🇬 Български 🇩🇰 Dansk 🇫🇮 Suomi 🇮🇱 עברית 🇭🇺 Magyar 🇮🇩 Bahasa Indonesia 🇰🇷 한국어 🇲\ud83c", + headings: [ "Quick Uninstall (v3.6.2+)", "Keep Your Data", "Full Removal", @@ -738,16 +764,17 @@ export const autoSearchIndex: AutoGenSearchItem[] = [ "Docker", "Docker Compose", "Electron Desktop App", - "Source Install (git clone)" - ] + "Source Install (git clone)", + ], }, { - "slug": "rfc-auto-assessment", - "title": "RFC: Auto-Assessment & Self-Healing Combo Engine", - "fileName": "RFC-AUTO-ASSESSMENT.md", - "section": "Other", - "content": "Omniroute's combo system currently requires manual configuration: users must know which providers and models are actually working, then manually wire them into combo chains. When providers fail (rate limits, auth errors, model deprecation), combos silently degrade — routing to dead endpoints that ti", - "headings": [ + slug: "rfc-auto-assessment", + title: "RFC: Auto-Assessment & Self-Healing Combo Engine", + fileName: "RFC-AUTO-ASSESSMENT.md", + section: "Other", + content: + "Omniroute's combo system currently requires manual configuration: users must know which providers and models are actually working, then manually wire them into combo chains. When providers fail (rate limits, auth errors, model deprecation), combos silently degrade — routing to dead endpoints that ti", + headings: [ "Summary", "Problem Statement", "What we encountered (real production incident)", @@ -757,20 +784,17 @@ export const autoSearchIndex: AutoGenSearchItem[] = [ "New Components", "Detailed Design", "1. Assessor — src/domain/assessor.ts", - "2. Categorizer — src/domain/categorizer.ts" - ] + "2. Categorizer — src/domain/categorizer.ts", + ], }, { - "slug": "api-explorer", - "title": "API Explorer", - "fileName": "API_REFERENCE.md", - "section": "API & Protocols", - "content": "interactive try it live api explorer endpoint test request response curl example", - "headings": [ - "Try It", - "Endpoints" - ] - } + slug: "api-explorer", + title: "API Explorer", + fileName: "API_REFERENCE.md", + section: "API & Protocols", + content: "interactive try it live api explorer endpoint test request response curl example", + headings: ["Try It", "Endpoints"], + }, ]; export const autoAllSlugs: string[] = [ @@ -803,5 +827,5 @@ export const autoAllSlugs: string[] = [ "i18n", "release-checklist", "uninstall", - "rfc-auto-assessment" + "rfc-auto-assessment", ]; diff --git a/src/shared/components/AutoRoutingBanner.test.tsx b/src/shared/components/AutoRoutingBanner.test.tsx new file mode 100644 index 0000000000..2249ef566e --- /dev/null +++ b/src/shared/components/AutoRoutingBanner.test.tsx @@ -0,0 +1,96 @@ +// @vitest-environment jsdom +import React from "react"; +import { act } from "react"; +import { createRoot } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; + +const cleanupCallbacks: Array<() => void> = []; + +function makeContainer(): HTMLElement { + const container = document.createElement("div"); + document.body.appendChild(container); + cleanupCallbacks.push(() => { + container.remove(); + }); + return container; +} + +describe("AutoRoutingBanner", () => { + beforeEach(() => { + ( + globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean } + ).IS_REACT_ACT_ENVIRONMENT = true; + localStorage.clear(); + }); + + afterEach(() => { + while (cleanupCallbacks.length > 0) { + cleanupCallbacks.pop()?.(); + } + document.body.innerHTML = ""; + localStorage.clear(); + }); + + it("renders banner on first mount", async () => { + const { default: AutoRoutingBanner } = await import("./AutoRoutingBanner"); + const container = makeContainer(); + const root = createRoot(container); + await act(async () => { + root.render(); + }); + expect(container.querySelector('[role="banner"]')).toBeTruthy(); + expect(container.textContent).toContain("Auto-Routing Active"); + }); + + it("includes link to Combos page", async () => { + const { default: AutoRoutingBanner } = await import("./AutoRoutingBanner"); + const container = makeContainer(); + const root = createRoot(container); + await act(async () => { + root.render(); + }); + const link = container.querySelector('a[href="/dashboard/combos"]'); + expect(link).toBeTruthy(); + }); + + it("can be dismissed by clicking close button", async () => { + const { default: AutoRoutingBanner } = await import("./AutoRoutingBanner"); + const container = makeContainer(); + const root = createRoot(container); + await act(async () => { + root.render(); + }); + expect(container.querySelector('[role="banner"]')).toBeTruthy(); + const closeButton = container.querySelector('button[aria-label="Dismiss auto-routing banner"]'); + expect(closeButton).toBeTruthy(); + await act(async () => { + closeButton?.click(); + }); + expect(container.querySelector('[role="banner"]')).toBeFalsy(); + }); + + it("persists dismissal to localStorage", async () => { + const { default: AutoRoutingBanner } = await import("./AutoRoutingBanner"); + const container = makeContainer(); + const root = createRoot(container); + await act(async () => { + root.render(); + }); + const closeButton = container.querySelector('button[aria-label="Dismiss auto-routing banner"]'); + await act(async () => { + closeButton?.click(); + }); + expect(localStorage.getItem("auto-routing-banner-dismissed")).toBe("true"); + }); + + it("remains hidden after dismissal on remount", async () => { + localStorage.setItem("auto-routing-banner-dismissed", "true"); + const { default: AutoRoutingBanner } = await import("./AutoRoutingBanner"); + const container = makeContainer(); + const root = createRoot(container); + await act(async () => { + root.render(); + }); + expect(container.querySelector('[role="banner"]')).toBeFalsy(); + }); +}); diff --git a/src/shared/components/AutoRoutingBanner.tsx b/src/shared/components/AutoRoutingBanner.tsx new file mode 100644 index 0000000000..062de5c001 --- /dev/null +++ b/src/shared/components/AutoRoutingBanner.tsx @@ -0,0 +1,79 @@ +"use client"; + +import { useEffect, useState } from "react"; + +const AUTO_ROUTING_DISMISSED_KEY = "auto-routing-banner-dismissed"; + +export default function AutoRoutingBanner() { + const [isDismissed, setIsDismissed] = useState(false); + + useEffect(() => { + try { + const dismissed = localStorage.getItem(AUTO_ROUTING_DISMISSED_KEY); + if (dismissed === "true") { + // eslint-disable-next-line react-hooks/set-state-in-effect + setIsDismissed(true); + } + } catch { + // localStorage unavailable (SSR or private mode) — do nothing + } + }, []); + + const handleDismiss = () => { + try { + localStorage.setItem(AUTO_ROUTING_DISMISSED_KEY, "true"); + } catch { + // ignore localStorage errors (private mode, quotas) + } + + setIsDismissed(true); + }; + + if (isDismissed) return null; + + return ( +
+
+
+ + + Auto-Routing Active + +
+
+ OmniRoute is automatically routing requests using combo-based strategies. + + View or change your routing configuration on the{" "} + + Combos page + + . + +
+ +
+
+ ); +} diff --git a/src/shared/components/layouts/DashboardLayout.tsx b/src/shared/components/layouts/DashboardLayout.tsx index 9c3e9c0751..a4b92b7005 100644 --- a/src/shared/components/layouts/DashboardLayout.tsx +++ b/src/shared/components/layouts/DashboardLayout.tsx @@ -7,6 +7,7 @@ import Breadcrumbs from "../Breadcrumbs"; import NotificationToast from "../NotificationToast"; import MaintenanceBanner from "../MaintenanceBanner"; import { useIsElectron } from "@/shared/hooks/useElectron"; +import AutoRoutingBanner from "../AutoRoutingBanner"; const SIDEBAR_COLLAPSED_KEY = "sidebar-collapsed"; const isE2EMode = process.env.NEXT_PUBLIC_OMNIROUTE_E2E_MODE === "1"; @@ -77,6 +78,7 @@ export default function DashboardLayout({ children }) { >
setSidebarOpen(true)} /> {!isE2EMode && } +
diff --git a/src/shared/constants/providers.ts b/src/shared/constants/providers.ts index 05dbcb5d46..5eaf8eafa9 100644 --- a/src/shared/constants/providers.ts +++ b/src/shared/constants/providers.ts @@ -1880,6 +1880,20 @@ export function isSelfHostedChatProvider(providerId: unknown): boolean { return typeof providerId === "string" && SELF_HOSTED_CHAT_PROVIDER_IDS.has(providerId); } +// ── System Providers (virtual, not user-connectable) ────────────────────────── +export const SYSTEM_PROVIDERS = { + auto: { + id: "auto", + alias: "auto", + name: "Auto (Zero-Config)", + icon: "auto_awesome", + color: "#6366F1", + textIcon: "Auto", + systemOnly: true, + description: "Zero-config auto-routing with LKGP across all connected providers", + }, +}; + // All providers (combined) export const AI_PROVIDERS = { ...FREE_PROVIDERS, @@ -1890,6 +1904,7 @@ export const AI_PROVIDERS = { ...SEARCH_PROVIDERS, ...AUDIO_ONLY_PROVIDERS, ...UPSTREAM_PROXY_PROVIDERS, + ...SYSTEM_PROVIDERS, // <-- system providers included }; export type AiProviderId = keyof typeof AI_PROVIDERS; diff --git a/src/shared/validation/settingsSchemas.ts b/src/shared/validation/settingsSchemas.ts index 2e4973b22b..7a5b1b3411 100644 --- a/src/shared/validation/settingsSchemas.ts +++ b/src/shared/validation/settingsSchemas.ts @@ -104,6 +104,11 @@ export const updateSettingsSchema = z.object({ lkgpEnabled: z.boolean().optional(), backgroundDegradation: z.unknown().optional(), bruteForceProtection: z.boolean().optional(), + // Auto-routing settings + autoRoutingEnabled: z.boolean().optional(), + autoRoutingDefaultVariant: z + .enum(["lkgp", "coding", "fast", "cheap", "offline", "smart"]) + .optional(), }); export const databaseSettingsSchema = z.object( diff --git a/src/sse/handlers/chat.ts b/src/sse/handlers/chat.ts index 56cbee63d9..3e1ef03a35 100644 --- a/src/sse/handlers/chat.ts +++ b/src/sse/handlers/chat.ts @@ -23,6 +23,7 @@ import { getModelTargetFormat, PROVIDER_ID_TO_ALIAS, } from "@omniroute/open-sse/config/providerModels.ts"; +import type { AutoVariant } from "@omniroute/open-sse/services/autoCombo/autoPrefix.ts"; import * as log from "../utils/logger"; import { checkAndRefreshToken } from "../services/tokenRefresh"; import { deleteHandoff, getHandoff } from "@/lib/db/contextHandoffs"; @@ -229,9 +230,9 @@ export async function handleChat(request: any, clientRawRequest: any = null) { // Guardrail pre-call pipeline — prompt injection, PII masking, and future custom rules. telemetry.startPhase("validate"); const preCallGuardrails = await guardrailRegistry.runPreCallHooks(body, { - apiKeyInfo, + apiKeyInfo: apiKeyInfo as any, disabledGuardrails: resolveDisabledGuardrails({ - apiKeyInfo: apiKeyInfo as Record | null, + apiKeyInfo: (apiKeyInfo ?? null) as any, body, headers: request.headers, }), @@ -295,6 +296,44 @@ export async function handleChat(request: any, clientRawRequest: any = null) { telemetry.endPhase(); } + // ── Zero-Config Auto-Routing (auto and auto/ prefix) ──────────────────────── + // If the model ID is "auto" or starts with "auto/", bypass DB combo lookup + // entirely and generate a virtual auto-combo on-the-fly from connected providers. + let autoVariant: AutoVariant | undefined; + let isAutoRouting = resolvedModelStr === "auto" || resolvedModelStr.startsWith("auto/"); + if (isAutoRouting) { + // C2: Enforce autoRoutingEnabled setting + const settings = await getSettings(); + if (settings?.autoRoutingEnabled === false) { + return errorResponse( + HTTP_STATUS.BAD_REQUEST, + "Auto routing is disabled. Enable it in Settings > Routing." + ); + } + + try { + const { parseAutoPrefix } = + await import("@omniroute/open-sse/services/autoCombo/autoPrefix.ts"); + const parsed = parseAutoPrefix(resolvedModelStr); + if (parsed.valid) { + autoVariant = parsed.variant; + // C3: Apply autoRoutingDefaultVariant from settings when bare "auto" is used + if (autoVariant === undefined && settings?.autoRoutingDefaultVariant) { + autoVariant = settings.autoRoutingDefaultVariant as AutoVariant; + } + log.info( + "AUTO", + `Zero-config routing variant: ${autoVariant || "default"} (model=${resolvedModelStr})` + ); + } else { + log.warn("AUTO", `Invalid auto prefix format: ${resolvedModelStr}`); + } + } catch (err) { + log.error("AUTO", "Failed to load auto-prefix parser", { err }); + } + } + // ──────────────────────────────────────────────────────────────────────────── + // Check if model is a combo (has multiple models with fallback) telemetry.startPhase("resolve"); let combo: any = await getComboForModel(resolvedModelStr); @@ -312,6 +351,23 @@ export async function handleChat(request: any, clientRawRequest: any = null) { } } + // Auto-prefix short-circuit: if auto/ prefix was detected, replace combo with virtual one + if (autoVariant !== undefined && combo === null) { + try { + const { createVirtualAutoCombo } = + await import("@omniroute/open-sse/services/autoCombo/virtualFactory.ts"); + const virtualCombo = await createVirtualAutoCombo(autoVariant); + virtualCombo.name = resolvedModelStr; + virtualCombo.id = resolvedModelStr; + combo = virtualCombo; + log.info( + "AUTO", + `Virtual auto-combo created: ${combo.name} (${virtualCombo.candidatePool?.length || 0} candidates)` + ); + } catch (err) { + log.error("AUTO", "Failed to create virtual auto-combo", { err }); + } + } if (combo) { log.info( "CHAT", @@ -388,6 +444,7 @@ export async function handleChat(request: any, clientRawRequest: any = null) { connectionId?: string | null; executionKey?: string | null; stepId?: string | null; + allowedConnectionIds?: string[] | null; } ) => handleSingleModelChat( diff --git a/vitest.mcp.config.ts b/vitest.mcp.config.ts index da4324af0b..acd2360f04 100644 --- a/vitest.mcp.config.ts +++ b/vitest.mcp.config.ts @@ -9,6 +9,9 @@ export default defineConfig({ "open-sse/mcp-server/__tests__/**/*.test.ts", "open-sse/services/autoCombo/__tests__/**/*.test.ts", "tests/unit/encryption.spec.ts", + "src/shared/components/**/*.test.tsx", + "src/shared/hooks/__tests__/**/*.test.tsx", + "src/app/(dashboard)/**/__tests__/**/*.test.tsx", ], exclude: ["**/node_modules/**", "**/.git/**"], coverage: {