Skip to content

Commit afc3d8a

Browse files
authored
Merge pull request #1134 from vertesia/feat/cost-dashboard
feat: cost analytics types, audit meters, and client methods
2 parents bc287f4 + afd0249 commit afc3d8a

8 files changed

Lines changed: 493 additions & 254 deletions

File tree

packages/client/src/store/AgentsApi.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,4 +782,5 @@ export class AgentsApi extends ApiTopic {
782782
): Promise<FirstResponseBehaviorAnalyticsResponse> {
783783
return this.post('/analytics/first-response-behavior', { payload: query });
784784
}
785+
785786
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { ApiTopic, ClientBase } from '@vertesia/api-fetch-client';
2+
import { CostAnalyticsQuery, CostAnalyticsResponse } from '@vertesia/common';
3+
4+
export class CostApi extends ApiTopic {
5+
constructor(parent: ClientBase) {
6+
super(parent, '/api/v1/cost');
7+
}
8+
9+
/**
10+
* Get cost analytics for the current project.
11+
* Returns cost breakdown by model/environment with pricing from billing export.
12+
* Covers all inference types: direct, agent, embedding.
13+
*/
14+
getAnalytics(
15+
query: CostAnalyticsQuery = {}
16+
): Promise<CostAnalyticsResponse> {
17+
return this.post('/analytics', { payload: query });
18+
}
19+
20+
/**
21+
* Get platform-wide cost analytics across all customers.
22+
* Requires Vertesia staff privileges. Defaults to group_by='account'.
23+
*/
24+
getGlobalAnalytics(
25+
query: CostAnalyticsQuery = {}
26+
): Promise<CostAnalyticsResponse> {
27+
return this.post('/analytics/global', { payload: query });
28+
}
29+
30+
/**
31+
* Get the CSV export URL for raw inference audit events.
32+
*/
33+
getExportUrl(params?: { from?: string | number; to?: string | number }): string {
34+
const searchParams = new URLSearchParams();
35+
if (params?.from) searchParams.set('from', typeof params.from === 'number' ? new Date(params.from).toISOString() : params.from);
36+
if (params?.to) searchParams.set('to', typeof params.to === 'number' ? new Date(params.to).toISOString() : params.to);
37+
const qs = searchParams.toString();
38+
return `${this.baseUrl}/export${qs ? `?${qs}` : ''}`;
39+
}
40+
}

packages/client/src/store/WorkflowsApi.ts

Lines changed: 327 additions & 252 deletions
Large diffs are not rendered by default.

packages/client/src/store/client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { AbstractFetchClient, RequestError } from "@vertesia/api-fetch-client";
22
import { BulkOperationPayload, BulkOperationResult } from "@vertesia/common";
33
import { AgentsApi } from "./AgentsApi.js";
44
import { CollectionsApi } from "./CollectionsApi.js";
5+
import { CostApi } from "./CostApi.js";
56
import { CommandsApi } from "./CommandsApi.js";
67
import { DataApi } from "./DataApi.js";
78
import { EmailApi } from "./EmailApi.js";
@@ -85,6 +86,7 @@ export class ZenoClient extends AbstractFetchClient<ZenoClient> {
8586
}
8687

8788
agents = new AgentsApi(this);
89+
cost = new CostApi(this);
8890
objects = new ObjectsApi(this);
8991
types = new TypesApi(this);
9092
workflows = new WorkflowsApi(this);

