feat(platform): add platform cost tracking for system credentials

Track real API costs incurred when users consume system-managed credentials.
Captures provider, tokens, duration, and model per block execution and
surfaces an admin dashboard with provider/user aggregation and raw logs.
This commit is contained in:
Zamil Majdy
2026-04-02 15:42:18 +02:00
parent 6b031085bd
commit 1a6c50c6cc
13 changed files with 1387 additions and 1 deletions

View File

@@ -0,0 +1,85 @@
import logging
import typing
from datetime import datetime
from autogpt_libs.auth import get_user_id, requires_admin_user
from fastapi import APIRouter, Query, Security
from pydantic import BaseModel
from backend.data.platform_cost import (
CostLogRow,
PlatformCostDashboard,
get_platform_cost_dashboard,
get_platform_cost_logs,
)
from backend.util.models import Pagination
logger = logging.getLogger(__name__)
router = APIRouter(
prefix="/admin",
tags=["platform-cost", "admin"],
dependencies=[Security(requires_admin_user)],
)
class PlatformCostLogsResponse(BaseModel):
logs: list[CostLogRow]
pagination: Pagination
@router.get(
"/platform_costs/dashboard",
response_model=PlatformCostDashboard,
summary="Get Platform Cost Dashboard",
)
async def get_cost_dashboard(
admin_user_id: str = Security(get_user_id),
start: typing.Optional[datetime] = Query(None),
end: typing.Optional[datetime] = Query(None),
provider: typing.Optional[str] = Query(None),
user_id: typing.Optional[str] = Query(None),
):
logger.info(f"Admin {admin_user_id} fetching platform cost dashboard")
return await get_platform_cost_dashboard(
start=start,
end=end,
provider=provider,
user_id=user_id,
)
@router.get(
"/platform_costs/logs",
response_model=PlatformCostLogsResponse,
summary="Get Platform Cost Logs",
)
async def get_cost_logs(
admin_user_id: str = Security(get_user_id),
start: typing.Optional[datetime] = Query(None),
end: typing.Optional[datetime] = Query(None),
provider: typing.Optional[str] = Query(None),
user_id: typing.Optional[str] = Query(None),
page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=200),
):
logger.info(f"Admin {admin_user_id} fetching platform cost logs")
logs, total = await get_platform_cost_logs(
start=start,
end=end,
provider=provider,
user_id=user_id,
page=page,
page_size=page_size,
)
total_pages = (total + page_size - 1) // page_size
return PlatformCostLogsResponse(
logs=logs,
pagination=Pagination(
total_items=total,
total_pages=total_pages,
current_page=page,
page_size=page_size,
),
)

View File

