Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c3fe96e221 | |||
| 1c6541f25d | |||
| 9f8dfa1398 | |||
| e4db9bff0e | |||
| 54aba4c087 |
@@ -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.
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Generated
+2
-2
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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
@@ -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 |
@@ -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 |
@@ -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 (
|
||||
|
||||
@@ -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" && (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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: {} });
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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://<host>/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://<host>/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 |
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user