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,
|
CheckLimitsSchema,
|
||||||
type UsageDoc,
|
type UsageDoc,
|
||||||
type ModelBreakdown,
|
type ModelBreakdown,
|
||||||
|
type SourceBreakdown,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -63,12 +64,19 @@ export async function usageRoutes(app: FastifyInstance) {
|
|||||||
const records = await repo.list({ userId, days: Number(days), productId });
|
const records = await repo.list({ userId, days: Number(days), productId });
|
||||||
|
|
||||||
const byModel: Record<string, { tokens: number; requests: number; cost: number }> = {};
|
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) {
|
for (const r of records) {
|
||||||
const m = r.model || 'gpt-4o-mini';
|
const m = r.model || 'gpt-4o-mini';
|
||||||
if (!byModel[m]) byModel[m] = { tokens: 0, requests: 0, cost: 0 };
|
if (!byModel[m]) byModel[m] = { tokens: 0, requests: 0, cost: 0 };
|
||||||
byModel[m].tokens += r.tokensUsed;
|
byModel[m].tokens += r.tokensUsed;
|
||||||
byModel[m].requests += r.dictations;
|
byModel[m].requests += r.dictations;
|
||||||
byModel[m].cost += r.costUsd;
|
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]) => ({
|
const modelBreakdown: ModelBreakdown[] = Object.entries(byModel).map(([model, s]) => ({
|
||||||
@ -76,6 +84,11 @@ export async function usageRoutes(app: FastifyInstance) {
|
|||||||
...s,
|
...s,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const sourceBreakdown: SourceBreakdown[] = Object.entries(bySource).map(([source, s]) => ({
|
||||||
|
source,
|
||||||
|
...s,
|
||||||
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalWords: records.reduce((sum, r) => sum + r.words, 0),
|
totalWords: records.reduce((sum, r) => sum + r.words, 0),
|
||||||
totalDictations: records.reduce((sum, r) => sum + r.dictations, 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),
|
totalCost: records.reduce((sum, r) => sum + r.costUsd, 0),
|
||||||
records,
|
records,
|
||||||
modelBreakdown,
|
modelBreakdown,
|
||||||
|
sourceBreakdown,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -95,7 +109,7 @@ export async function usageRoutes(app: FastifyInstance) {
|
|||||||
const input = parsed.data;
|
const input = parsed.data;
|
||||||
const productId = getRequestProductId(req);
|
const productId = getRequestProductId(req);
|
||||||
const doc: UsageDoc = {
|
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,
|
productId,
|
||||||
...input,
|
...input,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
|
|||||||
@ -15,9 +15,17 @@ export interface UsageDoc {
|
|||||||
tokensUsed: number;
|
tokensUsed: number;
|
||||||
costUsd: number;
|
costUsd: number;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
source?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SourceBreakdown {
|
||||||
|
source: string;
|
||||||
|
tokens: number;
|
||||||
|
requests: number;
|
||||||
|
cost: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UsageSummary {
|
export interface UsageSummary {
|
||||||
totalWords: number;
|
totalWords: number;
|
||||||
totalDictations: number;
|
totalDictations: number;
|
||||||
@ -25,6 +33,7 @@ export interface UsageSummary {
|
|||||||
totalCost: number;
|
totalCost: number;
|
||||||
records: UsageDoc[];
|
records: UsageDoc[];
|
||||||
modelBreakdown?: ModelBreakdown[];
|
modelBreakdown?: ModelBreakdown[];
|
||||||
|
sourceBreakdown?: SourceBreakdown[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModelBreakdown {
|
export interface ModelBreakdown {
|
||||||
@ -49,6 +58,7 @@ export const UpsertUsageSchema = z.object({
|
|||||||
tokensUsed: z.number().int().min(0).default(0),
|
tokensUsed: z.number().int().min(0).default(0),
|
||||||
costUsd: z.number().min(0).default(0),
|
costUsd: z.number().min(0).default(0),
|
||||||
model: z.string().optional(),
|
model: z.string().optional(),
|
||||||
|
source: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const CheckLimitsSchema = z.object({
|
export const CheckLimitsSchema = z.object({
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user