Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion client/src/pages/EmbeddingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -134,6 +137,20 @@ export default function EmbeddingsPage() {
{t('embeddings.autoDescription')}
</p>

{usage?.totalBudget !== undefined && usage.totalBudget > 0 && (
<TokenUsageBar
data={{
totalBudget: usage.totalBudget,
totalUsed: usage.totalUsed ?? 0,
models: (usage.families ?? []).map(f => ({
displayName: f.family,
platform: f.platform ?? 'cloudflare',
budget: f.budget ?? 0,
used: f.used ?? 0,
})),
}}
/>
)}
{isLoading ? (
<p className="text-sm text-muted-foreground">{t('common.loading')}</p>
) : (
Expand Down
4 changes: 2 additions & 2 deletions client/src/pages/FallbackPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }[]
Expand Down Expand Up @@ -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)
Expand Down
57 changes: 54 additions & 3 deletions server/src/routes/embeddings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, { budget: number; platform: string; displayName: string }>();
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,
Expand All @@ -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,
});
});