import * as Schema from "effect/Schema"; import { ForwardCompatibleArray, NonNegativeInt, TrimmedNonEmptyString } from "./baseSchemas.ts"; /** Bump for incompatible UsageSummary changes; skipped unknown array variants need no bump. */ export const USAGE_CONTRACT_VERSION = 6 as const; export const USAGE_MERGE_COMPATIBLE_SINCE = 4 as const; export const UsageProviderKind = Schema.Literals([ "claude", "codex", "grok", "cursor", "opencode", "antigravity", "pi", "muse", ]); export type UsageProviderKind = typeof UsageProviderKind.Type; const USAGE_DAY_PATTERN = /^\d{4}-\d{2}-\d{2}$/; export const UsageDay = TrimmedNonEmptyString.check(Schema.isPattern(USAGE_DAY_PATTERN)).pipe( Schema.brand("UsageDay"), ); export type UsageDay = typeof UsageDay.Type; export const UsageResolution = Schema.Literals(["day", "hour"]); export type UsageResolution = typeof UsageResolution.Type; export const UsageCostSource = Schema.Literals(["providerReported", "modelPriced", "unpriced"]); export type UsageCostSource = typeof UsageCostSource.Type; /** Sum uncached input, cache reads, and cache writes for input totals; reasoning is already included in output. */ export const UsageTokenTotals = Schema.Struct({ uncachedInputTokens: NonNegativeInt, cachedInputTokens: NonNegativeInt, cacheCreationTokens: NonNegativeInt, outputTokens: NonNegativeInt, reasoningTokens: NonNegativeInt, }); export type UsageTokenTotals = typeof UsageTokenTotals.Type; export const UsageCategoryCost = Schema.Struct({ input: Schema.Number, cacheRead: Schema.Number, cacheWrite: Schema.Number, output: Schema.Number, }); export type UsageCategoryCost = typeof UsageCategoryCost.Type; export const UsageBucket = Schema.Struct({ day: UsageDay, hourStart: Schema.optional(TrimmedNonEmptyString), provider: UsageProviderKind, model: TrimmedNonEmptyString, sourcePath: Schema.optional(TrimmedNonEmptyString), totals: UsageTokenTotals, costUsd: Schema.Number, cacheSavingsUsd: Schema.Number, categoryCostUsd: Schema.optional(UsageCategoryCost), fastCostUsd: Schema.optional(Schema.Number), ultrafastCostUsd: Schema.optional(Schema.Number), speedPremiumUsd: Schema.optional(Schema.Number), costSource: UsageCostSource, records: NonNegativeInt, unpricedRecords: NonNegativeInt, sessions: NonNegativeInt, }); export type UsageBucket = typeof UsageBucket.Type; export const UsageSourceFingerprint = Schema.Struct({ hostId: TrimmedNonEmptyString, provider: UsageProviderKind, resolvedHomePath: TrimmedNonEmptyString, /** Transcript directory device:inode distinguishes machines sharing a hostname and home path. */ volumeId: Schema.String, }); export type UsageSourceFingerprint = typeof UsageSourceFingerprint.Type; export const UsageSourceStatus = Schema.Literals(["ok", "missing", "partial", "failed"]); export type UsageSourceStatus = typeof UsageSourceStatus.Type; export const UsageSource = Schema.Struct({ fingerprint: UsageSourceFingerprint, status: UsageSourceStatus, scannedFiles: NonNegativeInt, skippedFiles: NonNegativeInt, malformedRecords: NonNegativeInt, /** Total this count; bucket counts double-count sessions spanning days or models. */ distinctSessions: NonNegativeInt, message: Schema.NullOr(TrimmedNonEmptyString), action: Schema.optionalKey(Schema.Literal("enableCursorKeychain")), /** Cached response during refresh; repeat with awaitRefresh for fresh data. */ refreshing: Schema.optionalKey(Schema.Literal(true)), }); export type UsageSource = typeof UsageSource.Type; export const UsagePricingStatus = Schema.Literals(["fresh", "cached", "unavailable"]); export type UsagePricingStatus = typeof UsagePricingStatus.Type; export const UsagePricing = Schema.Struct({ status: UsagePricingStatus, source: TrimmedNonEmptyString, fetchedAt: Schema.NullOr(Schema.String), knownModels: NonNegativeInt, }); export type UsagePricing = typeof UsagePricing.Type; /** * Matches a provider's original model ID, before usage model aliases apply. Omit provider to * match any; a string provider lets older clients carry selectors for providers they lack. */ export const UsageModelSelector = Schema.Struct({ provider: Schema.optional(TrimmedNonEmptyString), model: TrimmedNonEmptyString, }); export type UsageModelSelector = typeof UsageModelSelector.Type; /** Keeps records matching `include` (all when omitted) and not matching `exclude`. */ export const UsageModelFilter = Schema.Struct({ include: Schema.optional(Schema.Array(UsageModelSelector)), exclude: Schema.optional(Schema.Array(UsageModelSelector)), }); export type UsageModelFilter = typeof UsageModelFilter.Type; export const UsageSummaryInput = Schema.Struct({ sinceDay: UsageDay, untilDay: UsageDay, /** IANA bucketing zone; offsets fail across DST boundaries. */ timeZone: TrimmedNonEmptyString, resolution: Schema.optional(UsageResolution), sinceTime: Schema.optional(TrimmedNonEmptyString), untilTime: Schema.optional(TrimmedNonEmptyString), awaitRefresh: Schema.optional(Schema.Boolean), modelFilter: Schema.optional(UsageModelFilter), }); export type UsageSummaryInput = typeof UsageSummaryInput.Type; export const UsageSummary = Schema.Struct({ contractVersion: Schema.Number, readAt: Schema.String, timeZone: TrimmedNonEmptyString, sinceDay: UsageDay, untilDay: UsageDay, buckets: ForwardCompatibleArray(UsageBucket), sources: ForwardCompatibleArray(UsageSource), pricing: UsagePricing, scanDurationMs: NonNegativeInt, /** The filter the server applied; absent means unfiltered, including servers that predate filters. */ modelFilter: Schema.optional(UsageModelFilter), }); export type UsageSummary = typeof UsageSummary.Type; export class UsageReadError extends Schema.TaggedError()("UsageReadError", { reason: Schema.Literals(["scanFailed", "invalidWindow"]), detail: TrimmedNonEmptyString, cause: Schema.optional(Schema.Defect()), }) { override get message(): string { return `Usage read failed (${this.reason}): ${this.detail}`; } }