blob: e8a5e4a2ebadf3d8a231ec3a84d8799a9bd597c6 [file]
import { createHash } from 'node:crypto';
import { resolve } from 'node:path';
import type {
PricingConfig,
UsageBucket,
UsageGroupBy,
UsageLogRow,
UsageQuery,
UsageSummaryV2,
} from '@maka/core';
import {
canonicalPricingConfigsEqual,
comparePricingModelKeys,
normalizePricingConfig,
normalizePricingModelKey,
} from '@maka/core';
import { usageBucketKey } from '@maka/core';
import {
PricingRevisionConflictError,
PricingStoreClosedError,
PricingStoreNotLoadedError,
PricingStorePublicationError,
PricingValidationError,
type CreatePricingStoreOptions,
type PricingMutationResult,
type PricingSnapshot,
type PricingStore,
} from './pricing-store.js';
import {
acquireOperationalStateDatabase,
type OperationalStateDatabaseLease,
} from './operational-state-store.js';
import {
decodePersistedLlmCallRecord,
decodePersistedToolInvocationRecord,
type PersistedLlmCallRecord,
type PersistedToolInvocationRecord,
} from './telemetry-file-schema.js';
import {
resolveRange,
TelemetryQueryValidationError,
TelemetryRepoClosedError,
TelemetryRepoNotLoadedError,
TelemetryRepoPublicationError,
type CreateTelemetryRepoOptions,
type TelemetryRepo,
type ToolUsageQuery,
} from './telemetry-repo.js';
export interface CreateSqliteUsageStoreOptions {}
export function createSqliteTelemetryRepo(
workspaceRoot: string,
options: CreateTelemetryRepoOptions & CreateSqliteUsageStoreOptions = {},
): TelemetryRepo {
return new SqliteTelemetryRepo(
workspaceRoot,
options.createIfMissing ?? true,
options.managePricing ?? true,
);
}
export function createSqlitePricingStore(
workspaceRoot: string,
options: CreatePricingStoreOptions & CreateSqliteUsageStoreOptions = {},
): PricingStore {
return new SqlitePricingStore(workspaceRoot, options.createIfMissing ?? true);
}
class SqliteTelemetryRepo implements TelemetryRepo {
readonly #root: string;
readonly #lease: OperationalStateDatabaseLease;
readonly #pricingStore: PricingStore | undefined;
#loaded = false;
#state: 'open' | 'draining' | 'closed' = 'open';
#queue: Promise<void> = Promise.resolve();
#loadPromise: Promise<void> | undefined;
#closePromise: Promise<void> | undefined;
constructor(workspaceRoot: string, createIfMissing: boolean, managePricing: boolean) {
this.#root = resolve(workspaceRoot);
this.#lease = acquireOperationalStateDatabase(this.#root);
this.#pricingStore = managePricing
? createSqlitePricingStore(this.#root, { createIfMissing })
: undefined;
}
load(): Promise<void> {
if (this.#loaded) return Promise.resolve();
this.assertOpen();
if (this.#loadPromise) return this.#loadPromise;
const operation = (async () => {
if (this.#pricingStore) await this.#pricingStore.load();
this.#loaded = true;
})();
this.#loadPromise = operation;
void operation.catch(() => {
if (this.#state === 'open' && this.#loadPromise === operation) {
this.#loadPromise = undefined;
}
});
return operation;
}
insertLlmCall(record: PersistedLlmCallRecord): Promise<void> {
let admitted: PersistedLlmCallRecord;
try {
admitted = decodePersistedLlmCallRecord(record);
} catch (error) {
return Promise.reject(error);
}
return this.enqueueMutation(() => {
this.#lease.database
.prepare(`
INSERT INTO usage_llm_calls(storage_key, id, ts, record_json)
VALUES (?, ?, ?, ?)
ON CONFLICT(storage_key) DO UPDATE SET
id = excluded.id,
ts = excluded.ts,
record_json = excluded.record_json
`)
.run(usageIdentityKey(admitted.id), admitted.id, admitted.ts, JSON.stringify(admitted));
});
}
insertToolInvocation(record: PersistedToolInvocationRecord): Promise<void> {
let admitted: PersistedToolInvocationRecord;
try {
admitted = decodePersistedToolInvocationRecord(record);
} catch (error) {
return Promise.reject(error);
}
return this.enqueueMutation(() => {
this.#lease.database
.prepare(`
INSERT INTO usage_tool_invocations(storage_key, id, ts, record_json)
VALUES (?, ?, ?, ?)
ON CONFLICT(storage_key) DO UPDATE SET
id = excluded.id,
ts = excluded.ts,
record_json = excluded.record_json
`)
.run(usageIdentityKey(admitted.id), admitted.id, admitted.ts, JSON.stringify(admitted));
});
}
summary(query: UsageQuery): UsageSummaryV2 {
this.assertReady();
const { from, to } = resolveRange(query.range);
const rows = this.filteredUsageRows(query, from, to);
return detached({
range: { from, to },
totalRequests: rows.length,
totalCostUsd: sum(rows.map((row) => row.costUsd)),
totalTokens: {
input: sum(rows.map((row) => row.inputTokens)),
output: sum(rows.map((row) => row.outputTokens)),
cacheMiss: sum(rows.map((row) => row.cacheMissInputTokens)),
cacheRead: sum(rows.map((row) => row.cacheHitInputTokens)),
cacheWrite: sum(rows.map((row) => row.cacheWriteInputTokens)),
reasoning: sum(rows.map((row) => row.reasoningTokens)),
total: sum(rows.map((row) => row.totalTokens)),
},
cacheHitRequests: rows.filter((row) => row.cacheHitInputTokens > 0).length,
cacheCreateRequests: rows.filter((row) => row.cacheWriteInputTokens > 0).length,
errorRequests: rows.filter((row) => row.status === 'error').length,
});
}
buckets(query: UsageQuery, groupBy: UsageGroupBy): UsageBucket[] {
this.assertReady();
const { from, to } = resolveRange(query.range);
if (groupBy === 'tool') {
return detached(toolBuckets(this.filteredToolRows(query, from, to)));
}
const groups = new Map<string, PersistedLlmCallRecord[]>();
for (const row of this.filteredUsageRows(query, from, to)) {
const key = usageBucketKey(row, groupBy);
const group = groups.get(key);
if (group) group.push(row);
else groups.set(key, [row]);
}
return detached(
[...groups.entries()]
.map(([key, rows]) => usageBucket(key, rows))
.sort((left, right) => right.requests - left.requests),
);
}
logs(query: UsageQuery, offset = 0, limit = 100): { rows: UsageLogRow[]; total: number } {
this.assertReady();
if (query.toolName !== undefined) {
throw new TelemetryQueryValidationError('toolName is not applicable to LLM logs');
}
const { from, to } = resolveRange(query.range);
const rows = this.filteredUsageRows(query, from, to).sort((left, right) => right.ts - left.ts);
return detached({
rows: rows.slice(offset, offset + limit).map(toUsageLogRow),
total: rows.length,
});
}
toolLogs(
query: ToolUsageQuery,
offset = 0,
limit = 100,
): { rows: PersistedToolInvocationRecord[]; total: number } {
this.assertReady();
assertToolUsageQuery(query);
const { from, to } = resolveRange(query.range);
const rows = this.filteredToolRows(query, from, to).sort((left, right) => right.ts - left.ts);
return detached({ rows: rows.slice(offset, offset + limit), total: rows.length });
}
latestLlmRuntimeProbe(connectionSlug: string, modelId?: string): UsageLogRow | undefined {
return this.logs({ range: 'all', connectionSlug, ...(modelId ? { modelId } : {}) }, 0, 1)
.rows[0];
}
listPricingOverrides(): PricingConfig[] {
return this.requireManagedPricing()
.snapshot()
.overrides.map((item) => ({ ...item }));
}
async upsertPricing(pricing: PricingConfig): Promise<void> {
const store = this.requireManagedPricing();
await store.upsert(store.snapshot().revision, pricing);
}
async deletePricing(modelKey: string): Promise<void> {
const store = this.requireManagedPricing();
await store.delete(store.snapshot().revision, modelKey);
}
async flush(): Promise<void> {
this.assertLoaded();
await this.#queue;
}
close(): Promise<void> {
if (this.#closePromise) return this.#closePromise;
this.#state = 'draining';
this.#closePromise = Promise.allSettled([
this.#loadPromise ?? Promise.resolve(),
this.#queue,
this.#pricingStore?.close() ?? Promise.resolve(),
])
.then((results) => {
const failed = results.find((result) => result.status === 'rejected');
if (failed?.status === 'rejected') throw failed.reason;
})
.finally(() => {
this.#state = 'closed';
this.#lease.close();
});
return this.#closePromise;
}
private filteredUsageRows(query: UsageQuery, from: number, to: number) {
return this.readLlmRows().filter((row) => {
if (row.ts < from || row.ts > to) return false;
if (query.connectionSlug && row.connectionSlug !== query.connectionSlug) return false;
if (query.providerId && row.providerId !== query.providerId) return false;
if (query.modelId && row.modelId !== query.modelId) return false;
if (query.status && query.status !== 'all' && row.status !== query.status) return false;
return true;
});
}
private filteredToolRows(query: UsageQuery | ToolUsageQuery, from: number, to: number) {
return this.readToolRows().filter((row) => {
if (row.ts < from || row.ts > to) return false;
if (query.toolName && row.toolName !== query.toolName) return false;
if (query.status && query.status !== 'all' && row.status !== query.status) return false;
return true;
});
}
private readLlmRows(): PersistedLlmCallRecord[] {
return (
this.#lease.database.prepare('SELECT record_json FROM usage_llm_calls').all() as Array<{
record_json: string;
}>
).map((row) => decodePersistedLlmCallRecord(JSON.parse(row.record_json)));
}
private readToolRows(): PersistedToolInvocationRecord[] {
return (
this.#lease.database
.prepare('SELECT record_json FROM usage_tool_invocations')
.all() as Array<{ record_json: string }>
).map((row) => decodePersistedToolInvocationRecord(JSON.parse(row.record_json)));
}
private enqueueMutation(operation: () => void): Promise<void> {
this.assertReady();
const accepted = this.#queue.then(() => {
try {
this.#lease.transaction('write', operation);
} catch (cause) {
throw new TelemetryRepoPublicationError(false, { cause });
}
});
this.#queue = accepted.catch(() => undefined);
return accepted;
}
private requireManagedPricing(): PricingStore {
this.assertReady();
if (!this.#pricingStore) {
throw new Error('Telemetry repository does not own the managed pricing store');
}
return this.#pricingStore;
}
private assertLoaded(): void {
if (!this.#loaded) throw new TelemetryRepoNotLoadedError();
}
private assertOpen(): void {
if (this.#state !== 'open') throw new TelemetryRepoClosedError();
}
private assertReady(): void {
this.assertOpen();
this.assertLoaded();
}
}
class SqlitePricingStore implements PricingStore {
readonly #root: string;
readonly #lease: OperationalStateDatabaseLease;
#loaded = false;
#state: 'open' | 'draining' | 'closed' = 'open';
#queue: Promise<void> = Promise.resolve();
#loadPromise: Promise<void> | undefined;
#closePromise: Promise<void> | undefined;
constructor(workspaceRoot: string, createIfMissing: boolean) {
this.#root = resolve(workspaceRoot);
this.#lease = acquireOperationalStateDatabase(this.#root);
}
load(): Promise<void> {
if (this.#loaded) return Promise.resolve();
this.assertOpen();
if (this.#loadPromise) return this.#loadPromise;
const operation = Promise.resolve().then(() => {
this.#loaded = true;
});
this.#loadPromise = operation;
void operation.catch(() => {
if (this.#state === 'open' && this.#loadPromise === operation) {
this.#loadPromise = undefined;
}
});
return operation;
}
snapshot(): PricingSnapshot {
this.assertReady();
return readPricingSnapshot(this.#lease);
}
upsert(expectedRevision: number, pricing: PricingConfig): Promise<PricingMutationResult> {
assertRevision(expectedRevision, 'expectedRevision');
const normalized = normalizePricingConfig(pricing);
if (!normalized.ok) throw new PricingValidationError(normalized.error);
return this.enqueueMutation(expectedRevision, (current) => {
const existing = current.find((item) => item.modelKey === normalized.value.modelKey);
if (existing && canonicalPricingConfigsEqual(existing, normalized.value)) return current;
return [
...current.filter((item) => item.modelKey !== normalized.value.modelKey),
normalized.value,
].sort((left, right) => comparePricingModelKeys(left.modelKey, right.modelKey));
});
}
delete(expectedRevision: number, modelKey: string): Promise<PricingMutationResult> {
assertRevision(expectedRevision, 'expectedRevision');
const normalized = normalizePricingModelKey(modelKey);
if (!normalized.ok) throw new PricingValidationError(normalized.error);
return this.enqueueMutation(expectedRevision, (current) =>
current.some((item) => item.modelKey === normalized.value)
? current.filter((item) => item.modelKey !== normalized.value)
: current,
);
}
async flush(): Promise<void> {
this.assertLoaded();
await this.#queue;
}
beginDrain(): Promise<void> {
if (this.#state === 'open') this.#state = 'draining';
return this.flush();
}
close(): Promise<void> {
if (this.#closePromise) return this.#closePromise;
this.#state = 'draining';
this.#closePromise = Promise.allSettled([this.#loadPromise ?? Promise.resolve(), this.#queue])
.then((results) => {
const failed = results.find((result) => result.status === 'rejected');
if (failed?.status === 'rejected') throw failed.reason;
})
.finally(() => {
this.#state = 'closed';
this.#lease.close();
});
return this.#closePromise;
}
private enqueueMutation(
expectedRevision: number,
mutate: (current: readonly Readonly<PricingConfig>[]) => readonly Readonly<PricingConfig>[],
): Promise<PricingMutationResult> {
this.assertReady();
const operation = this.#queue.then(() => {
try {
return this.#lease.transaction('write', () => {
const current = readPricingSnapshot(this.#lease);
if (current.revision !== expectedRevision) {
throw new PricingRevisionConflictError(expectedRevision, current.revision);
}
const overrides = mutate(current.overrides);
if (overrides === current.overrides) {
return { committed: false, changed: false, snapshot: current };
}
if (current.revision === Number.MAX_SAFE_INTEGER) {
throw new PricingValidationError(
'revision cannot advance beyond Number.MAX_SAFE_INTEGER',
);
}
const revision = current.revision + 1;
this.#lease.database.prepare('DELETE FROM usage_pricing_overrides').run();
const insert = this.#lease.database.prepare(`
INSERT INTO usage_pricing_overrides(model_key, record_json)
VALUES (?, ?)
`);
for (const override of overrides) {
insert.run(override.modelKey, JSON.stringify(override));
}
this.#lease.database
.prepare('UPDATE usage_pricing_authority SET revision = ? WHERE singleton = 1')
.run(revision);
return {
committed: true,
changed: true,
snapshot: freezePricingSnapshot(revision, overrides),
};
});
} catch (error) {
if (
error instanceof PricingRevisionConflictError ||
error instanceof PricingValidationError
) {
throw error;
}
throw new PricingStorePublicationError({ cause: error });
}
});
this.#queue = operation.then(
() => undefined,
() => undefined,
);
return operation;
}
private assertLoaded(): void {
if (!this.#loaded) throw new PricingStoreNotLoadedError();
}
private assertOpen(): void {
if (this.#state !== 'open') throw new PricingStoreClosedError();
}
private assertReady(): void {
this.assertOpen();
this.assertLoaded();
}
}
function readPricingSnapshot(lease: OperationalStateDatabaseLease): PricingSnapshot {
const authority = lease.database
.prepare('SELECT revision FROM usage_pricing_authority WHERE singleton = 1')
.get() as { revision?: unknown } | undefined;
if (!authority || !Number.isSafeInteger(authority.revision)) {
throw new PricingValidationError('SQLite pricing authority is missing or invalid');
}
const overrides = (
lease.database
.prepare('SELECT record_json FROM usage_pricing_overrides ORDER BY model_key')
.all() as Array<{ record_json: string }>
).map((row) => {
const normalized = normalizePricingConfig(JSON.parse(row.record_json));
if (!normalized.ok) throw new PricingValidationError(normalized.error);
return normalized.value;
});
return freezePricingSnapshot(authority.revision as number, overrides);
}
function freezePricingSnapshot(
revision: number,
overrides: readonly Readonly<PricingConfig>[],
): PricingSnapshot {
return Object.freeze({
revision,
overrides: Object.freeze(overrides.map((value) => Object.freeze({ ...value }))),
});
}
function countRows(
database: OperationalStateDatabaseLease['database'],
table: 'usage_llm_calls' | 'usage_tool_invocations' | 'usage_pricing_overrides',
): number {
const row = database.prepare(`SELECT COUNT(*) AS count FROM ${table}`).get() as {
count?: unknown;
};
if (!Number.isSafeInteger(row.count)) throw new Error(`Invalid row count for ${table}`);
return row.count as number;
}
function usageIdentityKey(id: string): string {
return createHash('sha256').update(JSON.stringify(id)).digest('hex');
}
function toUsageLogRow(row: PersistedLlmCallRecord): UsageLogRow {
return {
id: row.id,
ts: row.ts,
...(row.callKind ? { callKind: row.callKind } : {}),
...(row.callId ? { callId: row.callId } : {}),
...(row.connectionSlug ? { connectionSlug: row.connectionSlug } : {}),
providerId: row.providerId,
modelId: row.modelId,
inputTokens: row.inputTokens,
outputTokens: row.outputTokens,
cacheMissTokens: row.cacheMissInputTokens,
cacheReadTokens: row.cacheHitInputTokens,
cacheWriteTokens: row.cacheWriteInputTokens,
...(row.cacheMissInputSource ? { cacheMissInputSource: row.cacheMissInputSource } : {}),
reasoningTokens: row.reasoningTokens,
totalTokens: row.totalTokens,
costUsd: row.costUsd,
latencyMs: row.latencyMs,
status: row.status,
...(row.errorClass ? { errorClass: row.errorClass } : {}),
...(row.sessionId ? { sessionId: row.sessionId } : {}),
...(row.turnId ? { turnId: row.turnId } : {}),
...(row.systemPromptHash ? { systemPromptHash: row.systemPromptHash } : {}),
...(row.prefixHash ? { prefixHash: row.prefixHash } : {}),
...(row.prefixChangeReason ? { prefixChangeReason: row.prefixChangeReason } : {}),
...(row.requestShapeHash ? { requestShapeHash: row.requestShapeHash } : {}),
...(row.requestShapeChangeReason
? { requestShapeChangeReason: row.requestShapeChangeReason }
: {}),
...(row.toolSchemaChangeReason ? { toolSchemaChangeReason: row.toolSchemaChangeReason } : {}),
...(row.toolAvailability ? { toolAvailability: row.toolAvailability } : {}),
...(row.promptSegments ? { promptSegments: row.promptSegments } : {}),
...(row.contextBudget ? { contextBudget: row.contextBudget } : {}),
};
}
function usageBucket(key: string, rows: readonly PersistedLlmCallRecord[]): UsageBucket {
const errors = rows.filter((row) => row.status === 'error').length;
return {
key,
label: key,
requests: rows.length,
inputTokens: sum(rows.map((row) => row.inputTokens)),
outputTokens: sum(rows.map((row) => row.outputTokens)),
cacheMissTokens: sum(rows.map((row) => row.cacheMissInputTokens)),
cacheReadTokens: sum(rows.map((row) => row.cacheHitInputTokens)),
cacheWriteTokens: sum(rows.map((row) => row.cacheWriteInputTokens)),
reasoningTokens: sum(rows.map((row) => row.reasoningTokens)),
totalTokens: sum(rows.map((row) => row.totalTokens)),
costUsd: sum(rows.map((row) => row.costUsd)),
avgLatencyMs: rows.length ? Math.round(sum(rows.map((row) => row.latencyMs)) / rows.length) : 0,
errorRate: rows.length ? errors / rows.length : 0,
};
}
function toolBuckets(rows: readonly PersistedToolInvocationRecord[]): UsageBucket[] {
const groups = new Map<string, PersistedToolInvocationRecord[]>();
for (const row of rows) {
const group = groups.get(row.toolName);
if (group) group.push(row);
else groups.set(row.toolName, [row]);
}
return [...groups.entries()]
.map(([key, group]) => {
const errors = group.filter((row) => row.status === 'error').length;
const bytesIn = sum(group.map((row) => row.bytesIn));
const bytesOut = sum(group.map((row) => row.bytesOut));
return {
key,
label: key,
requests: group.length,
inputTokens: bytesIn,
outputTokens: bytesOut,
cacheMissTokens: 0,
cacheReadTokens: 0,
cacheWriteTokens: 0,
reasoningTokens: 0,
totalTokens: bytesIn + bytesOut,
costUsd: 0,
avgLatencyMs: group.length
? Math.round(sum(group.map((row) => row.durationMs)) / group.length)
: 0,
errorRate: group.length ? errors / group.length : 0,
};
})
.sort((left, right) => right.requests - left.requests);
}
function sum(values: readonly number[]): number {
return values.reduce((total, value) => total + value, 0);
}
function assertToolUsageQuery(query: ToolUsageQuery): void {
if (Object.keys(query).some((key) => !['range', 'toolName', 'status'].includes(key))) {
throw new TelemetryQueryValidationError('tool logs accept only range, toolName, and status');
}
}
function assertRevision(value: unknown, label: string): asserts value is number {
if (!Number.isSafeInteger(value) || (value as number) < 0) {
throw new PricingValidationError(`${label} must be a nonnegative safe integer`);
}
}
function detached<T>(value: T): T {
return deepFreeze(structuredClone(value));
}
function deepFreeze<T>(value: T): T {
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
Object.freeze(value);
for (const nested of Object.values(value)) deepFreeze(nested);
return value;
}