OneLiteLLM team per company+virt keys
All checks were successful
Build and Push / build (push) Successful in 1m24s
All checks were successful
Build and Push / build (push) Successful in 1m24s
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user