mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
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:
@@ -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,
|
||||
),
|
||||
)
|
||||
@@ -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"],
|
||||
|
||||
292
autogpt_platform/backend/backend/data/platform_cost.py
Normal file
292
autogpt_platform/backend/backend/data/platform_cost.py
Normal 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
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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 ///////////////////////////
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
@@ -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 ////////////
|
||||
////////////////////////////////////////
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user