feat(costs): add billing, quota, and budget control plane
This commit is contained in:
@@ -164,6 +164,12 @@ const dashboard: DashboardSummary = {
|
||||
monthUtilizationPercent: 90,
|
||||
},
|
||||
pendingApprovals: 1,
|
||||
budgets: {
|
||||
activeIncidents: 0,
|
||||
pendingApprovals: 0,
|
||||
pausedAgents: 0,
|
||||
pausedProjects: 0,
|
||||
},
|
||||
};
|
||||
|
||||
describe("inbox helpers", () => {
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
Reference in New Issue
Block a user