/** * Codex subscription usage. The `account/rateLimits/read` response and the * `account/rateLimits/updated` notification carry the same snapshot shape, so * one mapper serves the status probe and the turn-driven update; both emit * windows with the same ids so they merge onto the same rows. * * @module provider/Layers/codexUsageLimits */ import type { ProviderUsageLimitsUpdate, ServerProviderResetCredits, ServerProviderUsageLimits, ServerProviderUsageWindow, } from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; import * as Option from "effect/Option"; import type * as CodexErrors from "effect-codex-app-server/errors"; import { clampPercent, makeUsageLimits } from "../providerUsageLimits.ts"; interface CodexRateLimitWindow { readonly usedPercent: number; readonly resetsAt?: number | null; readonly windowDurationMins?: number | null; } /** Structural view of the generated `RateLimitSnapshot`; both messages satisfy it. */ export interface CodexRateLimitSnapshot { readonly planType?: string | null; readonly primary?: CodexRateLimitWindow | null; readonly secondary?: CodexRateLimitWindow | null; } /** Structural view of the read response's `rateLimitResetCredits`. */ export interface CodexResetCreditsSummary { readonly availableCount: number; readonly credits?: ReadonlyArray<{ readonly status: string; readonly expiresAt?: number | null; }> | null; } const SESSION_MINS = 5 * 60; const WEEK_MINS = 7 * 24 * 60; const MONTH_MINS = 30 * 24 * 60; function isoFromEpochSeconds(value: number | null | undefined): string | undefined { if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return undefined; const dt = DateTime.make(value * 1000); return Option.isSome(dt) ? DateTime.formatIso(dt.value) : undefined; } function kindForDuration(mins: number): ServerProviderUsageWindow["kind"] { if (mins >= MONTH_MINS) return "monthly"; if (mins >= WEEK_MINS) return "weekly"; return "session"; } function labelForKind(kind: ServerProviderUsageWindow["kind"]): string { return kind === "session" ? "Session" : kind === "weekly" ? "Weekly" : "Monthly"; } /** * `primary` / `secondary` are positions, not durations. Codex usually sends * `windowDurationMins`; when it does not, paid plans expose the 5-hour and * weekly pair and Free/Go expose one monthly allowance. */ export function codexRateLimitsToWindows( snapshot: CodexRateLimitSnapshot, ): ReadonlyArray { const isMonthlyPlan = snapshot.planType === "free" || snapshot.planType === "go"; const positions = [ ["primary", snapshot.primary, isMonthlyPlan ? MONTH_MINS : SESSION_MINS], ["secondary", snapshot.secondary, WEEK_MINS], ] as const; const windows: ServerProviderUsageWindow[] = []; for (const [id, window, fallbackMins] of positions) { if (!window || !Number.isFinite(window.usedPercent)) continue; const windowDurationMins = typeof window.windowDurationMins === "number" ? window.windowDurationMins : fallbackMins; const kind = kindForDuration(windowDurationMins); const resetsAt = isoFromEpochSeconds(window.resetsAt); windows.push({ id, kind, label: labelForKind(kind), usedPercent: clampPercent(window.usedPercent), windowDurationMins, ...(resetsAt ? { resetsAt } : {}), }); } return windows; } export function codexResetCreditsToContract( summary: CodexResetCreditsSummary | null | undefined, ): ServerProviderResetCredits | undefined { if (!summary) return undefined; const expiries = (summary.credits ?? []) .filter((credit) => credit.status === "available") .map((credit) => credit.expiresAt) .filter((value): value is number => typeof value === "number"); const nextExpiresAt = expiries.length > 0 ? isoFromEpochSeconds(Math.min(...expiries)) : undefined; return { availableCount: Math.max(0, summary.availableCount), ...(nextExpiresAt ? { nextExpiresAt } : {}), }; } export function codexRateLimitsToLimits(input: { readonly snapshot: CodexRateLimitSnapshot; readonly resetCredits?: CodexResetCreditsSummary | null | undefined; readonly checkedAt: string; }): ServerProviderUsageLimits { const resetCredits = codexResetCreditsToContract(input.resetCredits); return { ...makeUsageLimits({ checkedAt: input.checkedAt, windows: codexRateLimitsToWindows(input.snapshot), }), ...(resetCredits ? { resetCredits } : {}), }; } export function codexRateLimitsToUpdate( snapshot: CodexRateLimitSnapshot, ): ProviderUsageLimitsUpdate | undefined { const windows = codexRateLimitsToWindows(snapshot); return windows.length > 0 ? { windows } : undefined; } /** * A bounded, client-safe reason for a failed `account/rateLimits/read`. The * raw error is for the log; only the category and, for a JSON-RPC failure, * the code reach the Limits view. */ export function codexRateLimitsFailureMessage(error: CodexErrors.CodexAppServerError): string { switch (error._tag) { case "CodexAppServerRequestError": return `Codex could not read usage (JSON-RPC ${error.code}).`; case "CodexAppServerSpawnError": return "Codex could not be started to read usage."; case "CodexAppServerProcessExitedError": return "Codex exited before it could report usage."; default: return "Codex did not answer the usage request."; } }