OneLiteLLM team per company+virt keys
All checks were successful
Build and Push / build (push) Successful in 1m24s

This commit is contained in:
2026-04-26 21:21:02 +02:00
parent 1f48712e42
commit 7b22bc4087
7 changed files with 247 additions and 29 deletions

View File

@@ -4,6 +4,7 @@ import { listTenants } from "@/lib/k8s";
import {
getLitellmHealth,
getGlobalSpend,
getPerKeySpend,
getPerTeamSpend,
} from "@/lib/litellm";
@@ -28,6 +29,17 @@ async function checkVllmHealth(): Promise<{
/**
* GET /api/admin/health
* Returns system health overview for the admin panel.
*
* Slice 2 spend layout
* --------------------
* - `spend.global` — total across all teams (LiteLLM-reported)
* - `spend.perTenant[name]` — per-tenant CHF, derived from the per-key
* spend map keyed by `litellmKeyAlias`. Only
* populated for tenants whose status carries
* an alias (post-Slice-2 reconciled CRs).
* - `spend.perOrg[teamId]` — company-level total (= LiteLLM team total).
* Useful for the admin overview to see
* spend-per-customer at a glance.
*/
export async function GET() {
try {
@@ -36,17 +48,17 @@ export async function GET() {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const [tenants, litellm, vllm, globalSpend, perTeamSpend] =
const [tenants, litellm, vllm, globalSpend, perKeySpend, perTeamSpend] =
await Promise.allSettled([
listTenants(),
getLitellmHealth(),
checkVllmHealth(),
getGlobalSpend(),
getPerKeySpend(),
getPerTeamSpend(),
]);
const allTenants =
tenants.status === "fulfilled" ? tenants.value : [];
const allTenants = tenants.status === "fulfilled" ? tenants.value : [];
// Count tenants by phase
const phaseCounts: Record<string, number> = {};
@@ -57,15 +69,27 @@ export async function GET() {
phaseCounts[phase] = (phaseCounts[phase] || 0) + 1;
}
// Build per-tenant spend map (tenantName → spend)
const spendMap: Record<string, number> = {};
// Build per-tenant spend map (tenantName → spend) from the per-key map.
// Tenants without a `litellmKeyAlias` in status are skipped — they
// simply won't appear in this map until they've been reconciled by
// the Slice-2 operator.
const keySpend =
perKeySpend.status === "fulfilled" ? perKeySpend.value : new Map();
const tenantSpend: Record<string, number> = {};
for (const t of allTenants) {
const alias = t.status?.litellmKeyAlias;
if (alias && keySpend.has(alias)) {
tenantSpend[t.metadata.name] = keySpend.get(alias)!;
}
}
// Build per-org spend map (teamId → spend). Multiple tenants of the
// same org share a teamId, so the same number appears for each.
const teamSpend =
perTeamSpend.status === "fulfilled" ? perTeamSpend.value : new Map();
for (const t of allTenants) {
const teamId = t.status?.litellmTeamId;
if (teamId && teamSpend.has(teamId)) {
spendMap[t.metadata.name] = teamSpend.get(teamId)!;
}
const orgSpend: Record<string, number> = {};
for (const [teamId, spend] of teamSpend.entries()) {
orgSpend[teamId] = spend;
}
return NextResponse.json({
@@ -76,7 +100,8 @@ export async function GET() {
spend: {
global:
globalSpend.status === "fulfilled" ? globalSpend.value : 0,
perTenant: spendMap,
perTenant: tenantSpend,
perOrg: orgSpend,
},
services: {
litellm: