Skip to content
Source
Sign inSign up

usage.ts

137 lines4.6 KB

1import type {
2 ProviderInstanceId,
3 ServerSettings,
4 UsageProviderKind,
5 UsageReadError,
6 UsageSource,
7 UsageTokenTotals,
8} from "@t3tools/contracts";
9import type * as Effect from "effect/Effect";
10import type * as Schema from "effect/Schema";
11​
12export type UsageSpeed = "standard" | "fast" | "ultrafast";
13​
14export interface UsageRecord {
15 readonly provider: UsageProviderKind;
16 readonly timestampMs: number;
17 readonly model: string;
18 readonly rateModel?: string;
19 readonly sessionId: string;
20 readonly totals: UsageTokenTotals;
21 /** The part of `totals.cacheCreationTokens` written with a 1-hour TTL. */
22 readonly cacheCreation1hTokens?: number;
23 readonly reportedCostUsd: number | null;
24 readonly speed: UsageSpeed;
25 readonly unresolvedServiceTierTurnId?: string;
26 readonly dedupeKey: string | null;
28​
29export const EMPTY_TOTALS: UsageTokenTotals = {
30 uncachedInputTokens: 0,
31 cachedInputTokens: 0,
32 cacheCreationTokens: 0,
33 outputTokens: 0,
34 reasoningTokens: 0,
35};
36​
37export function totalTokens(totals: UsageTokenTotals): number {
38 // reasoningTokens is a subset of outputTokens and must not be added again.
39 return (
40 totals.uncachedInputTokens +
41 totals.cachedInputTokens +
42 totals.cacheCreationTokens +
43 totals.outputTokens
44 );
46​
47export function tokenCount(value: unknown): number {
48 return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.trunc(value) : 0;
50​
51export function parseTimestampMs(value: unknown): number | null {
52 if (typeof value !== "string") return null;
53 const parsed = Date.parse(value);
54 return Number.isNaN(parsed) ? null : parsed;
56​
57export type SelectedFields = { readonly [key: string]: true | SelectedFields };
58​
59export interface TranscriptUsageFormat<State> {
60 readonly selectFields: SelectedFields;
61 /** ASCII markers must match every relevant line, including state-only updates. */
62 readonly usageMarkers: readonly [string, ...string[]];
63 parseLine(line: string, state: State): readonly UsageRecord[];
64 parseProjected(projected: unknown, state: State): readonly UsageRecord[];
65 /** Cumulative malformed or inconsistent usage records, including skipped records. */
66 readMalformedRecords?(state: State): number;
67 /**
68 * Present for formats whose records depend on earlier lines. The state at a
69 * resume point is persisted in the scan cache through `schema`, so a grown
70 * file resumes with it. Absent means every line stands alone.
71 */
72 readonly state?: {
73 readonly initial: () => State;
74 readonly schema: Schema.Codec<State, unknown>;
75 };
76 /** Enable cross-file deduplication when session records can appear in multiple files. */
77 readonly sharedSessionsAcrossFiles?: true;
79​
80export interface ProviderUsageInstance<Config> {
81 readonly instanceId: ProviderInstanceId;
82 /** Undefined for invalid stored config; config-independent readers should still read history. */
83 readonly config: Config | undefined;
84 readonly environment: NodeJS.ProcessEnv;
85 readonly configured: boolean;
87​
88export interface ProviderUsageScan {
89 readonly dir: string;
90 readonly volumeId?: string;
91 readonly hostId?: string;
92 readonly status?: UsageSource["status"];
93 readonly message?: string;
94 readonly files:
95 | readonly { readonly path: string; readonly records: readonly UsageRecord[] }[]
96 | null;
97 readonly refreshing?: true;
99​
100export interface ProviderUsageScanInput<Config> {
101 readonly instances: ReadonlyArray<ProviderUsageInstance<Config>>;
102 readonly settings: ServerSettings;
103 readonly windowStartMs: number;
104 readonly retentionCutoffMs: number;
105 readonly awaitRefresh: boolean;
107​
108export type ProviderUsageReader<Config, R> =
109 | {
110 readonly kind: "transcripts";
111 readonly provider: UsageProviderKind;
112 readonly format: TranscriptUsageFormat<unknown>;
113 readonly resolveServiceTiers?: (
114 records: readonly UsageRecord[],
115 ) => Effect.Effect<ReadonlyMap<UsageRecord, UsageSpeed>, never, R>;
116 /** A `refreshing` entry is not scanned; it reports its source as still being located. */
117 readonly directories: (
118 instance: ProviderUsageInstance<Config>,
119 options: { readonly awaitRefresh: boolean },
120 ) => Effect.Effect<
121 ReadonlyArray<{
122 readonly dir: string;
123 readonly fileName?: string;
124 readonly refreshing?: true;
125 }>,
126 never,
127 R
128 >;
129 }
130 | {
131 readonly kind: "scan";
132 readonly provider: UsageProviderKind;
133 /** Fails only when the whole usage read cannot go on; an unreadable source is a status. */
134 readonly scan: (
135 input: ProviderUsageScanInput<Config>,
136 ) => Effect.Effect<ReadonlyArray<ProviderUsageScan>, UsageReadError, R>;
137 };