feat(costs): add billing, quota, and budget control plane

This commit is contained in:
Dotta
2026-03-14 22:00:12 -05:00
parent 656b4659fc
commit 76e6cc08a6
91 changed files with 22406 additions and 769 deletions

View File

@@ -164,6 +164,12 @@ const dashboard: DashboardSummary = {
monthUtilizationPercent: 90,
},
pendingApprovals: 1,
budgets: {
activeIncidents: 0,
pendingApprovals: 0,
pausedAgents: 0,
pausedProjects: 0,
},
};
describe("inbox helpers", () => {

View File

@@ -43,6 +43,9 @@ export const queryKeys = {
list: (companyId: string) => ["goals", companyId] as const,
detail: (id: string) => ["goals", "detail", id] as const,
},
budgets: {
overview: (companyId: string) => ["budgets", "overview", companyId] as const,
},
approvals: {
list: (companyId: string, status?: string) =>
["approvals", companyId, status] as const,
@@ -73,6 +76,16 @@ export const queryKeys = {
["costs", companyId, from, to] as const,
usageByProvider: (companyId: string, from?: string, to?: string) =>
["usage-by-provider", companyId, from, to] as const,
usageByBiller: (companyId: string, from?: string, to?: string) =>
["usage-by-biller", companyId, from, to] as const,
financeSummary: (companyId: string, from?: string, to?: string) =>
["finance-summary", companyId, from, to] as const,
financeByBiller: (companyId: string, from?: string, to?: string) =>
["finance-by-biller", companyId, from, to] as const,
financeByKind: (companyId: string, from?: string, to?: string) =>
["finance-by-kind", companyId, from, to] as const,
financeEvents: (companyId: string, from?: string, to?: string, limit: number = 100) =>
["finance-events", companyId, from, to, limit] as const,
usageWindowSpend: (companyId: string) =>
["usage-window-spend", companyId] as const,
usageQuotaWindows: (companyId: string) =>

View File

@@ -1,6 +1,7 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { deriveAgentUrlKey, deriveProjectUrlKey } from "@paperclipai/shared";
import type { BillingType, FinanceDirection, FinanceEventKind } from "@paperclipai/shared";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
@@ -53,6 +54,8 @@ export function providerDisplayName(provider: string): string {
const map: Record<string, string> = {
anthropic: "Anthropic",
openai: "OpenAI",
openrouter: "OpenRouter",
chatgpt: "ChatGPT",
google: "Google",
cursor: "Cursor",
jetbrains: "JetBrains AI",
@@ -60,6 +63,84 @@ export function providerDisplayName(provider: string): string {
return map[provider.toLowerCase()] ?? provider;
}
export function billingTypeDisplayName(billingType: BillingType): string {
const map: Record<BillingType, string> = {
metered_api: "Metered API",
subscription_included: "Subscription",
subscription_overage: "Subscription overage",
credits: "Credits",
fixed: "Fixed",
unknown: "Unknown",
};
return map[billingType];
}
export function quotaSourceDisplayName(source: string): string {
const map: Record<string, string> = {
"anthropic-oauth": "Anthropic OAuth",
"claude-cli": "Claude CLI",
"codex-rpc": "Codex app server",
"codex-wham": "ChatGPT WHAM",
};
return map[source] ?? source;
}
function coerceBillingType(value: unknown): BillingType | null {
if (
value === "metered_api" ||
value === "subscription_included" ||
value === "subscription_overage" ||
value === "credits" ||
value === "fixed" ||
value === "unknown"
) {
return value;
}
return null;
}
function readRunCostUsd(payload: Record<string, unknown> | null): number {
if (!payload) return 0;
for (const key of ["costUsd", "cost_usd", "total_cost_usd"] as const) {
const value = payload[key];
if (typeof value === "number" && Number.isFinite(value)) return value;
}
return 0;
}
export function visibleRunCostUsd(
usage: Record<string, unknown> | null,
result: Record<string, unknown> | null = null,
): number {
const billingType = coerceBillingType(usage?.billingType) ?? coerceBillingType(result?.billingType);
if (billingType === "subscription_included") return 0;
return readRunCostUsd(usage) || readRunCostUsd(result);
}
export function financeEventKindDisplayName(eventKind: FinanceEventKind): string {
const map: Record<FinanceEventKind, string> = {
inference_charge: "Inference charge",
platform_fee: "Platform fee",
credit_purchase: "Credit purchase",
credit_refund: "Credit refund",
credit_expiry: "Credit expiry",
byok_fee: "BYOK fee",
gateway_overhead: "Gateway overhead",
log_storage_charge: "Log storage",
logpush_charge: "Logpush",
provisioned_capacity_charge: "Provisioned capacity",
training_charge: "Training",
custom_model_import_charge: "Custom model import",
custom_model_storage_charge: "Custom model storage",
manual_adjustment: "Manual adjustment",
};
return map[eventKind];
}
export function financeDirectionDisplayName(direction: FinanceDirection): string {
return direction === "credit" ? "Credit" : "Debit";
}
/** Build an issue URL using the human-readable identifier when available. */
export function issueUrl(issue: { id: string; identifier?: string | null }): string {
return `/issues/${issue.identifier ?? issue.id}`;