80 lines
3.0 KiB
TypeScript
80 lines
3.0 KiB
TypeScript
/**
|
|
* chatCore streaming usage-stats persistence (Quality Gate v2 / Fase 9 — chatCore god-file
|
|
* decomposition, #3501).
|
|
*
|
|
* Extracted from handleChatCore's onStreamComplete: records per-request usage analytics for a
|
|
* completed streaming response — the fire-and-forget `saveRequestUsage` row and the per-api-key
|
|
* billable-token counter (only on a 200 stream). Side-effect only (no handler state is mutated,
|
|
* nothing is returned); best-effort, every write swallows its own errors. The per-request context
|
|
* is threaded via `ctx` so the call site stays byte-identical; behaviour is unchanged. The
|
|
* compression usage-receipt attach stays in the handler (it is a handler-bound closure).
|
|
*/
|
|
|
|
import { saveRequestUsage } from "@/lib/usageDb";
|
|
import { recordTokenUsage } from "../../services/tokenLimitCounter.ts";
|
|
import { computeBillableTokens } from "./upstreamTimeouts.ts";
|
|
import { type EffectiveServiceTier } from "./serviceTier.ts";
|
|
|
|
export type RecordStreamingUsageStatsContext = {
|
|
provider: string | null | undefined;
|
|
model: string | null | undefined;
|
|
streamStatus: number;
|
|
startTime: number;
|
|
ttft: number;
|
|
streamErrorCode: string | null | undefined;
|
|
connectionId: string | null | undefined;
|
|
apiKeyInfo: { id?: string | null; name?: string | null } | null | undefined;
|
|
effectiveServiceTier: EffectiveServiceTier;
|
|
isCombo: boolean;
|
|
comboStrategy: string | null | undefined;
|
|
endpoint?: string | null | undefined;
|
|
};
|
|
|
|
function persistStreamingUsageRow(usage: object, ctx: RecordStreamingUsageStatsContext): void {
|
|
const { provider, model, streamStatus, startTime, ttft, streamErrorCode } = ctx;
|
|
saveRequestUsage({
|
|
provider: provider || "unknown",
|
|
model: model || "unknown",
|
|
tokens: usage,
|
|
status: String(streamStatus),
|
|
success: streamStatus === 200,
|
|
latencyMs: Date.now() - startTime,
|
|
timeToFirstTokenMs: ttft,
|
|
errorCode: streamStatus === 200 ? null : streamErrorCode || String(streamStatus),
|
|
timestamp: new Date().toISOString(),
|
|
connectionId: ctx.connectionId || undefined,
|
|
apiKeyId: ctx.apiKeyInfo?.id || undefined,
|
|
apiKeyName: ctx.apiKeyInfo?.name || undefined,
|
|
serviceTier: ctx.effectiveServiceTier,
|
|
comboStrategy: ctx.isCombo ? ctx.comboStrategy || undefined : undefined,
|
|
endpoint: ctx.endpoint || undefined,
|
|
}).catch((err) => {
|
|
console.error("Failed to save usage stats:", err.message);
|
|
});
|
|
}
|
|
|
|
function recordStreamingBillableTokens(usage: object, ctx: RecordStreamingUsageStatsContext): void {
|
|
if (!ctx.apiKeyInfo?.id || ctx.streamStatus !== 200) return;
|
|
try {
|
|
const billable = computeBillableTokens(usage);
|
|
if (billable > 0)
|
|
recordTokenUsage(
|
|
ctx.apiKeyInfo.id,
|
|
ctx.provider || "unknown",
|
|
ctx.model || "unknown",
|
|
billable
|
|
);
|
|
} catch {
|
|
// never block the stream on counter recording
|
|
}
|
|
}
|
|
|
|
export function recordStreamingUsageStats(
|
|
usage: unknown,
|
|
ctx: RecordStreamingUsageStatsContext
|
|
): void {
|
|
if (!usage || typeof usage !== "object") return;
|
|
persistStreamingUsageRow(usage, ctx);
|
|
recordStreamingBillableTokens(usage, ctx);
|
|
}
|