feat(#309): Add LLM usage tracking and analytics

Implements comprehensive LLM usage tracking with analytics endpoints.

Implementation:
- Added LlmUsageLog model to Prisma schema
- Created llm-usage module with service, controller, and DTOs
- Added tracking for token usage, costs, and durations
- Implemented analytics aggregation by provider, model, and task type
- Added filtering by workspace, provider, model, user, and date range

Testing:
- 20 unit tests with 90.8% coverage (exceeds 85% requirement)
- Tests for service and controller with full error handling
- Tests use Vitest following project conventions

API Endpoints:
- GET /api/llm-usage/analytics - Aggregated usage analytics
- GET /api/llm-usage/by-workspace/:workspaceId - Workspace usage logs
- GET /api/llm-usage/by-workspace/:workspaceId/provider/:provider - Provider logs
- GET /api/llm-usage/by-workspace/:workspaceId/model/:model - Model logs

Database:
- LlmUsageLog table with indexes for efficient queries
- Relations to User, Workspace, and LlmProviderInstance
- Ready for migration with: pnpm prisma migrate dev

Refs #309

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-04 13:41:45 -06:00
parent 6516843612
commit b836940b89
12 changed files with 2187 additions and 248 deletions

View File

@@ -0,0 +1,2 @@
export * from "./track-usage.dto";
export * from "./usage-analytics.dto";

View File

@@ -0,0 +1,49 @@
import { IsString, IsInt, IsOptional, IsNumber, Min, IsUUID } from "class-validator";
export class TrackUsageDto {
@IsUUID()
workspaceId!: string;
@IsUUID()
userId!: string;
@IsString()
provider!: string;
@IsString()
model!: string;
@IsOptional()
@IsUUID()
providerInstanceId?: string;
@IsInt()
@Min(0)
promptTokens!: number;
@IsInt()
@Min(0)
completionTokens!: number;
@IsInt()
@Min(0)
totalTokens!: number;
@IsOptional()
@IsNumber()
@Min(0)
costCents?: number;
@IsOptional()
@IsString()
taskType?: string;
@IsOptional()
@IsUUID()
conversationId?: string;
@IsOptional()
@IsInt()
@Min(0)
durationMs?: number;
}

View File

@@ -0,0 +1,69 @@
import { IsOptional, IsDateString, IsUUID, IsString } from "class-validator";
export class UsageAnalyticsQueryDto {
@IsOptional()
@IsUUID()
workspaceId?: string;
@IsOptional()
@IsString()
provider?: string;
@IsOptional()
@IsString()
model?: string;
@IsOptional()
@IsUUID()
userId?: string;
@IsOptional()
@IsDateString()
startDate?: string;
@IsOptional()
@IsDateString()
endDate?: string;
}
export interface UsageAnalyticsResponseDto {
totalCalls: number;
totalPromptTokens: number;
totalCompletionTokens: number;
totalTokens: number;
totalCostCents: number;
averageDurationMs: number;
byProvider: ProviderUsageDto[];
byModel: ModelUsageDto[];
byTaskType: TaskTypeUsageDto[];
}
export interface ProviderUsageDto {
provider: string;
calls: number;
promptTokens: number;
completionTokens: number;
totalTokens: number;
costCents: number;
averageDurationMs: number;
}
export interface ModelUsageDto {
model: string;
calls: number;
promptTokens: number;
completionTokens: number;
totalTokens: number;
costCents: number;
averageDurationMs: number;
}
export interface TaskTypeUsageDto {
taskType: string;
calls: number;
promptTokens: number;
completionTokens: number;
totalTokens: number;
costCents: number;
averageDurationMs: number;
}