Compare commits

..

5 Commits

Author SHA1 Message Date
Diego Rodrigues de Sa e Souza c3fe96e221 Merge pull request #44 from diegosouzapw/feature/release-0.6.0
chore: release v0.6.0
2026-02-16 00:23:19 -03:00
diegosouzapw 1c6541f25d chore: bump version to 0.6.0 and update changelog 2026-02-16 00:22:55 -03:00
diegosouzapw 9f8dfa1398 feat: add costs page and enhance health provider status display
Add dedicated Costs page combining Budget and Pricing tabs with a
sidebar navigation layout. Improve Health page provider section with
status legend (Healthy/Recovering/Down) and import AI_PROVIDERS
constants for richer provider display.
2026-02-15 23:01:27 -03:00
diegosouzapw e4db9bff0e feat: add provider metrics display and model import for passthrough providers
- Fetch and display request metrics (success rate, latency, total requests) on provider overview cards
- Create /api/provider-metrics endpoint to aggregate stats from request logs
- Add /api/providers/[id]/import-models endpoint to import models from provider's /models API
- Add "Import from /models" button on passthrough provider detail pages
- Include proper PropTypes for new metrics and alias fields
2026-02-15 21:53:27 -03:00
diegosouzapw 54aba4c087 feat: redesign app icons with network node graph and new color scheme
Replace text-based "9" favicon with a network/router node graph design
across all icon sizes. Add apple-touch-icon and icon-192 SVG assets.
Update brand gradient from orange (#f97815) to red (#E54D5E).
2026-02-15 20:56:48 -03:00
28 changed files with 987 additions and 165 deletions
+24
View File
@@ -10,6 +10,30 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
---
## [0.6.0] — 2026-02-16
Provider metrics, cost analytics page, health enhancements, and visual rebrand.
### Added
#### Dashboard & UI
- **Costs Page** — New dedicated `/dashboard/costs` page with cost analytics and breakdown
- **Provider Metrics API** — New `/api/provider-metrics` endpoint exposing per-provider usage metrics
- **Health Provider Status** — Enhanced health page with detailed provider status display and configuration indicators
- **Model Import for Passthrough Providers** — Model import now saves as default (non-custom) for passthrough providers (Deepgram, AssemblyAI, NanoBanana)
#### Visual & Branding
- **App Icon Redesign** — New network node graph icon with updated color scheme reflecting "Modern Tech Warmth" aesthetic
### Fixed
- **Provider Metrics Display** — Providers page now shows real-time usage metrics per provider
- **Home Page Providers Overview** — Enhanced provider card display on dashboard home
---
## [0.5.0] — 2026-02-15
Dashboard refinements, LLM evaluation framework, combo strategies expansion, and UI/UX polish.
+1 -1
View File
@@ -255,7 +255,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.
```bash
# Create a release — npm publish happens automatically
gh release create v0.5.0 --title "v0.5.0" --generate-notes
gh release create v0.6.0 --title "v0.6.0" --generate-notes
```
---
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "omniroute",
"version": "0.5.0",
"version": "0.6.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "omniroute",
"version": "0.5.0",
"version": "0.6.0",
"license": "MIT",
"workspaces": [
"open-sse"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "omniroute",
"version": "0.5.0",
"version": "0.6.0",
"description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.",
"type": "module",
"bin": {
+26
View File
@@ -0,0 +1,26 @@
<svg width="180" height="180" viewBox="0 0 180 180" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="180" height="180" rx="36" fill="url(#gradient)" />
<!-- Central node -->
<circle cx="90" cy="90" r="17" fill="white" />
<!-- Outer nodes -->
<circle cx="45" cy="45" r="11" fill="white" />
<circle cx="135" cy="45" r="11" fill="white" />
<circle cx="45" cy="135" r="11" fill="white" />
<circle cx="135" cy="135" r="11" fill="white" />
<circle cx="90" cy="28" r="8.5" fill="white" />
<circle cx="90" cy="152" r="8.5" fill="white" />
<!-- Connection lines -->
<line x1="90" y1="73" x2="45" y2="45" stroke="white" stroke-width="6.5" stroke-linecap="round" />
<line x1="90" y1="73" x2="135" y2="45" stroke="white" stroke-width="6.5" stroke-linecap="round" />
<line x1="90" y1="107" x2="45" y2="135" stroke="white" stroke-width="6.5" stroke-linecap="round" />
<line x1="90" y1="107" x2="135" y2="135" stroke="white" stroke-width="6.5"
stroke-linecap="round" />
<line x1="90" y1="73" x2="90" y2="28" stroke="white" stroke-width="6.5" stroke-linecap="round" />
<line x1="90" y1="107" x2="90" y2="152" stroke="white" stroke-width="6.5" stroke-linecap="round" />
<defs>
<linearGradient id="gradient" x1="0" y1="0" x2="180" y2="180" gradientUnits="userSpaceOnUse">
<stop stop-color="#E54D5E" />
<stop offset="1" stop-color="#C93D4E" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+18 -4
View File
@@ -1,11 +1,25 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="6" fill="url(#gradient)"/>
<text x="16" y="24" font-family="system-ui, -apple-system, sans-serif" font-size="20" font-weight="700" fill="white" text-anchor="middle">9</text>
<!-- Central node -->
<circle cx="16" cy="16" r="3" fill="white"/>
<!-- Outer nodes -->
<circle cx="8" cy="8" r="2" fill="white"/>
<circle cx="24" cy="8" r="2" fill="white"/>
<circle cx="8" cy="24" r="2" fill="white"/>
<circle cx="24" cy="24" r="2" fill="white"/>
<circle cx="16" cy="5" r="1.5" fill="white"/>
<circle cx="16" cy="27" r="1.5" fill="white"/>
<!-- Connection lines -->
<line x1="16" y1="13" x2="8" y2="8" stroke="white" stroke-width="1.2" stroke-linecap="round"/>
<line x1="16" y1="13" x2="24" y2="8" stroke="white" stroke-width="1.2" stroke-linecap="round"/>
<line x1="16" y1="19" x2="8" y2="24" stroke="white" stroke-width="1.2" stroke-linecap="round"/>
<line x1="16" y1="19" x2="24" y2="24" stroke="white" stroke-width="1.2" stroke-linecap="round"/>
<line x1="16" y1="13" x2="16" y2="5" stroke="white" stroke-width="1.2" stroke-linecap="round"/>
<line x1="16" y1="19" x2="16" y2="27" stroke="white" stroke-width="1.2" stroke-linecap="round"/>
<defs>
<linearGradient id="gradient" x1="0" y1="0" x2="32" y2="32" gradientUnits="userSpaceOnUse">
<stop stop-color="#f97815"/>
<stop offset="1" stop-color="#c2590a"/>
<stop stop-color="#E54D5E"/>
<stop offset="1" stop-color="#C93D4E"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 533 B

After

Width:  |  Height:  |  Size: 1.3 KiB

+25
View File
@@ -0,0 +1,25 @@
<svg width="192" height="192" viewBox="0 0 192 192" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="192" height="192" rx="36" fill="url(#gradient)" />
<!-- Central node -->
<circle cx="96" cy="96" r="18" fill="white" />
<!-- Outer nodes -->
<circle cx="48" cy="48" r="12" fill="white" />
<circle cx="144" cy="48" r="12" fill="white" />
<circle cx="48" cy="144" r="12" fill="white" />
<circle cx="144" cy="144" r="12" fill="white" />
<circle cx="96" cy="30" r="9" fill="white" />
<circle cx="96" cy="162" r="9" fill="white" />
<!-- Connection lines -->
<line x1="96" y1="78" x2="48" y2="48" stroke="white" stroke-width="7" stroke-linecap="round" />
<line x1="96" y1="78" x2="144" y2="48" stroke="white" stroke-width="7" stroke-linecap="round" />
<line x1="96" y1="114" x2="48" y2="144" stroke="white" stroke-width="7" stroke-linecap="round" />
<line x1="96" y1="114" x2="144" y2="144" stroke="white" stroke-width="7" stroke-linecap="round" />
<line x1="96" y1="78" x2="96" y2="30" stroke="white" stroke-width="7" stroke-linecap="round" />
<line x1="96" y1="114" x2="96" y2="162" stroke="white" stroke-width="7" stroke-linecap="round" />
<defs>
<linearGradient id="gradient" x1="0" y1="0" x2="192" y2="192" gradientUnits="userSpaceOnUse">
<stop stop-color="#E54D5E" />
<stop offset="1" stop-color="#C93D4E" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+162 -44
View File
@@ -6,7 +6,7 @@ import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Card, CardSkeleton, Button, Modal } from "@/shared/components";
import { AI_PROVIDERS } from "@/shared/constants/providers";
import { AI_PROVIDERS, FREE_PROVIDERS, OAUTH_PROVIDERS } from "@/shared/constants/providers";
import { useNotificationStore } from "@/store/notificationStore";
export default function HomePageClient({ machineId }) {
@@ -15,6 +15,7 @@ export default function HomePageClient({ machineId }) {
const [loading, setLoading] = useState(true);
const [baseUrl, setBaseUrl] = useState("/v1");
const [selectedProvider, setSelectedProvider] = useState(null);
const [providerMetrics, setProviderMetrics] = useState({});
useEffect(() => {
if (typeof window !== "undefined") {
@@ -24,9 +25,10 @@ export default function HomePageClient({ machineId }) {
const fetchData = useCallback(async () => {
try {
const [provRes, modelsRes] = await Promise.all([
const [provRes, modelsRes, metricsRes] = await Promise.all([
fetch("/api/providers"),
fetch("/api/models"),
fetch("/api/provider-metrics"),
]);
if (provRes.ok) {
const provData = await provRes.json();
@@ -36,6 +38,10 @@ export default function HomePageClient({ machineId }) {
const modelsData = await modelsRes.json();
setModels(modelsData.models || []);
}
if (metricsRes.ok) {
const metricsData = await metricsRes.json();
setProviderMetrics(metricsData.metrics || {});
}
} catch (e) {
console.log("Error fetching data:", e);
} finally {
@@ -68,6 +74,13 @@ export default function HomePageClient({ machineId }) {
const providerKeys = new Set([providerId, providerInfo.alias].filter(Boolean));
const providerModels = models.filter((m) => providerKeys.has(m.provider));
// Determine auth type
const authType = FREE_PROVIDERS[providerId]
? "free"
: OAUTH_PROVIDERS[providerId]
? "oauth"
: "apikey";
return {
id: providerId,
provider: providerInfo,
@@ -75,6 +88,7 @@ export default function HomePageClient({ machineId }) {
connected,
errors,
modelCount: providerModels.length,
authType,
};
});
}, [providerConnections, models]);
@@ -89,10 +103,18 @@ export default function HomePageClient({ machineId }) {
}, [selectedProvider, models]);
const quickStartLinks = [
{ label: "Documentation", href: "/docs" },
{ label: "OpenAI API compatibility", href: "/docs#api-reference" },
{ label: "Cherry/Codex compatibility", href: "/docs#client-compatibility" },
{ label: "Report issue", href: "https://github.com/decolua/omniroute/issues", external: true },
{ label: "Documentation", href: "/docs", icon: "menu_book" },
{ label: "Providers", href: "/dashboard/providers", icon: "dns" },
{ label: "Combos", href: "/dashboard/combos", icon: "layers" },
{ label: "Analytics", href: "/dashboard/analytics", icon: "analytics" },
{ label: "Health Monitor", href: "/dashboard/health", icon: "health_and_safety" },
{ label: "CLI Tools", href: "/dashboard/cli-tools", icon: "terminal" },
{
label: "Report issue",
href: "https://github.com/diegosouzapw/OmniRoute/issues",
external: true,
icon: "bug_report",
},
];
if (loading) {
@@ -110,39 +132,87 @@ export default function HomePageClient({ machineId }) {
<div className="flex flex-col gap-8">
{/* Quick Start */}
<Card>
<div className="flex flex-col gap-4">
<div>
<h2 className="text-lg font-semibold">Quick Start</h2>
<p className="text-sm text-text-muted">
First-time setup checklist for API clients and IDE tools.
</p>
<div className="flex flex-col gap-5">
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold">Quick Start</h2>
<p className="text-sm text-text-muted">
Get up and running in 4 steps. Connect providers, route models, monitor everything.
</p>
</div>
<Link
href="/docs"
className="hidden sm:inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border border-border text-text-muted hover:text-text-main hover:bg-bg-subtle transition-colors"
>
<span className="material-symbols-outlined text-[14px]">menu_book</span>
Full Docs
</Link>
</div>
<ol className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
<li className="rounded-lg border border-border bg-bg-subtle p-3">
<span className="font-semibold">1. Create API key</span>
<p className="text-text-muted mt-1">
Generate one key per environment to isolate usage and revoke safely.
</p>
<li className="rounded-lg border border-border bg-bg-subtle p-4 flex gap-3">
<div className="flex items-center justify-center size-8 rounded-lg bg-primary/10 text-primary shrink-0">
<span className="material-symbols-outlined text-[18px]">key</span>
</div>
<div>
<span className="font-semibold">1. Create API key</span>
<p className="text-text-muted mt-0.5">
Go to{" "}
<Link href="/dashboard/settings" className="text-primary hover:underline">
Settings
</Link>{" "}
API Keys. Generate one key per environment.
</p>
</div>
</li>
<li className="rounded-lg border border-border bg-bg-subtle p-3">
<span className="font-semibold">2. Connect provider account</span>
<p className="text-text-muted mt-1">
Configure providers in Dashboard and validate with Test Connection.
</p>
<li className="rounded-lg border border-border bg-bg-subtle p-4 flex gap-3">
<div className="flex items-center justify-center size-8 rounded-lg bg-green-500/10 text-green-500 shrink-0">
<span className="material-symbols-outlined text-[18px]">dns</span>
</div>
<div>
<span className="font-semibold">2. Connect providers</span>
<p className="text-text-muted mt-0.5">
Add accounts in{" "}
<Link href="/dashboard/providers" className="text-primary hover:underline">
Providers
</Link>
. Supports OAuth, API Key, and free tiers.
</p>
</div>
</li>
<li className="rounded-lg border border-border bg-bg-subtle p-3">
<span className="font-semibold">3. Use endpoint</span>
<p className="text-text-muted mt-1">
Point clients to <code>{currentEndpoint}</code> and send requests to{" "}
<code>/chat/completions</code>.
</p>
<li className="rounded-lg border border-border bg-bg-subtle p-4 flex gap-3">
<div className="flex items-center justify-center size-8 rounded-lg bg-blue-500/10 text-blue-500 shrink-0">
<span className="material-symbols-outlined text-[18px]">link</span>
</div>
<div>
<span className="font-semibold">3. Point your client</span>
<p className="text-text-muted mt-0.5">
Set base URL to{" "}
<code className="px-1.5 py-0.5 rounded bg-surface text-xs font-mono">
{currentEndpoint}
</code>{" "}
in your IDE or API client.
</p>
</div>
</li>
<li className="rounded-lg border border-border bg-bg-subtle p-3">
<span className="font-semibold">4. Monitor usage</span>
<p className="text-text-muted mt-1">
Track requests, tokens, errors, and cost in Usage and Request Logger.
</p>
<li className="rounded-lg border border-border bg-bg-subtle p-4 flex gap-3">
<div className="flex items-center justify-center size-8 rounded-lg bg-amber-500/10 text-amber-500 shrink-0">
<span className="material-symbols-outlined text-[18px]">analytics</span>
</div>
<div>
<span className="font-semibold">4. Monitor & optimize</span>
<p className="text-text-muted mt-0.5">
Track tokens, cost and errors in{" "}
<Link href="/dashboard/usage" className="text-primary hover:underline">
Usage
</Link>{" "}
and{" "}
<Link href="/dashboard/analytics" className="text-primary hover:underline">
Analytics
</Link>
.
</p>
</div>
</li>
</ol>
@@ -156,7 +226,7 @@ export default function HomePageClient({ machineId }) {
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border border-border text-text-muted hover:text-text-main hover:bg-bg-subtle transition-colors"
>
<span className="material-symbols-outlined text-[14px]">
{link.external ? "open_in_new" : "arrow_forward"}
{link.icon || (link.external ? "open_in_new" : "arrow_forward")}
</span>
{link.label}
</a>
@@ -175,13 +245,26 @@ export default function HomePageClient({ machineId }) {
{providerStats.length} available providers
</p>
</div>
<Link
href="/dashboard/providers"
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border border-border text-text-muted hover:text-text-main hover:bg-bg-subtle transition-colors"
>
<span className="material-symbols-outlined text-[14px]">settings</span>
Manage Providers
</Link>
<div className="flex items-center gap-4">
<div className="hidden sm:flex items-center gap-3 text-[11px] text-text-muted">
<span className="flex items-center gap-1">
<span className="size-2 rounded-full bg-green-500" /> Free
</span>
<span className="flex items-center gap-1">
<span className="size-2 rounded-full bg-blue-500" /> OAuth
</span>
<span className="flex items-center gap-1">
<span className="size-2 rounded-full bg-amber-500" /> API Key
</span>
</div>
<Link
href="/dashboard/providers"
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border border-border text-text-muted hover:text-text-main hover:bg-bg-subtle transition-colors"
>
<span className="material-symbols-outlined text-[14px]">settings</span>
Manage
</Link>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
@@ -189,6 +272,7 @@ export default function HomePageClient({ machineId }) {
<ProviderOverviewCard
key={item.id}
item={item}
metrics={providerMetrics[item.provider.alias] || providerMetrics[item.id]}
onClick={() => setSelectedProvider(item)}
/>
))}
@@ -211,12 +295,19 @@ HomePageClient.propTypes = {
machineId: PropTypes.string,
};
function ProviderOverviewCard({ item, onClick }) {
function ProviderOverviewCard({ item, metrics, onClick }) {
const [imgError, setImgError] = useState(false);
const statusVariant =
item.errors > 0 ? "text-red-500" : item.connected > 0 ? "text-green-500" : "text-text-muted";
const authTypeConfig = {
free: { color: "bg-green-500", label: "Free" },
oauth: { color: "bg-blue-500", label: "OAuth" },
apikey: { color: "bg-amber-500", label: "API Key" },
};
const authInfo = authTypeConfig[item.authType] || authTypeConfig.apikey;
return (
<button
onClick={onClick}
@@ -248,15 +339,34 @@ function ProviderOverviewCard({ item, onClick }) {
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-semibold truncate">{item.provider.name}</p>
<div className="flex items-center gap-1.5">
<p className="text-sm font-semibold truncate">{item.provider.name}</p>
<span
className={`size-2 rounded-full ${authInfo.color} shrink-0`}
title={authInfo.label}
/>
</div>
<p className={`text-xs ${statusVariant}`}>
{item.total === 0
? "Not configured"
: `${item.connected} active · ${item.errors} error`}
</p>
{metrics && metrics.totalRequests > 0 && (
<div className="flex items-center gap-2 mt-0.5">
<span className="text-[10px] text-text-muted">
<span className="text-emerald-500">{metrics.totalSuccesses}</span>/
{metrics.totalRequests} reqs
</span>
<span className="text-[10px] text-text-muted">{metrics.successRate}%</span>
<span className="text-[10px] text-text-muted">~{metrics.avgLatencyMs}ms</span>
</div>
)}
</div>
<span className="text-xs text-text-muted">#{item.modelCount}</span>
<div className="text-right shrink-0">
<p className="text-xs font-medium text-text-main">{item.modelCount}</p>
<p className="text-[10px] text-text-muted">models</p>
</div>
</div>
</button>
);
@@ -270,12 +380,20 @@ ProviderOverviewCard.propTypes = {
name: PropTypes.string.isRequired,
color: PropTypes.string,
textIcon: PropTypes.string,
alias: PropTypes.string,
}).isRequired,
total: PropTypes.number.isRequired,
connected: PropTypes.number.isRequired,
errors: PropTypes.number.isRequired,
modelCount: PropTypes.number.isRequired,
authType: PropTypes.string.isRequired,
}).isRequired,
metrics: PropTypes.shape({
totalRequests: PropTypes.number,
totalSuccesses: PropTypes.number,
successRate: PropTypes.number,
avgLatencyMs: PropTypes.number,
}),
onClick: PropTypes.func.isRequired,
};
@@ -0,0 +1,77 @@
"use client";
import { useState } from "react";
import { cn } from "@/shared/utils/cn";
import BudgetTab from "../usage/components/BudgetTab";
import PricingTab from "../settings/components/PricingTab";
const sections = [
{
id: "budget",
label: "Budget",
icon: "account_balance_wallet",
description: "Daily and monthly spend limits",
},
{
id: "pricing",
label: "Pricing",
icon: "payments",
description: "Per-model cost configuration",
},
];
export default function CostsPage() {
const [activeSection, setActiveSection] = useState("budget");
return (
<div className="flex flex-col gap-6">
{/* Header */}
<div>
<h1 className="text-2xl font-semibold">Costs</h1>
<p className="text-sm text-text-muted mt-1">
Budget limits and model pricing configuration
</p>
</div>
{/* Layout: sidebar + content */}
<div className="flex gap-6">
{/* Sidebar */}
<nav className="shrink-0 w-48">
<div className="flex flex-col gap-1 sticky top-4">
{sections.map((section) => (
<button
key={section.id}
onClick={() => setActiveSection(section.id)}
className={cn(
"flex items-center gap-2.5 px-3 py-2.5 rounded-lg text-sm font-medium transition-all text-left w-full",
activeSection === section.id
? "bg-primary/10 text-primary shadow-sm"
: "text-text-muted hover:text-text-main hover:bg-black/5 dark:hover:bg-white/5"
)}
>
<span className="material-symbols-outlined text-[18px]">{section.icon}</span>
<div className="min-w-0">
<div>{section.label}</div>
<div
className={cn(
"text-[10px] font-normal truncate",
activeSection === section.id ? "text-primary/70" : "text-text-muted"
)}
>
{section.description}
</div>
</div>
</button>
))}
</div>
</nav>
{/* Content */}
<div className="flex-1 min-w-0">
{activeSection === "budget" && <BudgetTab />}
{activeSection === "pricing" && <PricingTab />}
</div>
</div>
</div>
);
}
@@ -309,7 +309,11 @@ export default function APIPageClient({ machineId }) {
{ label: "Documentation", href: "/docs" },
{ label: "OpenAI API compatibility", href: "/docs#api-reference" },
{ label: "Cherry/Codex compatibility", href: "/docs#client-compatibility" },
{ label: "Report issue", href: "https://github.com/decolua/omniroute/issues", external: true },
{
label: "Report issue",
href: "https://github.com/diegosouzapw/OmniRoute/issues",
external: true,
},
];
return (
+107 -24
View File
@@ -14,6 +14,7 @@
import { useState, useEffect, useCallback } from "react";
import { Card } from "@/shared/components";
import { AI_PROVIDERS } from "@/shared/constants/providers";
function formatUptime(seconds) {
const d = Math.floor(seconds / 86400);
@@ -324,38 +325,120 @@ export default function HealthPage() {
{/* Provider Health */}
<Card className="p-5" role="region" aria-label="Provider health status">
<h2 className="text-lg font-semibold text-text-main mb-4 flex items-center gap-2">
<span className="material-symbols-outlined text-[20px] text-primary">
health_and_safety
</span>
Provider Health
</h2>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-text-main flex items-center gap-2">
<span className="material-symbols-outlined text-[20px] text-primary">
health_and_safety
</span>
Provider Health
</h2>
{cbEntries.length > 0 && (
<div className="flex items-center gap-3 text-xs text-text-muted">
<span className="flex items-center gap-1">
<span className="size-2 rounded-full bg-green-500" /> Healthy
</span>
<span className="flex items-center gap-1">
<span className="size-2 rounded-full bg-amber-500" /> Recovering
</span>
<span className="flex items-center gap-1">
<span className="size-2 rounded-full bg-red-500" /> Down
</span>
</div>
)}
</div>
{cbEntries.length === 0 ? (
<p className="text-sm text-text-muted text-center py-4">
No circuit breaker data available. Make some requests first.
</p>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{cbEntries.map(([provider, cb]) => {
const style = CB_COLORS[cb.state] || CB_COLORS.CLOSED;
return (
<div key={provider} className={`rounded-lg p-3 ${style.bg} border border-white/5`}>
<div className="flex items-center justify-between mb-1">
<span className="text-sm font-medium text-text-main">{provider}</span>
<span className={`text-xs font-semibold ${style.text}`}>{style.label}</span>
(() => {
const unhealthy = cbEntries.filter(([, cb]) => cb.state !== "CLOSED");
const healthy = cbEntries.filter(([, cb]) => cb.state === "CLOSED");
return (
<div className="space-y-4">
{/* Unhealthy providers first */}
{unhealthy.length > 0 && (
<div className="space-y-2">
<p className="text-xs font-medium text-red-400 uppercase tracking-wide">
Issues Detected
</p>
{unhealthy.map(([provider, cb]) => {
const style = CB_COLORS[cb.state] || CB_COLORS.OPEN;
const providerInfo = AI_PROVIDERS[provider];
const displayName = providerInfo?.name || provider;
return (
<div
key={provider}
className={`rounded-lg p-3 ${style.bg} border border-white/5 flex items-center gap-3`}
>
<div
className="size-8 rounded-lg flex items-center justify-center shrink-0 text-xs font-bold"
style={{
backgroundColor: `${providerInfo?.color || "#888"}15`,
color: providerInfo?.color || "#888",
}}
>
{providerInfo?.textIcon || provider.slice(0, 2).toUpperCase()}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-text-main truncate">
{displayName}
</span>
<span
className={`text-xs font-semibold px-1.5 py-0.5 rounded ${style.bg} ${style.text}`}
>
{style.label}
</span>
</div>
<div className="text-xs text-text-muted mt-0.5">
{cb.failures} failure{cb.failures !== 1 ? "s" : ""}
{cb.lastFailure && (
<span className="ml-2">
· Last: {new Date(cb.lastFailure).toLocaleTimeString()}
</span>
)}
</div>
</div>
</div>
);
})}
</div>
<div className="text-xs text-text-muted">
Failures: {cb.failures || 0}
{cb.lastFailure && (
<span className="ml-2">
Last: {new Date(cb.lastFailure).toLocaleTimeString()}
</span>
)}
{/* Healthy providers in compact grid */}
{healthy.length > 0 && (
<div>
{unhealthy.length > 0 && (
<p className="text-xs font-medium text-green-400 uppercase tracking-wide mb-2">
Operational
</p>
)}
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-2">
{healthy.map(([provider]) => {
const providerInfo = AI_PROVIDERS[provider];
const displayName = providerInfo?.name || provider;
return (
<div
key={provider}
className="rounded-lg p-2.5 bg-green-500/5 border border-white/5 flex items-center gap-2"
>
<span className="size-2 rounded-full bg-green-500 shrink-0" />
<span
className="text-xs font-medium text-text-main truncate"
title={displayName}
>
{displayName}
</span>
</div>
);
})}
</div>
</div>
</div>
);
})}
</div>
)}
</div>
);
})()
)}
</Card>
@@ -418,14 +418,30 @@ export default function ProviderDetailPage() {
}
if (providerInfo.passthroughModels) {
return (
<PassthroughModelsSection
providerAlias={providerAlias}
modelAliases={modelAliases}
copied={copied}
onCopy={copy}
onSetAlias={handleSetAlias}
onDeleteAlias={handleDeleteAlias}
/>
<div>
<div className="flex items-center gap-2 mb-4">
<Button
size="sm"
variant="secondary"
icon="download"
onClick={handleImportModels}
disabled={!canImportModels || importingModels}
>
{importingModels ? "Importing..." : "Import from /models"}
</Button>
{!canImportModels && (
<span className="text-xs text-text-muted">Add a connection to enable importing.</span>
)}
</div>
<PassthroughModelsSection
providerAlias={providerAlias}
modelAliases={modelAliases}
copied={copied}
onCopy={copy}
onSetAlias={handleSetAlias}
onDeleteAlias={handleDeleteAlias}
/>
</div>
);
}
@@ -717,9 +733,7 @@ export default function ProviderDetailPage() {
{/* Models */}
<Card>
<h2 className="text-lg font-semibold mb-4">
{providerInfo.passthroughModels ? "Model Aliases" : "Available Models"}
</h2>
<h2 className="text-lg font-semibold mb-4">Available Models</h2>
{renderModelsSection()}
{/* Custom Models — available for ALL providers */}
@@ -202,7 +202,9 @@ export default function ProvidersPage() {
{/* OAuth Providers */}
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold">OAuth Providers</h2>
<h2 className="text-xl font-semibold flex items-center gap-2">
OAuth Providers <span className="size-2.5 rounded-full bg-blue-500" title="OAuth" />
</h2>
<div className="flex items-center gap-2">
<ModelAvailabilityBadge />
<button
@@ -230,6 +232,7 @@ export default function ProvidersPage() {
providerId={key}
provider={info}
stats={getProviderStats(key, "oauth")}
authType="oauth"
/>
))}
</div>
@@ -238,7 +241,9 @@ export default function ProvidersPage() {
{/* Free Providers */}
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold">Free Providers</h2>
<h2 className="text-xl font-semibold flex items-center gap-2">
Free Providers <span className="size-2.5 rounded-full bg-green-500" title="Free" />
</h2>
<button
onClick={() => handleBatchTest("free")}
disabled={!!testingMode}
@@ -263,6 +268,7 @@ export default function ProvidersPage() {
providerId={key}
provider={info}
stats={getProviderStats(key, "oauth")}
authType="free"
/>
))}
</div>
@@ -271,7 +277,10 @@ export default function ProvidersPage() {
{/* API Key Providers — fixed list */}
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold">API Key Providers</h2>
<h2 className="text-xl font-semibold flex items-center gap-2">
API Key Providers{" "}
<span className="size-2.5 rounded-full bg-amber-500" title="API Key" />
</h2>
<button
onClick={() => handleBatchTest("apikey")}
disabled={!!testingMode}
@@ -296,6 +305,7 @@ export default function ProvidersPage() {
providerId={key}
provider={info}
stats={getProviderStats(key, "apikey")}
authType="apikey"
/>
))}
</div>
@@ -304,7 +314,10 @@ export default function ProvidersPage() {
{/* API Key Compatible Providers — dynamic (OpenAI/Anthropic compatible) */}
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold">API Key Compatible Providers</h2>
<h2 className="text-xl font-semibold flex items-center gap-2">
API Key Compatible Providers{" "}
<span className="size-2.5 rounded-full bg-orange-500" title="Compatible" />
</h2>
<div className="flex gap-2">
{(compatibleProviders.length > 0 || anthropicCompatibleProviders.length > 0) && (
<button
@@ -355,6 +368,7 @@ export default function ProvidersPage() {
providerId={info.id}
provider={info}
stats={getProviderStats(info.id, "apikey")}
authType="compatible"
/>
))}
</div>
@@ -407,10 +421,18 @@ export default function ProvidersPage() {
);
}
function ProviderCard({ providerId, provider, stats }) {
function ProviderCard({ providerId, provider, stats, authType }) {
const { connected, error, errorCode, errorTime } = stats;
const [imgError, setImgError] = useState(false);
const dotColors = {
free: "bg-green-500",
oauth: "bg-blue-500",
apikey: "bg-amber-500",
compatible: "bg-orange-500",
};
const dotLabels = { free: "Free", oauth: "OAuth", apikey: "API Key", compatible: "Compatible" };
return (
<Link href={`/dashboard/providers/${providerId}`} className="group">
<Card
@@ -440,7 +462,13 @@ function ProviderCard({ providerId, provider, stats }) {
)}
</div>
<div>
<h3 className="font-semibold">{provider.name}</h3>
<h3 className="font-semibold flex items-center gap-1.5">
{provider.name}
<span
className={`size-2 rounded-full ${dotColors[authType] || dotColors.oauth} shrink-0`}
title={dotLabels[authType] || "OAuth"}
/>
</h3>
<div className="flex items-center gap-2 text-xs flex-wrap">
{getStatusDisplay(connected, error, errorCode)}
{errorTime && <span className="text-text-muted"> {errorTime}</span>}
@@ -470,15 +498,24 @@ ProviderCard.propTypes = {
errorCode: PropTypes.string,
errorTime: PropTypes.string,
}).isRequired,
authType: PropTypes.string,
};
// API Key providers - use image with textIcon fallback (same as OAuth providers)
function ApiKeyProviderCard({ providerId, provider, stats }) {
function ApiKeyProviderCard({ providerId, provider, stats, authType }) {
const { connected, error, errorCode, errorTime } = stats;
const isCompatible = providerId.startsWith(OPENAI_COMPATIBLE_PREFIX);
const isAnthropicCompatible = providerId.startsWith(ANTHROPIC_COMPATIBLE_PREFIX);
const [imgError, setImgError] = useState(false);
const dotColors = {
free: "bg-green-500",
oauth: "bg-blue-500",
apikey: "bg-amber-500",
compatible: "bg-orange-500",
};
const dotLabels = { free: "Free", oauth: "OAuth", apikey: "API Key", compatible: "Compatible" };
// Determine icon path: OpenAI Compatible providers use specialized icons
const getIconPath = () => {
if (isCompatible) {
@@ -519,7 +556,13 @@ function ApiKeyProviderCard({ providerId, provider, stats }) {
)}
</div>
<div>
<h3 className="font-semibold">{provider.name}</h3>
<h3 className="font-semibold flex items-center gap-1.5">
{provider.name}
<span
className={`size-2 rounded-full ${dotColors[authType] || dotColors.apikey} shrink-0`}
title={dotLabels[authType] || "API Key"}
/>
</h3>
<div className="flex items-center gap-2 text-xs flex-wrap">
{getStatusDisplay(connected, error, errorCode)}
{isCompatible && (
@@ -560,6 +603,7 @@ ApiKeyProviderCard.propTypes = {
errorCode: PropTypes.string,
errorTime: PropTypes.string,
}).isRequired,
authType: PropTypes.string,
};
function AddOpenAICompatibleModal({ isOpen, onClose, onCreated }) {
@@ -11,7 +11,6 @@ import ProxyTab from "./components/ProxyTab";
import AppearanceTab from "./components/AppearanceTab";
import ThinkingBudgetTab from "./components/ThinkingBudgetTab";
import SystemPromptTab from "./components/SystemPromptTab";
import PricingTab from "./components/PricingTab";
import ComplianceTab from "./components/ComplianceTab";
import CacheStatsCard from "./components/CacheStatsCard";
import ResilienceTab from "./components/ResilienceTab";
@@ -22,7 +21,6 @@ const tabs = [
{ id: "security", label: "Security", icon: "shield" },
{ id: "routing", label: "Routing", icon: "route" },
{ id: "resilience", label: "Resilience", icon: "electrical_services" },
{ id: "pricing", label: "Pricing", icon: "payments" },
{ id: "advanced", label: "Advanced", icon: "tune" },
{ id: "compliance", label: "Compliance", icon: "policy" },
];
@@ -88,8 +86,6 @@ export default function SettingsPage() {
</div>
)}
{activeTab === "pricing" && <PricingTab />}
{activeTab === "resilience" && <ResilienceTab />}
{activeTab === "advanced" && (
+2 -5
View File
@@ -5,19 +5,17 @@ import { RequestLoggerV2, ProxyLogger, CardSkeleton, SegmentedControl } from "@/
import ProviderLimits from "./components/ProviderLimits";
import SessionsTab from "./components/SessionsTab";
import RateLimitStatus from "./components/RateLimitStatus";
import BudgetTab from "./components/BudgetTab";
export default function UsagePage() {
const [activeTab, setActiveTab] = useState("limits");
const [activeTab, setActiveTab] = useState("logs");
return (
<div className="flex flex-col gap-6">
<SegmentedControl
options={[
{ value: "limits", label: "Limits" },
{ value: "logs", label: "Logger" },
{ value: "proxy-logs", label: "Proxy" },
{ value: "budget", label: "Budget" },
{ value: "limits", label: "Limits" },
]}
value={activeTab}
onChange={setActiveTab}
@@ -35,7 +33,6 @@ export default function UsagePage() {
)}
{activeTab === "logs" && <RequestLoggerV2 />}
{activeTab === "proxy-logs" && <ProxyLogger />}
{activeTab === "budget" && <BudgetTab />}
</div>
);
}
+7 -6
View File
@@ -31,14 +31,15 @@ export async function GET() {
platform: process.platform,
};
// Provider health summary
// Provider health summary (circuitBreakers is an Array of { name, state, ... })
const providerHealth = {};
for (const [key, cb] of Object.entries(circuitBreakers)) {
providerHealth[key] = {
for (const cb of circuitBreakers) {
// Skip test circuit breakers (leftover from unit tests)
if (cb.name.startsWith("test-") || cb.name.startsWith("test_")) continue;
providerHealth[cb.name] = {
state: cb.state,
failures: cb.failures,
lastFailure: cb.lastFailure,
nextRetry: cb.nextRetry,
failures: cb.failureCount || 0,
lastFailure: cb.lastFailureTime,
};
}
+40
View File
@@ -0,0 +1,40 @@
import { NextResponse } from "next/server";
import { getDbInstance } from "@/lib/db/core.js";
/**
* GET /api/providers/metrics Aggregate per-provider stats from call_logs
* Returns: { metrics: { [provider]: { totalRequests, totalSuccesses, successRate, avgLatencyMs } } }
*/
export async function GET() {
try {
const db = getDbInstance();
const rows = db
.prepare(
`SELECT
provider,
COUNT(*) as totalRequests,
SUM(CASE WHEN status >= 200 AND status < 400 THEN 1 ELSE 0 END) as totalSuccesses,
ROUND(AVG(duration)) as avgLatencyMs
FROM call_logs
WHERE provider IS NOT NULL AND provider != '-'
GROUP BY provider`
)
.all();
const metrics = {};
for (const row of rows) {
metrics[row.provider] = {
totalRequests: row.totalRequests,
totalSuccesses: row.totalSuccesses,
successRate:
row.totalRequests > 0 ? Math.round((row.totalSuccesses / row.totalRequests) * 100) : 0,
avgLatencyMs: row.avgLatencyMs || 0,
};
}
return NextResponse.json({ metrics });
} catch (error) {
console.error("[providers/metrics] Error:", error);
return NextResponse.json({ metrics: {} });
}
}
+8 -8
View File
@@ -23,6 +23,13 @@ const STATIC_MODEL_PROVIDERS = {
{ id: "nanobanana-flash", name: "NanoBanana Flash (Gemini 2.5 Flash)" },
{ id: "nanobanana-pro", name: "NanoBanana Pro (Gemini 3 Pro)" },
],
perplexity: () => [
{ id: "sonar", name: "Sonar (Fast Search)" },
{ id: "sonar-pro", name: "Sonar Pro (Advanced Search)" },
{ id: "sonar-reasoning", name: "Sonar Reasoning (CoT + Search)" },
{ id: "sonar-reasoning-pro", name: "Sonar Reasoning Pro (Advanced CoT + Search)" },
{ id: "sonar-deep-research", name: "Sonar Deep Research (Expert Analysis)" },
],
};
// Provider models endpoints configuration
@@ -142,14 +149,7 @@ const PROVIDER_MODELS_CONFIG = {
authPrefix: "Bearer ",
parseResponse: (data) => data.data || data.models || [],
},
perplexity: {
url: "https://api.perplexity.ai/models",
method: "GET",
headers: { "Content-Type": "application/json" },
authHeader: "Authorization",
authPrefix: "Bearer ",
parseResponse: (data) => data.data || data.models || [],
},
together: {
url: "https://api.together.xyz/v1/models",
method: "GET",
+288 -26
View File
@@ -1,13 +1,67 @@
import Link from "next/link";
import { APP_CONFIG } from "@/shared/constants/config";
import { FREE_PROVIDERS, OAUTH_PROVIDERS, APIKEY_PROVIDERS } from "@/shared/constants/providers";
const endpointRows = [
{ path: "/v1/chat/completions", note: "OpenAI-compatible chat endpoint (default)." },
{ path: "/v1/responses", note: "Responses API endpoint (supported)." },
{ path: "/v1/models", note: "Model catalog for connected providers." },
{ path: "/chat/completions", note: "Rewrite helper for clients that do not include /v1." },
{ path: "/responses", note: "Rewrite helper for Responses clients without /v1." },
{ path: "/models", note: "Rewrite helper for model discovery without /v1." },
{
path: "/v1/chat/completions",
method: "POST",
note: "OpenAI-compatible chat endpoint (default).",
},
{ path: "/v1/responses", method: "POST", note: "Responses API endpoint (Codex, o-series)." },
{ path: "/v1/models", method: "GET", note: "Model catalog for all connected providers." },
{
path: "/v1/audio/transcriptions",
method: "POST",
note: "Audio transcription (Deepgram, AssemblyAI).",
},
{ path: "/v1/images/generations", method: "POST", note: "Image generation (NanoBanana)." },
{ path: "/chat/completions", method: "POST", note: "Rewrite helper for clients without /v1." },
{ path: "/responses", method: "POST", note: "Rewrite helper for Responses without /v1." },
{ path: "/models", method: "GET", note: "Rewrite helper for model discovery without /v1." },
];
const featureItems = [
{
icon: "hub",
title: "Multi-Provider Routing",
text: "Route requests to 30+ AI providers through a single OpenAI-compatible endpoint. Supports chat, responses, audio, and image APIs.",
},
{
icon: "layers",
title: "Combos & Balancing",
text: "Create model combos with fallback chains and balancing strategies: round-robin, priority, random, least-used, and cost-optimized.",
},
{
icon: "bar_chart",
title: "Usage & Cost Tracking",
text: "Real-time token counting, cost calculation per provider/model, and detailed usage breakdown by API key and account.",
},
{
icon: "analytics",
title: "Analytics Dashboard",
text: "Visual analytics with charts for requests, tokens, errors, latency, costs, and model popularity over time.",
},
{
icon: "health_and_safety",
title: "Health Monitoring",
text: "Live health checks, provider status, circuit breaker states, and automatic rate limit detection with exponential backoff.",
},
{
icon: "terminal",
title: "CLI Tools",
text: "Manage IDE configurations, export/import backups, discover codex profiles, and configure settings from the dashboard.",
},
{
icon: "shield",
title: "Security & Policies",
text: "API key authentication, IP filtering, prompt injection guard, domain policies, session management, and audit logging.",
},
{
icon: "cloud_sync",
title: "Cloud Sync",
text: "Sync your configuration to Cloudflare Workers for remote access with encrypted credentials and automatic failover.",
},
];
const useCases = [
@@ -21,7 +75,7 @@ const useCases = [
},
{
title: "Usage, cost and debug visibility",
text: "Track tokens/cost by provider, account and API key in Usage + Logger tabs.",
text: "Track tokens/cost by provider, account and API key in Usage + Analytics tabs.",
},
];
@@ -30,8 +84,36 @@ const troubleshootingItems = [
"If you receive ambiguous model errors, pick a provider prefix instead of a bare model ID.",
"For GitHub Codex-family models, keep model as gh/<codex-model>; router selects /responses automatically.",
"Use Dashboard > Providers > Test Connection before testing from IDEs or external clients.",
"If a provider shows circuit breaker open, wait for the cooldown or check Health page for details.",
"For OAuth providers, re-authenticate if tokens expire. Check the provider card status indicator.",
];
function ProviderTable({ title, providers, colorDot }) {
const entries = Object.values(providers);
return (
<div className="rounded-lg border border-border bg-bg p-4">
<div className="flex items-center gap-2 mb-3">
<span className={`size-2.5 rounded-full ${colorDot}`} />
<h3 className="font-semibold">{title}</h3>
<span className="text-xs text-text-muted ml-auto">{entries.length} providers</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-1 text-sm">
{entries.map((p) => (
<div
key={p.id}
className="flex items-center justify-between py-1.5 border-b border-border/40 last:border-0"
>
<span className="font-medium">{p.name}</span>
<code className="text-xs text-text-muted px-1.5 py-0.5 rounded bg-bg-subtle">
{p.alias}/
</code>
</div>
))}
</div>
</div>
);
}
export default function DocsPage() {
return (
<div className="min-h-screen bg-bg text-text-main">
@@ -40,24 +122,37 @@ export default function DocsPage() {
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<p className="text-xs uppercase tracking-wider text-text-muted">
In-App Documentation
Documentation v{APP_CONFIG.version}
</p>
<h1 className="text-3xl md:text-4xl font-bold mt-1">{APP_CONFIG.name} Docs</h1>
<p className="text-sm md:text-base text-text-muted mt-2 max-w-3xl">
Quick setup, client compatibility notes, and endpoint reference to run
OpenAI-compatible clients, Codex/Copilot models, and Cherry Studio integrations on
this server.
AI gateway for multi-provider LLMs. One endpoint for OpenAI, Anthropic, Gemini,
GitHub Copilot, Claude Code, Cursor, and 20+ more providers.
</p>
</div>
<div className="flex flex-wrap gap-2">
<Link
href="/dashboard"
className="px-3 py-2 rounded-lg border border-border text-sm hover:bg-bg transition-colors"
>
Open Dashboard
</Link>
<Link
href="/dashboard/endpoint"
className="px-3 py-2 rounded-lg border border-border text-sm hover:bg-bg transition-colors"
>
Open Endpoint Page
Endpoint Page
</Link>
<a
href="https://github.com/decolua/omniroute/issues"
href="https://github.com/diegosouzapw/OmniRoute"
target="_blank"
rel="noopener noreferrer"
className="px-3 py-2 rounded-lg border border-border text-sm hover:bg-bg transition-colors flex items-center gap-1"
>
GitHub <span className="material-symbols-outlined text-[14px]">open_in_new</span>
</a>
<a
href="https://github.com/diegosouzapw/OmniRoute/issues"
target="_blank"
rel="noopener noreferrer"
className="px-3 py-2 rounded-lg border border-border text-sm hover:bg-bg transition-colors"
@@ -68,35 +163,118 @@ export default function DocsPage() {
</div>
</header>
{/* Table of Contents */}
<nav className="rounded-2xl border border-border bg-bg-subtle p-6">
<h2 className="text-sm font-semibold uppercase tracking-wider text-text-muted mb-3">
On this page
</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-sm">
{[
{ href: "#quick-start", label: "Quick Start" },
{ href: "#features", label: "Features" },
{ href: "#supported-providers", label: "Providers" },
{ href: "#use-cases", label: "Use Cases" },
{ href: "#client-compatibility", label: "Client Compatibility" },
{ href: "#api-reference", label: "API Reference" },
{ href: "#model-prefixes", label: "Model Prefixes" },
{ href: "#troubleshooting", label: "Troubleshooting" },
].map((item) => (
<a
key={item.href}
href={item.href}
className="flex items-center gap-1.5 px-3 py-2 rounded-lg border border-border hover:bg-bg transition-colors"
>
<span className="material-symbols-outlined text-[14px] text-text-muted">tag</span>
{item.label}
</a>
))}
</div>
</nav>
<section id="quick-start" className="rounded-2xl border border-border bg-bg-subtle p-6">
<h2 className="text-xl font-semibold">Quick Start</h2>
<ol className="mt-4 grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
<li className="rounded-lg border border-border p-3 bg-bg">
<span className="font-semibold">1. Create API key</span>
<p className="text-text-muted mt-1">Generate one key per app/environment.</p>
</li>
<li className="rounded-lg border border-border p-3 bg-bg">
<span className="font-semibold">2. Connect providers</span>
<span className="font-semibold">1. Install & run</span>
<p className="text-text-muted mt-1">
Add provider accounts in Dashboard and run Test Connection.
<code className="px-1 rounded bg-bg-subtle">npx omniroute</code> or clone from
GitHub and run <code className="px-1 rounded bg-bg-subtle">npm start</code>.
</p>
</li>
<li className="rounded-lg border border-border p-3 bg-bg">
<span className="font-semibold">3. Set client base URL</span>
<span className="font-semibold">2. Create API key</span>
<p className="text-text-muted mt-1">
Prefer <code className="px-1 rounded bg-bg-subtle">https://&lt;host&gt;/v1</code>.
Go to Settings API Keys. Generate one key per environment.
</p>
</li>
<li className="rounded-lg border border-border p-3 bg-bg">
<span className="font-semibold">4. Choose model</span>
<span className="font-semibold">3. Connect providers</span>
<p className="text-text-muted mt-1">
Prefer explicit provider prefix, for example{" "}
Add provider accounts via OAuth login, API key, or free-tier auto-connect.
</p>
</li>
<li className="rounded-lg border border-border p-3 bg-bg">
<span className="font-semibold">4. Set client base URL</span>
<p className="text-text-muted mt-1">
Point your IDE or API client to{" "}
<code className="px-1 rounded bg-bg-subtle">https://&lt;host&gt;/v1</code>. Use
provider prefix, e.g.{" "}
<code className="px-1 rounded bg-bg-subtle">gh/gpt-5.1-codex</code>.
</p>
</li>
</ol>
</section>
{/* Features */}
<section id="features" className="rounded-2xl border border-border bg-bg-subtle p-6">
<h2 className="text-xl font-semibold">Features</h2>
<div className="mt-4 grid grid-cols-1 md:grid-cols-2 gap-3">
{featureItems.map((item) => (
<article
key={item.title}
className="rounded-lg border border-border p-4 bg-bg flex gap-3"
>
<span className="material-symbols-outlined text-[20px] text-primary shrink-0 mt-0.5">
{item.icon}
</span>
<div>
<h3 className="font-semibold text-sm">{item.title}</h3>
<p className="text-sm text-text-muted mt-1">{item.text}</p>
</div>
</article>
))}
</div>
</section>
{/* Supported Providers */}
<section
id="supported-providers"
className="rounded-2xl border border-border bg-bg-subtle p-6"
>
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="text-xl font-semibold">Supported Providers</h2>
<p className="text-sm text-text-muted mt-1">
{Object.keys(FREE_PROVIDERS).length +
Object.keys(OAUTH_PROVIDERS).length +
Object.keys(APIKEY_PROVIDERS).length}{" "}
providers across three connection types.
</p>
</div>
<Link
href="/dashboard/providers"
className="px-3 py-2 rounded-lg border border-border text-sm hover:bg-bg transition-colors"
>
Manage Providers
</Link>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
<ProviderTable title="Free Tier" providers={FREE_PROVIDERS} colorDot="bg-green-500" />
<ProviderTable title="OAuth" providers={OAUTH_PROVIDERS} colorDot="bg-blue-500" />
<ProviderTable title="API Key" providers={APIKEY_PROVIDERS} colorDot="bg-amber-500" />
</div>
</section>
<section id="use-cases" className="rounded-2xl border border-border bg-bg-subtle p-6">
<h2 className="text-xl font-semibold">Common Use Cases</h2>
<div className="mt-4 grid grid-cols-1 md:grid-cols-3 gap-3">
@@ -115,7 +293,7 @@ export default function DocsPage() {
>
<h2 className="text-xl font-semibold">Client Compatibility</h2>
<div className="mt-4 grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
<article id="cherry-studio" className="rounded-lg border border-border p-4 bg-bg">
<article className="rounded-lg border border-border p-4 bg-bg">
<h3 className="font-semibold">Cherry Studio</h3>
<ul className="mt-2 text-text-muted space-y-1">
<li>
@@ -133,7 +311,7 @@ export default function DocsPage() {
</li>
</ul>
</article>
<article id="codex-copilot" className="rounded-lg border border-border p-4 bg-bg">
<article className="rounded-lg border border-border p-4 bg-bg">
<h3 className="font-semibold">Codex / GitHub Copilot Models</h3>
<ul className="mt-2 text-text-muted space-y-1">
<li>
@@ -149,15 +327,38 @@ export default function DocsPage() {
</li>
</ul>
</article>
<article className="rounded-lg border border-border p-4 bg-bg">
<h3 className="font-semibold">Cursor IDE</h3>
<ul className="mt-2 text-text-muted space-y-1">
<li>
Use <code className="px-1 rounded bg-bg-subtle">cu/</code> prefix for Cursor
models.
</li>
<li>OAuth connection login from the Providers page.</li>
<li>Supports both chat and responses endpoints.</li>
</ul>
</article>
<article className="rounded-lg border border-border p-4 bg-bg">
<h3 className="font-semibold">Claude Code / Antigravity</h3>
<ul className="mt-2 text-text-muted space-y-1">
<li>
Use <code className="px-1 rounded bg-bg-subtle">cc/</code> (Claude) or{" "}
<code className="px-1 rounded bg-bg-subtle">ag/</code> (Antigravity) prefix.
</li>
<li>OAuth connection with automatic token refresh.</li>
<li>Full streaming support for all models.</li>
</ul>
</article>
</div>
</section>
<section id="api-reference" className="rounded-2xl border border-border bg-bg-subtle p-6">
<h2 className="text-xl font-semibold">Endpoint Reference</h2>
<h2 className="text-xl font-semibold">API Reference</h2>
<div className="mt-4 overflow-x-auto">
<table className="w-full text-sm border-collapse">
<thead>
<tr className="border-b border-border">
<th className="text-left py-2 pr-4">Method</th>
<th className="text-left py-2 pr-4">Path</th>
<th className="text-left py-2">Notes</th>
</tr>
@@ -165,6 +366,11 @@ export default function DocsPage() {
<tbody>
{endpointRows.map((row) => (
<tr key={row.path} className="border-b border-border/60">
<td className="py-2 pr-4">
<code className="px-1.5 py-0.5 rounded bg-bg text-xs font-semibold">
{row.method}
</code>
</td>
<td className="py-2 pr-4 font-mono">{row.path}</td>
<td className="py-2 text-text-muted">{row.note}</td>
</tr>
@@ -174,6 +380,62 @@ export default function DocsPage() {
</div>
</section>
{/* Model prefixes */}
<section id="model-prefixes" className="rounded-2xl border border-border bg-bg-subtle p-6">
<h2 className="text-xl font-semibold">Model Prefixes</h2>
<p className="text-sm text-text-muted mt-2 mb-4">
Use the provider prefix before the model name to route to a specific provider. Example:{" "}
<code className="px-1 rounded bg-bg">gh/gpt-5.1-codex</code> routes to GitHub Copilot.
</p>
<div className="overflow-x-auto">
<table className="w-full text-sm border-collapse">
<thead>
<tr className="border-b border-border">
<th className="text-left py-2 pr-4">Prefix</th>
<th className="text-left py-2 pr-4">Provider</th>
<th className="text-left py-2">Type</th>
</tr>
</thead>
<tbody>
{[
...Object.values(FREE_PROVIDERS).map((p) => ({ ...p, type: "Free" })),
...Object.values(OAUTH_PROVIDERS).map((p) => ({ ...p, type: "OAuth" })),
...Object.values(APIKEY_PROVIDERS).map((p) => ({ ...p, type: "API Key" })),
].map((p) => (
<tr key={p.id} className="border-b border-border/60">
<td className="py-2 pr-4 font-mono">
<code className="px-1.5 py-0.5 rounded bg-bg">{p.alias}/</code>
</td>
<td className="py-2 pr-4">{p.name}</td>
<td className="py-2">
<span
className={`inline-flex items-center gap-1 text-xs ${
p.type === "Free"
? "text-green-500"
: p.type === "OAuth"
? "text-blue-500"
: "text-amber-500"
}`}
>
<span
className={`size-1.5 rounded-full ${
p.type === "Free"
? "bg-green-500"
: p.type === "OAuth"
? "bg-blue-500"
: "bg-amber-500"
}`}
/>
{p.type}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
<section id="troubleshooting" className="rounded-2xl border border-border bg-bg-subtle p-6">
<h2 className="text-xl font-semibold">Troubleshooting</h2>
<ul className="mt-4 list-disc list-inside text-sm text-text-muted space-y-2">
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

+7 -6
View File
@@ -1,4 +1,5 @@
"use client";
import OmniRouteLogo from "@/shared/components/OmniRouteLogo";
export default function Footer() {
return (
@@ -9,7 +10,7 @@ export default function Footer() {
<div className="col-span-2 lg:col-span-2">
<div className="flex items-center gap-3 mb-6">
<div className="size-6 rounded bg-[#E54D5E] flex items-center justify-center text-white">
<span className="material-symbols-outlined text-[16px]">hub</span>
<OmniRouteLogo size={16} className="text-white" />
</div>
<h3 className="text-white text-lg font-bold">OmniRoute</h3>
</div>
@@ -20,7 +21,7 @@ export default function Footer() {
<div className="flex gap-4">
<a
className="text-gray-400 hover:text-white transition-colors"
href="https://github.com/decolua/omniroute"
href="https://github.com/diegosouzapw/OmniRoute"
target="_blank"
rel="noopener noreferrer"
>
@@ -46,7 +47,7 @@ export default function Footer() {
</a>
<a
className="text-gray-400 hover:text-[#E54D5E] text-sm transition-colors"
href="https://github.com/decolua/omniroute/releases"
href="https://github.com/diegosouzapw/OmniRoute/releases"
target="_blank"
rel="noopener noreferrer"
>
@@ -65,7 +66,7 @@ export default function Footer() {
</a>
<a
className="text-gray-400 hover:text-[#E54D5E] text-sm transition-colors"
href="https://github.com/decolua/omniroute"
href="https://github.com/diegosouzapw/OmniRoute"
target="_blank"
rel="noopener noreferrer"
>
@@ -86,7 +87,7 @@ export default function Footer() {
<h4 className="font-bold text-white">Legal</h4>
<a
className="text-gray-400 hover:text-[#E54D5E] text-sm transition-colors"
href="https://github.com/decolua/omniroute/blob/main/LICENSE"
href="https://github.com/diegosouzapw/OmniRoute/blob/main/LICENSE"
target="_blank"
rel="noopener noreferrer"
>
@@ -101,7 +102,7 @@ export default function Footer() {
<div className="flex gap-6">
<a
className="text-gray-600 hover:text-white text-sm transition-colors"
href="https://github.com/decolua/omniroute"
href="https://github.com/diegosouzapw/OmniRoute"
target="_blank"
rel="noopener noreferrer"
>
+1 -1
View File
@@ -32,7 +32,7 @@ export default function HeroSection() {
Get Started
</button>
<a
href="https://github.com/decolua/omniroute"
href="https://github.com/diegosouzapw/OmniRoute"
target="_blank"
rel="noopener noreferrer"
className="h-12 px-8 rounded-lg border border-[#2D333B] bg-[#111520] hover:bg-[#2D333B] text-white text-base font-bold transition-all flex items-center gap-2"
+4 -3
View File
@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import OmniRouteLogo from "@/shared/components/OmniRouteLogo";
export default function Navigation() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
@@ -17,7 +18,7 @@ export default function Navigation() {
aria-label="Navigate to home"
>
<div className="size-8 rounded bg-linear-to-br from-[#E54D5E] to-[#C93D4E] flex items-center justify-center text-white">
<span className="material-symbols-outlined text-[20px]">hub</span>
<OmniRouteLogo size={20} className="text-white" />
</div>
<h2 className="text-white text-xl font-bold tracking-tight">OmniRoute</h2>
</button>
@@ -44,7 +45,7 @@ export default function Navigation() {
</a>
<a
className="text-gray-300 hover:text-white text-sm font-medium transition-colors flex items-center gap-1"
href="https://github.com/decolua/omniroute"
href="https://github.com/diegosouzapw/OmniRoute"
target="_blank"
rel="noopener noreferrer"
>
@@ -95,7 +96,7 @@ export default function Navigation() {
</a>
<a
className="text-gray-300 hover:text-white text-sm font-medium transition-colors"
href="https://github.com/decolua/omniroute"
href="https://github.com/diegosouzapw/OmniRoute"
target="_blank"
rel="noopener noreferrer"
>
+1
View File
@@ -14,6 +14,7 @@ export const metadata = {
"OmniRoute is an AI gateway for multi-provider LLMs. One endpoint for all your AI providers.",
icons: {
icon: "/favicon.svg",
apple: "/apple-touch-icon.svg",
},
};
+1 -1
View File
@@ -97,7 +97,7 @@ export default function TermsPage() {
<p>
Questions? Visit our{" "}
<a
href="https://github.com/decolua/omniroute"
href="https://github.com/diegosouzapw/OmniRoute"
className="text-primary hover:underline"
target="_blank"
rel="noopener noreferrer"
+12 -8
View File
@@ -7,23 +7,27 @@ const footerLinks = {
product: [
{ label: "Features", href: "#features" },
{ label: "Pricing", href: "#pricing" },
{ label: "Changelog", href: "https://github.com/decolua/omniroute/releases", external: true },
{
label: "Changelog",
href: "https://github.com/diegosouzapw/OmniRoute/releases",
external: true,
},
],
resources: [
{ label: "Documentation", href: "/docs" },
{ label: "API Reference", href: "/docs#api-reference" },
{
label: "Help Center",
href: "https://github.com/decolua/omniroute/discussions",
href: "https://github.com/diegosouzapw/OmniRoute/discussions",
external: true,
},
],
company: [
{ label: "About", href: "https://github.com/decolua/omniroute", external: true },
{ label: "Blog", href: "https://github.com/decolua/omniroute/releases", external: true },
{ label: "About", href: "https://github.com/diegosouzapw/OmniRoute", external: true },
{ label: "Blog", href: "https://github.com/diegosouzapw/OmniRoute/releases", external: true },
{
label: "Contact",
href: "https://github.com/decolua/omniroute/issues/new/choose",
href: "https://github.com/diegosouzapw/OmniRoute/issues/new/choose",
external: true,
},
{ label: "Terms", href: "/terms" },
@@ -76,7 +80,7 @@ export default function Footer() {
{/* Social links */}
<div className="flex gap-4">
<a
href="https://github.com/decolua/omniroute/discussions"
href="https://github.com/diegosouzapw/OmniRoute/discussions"
target="_blank"
rel="noopener noreferrer"
className="text-gray-400 hover:text-primary transition-colors"
@@ -87,7 +91,7 @@ export default function Footer() {
</svg>
</a>
<a
href="https://github.com/decolua/omniroute"
href="https://github.com/diegosouzapw/OmniRoute"
target="_blank"
rel="noopener noreferrer"
className="text-gray-400 hover:text-primary transition-colors"
@@ -147,7 +151,7 @@ export default function Footer() {
Privacy
</Link>
<a
href="https://github.com/decolua/omniroute/blob/main/LICENSE"
href="https://github.com/diegosouzapw/OmniRoute/blob/main/LICENSE"
target="_blank"
rel="noopener noreferrer"
className="hover:text-primary transition-colors"
+88
View File
@@ -0,0 +1,88 @@
import PropTypes from "prop-types";
/**
* OmniRoute logo SVG network hub icon with connected nodes.
* Matches the favicon and app icon design.
*/
export default function OmniRouteLogo({ size = 20, className = "" }) {
return (
<svg
width={size}
height={size}
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
{/* Central node */}
<circle cx="16" cy="16" r="3" fill="currentColor" />
{/* Outer nodes */}
<circle cx="8" cy="8" r="2" fill="currentColor" />
<circle cx="24" cy="8" r="2" fill="currentColor" />
<circle cx="8" cy="24" r="2" fill="currentColor" />
<circle cx="24" cy="24" r="2" fill="currentColor" />
<circle cx="16" cy="5" r="1.5" fill="currentColor" />
<circle cx="16" cy="27" r="1.5" fill="currentColor" />
{/* Connection lines */}
<line
x1="16"
y1="13"
x2="8"
y2="8"
stroke="currentColor"
strokeWidth="1.2"
strokeLinecap="round"
/>
<line
x1="16"
y1="13"
x2="24"
y2="8"
stroke="currentColor"
strokeWidth="1.2"
strokeLinecap="round"
/>
<line
x1="16"
y1="19"
x2="8"
y2="24"
stroke="currentColor"
strokeWidth="1.2"
strokeLinecap="round"
/>
<line
x1="16"
y1="19"
x2="24"
y2="24"
stroke="currentColor"
strokeWidth="1.2"
strokeLinecap="round"
/>
<line
x1="16"
y1="13"
x2="16"
y2="5"
stroke="currentColor"
strokeWidth="1.2"
strokeLinecap="round"
/>
<line
x1="16"
y1="19"
x2="16"
y2="27"
stroke="currentColor"
strokeWidth="1.2"
strokeLinecap="round"
/>
</svg>
);
}
OmniRouteLogo.propTypes = {
size: PropTypes.number,
className: PropTypes.string,
};
+3 -1
View File
@@ -6,6 +6,7 @@ import Link from "next/link";
import { usePathname } from "next/navigation";
import { cn } from "@/shared/utils/cn";
import { APP_CONFIG } from "@/shared/constants/config";
import OmniRouteLogo from "./OmniRouteLogo";
import Button from "./Button";
import { ConfirmModal } from "./Modal";
import CloudSyncStatus from "./CloudSyncStatus";
@@ -16,6 +17,7 @@ const navItems = [
{ href: "/dashboard/providers", label: "Providers", icon: "dns" },
{ href: "/dashboard/combos", label: "Combos", icon: "layers" },
{ href: "/dashboard/usage", label: "Usage", icon: "bar_chart" },
{ href: "/dashboard/costs", label: "Costs", icon: "account_balance_wallet" },
{ href: "/dashboard/analytics", label: "Analytics", icon: "analytics" },
{ href: "/dashboard/health", label: "Health", icon: "health_and_safety" },
{ href: "/dashboard/cli-tools", label: "CLI Tools", icon: "terminal" },
@@ -189,7 +191,7 @@ export default function Sidebar({ onClose, collapsed = false, onToggleCollapse }
className={cn("flex items-center", collapsed ? "justify-center" : "gap-3")}
>
<div className="flex items-center justify-center size-9 rounded bg-linear-to-br from-[#E54D5E] to-[#C93D4E] shrink-0">
<span className="material-symbols-outlined text-white text-[20px]">hub</span>
<OmniRouteLogo size={20} className="text-white" />
</div>
{!collapsed && (
<div className="flex flex-col">