packages/common/src/access-control.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export enum Permission {
3030
account_write = "account:write",
3131
account_admin = "account:admin",
3232
manage_billing = "account:billing",
33+
/** View cost and usage analytics */
34+
billing_read = "billing:read",
3335
account_member = "account:member",
3436

3537

packages/common/src/audit-trail.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export type AuditAction =
2+
// CRUD operations
23
| 'create'
34
| 'update'
45
| 'delete'
@@ -8,7 +9,34 @@ export type AuditAction =
89
| 'attach'
910
| 'detach'
1011
| 'publish'
11-
| 'unpublish';
12+
| 'unpublish'
13+
// Billable operations
14+
| 'inference'
15+
| 'embedding'
16+
| 'image_generation';
17+
18+
/** Billable audit actions for cost analytics queries */
19+
export const BILLABLE_AUDIT_ACTIONS: AuditAction[] = [
20+
'inference',
21+
'embedding',
22+
'image_generation',
23+
];
24+
25+
/**
26+
* Generic metering entry attached to audit events.
27+
* Used for cost attribution, usage tracking, and billing.
28+
*
29+
* Examples:
30+
* { category: "tokens", type: "input", quantity: 1234 }
31+
* { category: "tokens", type: "output", quantity: 567 }
32+
* { category: "compute", type: "duration_ms", quantity: 2100 }
33+
* { category: "processing", type: "pages", quantity: 12 }
34+
*/
35+
export interface AuditMeter {
36+
category: string;
37+
type: string;
38+
quantity: number;
39+
}
1240

1341
export interface AuditTrailEvent {
1442
event_type: 'audit';
@@ -28,6 +56,10 @@ export interface AuditTrailEvent {
2856
tenant_id: string | null;
2957
account_name: string | null;
3058
project_name: string | null;
59+
/** Generic metering data for cost attribution and usage tracking */
60+
meters?: AuditMeter[];
61+
/** Event-specific metadata — shape varies by action/resource_type */
62+
details?: Record<string, unknown>;
3163
}
3264

3365
export interface AuditTrailQuery {
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* Cost Analytics Types
3+
*
4+
* Types for the cost attribution dashboard.
5+
* Combines audit trail metering data with billing export pricing
6+
* to compute per-org/project/env/model cost breakdowns.
7+
*/
8+
9+
import { BILLABLE_AUDIT_ACTIONS } from './audit-trail.js';
10+
11+
export { BILLABLE_AUDIT_ACTIONS };
12+
13+
// ============================================================================
14+
// Query
15+
// ============================================================================
16+
17+
export interface CostAnalyticsQuery {
18+
/** Start time (ISO string or epoch ms) */
19+
from?: string | number;
20+
/** End time (ISO string or epoch ms) */
21+
to?: string | number;
22+
/** Group results by this dimension */
23+
group_by?: 'model' | 'environment' | 'account' | 'project' | 'provider' | 'interaction';
24+
/** Time series resolution */
25+
resolution?: 'hour' | 'day' | 'week' | 'month';
26+
/** Filter by model pattern */
27+
model?: string;
28+
/** Filter by environment ID */
29+
environment_id?: string;
30+
/** Filter by provider */
31+
provider?: string;
32+
/** Filter by project ID (optional, for org scope) */
33+
project_id?: string;
34+
/** Filter by account ID (set automatically by server) */
35+
account_id?: string;
36+
/** Scope: 'project' (default, current project) or 'org' (all projects in account) */
37+
scope?: 'project' | 'org';
38+
/** Pricing source: 'list' (current list prices) or 'historical' (actual prices from billing period). Default: 'list' */
39+
pricing_source?: 'list' | 'historical';
40+
/** Skip cache and force fresh query */
41+
no_cache?: boolean;
42+
}
43+
44+
// ============================================================================
45+
// Response
46+
// ============================================================================
47+
48+
export interface CostSummary {
49+
total_cost: number;
50+
total_input_tokens: number;
51+
total_output_tokens: number;
52+
total_calls: number;
53+
total_duration_ms: number;
54+
}
55+
56+
export interface CostByDimension {
57+
dimension: string;
58+
cost: number;
59+
input_tokens: number;
60+
output_tokens: number;
61+
calls: number;
62+
}
63+
64+
export interface CostTimeSeriesPoint {
65+
timestamp: string;
66+
cost: number;
67+
input_tokens: number;
68+
output_tokens: number;
69+
calls: number;
70+
}
71+
72+
export interface ModelPricing {
73+
model: string;
74+
input_price_per_m_tokens: number;
75+
output_price_per_m_tokens: number;
76+
source: 'billing_export' | 'unavailable';
77+
}
78+
79+
export interface CostAnalyticsResponse {
80+
summary: CostSummary;
81+
by_dimension: CostByDimension[];
82+
time_series: CostTimeSeriesPoint[];
83+
pricing: ModelPricing[];
84+
query_range: { from: string; to: string };
85+
cached: boolean;
86+
}

packages/common/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from './access-control.js';
22
export * from './analytics.js';
33
export * from './audit-trail.js';
4+
export * from './cost-analytics.js';
45
export * from './apikey.js';
56
export * from './apps.js';
67
export * from './ask-user.js';
@@ -11,13 +12,13 @@ export * from './environment.js';
1112
export * from "./facets.js";
1213
export * from './group.js';
1314
export * from './integrations.js';
14-
export * from './oauth.js';
1515
export * from './interaction.js';
1616
export * from './pending-asks.js';
1717
export * from './json-schema.js';
1818
export * from './json.js';
1919
export * from './meters.js';
2020
export * from './model_utility.js';
21+
export * from './oauth.js';
2122
export * from './payload.js';
2223
export * from "./Progress.js";
2324
export * from './project.js';

0 commit comments

Comments
 (0)