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 && ( +{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