@@ -18,6 +18,7 @@ from prisma.errors import PrismaError
import backend.api.features.admin.credit_admin_routes
import backend.api.features.admin.execution_analytics_routes
import backend.api.features.admin.platform_cost_routes
import backend.api.features.admin.rate_limit_admin_routes
import backend.api.features.admin.store_admin_routes
import backend.api.features.builder
@@ -329,6 +330,11 @@ app.include_router(
tags=["v2", "admin"],
prefix="/api/copilot",
)
app.include_router(
backend.api.features.admin.platform_cost_routes.router,
tags=["v2", "admin"],
prefix="/api/platform-costs",
)
app.include_router(
backend.api.features.executions.review.routes.router,
tags=["v2", "executions", "review"],

View File

@@ -0,0 +1,292 @@
import logging
from datetime import datetime
from typing import Any
from pydantic import BaseModel
from backend.data.db import query_raw_with_schema
logger = logging.getLogger(__name__)
class PlatformCostEntry(BaseModel):
user_id: str
graph_exec_id: str | None = None
node_exec_id: str | None = None
graph_id: str | None = None
node_id: str | None = None
block_id: str
block_name: str
provider: str
credential_id: str
cost_microdollars: int | None = None
input_tokens: int | None = None
output_tokens: int | None = None
data_size: int | None = None
duration: float | None = None
model: str | None = None
metadata: dict[str, Any] | None = None
async def log_platform_cost(entry: PlatformCostEntry) -> None:
await query_raw_with_schema(
"""
INSERT INTO {schema_prefix}"PlatformCostLog"
("id", "createdAt", "userId", "graphExecId", "nodeExecId",
"graphId", "nodeId", "blockId", "blockName", "provider",
"credentialId", "costMicrodollars", "inputTokens", "outputTokens",
"dataSize", "duration", "model", "metadata")
VALUES (
gen_random_uuid(), NOW(), $1, $2, $3, $4, $5, $6, $7, $8, $9,
$10, $11, $12, $13, $14, $15, $16::jsonb
)
""",
entry.user_id,
entry.graph_exec_id,
entry.node_exec_id,
entry.graph_id,
entry.node_id,
entry.block_id,
entry.block_name,
entry.provider,
entry.credential_id,
entry.cost_microdollars,
entry.input_tokens,
entry.output_tokens,
entry.data_size,
entry.duration,
entry.model,
_json_or_none(entry.metadata),
)
async def log_platform_cost_safe(entry: PlatformCostEntry) -> None:
"""Fire-and-forget wrapper that never raises."""
try:
await log_platform_cost(entry)
except Exception:
logger.exception(
"Failed to log platform cost for user=%s provider=%s block=%s",
entry.user_id,
entry.provider,
entry.block_name,
)
def _json_or_none(data: dict[str, Any] | None) -> str | None:
if data is None:
return None
import json
return json.dumps(data)
class ProviderCostSummary(BaseModel):
provider: str
total_cost_microdollars: int
total_input_tokens: int
total_output_tokens: int
request_count: int
class UserCostSummary(BaseModel):
user_id: str
email: str | None = None
total_cost_microdollars: int
total_input_tokens: int
total_output_tokens: int
request_count: int
class CostLogRow(BaseModel):
id: str
created_at: datetime
user_id: str
email: str | None = None
graph_exec_id: str | None = None
node_exec_id: str | None = None
block_name: str
provider: str
cost_microdollars: int | None = None
input_tokens: int | None = None
output_tokens: int | None = None
model: str | None = None
class PlatformCostDashboard(BaseModel):
by_provider: list[ProviderCostSummary]
by_user: list[UserCostSummary]
total_cost_microdollars: int
total_requests: int
def _build_where(
start: datetime | None,
end: datetime | None,
provider: str | None,
user_id: str | None,
table_alias: str = "",
) -> tuple[str, list[Any]]:
prefix = f"{table_alias}." if table_alias else ""
clauses: list[str] = []
params: list[Any] = []
idx = 1
if start:
clauses.append(f'{prefix}"createdAt" >= ${idx}::timestamptz')
params.append(start)
idx += 1
if end:
clauses.append(f'{prefix}"createdAt" <= ${idx}::timestamptz')
params.append(end)
idx += 1
if provider:
clauses.append(f'{prefix}"provider" = ${idx}')
params.append(provider)
idx += 1
if user_id:
clauses.append(f'{prefix}"userId" = ${idx}')
params.append(user_id)
idx += 1
return (" AND ".join(clauses) if clauses else "TRUE", params)
async def get_platform_cost_dashboard(
start: datetime | None = None,
end: datetime | None = None,
provider: str | None = None,
user_id: str | None = None,
) -> PlatformCostDashboard:
where_sql, params = _build_where(start, end, provider, user_id)
by_provider_rows = await query_raw_with_schema(
f"""
SELECT
"provider",
COALESCE(SUM("costMicrodollars"), 0)::bigint AS total_cost,
COALESCE(SUM("inputTokens"), 0)::bigint AS total_input_tokens,
COALESCE(SUM("outputTokens"), 0)::bigint AS total_output_tokens,
COUNT(*)::bigint AS request_count
FROM {{schema_prefix}}"PlatformCostLog"
WHERE {where_sql}
GROUP BY "provider"
ORDER BY total_cost DESC
""",
*params,
)
where_p, params_p = _build_where(start, end, provider, user_id, "p")
by_user_rows = await query_raw_with_schema(
f"""
SELECT
p."userId" AS user_id,
u."email",
COALESCE(SUM(p."costMicrodollars"), 0)::bigint AS total_cost,
COALESCE(SUM(p."inputTokens"), 0)::bigint AS total_input_tokens,
COALESCE(SUM(p."outputTokens"), 0)::bigint AS total_output_tokens,
COUNT(*)::bigint AS request_count
FROM {{schema_prefix}}"PlatformCostLog" p
LEFT JOIN {{schema_prefix}}"User" u ON u."id" = p."userId"
WHERE {where_p}
GROUP BY p."userId", u."email"
ORDER BY total_cost DESC
LIMIT 100
""",
*params_p,
)
total_cost = sum(r["total_cost"] for r in by_provider_rows)
total_requests = sum(r["request_count"] for r in by_provider_rows)
return PlatformCostDashboard(
by_provider=[
ProviderCostSummary(
provider=r["provider"],
total_cost_microdollars=r["total_cost"],
total_input_tokens=r["total_input_tokens"],
total_output_tokens=r["total_output_tokens"],
request_count=r["request_count"],
)
for r in by_provider_rows
],
by_user=[
UserCostSummary(
user_id=r["user_id"],
email=r.get("email"),
total_cost_microdollars=r["total_cost"],
total_input_tokens=r["total_input_tokens"],
total_output_tokens=r["total_output_tokens"],
request_count=r["request_count"],
)
for r in by_user_rows
],
total_cost_microdollars=total_cost,
total_requests=total_requests,
)
async def get_platform_cost_logs(
start: datetime | None = None,
end: datetime | None = None,
provider: str | None = None,
user_id: str | None = None,
page: int = 1,
page_size: int = 50,
) -> tuple[list[CostLogRow], int]:
where_sql, params = _build_where(start, end, provider, user_id, "p")
count_rows = await query_raw_with_schema(
f"""
SELECT COUNT(*)::bigint AS cnt
FROM {{schema_prefix}}"PlatformCostLog" p
WHERE {where_sql}
""",
*params,
)
total = count_rows[0]["cnt"] if count_rows else 0
offset = (page - 1) * page_size
rows = await query_raw_with_schema(
f"""
SELECT
p."id",
p."createdAt" AS created_at,
p."userId" AS user_id,
u."email",
p."graphExecId" AS graph_exec_id,
p."nodeExecId" AS node_exec_id,
p."blockName" AS block_name,
p."provider",
p."costMicrodollars" AS cost_microdollars,
p."inputTokens" AS input_tokens,
p."outputTokens" AS output_tokens,
p."model"
FROM {{schema_prefix}}"PlatformCostLog" p
LEFT JOIN {{schema_prefix}}"User" u ON u."id" = p."userId"
WHERE {where_sql}
ORDER BY p."createdAt" DESC
LIMIT {page_size} OFFSET {offset}
""",
*params,
)
logs = [
CostLogRow(
id=r["id"],
created_at=r["created_at"],
user_id=r["user_id"],
email=r.get("email"),
graph_exec_id=r.get("graph_exec_id"),
node_exec_id=r.get("node_exec_id"),
block_name=r["block_name"],
provider=r["provider"],
cost_microdollars=r.get("cost_microdollars"),
input_tokens=r.get("input_tokens"),
output_tokens=r.get("output_tokens"),
model=r.get("model"),
)
for r in rows
]
return logs, total

View File

@@ -44,7 +44,9 @@ from backend.data.notifications import (
NotificationType,
ZeroBalanceData,
)
from backend.data.platform_cost import PlatformCostEntry, log_platform_cost_safe
from backend.data.rabbitmq import SyncRabbitMQ
from backend.integrations.credentials_store import is_system_credential
from backend.integrations.creds_manager import IntegrationCredentialsManager
from backend.notifications.notifications import queue_notification
from backend.util import json
@@ -99,6 +101,7 @@ from .utils import (
)
if TYPE_CHECKING:
from backend.blocks._base import Block
from backend.data.db_manager import (
DatabaseManagerAsyncClient,
DatabaseManagerClient,
@@ -671,6 +674,13 @@ class ExecutionProcessor:
stats=graph_stats,
)
# Log platform cost if system credentials were used
await _log_system_credential_cost(
node_exec=node_exec,
block=node.block,
stats=execution_stats,
)
return execution_stats
@async_time_measured
@@ -2031,6 +2041,48 @@ class ExecutionManager(AppProcess):
# ------- UTILITIES ------- #
async def _log_system_credential_cost(
node_exec: NodeExecutionEntry,
block: "Block",
stats: NodeExecutionStats,
) -> None:
"""Check if a system credential was used and log the platform cost."""
input_data = node_exec.inputs
input_model = cast(type[BlockSchema], block.input_schema)
for field_name in input_model.get_credentials_fields():
cred_data = input_data.get(field_name)
if not cred_data or not isinstance(cred_data, dict):
continue
cred_id = cred_data.get("id", "")
if not cred_id or not is_system_credential(cred_id):
continue
model_name = input_data.get("model")
if isinstance(model_name, dict):
model_name = None
await log_platform_cost_safe(
PlatformCostEntry(
user_id=node_exec.user_id,
graph_exec_id=node_exec.graph_exec_id,
node_exec_id=node_exec.node_exec_id,
graph_id=node_exec.graph_id,
node_id=node_exec.node_id,
block_id=node_exec.block_id,
block_name=block.name,
provider=cred_data.get("provider", "unknown"),
credential_id=cred_id,
input_tokens=stats.input_token_count or None,
output_tokens=stats.output_token_count or None,
data_size=stats.output_size or None,
duration=stats.walltime or None,
model=model_name,
)
)
return # One log per execution is enough
def get_db_client() -> "DatabaseManagerClient":
return get_database_manager_client()

View File

@@ -0,0 +1,35 @@
-- CreateTable
CREATE TABLE "PlatformCostLog" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"userId" TEXT NOT NULL,
"graphExecId" TEXT,
"nodeExecId" TEXT,
"graphId" TEXT,
"nodeId" TEXT,
"blockId" TEXT NOT NULL,
"blockName" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"credentialId" TEXT NOT NULL,
"costMicrodollars" INTEGER,
"inputTokens" INTEGER,
"outputTokens" INTEGER,
"dataSize" INTEGER,
"duration" DOUBLE PRECISION,
"model" TEXT,
"metadata" JSONB,
CONSTRAINT "PlatformCostLog_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "PlatformCostLog_userId_createdAt_idx" ON "PlatformCostLog"("userId", "createdAt");
-- CreateIndex
CREATE INDEX "PlatformCostLog_provider_createdAt_idx" ON "PlatformCostLog"("provider", "createdAt");
-- CreateIndex
CREATE INDEX "PlatformCostLog_graphExecId_idx" ON "PlatformCostLog"("graphExecId");
-- AddForeignKey
ALTER TABLE "PlatformCostLog" ADD CONSTRAINT "PlatformCostLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -66,6 +66,8 @@ model User {
PendingHumanReviews PendingHumanReview[]
Workspace UserWorkspace?
PlatformCostLogs PlatformCostLog[]
// OAuth Provider relations
OAuthApplications OAuthApplication[]
OAuthAuthorizationCodes OAuthAuthorizationCode[]
@@ -799,6 +801,41 @@ model CreditRefundRequest {
@@index([userId, transactionKey])
}
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////// Platform Cost Tracking TABLES //////////////
////////////////////////////////////////////////////////////
model PlatformCostLog {
id String @id @default(uuid())
createdAt DateTime @default(now())
userId String
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
graphExecId String?
nodeExecId String?
graphId String?
nodeId String?
blockId String
blockName String
provider String
credentialId String
// Cost in microdollars (1 USD = 1,000,000). Null if unknown.
costMicrodollars Int?
inputTokens Int?
outputTokens Int?
dataSize Int? // bytes
duration Float? // seconds
model String?
metadata Json?
@@index([userId, createdAt])
@@index([provider, createdAt])
@@index([graphExecId])
}
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////// Store TABLES ///////////////////////////

View File

@@ -1,5 +1,11 @@
import { Sidebar } from "@/components/__legacy__/Sidebar";
import { Users, DollarSign, UserSearch, FileText } from "lucide-react";
import {
Users,
DollarSign,
UserSearch,
FileText,
ServerCrash,
} from "lucide-react";
import { Gauge } from "@phosphor-icons/react/dist/ssr";
import { IconSliders } from "@/components/__legacy__/ui/icons";
@@ -27,6 +33,11 @@ const sidebarLinkGroups = [
href: "/admin/rate-limits",
icon: <Gauge className="h-6 w-6" />,
},
{
text: "Platform Costs",
href: "/admin/platform-costs",
icon: <ServerCrash className="h-6 w-6" />,
},
{
text: "Execution Analytics",
href: "/admin/execution-analytics",

View File

@@ -0,0 +1,29 @@
"use server";
import BackendApi from "@/lib/autogpt-server-api";
import type {
PlatformCostDashboard,
PlatformCostLogsResponse,
} from "@/lib/autogpt-server-api";
export async function getPlatformCostDashboard(params?: {
start?: string;
end?: string;
provider?: string;
user_id?: string;
}): Promise<PlatformCostDashboard> {
const api = new BackendApi();
return api.getPlatformCostDashboard(params);
}
export async function getPlatformCostLogs(params?: {
start?: string;
end?: string;
provider?: string;
user_id?: string;
page?: number;
page_size?: number;
}): Promise<PlatformCostLogsResponse> {
const api = new BackendApi();
return api.getPlatformCostLogs(params);
}

View File

@@ -0,0 +1,407 @@
"use client";
import { useEffect, useState } from "react";
import type {
PlatformCostDashboard,
CostLogRow,
Pagination,
} from "@/lib/autogpt-server-api";
import { getPlatformCostDashboard, getPlatformCostLogs } from "../actions";
import { useRouter, useSearchParams } from "next/navigation";
interface Props {
searchParams: {
start?: string;
end?: string;
provider?: string;
user_id?: string;
page?: string;
tab?: string;
};
}
function formatMicrodollars(microdollars: number) {
return `$${(microdollars / 1_000_000).toFixed(4)}`;
}
function formatTokens(tokens: number) {
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`;
if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}K`;
return tokens.toString();
}
function PlatformCostContent({ searchParams }: Props) {
const router = useRouter();
const urlParams = useSearchParams();
const [dashboard, setDashboard] = useState<PlatformCostDashboard | null>(
null,
);
const [logs, setLogs] = useState<CostLogRow[]>([]);
const [pagination, setPagination] = useState<Pagination | null>(null);
const [loading, setLoading] = useState(true);
const tab = searchParams.tab || "overview";
const page = searchParams.page ? parseInt(searchParams.page) : 1;
const [startDate, setStartDate] = useState(searchParams.start || "");
const [endDate, setEndDate] = useState(searchParams.end || "");
const [providerFilter, setProviderFilter] = useState(
searchParams.provider || "",
);
const [userFilter, setUserFilter] = useState(searchParams.user_id || "");
useEffect(() => {
async function load() {
setLoading(true);
const filters: Record<string, string> = {};
if (startDate) filters.start = startDate;
if (endDate) filters.end = endDate;
if (providerFilter) filters.provider = providerFilter;
if (userFilter) filters.user_id = userFilter;
const [dashData, logsData] = await Promise.all([
getPlatformCostDashboard(filters),
getPlatformCostLogs({ ...filters, page, page_size: 50 }),
]);
setDashboard(dashData);
setLogs(logsData.logs);
setPagination(logsData.pagination);
setLoading(false);
}
load();
}, [startDate, endDate, providerFilter, userFilter, page]);
function updateUrl(overrides: Record<string, string>) {
const params = new URLSearchParams(urlParams.toString());
for (const [k, v] of Object.entries(overrides)) {
if (v) params.set(k, v);
else params.delete(k);
}
router.push(`/admin/platform-costs?${params.toString()}`);
}
function handleFilter() {
updateUrl({
start: startDate,
end: endDate,
provider: providerFilter,
user_id: userFilter,
page: "1",
});
}
return (
<div className="flex flex-col gap-6">
{/* Filters */}
<div className="flex flex-wrap items-end gap-3 rounded-lg border p-4">
<div className="flex flex-col gap-1">
<label className="text-sm text-gray-500">Start Date</label>
<input
type="datetime-local"
className="rounded border px-3 py-1.5 text-sm"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-sm text-gray-500">End Date</label>
<input
type="datetime-local"
className="rounded border px-3 py-1.5 text-sm"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-sm text-gray-500">Provider</label>
<input
type="text"
placeholder="e.g. openai"
className="rounded border px-3 py-1.5 text-sm"
value={providerFilter}
onChange={(e) => setProviderFilter(e.target.value)}
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-sm text-gray-500">User ID</label>
<input
type="text"
placeholder="Filter by user"
className="rounded border px-3 py-1.5 text-sm"
value={userFilter}
onChange={(e) => setUserFilter(e.target.value)}
/>
</div>
<button
onClick={handleFilter}
className="rounded bg-blue-600 px-4 py-1.5 text-sm text-white hover:bg-blue-700"
>
Apply
</button>
</div>
{loading ? (
<div className="py-10 text-center text-gray-500">Loading...</div>
) : (
<>
{/* Summary cards */}
{dashboard && (
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
<SummaryCard
label="Total Cost"
value={formatMicrodollars(dashboard.total_cost_microdollars)}
/>
<SummaryCard
label="Total Requests"
value={dashboard.total_requests.toLocaleString()}
/>
<SummaryCard
label="Providers"
value={dashboard.by_provider.length.toString()}
/>
<SummaryCard
label="Active Users"
value={dashboard.by_user.length.toString()}
/>
</div>
)}
{/* Tabs */}
<div className="flex gap-2 border-b">
{["overview", "by-user", "logs"].map((t) => (
<button
key={t}
onClick={() => updateUrl({ tab: t, page: "1" })}
className={`px-4 py-2 text-sm font-medium ${
tab === t
? "border-b-2 border-blue-600 text-blue-600"
: "text-gray-500 hover:text-gray-700"
}`}
>
{t === "overview"
? "By Provider"
: t === "by-user"
? "By User"
: "Raw Logs"}
</button>
))}
</div>
{/* Tab content */}
{tab === "overview" && dashboard && (
<ProviderTable data={dashboard.by_provider} />
)}
{tab === "by-user" && dashboard && (
<UserTable data={dashboard.by_user} />
)}
{tab === "logs" && (
<LogsTable
logs={logs}
pagination={pagination}
onPageChange={(p) => updateUrl({ page: p.toString() })}
/>
)}
</>
)}
</div>
);
}
function SummaryCard({ label, value }: { label: string; value: string }) {
return (
<div className="rounded-lg border p-4">
<div className="text-sm text-gray-500">{label}</div>
<div className="text-2xl font-bold">{value}</div>
</div>
);
}
function ProviderTable({
data,
}: {
data: PlatformCostDashboard["by_provider"];
}) {
return (
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead className="border-b text-xs uppercase text-gray-500">
<tr>
<th className="px-4 py-3">Provider</th>
<th className="px-4 py-3 text-right">Total Cost</th>
<th className="px-4 py-3 text-right">Requests</th>
<th className="px-4 py-3 text-right">Input Tokens</th>
<th className="px-4 py-3 text-right">Output Tokens</th>
</tr>
</thead>
<tbody>
{data.map((row) => (
<tr key={row.provider} className="border-b hover:bg-gray-50">
<td className="px-4 py-3 font-medium">{row.provider}</td>
<td className="px-4 py-3 text-right">
{formatMicrodollars(row.total_cost_microdollars)}
</td>
<td className="px-4 py-3 text-right">
{row.request_count.toLocaleString()}
</td>
<td className="px-4 py-3 text-right">
{formatTokens(row.total_input_tokens)}
</td>
<td className="px-4 py-3 text-right">
{formatTokens(row.total_output_tokens)}
</td>
</tr>
))}
{data.length === 0 && (
<tr>
<td colSpan={5} className="px-4 py-8 text-center text-gray-400">
No cost data yet
</td>
</tr>
)}
</tbody>
</table>
</div>
);
}
function UserTable({ data }: { data: PlatformCostDashboard["by_user"] }) {
return (
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead className="border-b text-xs uppercase text-gray-500">
<tr>
<th className="px-4 py-3">User</th>
<th className="px-4 py-3 text-right">Total Cost</th>
<th className="px-4 py-3 text-right">Requests</th>
<th className="px-4 py-3 text-right">Input Tokens</th>
<th className="px-4 py-3 text-right">Output Tokens</th>
</tr>
</thead>
<tbody>
{data.map((row) => (
<tr key={row.user_id} className="border-b hover:bg-gray-50">
<td className="px-4 py-3">
<div className="font-medium">{row.email || "Unknown"}</div>
<div className="text-xs text-gray-400">{row.user_id}</div>
</td>
<td className="px-4 py-3 text-right">
{formatMicrodollars(row.total_cost_microdollars)}
</td>
<td className="px-4 py-3 text-right">
{row.request_count.toLocaleString()}
</td>
<td className="px-4 py-3 text-right">
{formatTokens(row.total_input_tokens)}
</td>
<td className="px-4 py-3 text-right">
{formatTokens(row.total_output_tokens)}
</td>
</tr>
))}
{data.length === 0 && (
<tr>
<td colSpan={5} className="px-4 py-8 text-center text-gray-400">
No cost data yet
</td>
</tr>
)}
</tbody>
</table>
</div>
);
}
function LogsTable({
logs,
pagination,
onPageChange,
}: {
logs: CostLogRow[];
pagination: Pagination | null;
onPageChange: (page: number) => void;
}) {
return (
<div className="flex flex-col gap-4">
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead className="border-b text-xs uppercase text-gray-500">
<tr>
<th className="px-3 py-3">Time</th>
<th className="px-3 py-3">User</th>
<th className="px-3 py-3">Block</th>
<th className="px-3 py-3">Provider</th>
<th className="px-3 py-3">Model</th>
<th className="px-3 py-3 text-right">Cost</th>
<th className="px-3 py-3 text-right">Tokens (in/out)</th>
<th className="px-3 py-3">Session</th>
</tr>
</thead>
<tbody>
{logs.map((log) => (
<tr key={log.id} className="border-b hover:bg-gray-50">
<td className="whitespace-nowrap px-3 py-2 text-xs">
{new Date(log.created_at).toLocaleString()}
</td>
<td className="px-3 py-2 text-xs">
{log.email || log.user_id.slice(0, 8)}
</td>
<td className="px-3 py-2 text-xs font-medium">
{log.block_name}
</td>
<td className="px-3 py-2 text-xs">{log.provider}</td>
<td className="px-3 py-2 text-xs">{log.model || "-"}</td>
<td className="px-3 py-2 text-right text-xs">
{log.cost_microdollars != null
? formatMicrodollars(log.cost_microdollars)
: "-"}
</td>
<td className="px-3 py-2 text-right text-xs">
{log.input_tokens != null || log.output_tokens != null
? `${formatTokens(log.input_tokens ?? 0)} / ${formatTokens(log.output_tokens ?? 0)}`
: "-"}
</td>
<td className="px-3 py-2 text-xs text-gray-400">
{log.graph_exec_id?.slice(0, 8) || "-"}
</td>
</tr>
))}
{logs.length === 0 && (
<tr>
<td colSpan={8} className="px-4 py-8 text-center text-gray-400">
No logs found
</td>
</tr>
)}
</tbody>
</table>
</div>
{pagination && pagination.total_pages > 1 && (
<div className="flex items-center justify-between px-4">
<span className="text-sm text-gray-500">
Page {pagination.current_page} of {pagination.total_pages} (
{pagination.total_items} total)
</span>
<div className="flex gap-2">
<button
disabled={pagination.current_page <= 1}
onClick={() => onPageChange(pagination.current_page - 1)}
className="rounded border px-3 py-1 text-sm disabled:opacity-50"
>
Previous
</button>
<button
disabled={pagination.current_page >= pagination.total_pages}
onClick={() => onPageChange(pagination.current_page + 1)}
className="rounded border px-3 py-1 text-sm disabled:opacity-50"
>
Next
</button>
</div>
</div>
)}
</div>
);
}
export { PlatformCostContent };

View File

@@ -0,0 +1,51 @@
import { withRoleAccess } from "@/lib/withRoleAccess";
import { Suspense } from "react";
import { PlatformCostContent } from "./components/PlatformCostContent";
type SearchParams = {
start?: string;
end?: string;
provider?: string;
user_id?: string;
page?: string;
tab?: string;
};
function PlatformCostDashboard({
searchParams,
}: {
searchParams: SearchParams;
}) {
return (
<div className="mx-auto p-6">
<div className="flex flex-col gap-4">
<div>
<h1 className="text-3xl font-bold">Platform Costs</h1>
<p className="text-gray-500">
Track real API costs incurred by system credentials across providers
</p>
</div>
<Suspense
key={JSON.stringify(searchParams)}
fallback={
<div className="py-10 text-center">Loading cost data...</div>
}
>
<PlatformCostContent searchParams={searchParams} />
</Suspense>
</div>
</div>
);
}
export default async function PlatformCostDashboardPage({
searchParams,
}: {
searchParams: Promise<SearchParams>;
}) {
"use server";
const withAdminAccess = await withRoleAccess(["admin"]);
const ProtectedDashboard = await withAdminAccess(PlatformCostDashboard);
return <ProtectedDashboard searchParams={await searchParams} />;
}

View File

@@ -5478,6 +5478,179 @@
"security": [{ "HTTPBearerJWT": [] }]
}
},
"/api/platform-costs/admin/platform_costs/dashboard": {
"get": {
"tags": ["v2", "admin", "platform-cost", "admin"],
"summary": "Get Platform Cost Dashboard",
"operationId": "getV2Get platform cost dashboard",
"security": [{ "HTTPBearerJWT": [] }],
"parameters": [
{
"name": "start",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{ "type": "string", "format": "date-time" },
{ "type": "null" }
],
"title": "Start"
}
},
{
"name": "end",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{ "type": "string", "format": "date-time" },
{ "type": "null" }
],
"title": "End"
}
},
{
"name": "provider",
"in": "query",
"required": false,
"schema": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Provider"
}
},
{
"name": "user_id",
"in": "query",
"required": false,
"schema": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "User Id"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PlatformCostDashboard"
}
}
}
},
"401": {
"$ref": "#/components/responses/HTTP401NotAuthenticatedError"
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}
},
"/api/platform-costs/admin/platform_costs/logs": {
"get": {
"tags": ["v2", "admin", "platform-cost", "admin"],
"summary": "Get Platform Cost Logs",
"operationId": "getV2Get platform cost logs",
"security": [{ "HTTPBearerJWT": [] }],
"parameters": [
{
"name": "start",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{ "type": "string", "format": "date-time" },
{ "type": "null" }
],
"title": "Start"
}
},
{
"name": "end",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{ "type": "string", "format": "date-time" },
{ "type": "null" }
],
"title": "End"
}
},
{
"name": "provider",
"in": "query",
"required": false,
"schema": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Provider"
}
},
{
"name": "user_id",
"in": "query",
"required": false,
"schema": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "User Id"
}
},
{
"name": "page",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"minimum": 1,
"default": 1,
"title": "Page"
}
},
{
"name": "page_size",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"maximum": 200,
"minimum": 1,
"default": 50,
"title": "Page Size"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PlatformCostLogsResponse"
}
}
}
},
"401": {
"$ref": "#/components/responses/HTTP401NotAuthenticatedError"
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}
},
"/api/public/shared/{share_token}": {
"get": {
"tags": ["v1"],
@@ -8477,6 +8650,50 @@
],
"title": "ContentType"
},
"CostLogRow": {
"properties": {
"id": { "type": "string", "title": "Id" },
"created_at": {
"type": "string",
"format": "date-time",
"title": "Created At"
},
"user_id": { "type": "string", "title": "User Id" },
"email": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Email"
},
"graph_exec_id": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Graph Exec Id"
},
"node_exec_id": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Node Exec Id"
},
"block_name": { "type": "string", "title": "Block Name" },
"provider": { "type": "string", "title": "Provider" },
"cost_microdollars": {
"anyOf": [{ "type": "integer" }, { "type": "null" }],
"title": "Cost Microdollars"
},
"input_tokens": {
"anyOf": [{ "type": "integer" }, { "type": "null" }],
"title": "Input Tokens"
},
"output_tokens": {
"anyOf": [{ "type": "integer" }, { "type": "null" }],
"title": "Output Tokens"
},
"model": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Model"
}
},
"type": "object",
"required": ["id", "created_at", "user_id", "block_name", "provider"],
"title": "CostLogRow"
},
"CountResponse": {
"properties": {
"all_blocks": { "type": "integer", "title": "All Blocks" },
@@ -11368,6 +11585,46 @@
"title": "PendingHumanReviewModel",
"description": "Response model for pending human review data.\n\nRepresents a human review request that is awaiting user action.\nContains all necessary information for a user to review and approve\nor reject data from a Human-in-the-Loop block execution.\n\nAttributes:\n id: Unique identifier for the review record\n user_id: ID of the user who must perform the review\n node_exec_id: ID of the node execution that created this review\n node_id: ID of the node definition (for grouping reviews from same node)\n graph_exec_id: ID of the graph execution containing the node\n graph_id: ID of the graph template being executed\n graph_version: Version number of the graph template\n payload: The actual data payload awaiting review\n instructions: Instructions or message for the reviewer\n editable: Whether the reviewer can edit the data\n status: Current review status (WAITING, APPROVED, or REJECTED)\n review_message: Optional message from the reviewer\n created_at: Timestamp when review was created\n updated_at: Timestamp when review was last modified\n reviewed_at: Timestamp when review was completed (if applicable)"
},
"PlatformCostDashboard": {
"properties": {
"by_provider": {
"items": { "$ref": "#/components/schemas/ProviderCostSummary" },
"type": "array",
"title": "By Provider"
},
"by_user": {
"items": { "$ref": "#/components/schemas/UserCostSummary" },
"type": "array",
"title": "By User"
},
"total_cost_microdollars": {
"type": "integer",
"title": "Total Cost Microdollars"
},
"total_requests": { "type": "integer", "title": "Total Requests" }
},
"type": "object",
"required": [
"by_provider",
"by_user",
"total_cost_microdollars",
"total_requests"
],
"title": "PlatformCostDashboard"
},
"PlatformCostLogsResponse": {
"properties": {
"logs": {
"items": { "$ref": "#/components/schemas/CostLogRow" },
"type": "array",
"title": "Logs"
},
"pagination": { "$ref": "#/components/schemas/Pagination" }
},
"type": "object",
"required": ["logs", "pagination"],
"title": "PlatformCostLogsResponse"
},
"PostmarkBounceEnum": {
"type": "integer",
"enum": [
@@ -11762,6 +12019,33 @@
"title": "ProviderConstants",
"description": "Model that exposes all provider names as a constant in the OpenAPI schema.\nThis is designed to be converted by Orval into a TypeScript constant."
},
"ProviderCostSummary": {
"properties": {
"provider": { "type": "string", "title": "Provider" },
"total_cost_microdollars": {
"type": "integer",
"title": "Total Cost Microdollars"
},
"total_input_tokens": {
"type": "integer",
"title": "Total Input Tokens"
},
"total_output_tokens": {
"type": "integer",
"title": "Total Output Tokens"
},
"request_count": { "type": "integer", "title": "Request Count" }
},
"type": "object",
"required": [
"provider",
"total_cost_microdollars",
"total_input_tokens",
"total_output_tokens",
"request_count"
],
"title": "ProviderCostSummary"
},
"ProviderEnumResponse": {
"properties": {
"provider": {
@@ -14610,6 +14894,37 @@
"title": "UsageWindow",
"description": "Usage within a single time window."
},
"UserCostSummary": {
"properties": {
"user_id": { "type": "string", "title": "User Id" },
"email": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Email"
},
"total_cost_microdollars": {
"type": "integer",
"title": "Total Cost Microdollars"
},
"total_input_tokens": {
"type": "integer",
"title": "Total Input Tokens"
},
"total_output_tokens": {
"type": "integer",
"title": "Total Output Tokens"
},
"request_count": { "type": "integer", "title": "Request Count" }
},
"type": "object",
"required": [
"user_id",
"total_cost_microdollars",
"total_input_tokens",
"total_output_tokens",
"request_count"
],
"title": "UserCostSummary"
},
"UserHistoryResponse": {
"properties": {
"history": {

View File

@@ -54,6 +54,8 @@ import type {
User,
UserPasswordCredentials,
UsersBalanceHistoryResponse,
PlatformCostDashboard,
PlatformCostLogsResponse,
WebSocketNotification,
} from "./types";
@@ -480,6 +482,26 @@ export default class BackendAPI {
return this._get("/credits/admin/users_history", params);
}
getPlatformCostDashboard(params?: {
start?: string;
end?: string;
provider?: string;
user_id?: string;
}): Promise<PlatformCostDashboard> {
return this._get("/platform-costs/admin/platform_costs/dashboard", params);
}
getPlatformCostLogs(params?: {
start?: string;
end?: string;
provider?: string;
user_id?: string;
page?: number;
page_size?: number;
}): Promise<PlatformCostLogsResponse> {
return this._get("/platform-costs/admin/platform_costs/logs", params);
}
////////////////////////////////////////
//////////// V2 LIBRARY API ////////////
////////////////////////////////////////

View File

@@ -966,6 +966,50 @@ export type AddUserCreditsResponse = {
transaction_key: string;
};
export type ProviderCostSummary = {
provider: string;
total_cost_microdollars: number;
total_input_tokens: number;
total_output_tokens: number;
request_count: number;
};
export type UserCostSummary = {
user_id: string;
email: string | null;
total_cost_microdollars: number;
total_input_tokens: number;
total_output_tokens: number;
request_count: number;
};
export type PlatformCostDashboard = {
by_provider: ProviderCostSummary[];
by_user: UserCostSummary[];
total_cost_microdollars: number;
total_requests: number;
};
export type CostLogRow = {
id: string;
created_at: string;
user_id: string;
email: string | null;
graph_exec_id: string | null;
node_exec_id: string | null;
block_name: string;
provider: string;
cost_microdollars: number | null;
input_tokens: number | null;
output_tokens: number | null;
model: string | null;
};
export type PlatformCostLogsResponse = {
logs: CostLogRow[];
pagination: Pagination;
};
const _stringFormatToDataTypeMap: Partial<Record<string, DataType>> = {
date: DataType.DATE,
time: DataType.TIME,