diff --git a/src/app/(app)/usage/page.tsx b/src/app/(app)/usage/page.tsx index 9a192df0e..43daf2fca 100644 --- a/src/app/(app)/usage/page.tsx +++ b/src/app/(app)/usage/page.tsx @@ -30,6 +30,7 @@ import { useTRPC } from '@/lib/trpc/utils'; type UsageData = { date: string; model?: string; + feature?: string | null; total_cost: number; request_count: number; total_input_tokens: number; @@ -368,6 +369,11 @@ export default function UsagePage() { }, ] : []), + { + key: 'feature', + label: 'Feature', + render: (value: unknown) => (value as string) || '—', + }, { key: 'cost', label: 'Cost', @@ -396,9 +402,10 @@ export default function UsagePage() { ]; const tableData: UsageTableRow[] = usageData.usage.map(item => ({ - id: `${item.date}-${item.model || 'all'}`, + id: `${item.date}-${item.model || 'all'}-${item.feature || 'none'}`, date: item.date, ...(groupByModel && { model: item.model }), + feature: item.feature || null, cost: item.total_cost, requests: item.request_count, inputTokens: item.total_input_tokens, diff --git a/src/app/api/profile/usage/route.ts b/src/app/api/profile/usage/route.ts index 3662f007a..1b9d5ff4a 100644 --- a/src/app/api/profile/usage/route.ts +++ b/src/app/api/profile/usage/route.ts @@ -2,7 +2,7 @@ import type { NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; import { getUserFromAuth } from '@/lib/user.server'; import { db } from '@/lib/drizzle'; -import { microdollar_usage } from '@/db/schema'; +import { microdollar_usage, microdollar_usage_metadata, feature } from '@/db/schema'; import { eq, sql, desc, isNull, and } from 'drizzle-orm'; export async function GET(request: NextRequest) { @@ -22,6 +22,7 @@ export async function GET(request: NextRequest) { const selectFields = { date: sql`DATE(${microdollar_usage.created_at})`, ...(groupByModel && { model: microdollar_usage.model }), + feature: feature.feature, total_cost: sql`SUM(${microdollar_usage.cost})::float`, request_count: sql`COUNT(*)::float`, total_input_tokens: sql`SUM(${microdollar_usage.input_tokens})::float`, @@ -34,6 +35,7 @@ export async function GET(request: NextRequest) { const groupByClause = [ sql`DATE(${microdollar_usage.created_at})`, ...(groupByModel ? [microdollar_usage.model] : []), + feature.feature, ]; const orderByClause = [ desc(sql`DATE(${microdollar_usage.created_at})`), @@ -63,6 +65,8 @@ export async function GET(request: NextRequest) { const usage = await db .select(selectFields) .from(microdollar_usage) + .leftJoin(microdollar_usage_metadata, eq(microdollar_usage.id, microdollar_usage_metadata.id)) + .leftJoin(feature, eq(microdollar_usage_metadata.feature_id, feature.feature_id)) .where(whereClause) .groupBy(...groupByClause) .orderBy(...orderByClause); diff --git a/src/components/organizations/usage-details/hooks/useUsageTableData.tsx b/src/components/organizations/usage-details/hooks/useUsageTableData.tsx index 79a6f5c3f..96f9d5897 100644 --- a/src/components/organizations/usage-details/hooks/useUsageTableData.tsx +++ b/src/components/organizations/usage-details/hooks/useUsageTableData.tsx @@ -94,6 +94,11 @@ export function useUsageTableData( }, ] : []), + { + key: 'feature', + label: 'Feature', + render: (value: unknown) => (value as string) || '—', + }, { key: 'totalCost', label: 'Cost', @@ -154,6 +159,7 @@ export function useUsageTableData( date: rollup.date, user: usage.user, ...(groupByModel && { model: usage.model || 'Unknown' }), + feature: usage.feature || null, totalCost: usage.microdollarCost ? parseFloat(usage.microdollarCost) : 0, totalTokens: usage.tokenCount, inputTokens: usage.inputTokens, diff --git a/src/components/organizations/usage-details/utils/csvExport.ts b/src/components/organizations/usage-details/utils/csvExport.ts index dd6a35552..035fa5717 100644 --- a/src/components/organizations/usage-details/utils/csvExport.ts +++ b/src/components/organizations/usage-details/utils/csvExport.ts @@ -35,6 +35,7 @@ export function exportUsageToCSV( 'User Name', 'User Email', ...(groupByModel ? ['Model'] : []), + 'Feature', 'Cost', 'Requests', 'Input Tokens', @@ -51,6 +52,7 @@ export function exportUsageToCSV( escapeCsvValue(usage.user.name || ''), escapeCsvValue(usage.user.email || ''), ...(groupByModel ? [escapeCsvValue(usage.model || 'Unknown')] : []), + escapeCsvValue(usage.feature || ''), escapeCsvValue(`$${(parseFloat(usage.microdollarCost || '0') / 1000000).toFixed(6)}`), escapeCsvValue(formatLargeNumber(usage.requestCount)), escapeCsvValue(formatLargeNumber(usage.inputTokens)), diff --git a/src/lib/organizations/organization-types.ts b/src/lib/organizations/organization-types.ts index 4c16a2d88..1b2381618 100644 --- a/src/lib/organizations/organization-types.ts +++ b/src/lib/organizations/organization-types.ts @@ -122,6 +122,7 @@ export type UsageDetailByDay = Array<{ email: string; }; model?: string; + feature?: string | null; microdollarCost: string | null; tokenCount: number; inputTokens: number; diff --git a/src/routers/organizations/organization-usage-details-router.ts b/src/routers/organizations/organization-usage-details-router.ts index 2028550fa..c4c6e341e 100644 --- a/src/routers/organizations/organization-usage-details-router.ts +++ b/src/routers/organizations/organization-usage-details-router.ts @@ -6,7 +6,12 @@ import { organizationMemberProcedure, } from '@/routers/organizations/utils'; import { db } from '@/lib/drizzle'; -import { microdollar_usage, kilocode_users } from '@/db/schema'; +import { + microdollar_usage, + microdollar_usage_metadata, + feature, + kilocode_users, +} from '@/db/schema'; import { eq, sum, count, sql, and, gte, lte } from 'drizzle-orm'; import * as z from 'zod'; import { AUTOCOMPLETE_MODEL } from '@/lib/constants'; @@ -61,6 +66,7 @@ const UsageDetailsResponseSchema = z.object({ email: z.string(), }), model: z.string().optional(), + feature: z.string().nullable().optional(), microdollarCost: z.string().nullable(), tokenCount: z.number(), inputTokens: z.number(), @@ -299,6 +305,7 @@ export const organizationsUsageDetailsRouter = createTRPCRouter({ userName: kilocode_users.google_user_name, userEmail: kilocode_users.google_user_email, ...(groupByModel && { model: microdollar_usage.model }), + feature: feature.feature, microdollarCost: sum(microdollar_usage.cost), tokenCount: sum( sql`${microdollar_usage.input_tokens} + ${microdollar_usage.output_tokens} + ${microdollar_usage.cache_write_tokens} + ${microdollar_usage.cache_hit_tokens}` @@ -309,12 +316,18 @@ export const organizationsUsageDetailsRouter = createTRPCRouter({ }) .from(microdollar_usage) .innerJoin(kilocode_users, eq(kilocode_users.id, microdollar_usage.kilo_user_id)) + .leftJoin( + microdollar_usage_metadata, + eq(microdollar_usage.id, microdollar_usage_metadata.id) + ) + .leftJoin(feature, eq(microdollar_usage_metadata.feature_id, feature.feature_id)) .where(and(...whereConditions)) .groupBy( sql`DATE(${microdollar_usage.created_at})`, kilocode_users.google_user_name, kilocode_users.google_user_email, - ...(groupByModel ? [microdollar_usage.model] : []) + ...(groupByModel ? [microdollar_usage.model] : []), + feature.feature ) .orderBy(sql`DATE(${microdollar_usage.created_at}) DESC`); @@ -325,6 +338,7 @@ export const organizationsUsageDetailsRouter = createTRPCRouter({ email: row.userEmail, }, ...(groupByModel && { model: 'model' in row ? row.model || undefined : undefined }), + feature: row.feature ?? null, microdollarCost: row.microdollarCost?.toString() || null, tokenCount: Number(row.tokenCount) || 0, inputTokens: Number(row.inputTokens) || 0,