diff --git a/client/src/pages/EmbeddingsPage.tsx b/client/src/pages/EmbeddingsPage.tsx index 70fa24d64..f2b193fbd 100644 --- a/client/src/pages/EmbeddingsPage.tsx +++ b/client/src/pages/EmbeddingsPage.tsx @@ -2,6 +2,7 @@ import { useState } from 'react' import { Link } from 'react-router-dom' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { ArrowDown, ArrowUp } from 'lucide-react' +import { TokenUsageBar } from './FallbackPage' import { apiFetch } from '@/lib/api' import { Button } from '@/components/ui/button' import { Switch } from '@/components/ui/switch' @@ -36,7 +37,9 @@ interface EmbeddingsData { } interface UsageData { - families: { family: string; requestsToday: number; tokensMonth: number }[] + totalBudget?: number + totalUsed?: number + families: { family: string; displayName?: string; platform?: string; budget?: number; used?: number; requestsToday: number; tokensMonth: number }[] } function formatTokens(n: number): string { @@ -134,6 +137,20 @@ export default function EmbeddingsPage() { {t('embeddings.autoDescription')}

+ {usage?.totalBudget !== undefined && usage.totalBudget > 0 && ( + ({ + displayName: f.family, + platform: f.platform ?? 'cloudflare', + budget: f.budget ?? 0, + used: f.used ?? 0, + })), + }} + /> + )} {isLoading ? (

{t('common.loading')}

) : ( diff --git a/client/src/pages/FallbackPage.tsx b/client/src/pages/FallbackPage.tsx index c76b675a9..84440ed44 100644 --- a/client/src/pages/FallbackPage.tsx +++ b/client/src/pages/FallbackPage.tsx @@ -286,7 +286,7 @@ export function groupQuotaBadge( return null } -interface TokenUsageData { +export interface TokenUsageData { totalBudget: number totalUsed: number models: { displayName: string; platform: string; modelId?: string; budget: number; used?: number }[] @@ -332,7 +332,7 @@ export function AxisBar({ value, color }: { value: number | undefined; color: st // Legend rows visible while collapsed (~6 rows: 6 × 16px line + 5 × 6px gap). const LEGEND_COLLAPSED_PX = 126 -function TokenUsageBar({ data }: { data: TokenUsageData }) { +export function TokenUsageBar({ data }: { data: TokenUsageData }) { const { t } = useI18n() const { totalBudget, totalUsed, models } = data const remaining = Math.max(0, totalBudget - totalUsed) diff --git a/server/src/routes/embeddings.ts b/server/src/routes/embeddings.ts index f9bad75da..ad5788cf3 100644 --- a/server/src/routes/embeddings.ts +++ b/server/src/routes/embeddings.ts @@ -284,10 +284,44 @@ embeddingsRouter.delete('/custom/:id', (req: Request, res: Response) => { res.json({ success: true }); }); +function parseEmbeddingQuotaLabel(label: string): number { + if (!label) return 0; + if (label.includes('neurons/day')) { + const match = label.match(/(\d+)K\s*neurons/i); + const kNeurons = match ? parseInt(match[1]) : 10; + return kNeurons * 100_000 * 30; // 10K neurons/day ~ 30M tokens/month + } + if (label.includes('/mo credits')) { + const match = label.match(/\$([\d.]+)/); + const dollars = match ? parseFloat(match[1]) : 0.10; + return Math.round(dollars * 20_000_000); // $0.10/mo ~ 2M tokens + } + return 0; +} + // Per-family usage: requests today (most embedding quotas are daily/RPM) and // tokens this calendar month, from the tagged request log. embeddingsRouter.get('/usage', (_req: Request, res: Response) => { const db = getDb(); + const keyCountMap = new Map( + (db.prepare("SELECT platform, COUNT(*) AS count FROM api_keys WHERE enabled = 1 AND status IN ('healthy', 'unknown') GROUP BY platform").all() as { platform: string; count: number }[]) + .map(k => [k.platform, k.count]) + ); + + const models = db.prepare("SELECT id, family, platform, model_id, display_name, quota_label FROM embedding_models WHERE enabled = 1").all() as { id: number; family: string; platform: string; model_id: string; display_name: string; quota_label: string }[]; + + const familyMap = new Map(); + models.forEach(m => { + const keys = Math.max(1, keyCountMap.get(m.platform) ?? 1); + const b = parseEmbeddingQuotaLabel(m.quota_label) * keys; + const existing = familyMap.get(m.family); + if (!existing) { + familyMap.set(m.family, { budget: b, platform: m.platform, displayName: m.family }); + } else { + existing.budget += b; + } + }); + const usage = db.prepare(` SELECT em.family, COALESCE(SUM(CASE WHEN r.created_at >= datetime('now', 'start of day') THEN 1 ELSE 0 END), 0) AS requests_today, @@ -302,11 +336,28 @@ embeddingsRouter.get('/usage', (_req: Request, res: Response) => { GROUP BY em.family `).all() as { family: string; requests_today: number; tokens_month: number }[]; - res.json({ - families: usage.map(u => ({ + let totalBudget = 0; + let totalUsed = 0; + + const families = usage.map(u => { + const info = familyMap.get(u.family); + const budget = info?.budget ?? 0; + totalBudget += budget; + totalUsed += u.tokens_month; + return { family: u.family, + displayName: u.family, + platform: info?.platform ?? 'cloudflare', + budget, + used: u.tokens_month, requestsToday: u.requests_today, tokensMonth: u.tokens_month, - })), + }; + }); + + res.json({ + totalBudget, + totalUsed, + families, }); });