feat(usage): add source/platform tracking + source breakdown in summary
- Add optional 'source' field to UsageDoc (desktop/web/ios/android) - Add 'source' to UpsertUsageSchema validation - Include source in upsert document ID to avoid cross-platform overwrites - Add SourceBreakdown interface - Aggregate sourceBreakdown in GET /usage/summary alongside modelBreakdown - Clients can now pass source when reporting usage for per-app analytics
This commit is contained in:
parent
b977e85bc2
commit
1cf74d22fa
@ -16,6 +16,7 @@ import {
|
||||
CheckLimitsSchema,
|
||||
type UsageDoc,
|
||||
type ModelBreakdown,
|
||||
type SourceBreakdown,
|
||||
} from './types.js';
|
||||
|
||||
/**
|
||||
@ -63,12 +64,19 @@ export async function usageRoutes(app: FastifyInstance) {
|
||||
const records = await repo.list({ userId, days: Number(days), productId });
|
||||
|
||||
const byModel: Record<string, { tokens: number; requests: number; cost: number }> = {};
|
||||
const bySource: Record<string, { tokens: number; requests: number; cost: number }> = {};
|
||||
for (const r of records) {
|
||||
const m = r.model || 'gpt-4o-mini';
|
||||
if (!byModel[m]) byModel[m] = { tokens: 0, requests: 0, cost: 0 };
|
||||
byModel[m].tokens += r.tokensUsed;
|
||||
byModel[m].requests += r.dictations;
|
||||
byModel[m].cost += r.costUsd;
|
||||
|
||||
const s = r.source || 'unknown';
|
||||
if (!bySource[s]) bySource[s] = { tokens: 0, requests: 0, cost: 0 };
|
||||
bySource[s].tokens += r.tokensUsed;
|
||||
bySource[s].requests += r.dictations;
|
||||
bySource[s].cost += r.costUsd;
|
||||
}
|
||||
|
||||
const modelBreakdown: ModelBreakdown[] = Object.entries(byModel).map(([model, s]) => ({
|
||||
@ -76,6 +84,11 @@ export async function usageRoutes(app: FastifyInstance) {
|
||||
...s,
|
||||
}));
|
||||
|
||||
const sourceBreakdown: SourceBreakdown[] = Object.entries(bySource).map(([source, s]) => ({
|
||||
source,
|
||||
...s,
|
||||
}));
|
||||
|
||||
return {
|
||||
totalWords: records.reduce((sum, r) => sum + r.words, 0),
|
||||
totalDictations: records.reduce((sum, r) => sum + r.dictations, 0),
|
||||
@ -83,6 +96,7 @@ export async function usageRoutes(app: FastifyInstance) {
|
||||
totalCost: records.reduce((sum, r) => sum + r.costUsd, 0),
|
||||
records,
|
||||
modelBreakdown,
|
||||
sourceBreakdown,
|
||||
};
|
||||
});
|
||||
|
||||
@ -95,7 +109,7 @@ export async function usageRoutes(app: FastifyInstance) {
|
||||
const input = parsed.data;
|
||||
const productId = getRequestProductId(req);
|
||||
const doc: UsageDoc = {
|
||||
id: `usg_${input.date}_${input.userId}${input.model ? `_${input.model}` : ''}`,
|
||||
id: `usg_${input.date}_${input.userId}${input.model ? `_${input.model}` : ''}${input.source ? `_${input.source}` : ''}`,
|
||||
productId,
|
||||
...input,
|
||||
createdAt: new Date().toISOString(),
|
||||
|
||||
@ -15,9 +15,17 @@ export interface UsageDoc {
|
||||
tokensUsed: number;
|
||||
costUsd: number;
|
||||
model?: string;
|
||||
source?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface SourceBreakdown {
|
||||
source: string;
|
||||
tokens: number;
|
||||
requests: number;
|
||||
cost: number;
|
||||
}
|
||||
|
||||
export interface UsageSummary {
|
||||
totalWords: number;
|
||||
totalDictations: number;
|
||||
@ -25,6 +33,7 @@ export interface UsageSummary {
|
||||
totalCost: number;
|
||||
records: UsageDoc[];
|
||||
modelBreakdown?: ModelBreakdown[];
|
||||
sourceBreakdown?: SourceBreakdown[];
|
||||
}
|
||||
|
||||
export interface ModelBreakdown {
|
||||
@ -49,6 +58,7 @@ export const UpsertUsageSchema = z.object({
|
||||
tokensUsed: z.number().int().min(0).default(0),
|
||||
costUsd: z.number().min(0).default(0),
|
||||
model: z.string().optional(),
|
||||
source: z.string().optional(),
|
||||
});
|
||||
|
||||
export const CheckLimitsSchema = z.object({
|
||||
|
||||
Loading…
Reference in New Issue
Block a user