ai_api / src /lib /quota /spendRecorder.ts
Yogesh
initial deploy
cd8bd0a
Raw
History Blame Contribute Delete
4.23 kB
/**
* spendRecorder.ts β€” Fire-and-forget wrapper for POST-response consumption.
*
* Schedules `recordConsumption` on the next event-loop tick via `setImmediate`
* so it never adds latency to the client response path.
*
* Errors from `recordConsumption` are caught and logged via pino (if a logger
* is provided) but NEVER propagated β€” per B29, drift is acceptable and will
* self-correct through the global saturation signal on the next request.
*
* Part of: Group B β€” Quota Sharing Engine (plan 22, frente F7).
*/
import { recordConsumption } from "./enforce";
import type { RecordConsumptionInput } from "./types";
// Minimal pino-compatible logger surface (only warn is needed)
interface MinimalLogger {
warn?: (data: unknown, msg?: string) => void;
}
/**
* Schedule `recordConsumption` for the next event-loop tick.
*
* @param input Consumption data to record.
* @param log Optional pino logger; if omitted, errors are silently discarded.
*/
export function scheduleRecordConsumption(
input: RecordConsumptionInput,
log?: MinimalLogger | null
): void {
setImmediate(() => {
recordConsumption(input).catch((err: unknown) => {
if (log?.warn) {
log.warn(
{ err: err instanceof Error ? err.message : String(err) },
"[quotaShare] recordConsumption failed (drift expected)"
);
}
});
});
}
/**
* Build the per-request consumption cost payload shared by the streaming and
* non-streaming POST-hooks. Coerces token fields defensively (string/NaN safe)
* and clamps a negative/zero cost to 0 so a bad pricing lookup never records
* negative USD.
*/
export function buildConsumptionCost(
usage: unknown,
estimatedCost: number
): { tokens: number; usd: number; requests: number } {
const u = usage && typeof usage === "object" ? (usage as Record<string, unknown>) : null;
const tokens = u
? (Number(u.prompt_tokens ?? 0) || 0) + (Number(u.completion_tokens ?? 0) || 0)
: 0;
return {
tokens,
usd: estimatedCost > 0 ? estimatedCost : 0,
requests: 1,
};
}
/** Cost resolver injected for testability (matches `calculateCost`). */
type CostResolver = (
provider: string,
model: string,
usage: Record<string, number | undefined> | null | undefined,
options: { serviceTier?: string }
) => Promise<number>;
/**
* Record shared-quota consumption for a completed STREAMING response.
*
* Unlike the non-streaming path, the streaming completion previously recorded
* `usd: 0` (the cost was resolved asynchronously only for `recordCost`), so
* USD-unit pools (e.g. DeepSeek `usd/monthly`) never accrued on streaming
* traffic. This resolves the real cost via the injected `calculateCost` and
* schedules a single consumption record. Fire-and-forget / fail-open: never
* throws to the caller, and still records `requests: 1` when usage is absent.
*/
export async function recordStreamingConsumption(
params: {
apiKeyId?: string | null;
connectionId?: string | null;
provider?: string | null;
model: string;
streamUsage: unknown;
streamStatus: number;
serviceTier?: string;
},
deps: {
calculateCost: CostResolver;
schedule?: (input: RecordConsumptionInput, log?: MinimalLogger | null) => void;
log?: MinimalLogger | null;
}
): Promise<void> {
const { apiKeyId, connectionId, provider, model, streamUsage, streamStatus, serviceTier } =
params;
if (!apiKeyId || !connectionId || streamStatus !== 200) return;
const schedule = deps.schedule ?? scheduleRecordConsumption;
const resolvedProvider = provider ?? "unknown";
let estimatedCost = 0;
if (streamUsage && typeof streamUsage === "object") {
try {
estimatedCost = await deps.calculateCost(
resolvedProvider,
model,
streamUsage as Record<string, number | undefined>,
{ serviceTier }
);
} catch {
estimatedCost = 0;
}
}
schedule(
{
apiKeyId,
connectionId,
provider: resolvedProvider,
// Per-(key,model) cap accounting on streaming traffic β€” same resolved model id.
model: model || undefined,
cost: buildConsumptionCost(streamUsage, estimatedCost),
},
deps.log
);
}