diff --git a/products/01-llm-cost-router/ui/index.html b/products/01-llm-cost-router/ui/index.html new file mode 100644 index 0000000..4c8b4c3 --- /dev/null +++ b/products/01-llm-cost-router/ui/index.html @@ -0,0 +1,13 @@ + + + + + + dd0c/route β€” LLM Cost Router + + + +
+ + + diff --git a/products/01-llm-cost-router/ui/package.json b/products/01-llm-cost-router/ui/package.json new file mode 100644 index 0000000..7887f3d --- /dev/null +++ b/products/01-llm-cost-router/ui/package.json @@ -0,0 +1,29 @@ +{ + "name": "dd0c-route-ui", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.23.0", + "recharts": "^2.12.0", + "swr": "^2.2.5", + "clsx": "^2.1.1" + }, + "devDependencies": { + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.0", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.38", + "tailwindcss": "^3.4.3", + "typescript": "^5.4.5", + "vite": "^5.2.11" + } +} diff --git a/products/01-llm-cost-router/ui/postcss.config.js b/products/01-llm-cost-router/ui/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/products/01-llm-cost-router/ui/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/products/01-llm-cost-router/ui/src/App.tsx b/products/01-llm-cost-router/ui/src/App.tsx new file mode 100644 index 0000000..0cc3947 --- /dev/null +++ b/products/01-llm-cost-router/ui/src/App.tsx @@ -0,0 +1,20 @@ +import { Routes, Route, Navigate } from 'react-router-dom'; +import Layout from './components/Layout'; +import Dashboard from './pages/Dashboard'; +import Rules from './pages/Rules'; +import Keys from './pages/Keys'; +import Settings from './pages/Settings'; + +export default function App() { + return ( + + }> + } /> + } /> + } /> + } /> + } /> + + + ); +} diff --git a/products/01-llm-cost-router/ui/src/components/CostChart.tsx b/products/01-llm-cost-router/ui/src/components/CostChart.tsx new file mode 100644 index 0000000..d7e95c8 --- /dev/null +++ b/products/01-llm-cost-router/ui/src/components/CostChart.tsx @@ -0,0 +1,45 @@ +import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid } from 'recharts'; +import type { TimeseriesPoint } from '../lib/api'; + +interface Props { + data: TimeseriesPoint[]; +} + +export default function CostChart({ data }: Props) { + const formatted = data.map((d) => ({ + ...d, + time: new Date(d.bucket).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), + })); + + if (formatted.length === 0) { + return

No data yet. Start routing requests to see savings.

; + } + + return ( + + + + + + + + + + + `$${v.toFixed(2)}`} /> + [`$${value.toFixed(4)}`, 'Saved']} + /> + + + + ); +} diff --git a/products/01-llm-cost-router/ui/src/components/Layout.tsx b/products/01-llm-cost-router/ui/src/components/Layout.tsx new file mode 100644 index 0000000..3f2de9f --- /dev/null +++ b/products/01-llm-cost-router/ui/src/components/Layout.tsx @@ -0,0 +1,53 @@ +import { Outlet, NavLink } from 'react-router-dom'; +import clsx from 'clsx'; + +const nav = [ + { to: '/dashboard', label: 'Dashboard', icon: 'πŸ“Š' }, + { to: '/rules', label: 'Routing Rules', icon: 'πŸ”€' }, + { to: '/keys', label: 'API Keys', icon: 'πŸ”‘' }, + { to: '/settings', label: 'Settings', icon: 'βš™οΈ' }, +]; + +export default function Layout() { + return ( +
+ {/* Sidebar */} + + + {/* Main content */} +
+ +
+
+ ); +} diff --git a/products/01-llm-cost-router/ui/src/components/ModelTable.tsx b/products/01-llm-cost-router/ui/src/components/ModelTable.tsx new file mode 100644 index 0000000..7b98a98 --- /dev/null +++ b/products/01-llm-cost-router/ui/src/components/ModelTable.tsx @@ -0,0 +1,34 @@ +import type { ModelBreakdown } from '../lib/api'; + +interface Props { + data: ModelBreakdown[]; +} + +export default function ModelTable({ data }: Props) { + if (data.length === 0) { + return

No model usage data yet.

; + } + + return ( + + + + + + + + + + + {data.map((m) => ( + + + + + + + ))} + +
ModelRequestsTokensCost
{m.model}{m.request_count.toLocaleString()}{m.total_tokens.toLocaleString()}${m.total_cost.toFixed(4)}
+ ); +} diff --git a/products/01-llm-cost-router/ui/src/components/StatCard.tsx b/products/01-llm-cost-router/ui/src/components/StatCard.tsx new file mode 100644 index 0000000..b6d2ccf --- /dev/null +++ b/products/01-llm-cost-router/ui/src/components/StatCard.tsx @@ -0,0 +1,27 @@ +import clsx from 'clsx'; + +interface Props { + label: string; + value: string; + sub?: string; + accent?: 'primary' | 'success' | 'warning' | 'danger'; +} + +const accentColors = { + primary: 'text-dd0c-primary', + success: 'text-dd0c-success', + warning: 'text-dd0c-warning', + danger: 'text-dd0c-danger', +}; + +export default function StatCard({ label, value, sub, accent }: Props) { + return ( +
+

{label}

+

+ {value} +

+ {sub &&

{sub}

} +
+ ); +} diff --git a/products/01-llm-cost-router/ui/src/index.css b/products/01-llm-cost-router/ui/src/index.css new file mode 100644 index 0000000..7c25335 --- /dev/null +++ b/products/01-llm-cost-router/ui/src/index.css @@ -0,0 +1,7 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + font-family: 'Inter', system-ui, -apple-system, sans-serif; +} diff --git a/products/01-llm-cost-router/ui/src/lib/api.ts b/products/01-llm-cost-router/ui/src/lib/api.ts new file mode 100644 index 0000000..c6ba833 --- /dev/null +++ b/products/01-llm-cost-router/ui/src/lib/api.ts @@ -0,0 +1,111 @@ +const API_BASE = '/api/v1'; + +async function fetchApi(path: string, options?: RequestInit): Promise { + const token = localStorage.getItem('dd0c_token') || ''; + const res = await fetch(`${API_BASE}${path}`, { + ...options, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + ...options?.headers, + }, + }); + if (!res.ok) { + const body = await res.json().catch(() => ({ error: res.statusText })); + throw new Error(body.error || res.statusText); + } + return res.json(); +} + +// --- Types --- + +export interface AnalyticsSummary { + total_requests: number; + total_cost_original: number; + total_cost_actual: number; + total_cost_saved: number; + savings_pct: number; + avg_latency_ms: number; + p99_latency_ms: number; + routing_decisions: { + passthrough: number; + cheapest: number; + cascading: number; + }; +} + +export interface TimeseriesPoint { + bucket: string; + request_count: number; + cost_saved: number; + avg_latency_ms: number; +} + +export interface ModelBreakdown { + model: string; + request_count: number; + total_tokens: number; + total_cost: number; +} + +export interface RoutingRule { + id?: string; + priority: number; + name: string; + match_model?: string; + match_feature?: string; + match_team?: string; + match_complexity?: string; + strategy: string; + target_model?: string; + target_provider?: string; + fallback_models?: string[]; + enabled: boolean; +} + +export interface ApiKey { + id: string; + name: string; + key_prefix: string; + scopes: string[]; + last_used_at?: string; + created_at: string; +} + +export interface ApiKeyCreated { + id: string; + key: string; + name: string; +} + +export interface OrgInfo { + id: string; + name: string; + slug: string; + tier: string; +} + +// --- API calls --- + +export const api = { + getSummary: () => fetchApi('/analytics/summary'), + getTimeseries: (interval = 'hour') => + fetchApi(`/analytics/timeseries?interval=${interval}`), + getModels: () => fetchApi('/analytics/models'), + + listRules: () => fetchApi('/rules'), + createRule: (rule: RoutingRule) => + fetchApi('/rules', { method: 'POST', body: JSON.stringify(rule) }), + updateRule: (id: string, rule: RoutingRule) => + fetchApi(`/rules/${id}`, { method: 'PUT', body: JSON.stringify(rule) }), + deleteRule: (id: string) => + fetchApi(`/rules/${id}`, { method: 'DELETE' }), + + listKeys: () => fetchApi('/keys'), + createKey: (name: string) => + fetchApi('/keys', { method: 'POST', body: JSON.stringify({ name }) }), + revokeKey: (id: string) => + fetchApi(`/keys/${id}`, { method: 'DELETE' }), + + getOrg: () => fetchApi('/org'), +}; diff --git a/products/01-llm-cost-router/ui/src/main.tsx b/products/01-llm-cost-router/ui/src/main.tsx new file mode 100644 index 0000000..c68f490 --- /dev/null +++ b/products/01-llm-cost-router/ui/src/main.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import App from './App'; +import './index.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + , +); diff --git a/products/01-llm-cost-router/ui/src/pages/Dashboard.tsx b/products/01-llm-cost-router/ui/src/pages/Dashboard.tsx new file mode 100644 index 0000000..58986dc --- /dev/null +++ b/products/01-llm-cost-router/ui/src/pages/Dashboard.tsx @@ -0,0 +1,55 @@ +import useSWR from 'swr'; +import { api, type AnalyticsSummary, type TimeseriesPoint, type ModelBreakdown } from '../lib/api'; +import CostChart from '../components/CostChart'; +import StatCard from '../components/StatCard'; +import ModelTable from '../components/ModelTable'; + +export default function Dashboard() { + const { data: summary } = useSWR('summary', api.getSummary); + const { data: timeseries } = useSWR('timeseries', () => api.getTimeseries('hour')); + const { data: models } = useSWR('models', api.getModels); + + return ( +
+

