| 1 | /** |
| 2 | * The usage reader contract: how a driver tells the server's usage page where |
| 3 | * its history lives and how to read it. |
| 4 | * |
| 5 | * A driver either points at JSONL transcripts that the server's shared engine |
| 6 | * streams, caches and resumes (`kind: "transcripts"`), or scans its own |
| 7 | * sources and returns their records (`kind: "scan"`). The server owns |
| 8 | * pricing, aggregation, de-duplication and the scan cache. |
| 9 | * |
| 10 | * @module provider-core/server/usage |
| 11 | */ |
| 12 | import type { |
| 13 | ProviderInstanceId, |
| 14 | ServerSettings, |
| 15 | UsageProviderKind, |
| 16 | UsageReadError, |
| 17 | UsageSource, |
| 18 | UsageTokenTotals, |
| 19 | } from "@t3tools/contracts"; |
| 20 | import type * as Effect from "effect/Effect"; |
| 21 | import type * as Schema from "effect/Schema"; |
| 22 | |
| 23 | /** |
| 24 | * Billing speed of a request. Faster speeds bill at a model-specific premium. |
| 25 | * Claude fast mode and Codex `priority` are `fast`; Codex `ultrafast` is its |
| 26 | * own, more expensive tier. |
| 27 | */ |
| 28 | export type UsageSpeed = "standard" | "fast" | "ultrafast"; |
| 29 | |
| 30 | export interface UsageRecord { |
| 31 | readonly provider: UsageProviderKind; |
| 32 | readonly timestampMs: number; |
| 33 | readonly model: string; |
| 34 | /** |
| 35 | * Rate-table key when the provider's display name carries tiers the table |
| 36 | * does not know, such as Cursor's `claude-opus-5-5-high`. Defaults to `model`. |
| 37 | */ |
| 38 | readonly rateModel?: string; |
| 39 | readonly sessionId: string; |
| 40 | readonly totals: UsageTokenTotals; |
| 41 | readonly reportedCostUsd: number | null; |
| 42 | /** Only Claude Code and Codex record a speed; other providers are `standard`. */ |
| 43 | readonly speed: UsageSpeed; |
| 44 | /** |
| 45 | * Key for cross-file de-duplication, or `null` when the record is inherently |
| 46 | * unique and needs no dedup. |
| 47 | */ |
| 48 | readonly dedupeKey: string | null; |
| 49 | } |
| 50 | |
| 51 | export const EMPTY_TOTALS: UsageTokenTotals = { |
| 52 | uncachedInputTokens: 0, |
| 53 | cachedInputTokens: 0, |
| 54 | cacheCreationTokens: 0, |
| 55 | outputTokens: 0, |
| 56 | reasoningTokens: 0, |
| 57 | }; |
| 58 | |
| 59 | export function totalTokens(totals: UsageTokenTotals): number { |
| 60 | // reasoningTokens is a subset of outputTokens and must not be added again. |
| 61 | return ( |
| 62 | totals.uncachedInputTokens + |
| 63 | totals.cachedInputTokens + |
| 64 | totals.cacheCreationTokens + |
| 65 | totals.outputTokens |
| 66 | ); |
| 67 | } |
| 68 | |
| 69 | /** A token count from untrusted JSON: a positive finite number, truncated, else 0. */ |
| 70 | export function tokenCount(value: unknown): number { |
| 71 | return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.trunc(value) : 0; |
| 72 | } |
| 73 | |
| 74 | /** Epoch milliseconds of an ISO timestamp string, or `null`. */ |
| 75 | export function parseTimestampMs(value: unknown): number | null { |
| 76 | if (typeof value !== "string") return null; |
| 77 | const parsed = Date.parse(value); |
| 78 | return Number.isNaN(parsed) ? null : parsed; |
| 79 | } |
| 80 | |
| 81 | /** JSON fields a format reads. `true` keeps the whole subtree. */ |
| 82 | export type SelectedFields = { readonly [key: string]: true | SelectedFields }; |
| 83 | |
| 84 | /** |
| 85 | * How the server's JSONL engine reads one provider's transcript lines. |
| 86 | * |
| 87 | * Lines arrive one at a time. A line too large to parse whole is projected to |
| 88 | * `selectFields` and handed to `parseProjected` instead of `parseLine`. |
| 89 | */ |
| 90 | export interface TranscriptUsageFormat<State> { |
| 91 | readonly selectFields: SelectedFields; |
| 92 | /** |
| 93 | * ASCII substrings, at least one of which every line that matters contains. |
| 94 | * Transcripts are mostly tool output, so the engine matches these against a |
| 95 | * line's raw bytes and neither decodes nor parses the rest, which is worth an |
| 96 | * order of magnitude. Lines that only update `State` must match too. |
| 97 | */ |
| 98 | readonly usageMarkers: readonly [string, ...string[]]; |
| 99 | parseLine(line: string, state: State): readonly UsageRecord[]; |
| 100 | parseProjected(projected: unknown, state: State): readonly UsageRecord[]; |
| 101 | /** |
| 102 | * Present for formats whose records depend on earlier lines. The state at a |
| 103 | * resume point is persisted in the scan cache through `schema`, so a grown |
| 104 | * file resumes with it. Absent means every line stands alone. |
| 105 | */ |
| 106 | readonly state?: { |
| 107 | readonly initial: () => State; |
| 108 | readonly schema: Schema.Codec<State, unknown>; |
| 109 | }; |
| 110 | /** |
| 111 | * Whether one session's records may appear in several files, such as a |
| 112 | * rollout copied when it moves. The server then keys those sessions' |
| 113 | * records for cross-file de-duplication. |
| 114 | */ |
| 115 | readonly sharedSessionsAcrossFiles?: true; |
| 116 | } |
| 117 | |
| 118 | /** A configured instance of the driver, its config already decoded by `driver.configSchema`. */ |
| 119 | export interface ProviderUsageInstance<Config> { |
| 120 | readonly instanceId: ProviderInstanceId; |
| 121 | /** |
| 122 | * `undefined` when the stored config does not decode. The instance still has |
| 123 | * history on disk, so readers that need no config read it anyway. |
| 124 | */ |
| 125 | readonly config: Config | undefined; |
| 126 | /** The instance's environment merged over the host environment. */ |
| 127 | readonly environment: NodeJS.ProcessEnv; |
| 128 | /** Whether settings list the instance, rather than it being the implicit default. */ |
| 129 | readonly configured: boolean; |
| 130 | } |
| 131 | |
| 132 | /** One source a `scan` reader read. */ |
| 133 | export interface ProviderUsageScan { |
| 134 | readonly dir: string; |
| 135 | /** Identity of the source across hosts. Defaults to the filesystem identity of `dir`. */ |
| 136 | readonly volumeId?: string; |
| 137 | /** Overrides the server's host name in the source fingerprint, for account-wide sources. */ |
| 138 | readonly hostId?: string; |
| 139 | readonly status?: UsageSource["status"]; |
| 140 | readonly message?: string; |
| 141 | readonly action?: UsageSource["action"]; |
| 142 | /** Parsed records per file, or `null` when the source does not exist. */ |
| 143 | readonly files: |
| 144 | | readonly { readonly path: string; readonly records: readonly UsageRecord[] }[] |
| 145 | | null; |
| 146 | /** Answered from a cache while a refresh runs. */ |
| 147 | readonly refreshing?: true; |
| 148 | } |
| 149 | |
| 150 | export interface ProviderUsageScanInput<Config> { |
| 151 | readonly instances: ReadonlyArray<ProviderUsageInstance<Config>>; |
| 152 | /** The settings snapshot the whole read runs against. */ |
| 153 | readonly settings: ServerSettings; |
| 154 | /** Files last written before this cannot hold records inside the window. */ |
| 155 | readonly windowStartMs: number; |
| 156 | /** The oldest usage the server keeps; account caches cover back to here. */ |
| 157 | readonly retentionCutoffMs: number; |
| 158 | /** Wait for a slow refresh instead of answering from a cache. */ |
| 159 | readonly awaitRefresh: boolean; |
| 160 | } |
| 161 | |
| 162 | export type ProviderUsageReader<Config, R> = |
| 163 | | { |
| 164 | readonly kind: "transcripts"; |
| 165 | readonly provider: UsageProviderKind; |
| 166 | readonly format: TranscriptUsageFormat<unknown>; |
| 167 | /** |
| 168 | * Transcript directories for one instance. The server canonicalizes, |
| 169 | * de-duplicates and scans them; `fileName` limits a walk to one basename. |
| 170 | */ |
| 171 | readonly directories: ( |
| 172 | instance: ProviderUsageInstance<Config>, |
| 173 | ) => Effect.Effect< |
| 174 | ReadonlyArray<{ readonly dir: string; readonly fileName?: string }>, |
| 175 | never, |
| 176 | R |
| 177 | >; |
| 178 | } |
| 179 | | { |
| 180 | readonly kind: "scan"; |
| 181 | readonly provider: UsageProviderKind; |
| 182 | /** Fails only when the whole usage read cannot go on; an unreadable source is a status. */ |
| 183 | readonly scan: ( |
| 184 | input: ProviderUsageScanInput<Config>, |
| 185 | ) => Effect.Effect<ReadonlyArray<ProviderUsageScan>, UsageReadError, R>; |
| 186 | }; |