Skip to content
Source
Sign inSign up
Open History to see the last commit

usage.ts

167 lines6.1 KB

1import * as Schema from "effect/Schema";
2​
3import { ForwardCompatibleArray, NonNegativeInt, TrimmedNonEmptyString } from "./baseSchemas.ts";
4​
5/** Bump for incompatible UsageSummary changes; skipped unknown array variants need no bump. */
6export const USAGE_CONTRACT_VERSION = 6 as const;
7​
8export const USAGE_MERGE_COMPATIBLE_SINCE = 4 as const;
9​
10export const UsageProviderKind = Schema.Literals([
11 "claude",
12 "codex",
13 "grok",
14 "cursor",
15 "opencode",
16 "antigravity",
17 "pi",
18 "muse",
19]);
20export type UsageProviderKind = typeof UsageProviderKind.Type;
21​
22const USAGE_DAY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
23​
24export const UsageDay = TrimmedNonEmptyString.check(Schema.isPattern(USAGE_DAY_PATTERN)).pipe(
25 Schema.brand("UsageDay"),
26);
27export type UsageDay = typeof UsageDay.Type;
28​
29export const UsageResolution = Schema.Literals(["day", "hour"]);
30export type UsageResolution = typeof UsageResolution.Type;
31​
32export const UsageCostSource = Schema.Literals(["providerReported", "modelPriced", "unpriced"]);
33export type UsageCostSource = typeof UsageCostSource.Type;
34​
35/** Sum uncached input, cache reads, and cache writes for input totals; reasoning is already included in output. */
36export const UsageTokenTotals = Schema.Struct({
37 uncachedInputTokens: NonNegativeInt,
38 cachedInputTokens: NonNegativeInt,
39 cacheCreationTokens: NonNegativeInt,
40 outputTokens: NonNegativeInt,
41 reasoningTokens: NonNegativeInt,
42});
43export type UsageTokenTotals = typeof UsageTokenTotals.Type;
44​
45export const UsageCategoryCost = Schema.Struct({
46 input: Schema.Number,
47 cacheRead: Schema.Number,
48 cacheWrite: Schema.Number,
49 output: Schema.Number,
50});
51export type UsageCategoryCost = typeof UsageCategoryCost.Type;
52​
53export const UsageBucket = Schema.Struct({
54 day: UsageDay,
55 hourStart: Schema.optional(TrimmedNonEmptyString),
56 provider: UsageProviderKind,
57 model: TrimmedNonEmptyString,
58 sourcePath: Schema.optional(TrimmedNonEmptyString),
59 totals: UsageTokenTotals,
60 costUsd: Schema.Number,
61 cacheSavingsUsd: Schema.Number,
62 categoryCostUsd: Schema.optional(UsageCategoryCost),
63 fastCostUsd: Schema.optional(Schema.Number),
64 ultrafastCostUsd: Schema.optional(Schema.Number),
65 speedPremiumUsd: Schema.optional(Schema.Number),
66 costSource: UsageCostSource,
67 records: NonNegativeInt,
68 unpricedRecords: NonNegativeInt,
69 /** Absent from older servers; merges then estimate it from the unpriced record share. */
70 unpricedTokens: Schema.optional(NonNegativeInt),
71 sessions: NonNegativeInt,
72});
73export type UsageBucket = typeof UsageBucket.Type;
74​
75export const UsageSourceFingerprint = Schema.Struct({
76 hostId: TrimmedNonEmptyString,
77 provider: UsageProviderKind,
78 resolvedHomePath: TrimmedNonEmptyString,
79 /** Transcript directory device:inode distinguishes machines sharing a hostname and home path. */
80 volumeId: Schema.String,
81});
82export type UsageSourceFingerprint = typeof UsageSourceFingerprint.Type;
83​
84export const UsageSourceStatus = Schema.Literals(["ok", "missing", "partial", "failed"]);
85export type UsageSourceStatus = typeof UsageSourceStatus.Type;
86​
87export const UsageSource = Schema.Struct({
88 fingerprint: UsageSourceFingerprint,
89 status: UsageSourceStatus,
90 scannedFiles: NonNegativeInt,
91 skippedFiles: NonNegativeInt,
92 malformedRecords: NonNegativeInt,
93 /** Total this count; bucket counts double-count sessions spanning days or models. */
94 distinctSessions: NonNegativeInt,
95 message: Schema.NullOr(TrimmedNonEmptyString),
96 /** Cached response during refresh; repeat with awaitRefresh for fresh data. */
97 refreshing: Schema.optionalKey(Schema.Literal(true)),
98});
99export type UsageSource = typeof UsageSource.Type;
100​
101export const UsagePricingStatus = Schema.Literals(["fresh", "cached", "unavailable"]);
102export type UsagePricingStatus = typeof UsagePricingStatus.Type;
103​
104export const UsagePricing = Schema.Struct({
105 status: UsagePricingStatus,
106 source: TrimmedNonEmptyString,
107 fetchedAt: Schema.NullOr(Schema.String),
108 knownModels: NonNegativeInt,
109});
110export type UsagePricing = typeof UsagePricing.Type;
111​
112/**
113 * Matches a provider's original model ID, before usage model aliases apply. Omit provider to
114 * match any; a string provider lets older clients carry selectors for providers they lack.
115 */
116export const UsageModelSelector = Schema.Struct({
117 provider: Schema.optional(TrimmedNonEmptyString),
118 model: TrimmedNonEmptyString,
119});
120export type UsageModelSelector = typeof UsageModelSelector.Type;
121​
122/** Keeps records matching `include` (all when omitted) and not matching `exclude`. */
123export const UsageModelFilter = Schema.Struct({
124 include: Schema.optional(Schema.Array(UsageModelSelector)),
125 exclude: Schema.optional(Schema.Array(UsageModelSelector)),
126});
127export type UsageModelFilter = typeof UsageModelFilter.Type;
128​
129export const UsageSummaryInput = Schema.Struct({
130 sinceDay: UsageDay,
131 untilDay: UsageDay,
132 /** IANA bucketing zone; offsets fail across DST boundaries. */
133 timeZone: TrimmedNonEmptyString,
134 resolution: Schema.optional(UsageResolution),
135 sinceTime: Schema.optional(TrimmedNonEmptyString),
136 untilTime: Schema.optional(TrimmedNonEmptyString),
137 awaitRefresh: Schema.optional(Schema.Boolean),
138 /** Accept an immediate answer from the last scan; its sources are marked refreshing. */
139 provisional: Schema.optional(Schema.Boolean),
140 modelFilter: Schema.optional(UsageModelFilter),
141});
142export type UsageSummaryInput = typeof UsageSummaryInput.Type;
143​
144export const UsageSummary = Schema.Struct({
145 contractVersion: Schema.Number,
146 readAt: Schema.String,
147 timeZone: TrimmedNonEmptyString,
148 sinceDay: UsageDay,
149 untilDay: UsageDay,
150 buckets: ForwardCompatibleArray(UsageBucket),
151 sources: ForwardCompatibleArray(UsageSource),
152 pricing: UsagePricing,
153 scanDurationMs: NonNegativeInt,
154 /** The filter the server applied; absent means unfiltered, including servers that predate filters. */
155 modelFilter: Schema.optional(UsageModelFilter),
156});
157export type UsageSummary = typeof UsageSummary.Type;
158​
159export class UsageReadError extends Schema.TaggedError<UsageReadError>()("UsageReadError", {
160 reason: Schema.Literals(["scanFailed", "invalidWindow"]),
161 detail: TrimmedNonEmptyString,
162 cause: Schema.optional(Schema.Defect()),
163}) {
164 override get message(): string {
165 return `Usage read failed (${this.reason}): ${this.detail}`;
166 }