Dashboard

+ + {/* Stat cards */} +
+ + + + +
+ + {/* Cost savings chart */} +
+

Cost Savings Over Time

+ +
+ + {/* Model breakdown */} +
+

Model Usage

+ +
+
+ ); +} diff --git a/products/01-llm-cost-router/ui/src/pages/Keys.tsx b/products/01-llm-cost-router/ui/src/pages/Keys.tsx new file mode 100644 index 0000000..c4c91f6 --- /dev/null +++ b/products/01-llm-cost-router/ui/src/pages/Keys.tsx @@ -0,0 +1,128 @@ +import { useState } from 'react'; +import useSWR, { mutate } from 'swr'; +import { api, type ApiKey } from '../lib/api'; + +export default function Keys() { + const { data: keys } = useSWR('keys', api.listKeys); + const [newKeyName, setNewKeyName] = useState(''); + const [createdKey, setCreatedKey] = useState(null); + + const create = async () => { + if (!newKeyName.trim()) return; + const result = await api.createKey(newKeyName.trim()); + setCreatedKey(result.key); + setNewKeyName(''); + mutate('keys'); + }; + + const revoke = async (id: string, name: string) => { + if (!confirm(`Revoke key "${name}"? This cannot be undone.`)) return; + await api.revokeKey(id); + mutate('keys'); + }; + + return ( +
+

API Keys

+ + {/* Create key */} +
+

Create New Key

+
+ setNewKeyName(e.target.value)} + placeholder="Key name (e.g. Production, Staging)" + className="flex-1 bg-dd0c-bg border border-dd0c-border rounded px-3 py-1.5 text-sm" + onKeyDown={(e) => e.key === 'Enter' && create()} + /> + +
+ + {createdKey && ( +
+

Copy this key now β€” it won't be shown again.

+ {createdKey} + +
+ )} +
+ + {/* Keys table */} +
+ + + + + + + + + + + + + {(keys ?? []).map((key) => ( + + + + + + + + + ))} + {(keys ?? []).length === 0 && ( + + + + )} + +
NameKeyScopesLast UsedCreatedActions
{key.name}{key.key_prefix}β€’β€’β€’β€’β€’β€’β€’β€’ + {key.scopes.map((s) => ( + + {s} + + ))} + + {key.last_used_at ? new Date(key.last_used_at).toLocaleDateString() : 'Never'} + + {new Date(key.created_at).toLocaleDateString()} + + +
+ No API keys. Generate one to start routing. +
+
+ + {/* Usage instructions */} +
+

Quick Start

+
+{`# Replace your OpenAI base URL
+export OPENAI_BASE_URL=https://route.dd0c.dev/v1
+export OPENAI_API_KEY=dd0c_your_key_here
+
+# Your existing code works unchanged
+from openai import OpenAI
+client = OpenAI()  # Uses dd0c proxy automatically`}
+        
+
+
+ ); +} diff --git a/products/01-llm-cost-router/ui/src/pages/Rules.tsx b/products/01-llm-cost-router/ui/src/pages/Rules.tsx new file mode 100644 index 0000000..29d70c7 --- /dev/null +++ b/products/01-llm-cost-router/ui/src/pages/Rules.tsx @@ -0,0 +1,208 @@ +import { useState } from 'react'; +import useSWR, { mutate } from 'swr'; +import { api, type RoutingRule } from '../lib/api'; + +const STRATEGIES = ['passthrough', 'cheapest', 'quality-first', 'cascading']; +const COMPLEXITIES = ['low', 'medium', 'high']; + +export default function Rules() { + const { data: rules } = useSWR('rules', api.listRules); + const [editing, setEditing] = useState(null); + const [isNew, setIsNew] = useState(false); + + const blank: RoutingRule = { + priority: (rules?.length ?? 0) + 1, + name: '', + strategy: 'cheapest', + enabled: true, + }; + + const save = async () => { + if (!editing) return; + if (isNew) { + await api.createRule(editing); + } else if (editing.id) { + await api.updateRule(editing.id, editing); + } + setEditing(null); + setIsNew(false); + mutate('rules'); + }; + + const remove = async (id: string) => { + if (!confirm('Delete this rule?')) return; + await api.deleteRule(id); + mutate('rules'); + }; + + return ( +
+
+

Routing Rules

+ +
+ + {/* Rules table */} +
+ + + + + + + + + + + + + + {(rules ?? []).map((rule) => ( + + + + + + + + + + ))} + {(rules ?? []).length === 0 && ( + + + + )} + +
PriorityNameMatchStrategyTargetEnabledActions
{rule.priority}{rule.name} + {[ + rule.match_model && `model=${rule.match_model}`, + rule.match_feature && `feature=${rule.match_feature}`, + rule.match_team && `team=${rule.match_team}`, + rule.match_complexity && `complexity=${rule.match_complexity}`, + ].filter(Boolean).join(', ') || '*'} + + + {rule.strategy} + + + {rule.target_model || 'β€”'} + + {rule.enabled ? 'βœ…' : '⏸️'} + + + +
+ No routing rules yet. Add one to start saving. +
+
+ + {/* Edit modal */} + {editing && ( +
+
+

{isNew ? 'New Rule' : 'Edit Rule'}

+ +
+ + + + + + +
+ + + +
+ + +
+
+
+ )} +
+ ); +} diff --git a/products/01-llm-cost-router/ui/src/pages/Settings.tsx b/products/01-llm-cost-router/ui/src/pages/Settings.tsx new file mode 100644 index 0000000..62e41bd --- /dev/null +++ b/products/01-llm-cost-router/ui/src/pages/Settings.tsx @@ -0,0 +1,73 @@ +import useSWR from 'swr'; +import { api } from '../lib/api'; + +export default function Settings() { + const { data: org } = useSWR('org', api.getOrg); + + return ( +
+

Settings

+ + {/* Org info */} +
+

Organization

+
+
+

Name

+

{org?.name ?? 'β€”'}

+
+
+

Slug

+

{org?.slug ?? 'β€”'}

+
+
+

Tier

+

+ + {org?.tier ?? 'β€”'} + +

+
+
+

ID

+

{org?.id ?? 'β€”'}

+
+
+
+ + {/* Provider configs */} +
+

Providers

+

+ Configure your LLM provider API keys. Keys are encrypted at rest. +

+
+ + +
+
+ + {/* Danger zone */} +
+

Danger Zone

+ +
+
+ ); +} + +function ProviderRow({ name, provider }: { name: string; provider: string }) { + return ( +
+
+

{name}

+

{provider}

+
+ +
+ ); +} diff --git a/products/01-llm-cost-router/ui/tailwind.config.js b/products/01-llm-cost-router/ui/tailwind.config.js new file mode 100644 index 0000000..1c6bcc5 --- /dev/null +++ b/products/01-llm-cost-router/ui/tailwind.config.js @@ -0,0 +1,23 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + theme: { + extend: { + colors: { + dd0c: { + bg: '#0a0a0f', + surface: '#12121a', + border: '#1e1e2e', + primary: '#6366f1', + accent: '#06b6d4', + success: '#22c55e', + warning: '#f59e0b', + danger: '#ef4444', + text: '#e2e8f0', + muted: '#64748b', + }, + }, + }, + }, + plugins: [], +}; diff --git a/products/01-llm-cost-router/ui/tsconfig.json b/products/01-llm-cost-router/ui/tsconfig.json new file mode 100644 index 0000000..1921e54 --- /dev/null +++ b/products/01-llm-cost-router/ui/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { "@/*": ["src/*"] } + }, + "include": ["src"] +} diff --git a/products/01-llm-cost-router/ui/vite.config.ts b/products/01-llm-cost-router/ui/vite.config.ts new file mode 100644 index 0000000..21e784b --- /dev/null +++ b/products/01-llm-cost-router/ui/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + proxy: { + '/api': 'http://localhost:3000', + }, + }, + build: { + outDir: 'dist', + }, +});