1import * as Schema from "effect/Schema";
2
3import {
4 ForwardCompatibleArray,
5 IsoDateTime,
6 NonNegativeInt,
7 TrimmedNonEmptyString,
8} from "./baseSchemas.ts";
9import { ProviderDriverKind, ProviderInstanceId } from "./providerInstance.ts";
10import { UsageLimitSourceId } from "./usageLimitSourceId.ts";
11
12/**
13 * One rolling quota window a subscription provider reports for the signed-in
14 * account, e.g. Claude's five-hour session or Codex's weekly allowance.
15 *
16 * `id` is stable per provider (`five_hour`, `seven_day_opus`, `primary`) so a
17 * sparse turn-driven update lands on the same row a full probe produced.
18 * `kind` only orders and labels the bar.
19 */
20export const ServerProviderUsageWindow = Schema.Struct({
21 id: TrimmedNonEmptyString,
22 kind: Schema.Literals(["session", "weekly", "monthly", "other"]),
23 label: TrimmedNonEmptyString,
24 usedPercent: Schema.Number.check(Schema.isBetween({ minimum: 0, maximum: 100 })),
25 resetsAt: Schema.optional(IsoDateTime),
26 windowDurationMins: Schema.optional(NonNegativeInt),
27});
28export type ServerProviderUsageWindow = typeof ServerProviderUsageWindow.Type;
29
30/**
31 * Reset credits a provider banks on the account. Codex grants these when it
32 * has rate-limited the user unfairly; redeeming one clears the current
33 * windows. Only present when the provider reports them at all.
34 */
35export const ServerProviderResetCredits = Schema.Struct({
36 availableCount: NonNegativeInt,
37 nextExpiresAt: Schema.optional(IsoDateTime),
38 /** Pins hub redemption to the displayed credit, including retries from another client. */
39 nextCreditId: Schema.optional(TrimmedNonEmptyString),
40});
41export type ServerProviderResetCredits = typeof ServerProviderResetCredits.Type;
42
43/**
44 * Subscription usage the provider knows about the signed-in account.
45 *
46 * `unavailable` distinguishes an account that can never report windows (API
47 * key, Bedrock) from a probe that failed this time, so clients can keep the
48 * last good bars for the latter and clear them for the former.
49 */
50export const ServerProviderUsageLimits = Schema.Struct({
51 checkedAt: IsoDateTime,
52 windows: ForwardCompatibleArray(ServerProviderUsageWindow),
53 /** Opaque credential identity when the provider does not report an account. */
54 credentialFingerprint: Schema.optional(TrimmedNonEmptyString),
55 resetCredits: Schema.optional(ServerProviderResetCredits),
56 /** Provider-owned usage settings when quota windows are not available to the client. */
57 externalUsage: Schema.optional(
58 Schema.Struct({
59 label: TrimmedNonEmptyString,
60 url: TrimmedNonEmptyString,
61 }),
62 ),
63 unavailable: Schema.optional(
64 Schema.Struct({
65 reason: Schema.Literals(["unsupported", "probeFailed"]),
66 message: Schema.optional(TrimmedNonEmptyString),
67 }),
68 ),
69});
70export type ServerProviderUsageLimits = typeof ServerProviderUsageLimits.Type;
71
72/**
73 * What an adapter reports when its runtime pushes a rate-limit update during
74 * a turn. Sparse by contract: Claude's `rate_limit_event` names one window at
75 * a time and Codex documents its notification as a partial. Windows merge by
76 * `id` onto the instance's published snapshot; omitted windows are unchanged.
77 */
78export const ProviderUsageLimitsUpdate = Schema.Struct({
79 windows: Schema.Array(ServerProviderUsageWindow),
80});
81export type ProviderUsageLimitsUpdate = typeof ProviderUsageLimitsUpdate.Type;
82
83/**
84 * One account a usage-limit source reports on. `driver` is the provider the
85 * account belongs to, for the icon and colour clients already have; the
86 * account itself is not something this environment can run turns on.
87 */
88export const UsageLimitSourceAccount = Schema.Struct({
89 id: TrimmedNonEmptyString,
90 driver: ProviderDriverKind,
91 /** The signed-in address, when the source names one; clients blur it like provider auth. */
92 email: Schema.optional(TrimmedNonEmptyString),
93 /** Plan as the matching provider would label it (`ChatGPT Pro 20x Subscription`). */
94 plan: Schema.optional(TrimmedNonEmptyString),
95 /** The workspace the quota belongs to, when the source names one; see `ServerProviderAuth`. */
96 workspaceId: Schema.optional(TrimmedNonEmptyString),
97 usageLimits: ServerProviderUsageLimits,
98});
99export type UsageLimitSourceAccount = typeof UsageLimitSourceAccount.Type;
100
101/**
102 * The published state of one configured `usageLimitSources` entry. A source
103 * that could not be read keeps `error` beside an empty account list rather
104 * than vanishing, so the user can see it is configured but failing.
105 */
106export const UsageLimitSourceSnapshot = Schema.Struct({
107 id: UsageLimitSourceId,
108 kind: Schema.Literal("cliproxy"),
109 label: TrimmedNonEmptyString,
110 checkedAt: IsoDateTime,
111 accounts: ForwardCompatibleArray(UsageLimitSourceAccount),
112 error: Schema.optional(TrimmedNonEmptyString),
113});
114export type UsageLimitSourceSnapshot = typeof UsageLimitSourceSnapshot.Type;
115
116export const UsageLimitSourceSnapshots = ForwardCompatibleArray(UsageLimitSourceSnapshot);
117export type UsageLimitSourceSnapshots = typeof UsageLimitSourceSnapshots.Type;
118
119export const UsageLimitSourceConsumeResetCreditInput = Schema.Struct({
120 sourceId: UsageLimitSourceId,
121 accountId: TrimmedNonEmptyString,
122 creditId: TrimmedNonEmptyString,
123});
124export type UsageLimitSourceConsumeResetCreditInput =
125 typeof UsageLimitSourceConsumeResetCreditInput.Type;
126
127export const ProviderConsumeResetCreditInput = Schema.Union([
128 Schema.Struct({ instanceId: ProviderInstanceId }),
129 UsageLimitSourceConsumeResetCreditInput,
130]);
131export type ProviderConsumeResetCreditInput = typeof ProviderConsumeResetCreditInput.Type;
132
133export class UsageLimitSourceError extends Schema.TaggedError<UsageLimitSourceError>()(
134 "UsageLimitSourceError",
135 { detail: Schema.String },
136) {
137 override get message(): string {
138 return this.detail;
139 }
140}
141
142/** Mirrors Codex's own outcome set; other providers map onto it. */
143export const ProviderConsumeResetCreditOutcome = Schema.Literals([
144 "reset",
145 "nothingToReset",
146 "noCredit",
147 "alreadyRedeemed",
148]);
149export type ProviderConsumeResetCreditOutcome = typeof ProviderConsumeResetCreditOutcome.Type;
150
151export const ProviderConsumeResetCreditResult = Schema.Struct({
152 outcome: ProviderConsumeResetCreditOutcome,
153 /** Redemption succeeded, but a follow-up such as clearing the hub cooldown failed. */
154 warning: Schema.optional(TrimmedNonEmptyString),
155});
156export type ProviderConsumeResetCreditResult = typeof ProviderConsumeResetCreditResult.Type;
157
158/** A point-in-time view of one provider's limits, built for the /usage-limits panel. */
159export const UsageLimitsReport = Schema.Struct({
160 createdAt: IsoDateTime,
161 accounts: Schema.Array(
162 Schema.Struct({
163 id: TrimmedNonEmptyString,
164 driver: ProviderDriverKind,
165 label: TrimmedNonEmptyString,
166 plan: Schema.optional(TrimmedNonEmptyString),
167 email: Schema.optional(TrimmedNonEmptyString),
168 sourceLabel: Schema.optional(TrimmedNonEmptyString),
169 instanceId: Schema.optional(ProviderInstanceId),
170 resetCreditInput: Schema.optional(ProviderConsumeResetCreditInput),
171 displayName: Schema.optional(Schema.String),
172 accentColor: Schema.optional(Schema.String),
173 limits: ServerProviderUsageLimits,
174 }),
175 ),
176 notices: Schema.Array(Schema.String),
177});
178export type UsageLimitsReport = typeof UsageLimitsReport.Type;