import { sanitizeCodexText } from "./safety.ts"; import type { AppServerRateLimitResetCredit, AppServerRateLimitResetCredits, AppServerRateLimitResponse, AppServerRateLimitSnapshot, AppServerWindowSnapshot, BackendAdditionalRateLimit, BackendRateLimitDetails, BackendRateLimitResetCredits, BackendWindowSnapshot, CodexUsageReport, NormalizedRateLimitResetCredit, NormalizedRateLimitResetCredits, NormalizedRateLimitSnapshot, NormalizedRateLimitWindow, RateLimitStatusPayload, UsageSource, } from "./types.ts"; export function normalizeBackendPayload( payload: RateLimitStatusPayload, capturedAt: number, source: UsageSource, ): CodexUsageReport { const snapshots: NormalizedRateLimitSnapshot[] = []; const planType = asString(payload.plan_type); const primary = normalizeBackendSnapshot("codex", undefined, payload.rate_limit); if (primary) snapshots.push(primary); const additional = Array.isArray(payload.additional_rate_limits) ? payload.additional_rate_limits : []; for (const item of additional) { const additionalLimit = asObject(item) as BackendAdditionalRateLimit | undefined; if (!additionalLimit) continue; const limitId = asString(additionalLimit.metered_feature) ?? asString(additionalLimit.limit_name); if (!limitId) continue; try { const snapshot = normalizeBackendSnapshot( limitId, asString(additionalLimit.limit_name), additionalLimit.rate_limit, ); if (snapshot) snapshots.push(snapshot); } catch { // Optional additional buckets must not hide otherwise usable primary/reset usage. } } const resetCredits = normalizeBackendRateLimitResetCredits(payload.rate_limit_reset_credits); if (snapshots.length === 0 && !resetCredits) { throw new Error("Codex usage endpoint returned no displayable usage data."); } return { source, capturedAt, planType, snapshots, resetCredits }; } function normalizeBackendSnapshot( limitId: string, limitName: string | undefined, rateLimit: unknown, ): NormalizedRateLimitSnapshot | undefined { if (rateLimit === null || rateLimit === undefined) return undefined; const details = assertObject(rateLimit, "rate limit") as BackendRateLimitDetails; const primary = normalizeBackendWindow(details.primary_window); const secondary = normalizeBackendWindow(details.secondary_window); if (!primary && !secondary) return undefined; return { limitId, limitName, primary, secondary }; } function normalizeBackendWindow(value: unknown): NormalizedRateLimitWindow | undefined { if (value === null || value === undefined) return undefined; const window = assertObject(value, "rate-limit window") as BackendWindowSnapshot; const usedPercent = asNumber(window.used_percent); if (usedPercent === undefined) return undefined; const limitSeconds = asNumber(window.limit_window_seconds); const resetsAt = asNumber(window.reset_at); return { usedPercent, windowMinutes: limitSeconds && limitSeconds > 0 ? Math.ceil(limitSeconds / 60) : undefined, resetsAt, }; } function normalizeBackendRateLimitResetCredits( value: unknown, ): NormalizedRateLimitResetCredits | undefined { const resetCredits = asObject(value) as BackendRateLimitResetCredits | undefined; const availableCount = asNonnegativeInteger(resetCredits?.available_count); return availableCount === undefined ? undefined : { availableCount }; } export function normalizeAppServerResponse( response: AppServerRateLimitResponse, capturedAt: number, ): CodexUsageReport { const snapshots: NormalizedRateLimitSnapshot[] = []; const addSnapshot = (raw: unknown, fallbackId: string, optional = false) => { let snapshot: NormalizedRateLimitSnapshot | undefined; try { snapshot = normalizeAppServerSnapshot(raw, fallbackId); } catch (error) { if (optional) return; throw error; } if (!snapshot) return; const resolvedSnapshot = snapshot; const existingIndex = snapshots.findIndex((item) => item.limitId === resolvedSnapshot.limitId); const existing = existingIndex >= 0 ? snapshots[existingIndex] : undefined; if (existingIndex >= 0 && existing) snapshots[existingIndex] = mergeSnapshot(existing, resolvedSnapshot); else snapshots.push(resolvedSnapshot); }; addSnapshot(response.rateLimits, "codex"); const snapshotsByLimitId = asObject(response.rateLimitsByLimitId); if (snapshotsByLimitId) { for (const [limitId, raw] of Object.entries(snapshotsByLimitId)) { if (limitId) addSnapshot(raw, limitId, true); } } const resetCredits = normalizeAppServerRateLimitResetCredits(response.rateLimitResetCredits); if (snapshots.length === 0 && !resetCredits) { throw new Error("codex app-server returned no displayable usage data."); } const planType = asAppServerPlanType(response.rateLimits); return { source: "codex-app-server", capturedAt, planType, snapshots, resetCredits, }; } function asAppServerPlanType(raw: unknown): string | undefined { if (raw === null || raw === undefined) return undefined; const snapshot = assertObject( raw, "app-server rate-limit snapshot", ) as AppServerRateLimitSnapshot; return asString(snapshot.planType); } function normalizeAppServerSnapshot( raw: unknown, fallbackId: string, ): NormalizedRateLimitSnapshot | undefined { if (raw === null || raw === undefined) return undefined; const snapshot = assertObject( raw, "app-server rate-limit snapshot", ) as AppServerRateLimitSnapshot; const limitId = asString(snapshot.limitId) ?? fallbackId; const limitName = asString(snapshot.limitName); const primary = normalizeAppServerWindow(snapshot.primary); const secondary = normalizeAppServerWindow(snapshot.secondary); if (!primary && !secondary) return undefined; return { limitId, limitName, primary, secondary }; } function normalizeAppServerWindow(value: unknown): NormalizedRateLimitWindow | undefined { if (value === null || value === undefined) return undefined; const window = assertObject(value, "app-server rate-limit window") as AppServerWindowSnapshot; const usedPercent = asNumber(window.usedPercent); if (usedPercent === undefined) return undefined; return { usedPercent, windowMinutes: asNumber(window.windowDurationMins), resetsAt: asNumber(window.resetsAt), }; } function normalizeAppServerRateLimitResetCredits( value: unknown, ): NormalizedRateLimitResetCredits | undefined { const resetCredits = asObject(value) as AppServerRateLimitResetCredits | undefined; const availableCount = asNonnegativeInteger(resetCredits?.availableCount); if (availableCount === undefined) return undefined; const rawCredits = resetCredits?.credits; if (!Array.isArray(rawCredits)) return { availableCount }; const credits = rawCredits .map(normalizeAppServerRateLimitResetCredit) .filter((credit): credit is NormalizedRateLimitResetCredit => credit !== undefined) .slice(0, availableCount); if (rawCredits.length > 0 && credits.length === 0 && availableCount > 0) { return { availableCount }; } return { availableCount, credits }; } function normalizeAppServerRateLimitResetCredit( value: unknown, ): NormalizedRateLimitResetCredit | undefined { const credit = asObject(value) as AppServerRateLimitResetCredit | undefined; const id = asString(credit?.id); if (!id) return undefined; const normalized: NormalizedRateLimitResetCredit = { id }; const resetType = asString(credit?.resetType); const status = asString(credit?.status); const grantedAt = asNumber(credit?.grantedAt); const expiresAt = asNumber(credit?.expiresAt); const title = asString(credit?.title); const description = asString(credit?.description); if (resetType !== undefined) normalized.resetType = resetType; if (status !== undefined) normalized.status = status; if (grantedAt !== undefined) normalized.grantedAt = grantedAt; if (expiresAt !== undefined) normalized.expiresAt = expiresAt; if (title !== undefined) normalized.title = title; if (description !== undefined) normalized.description = description; return normalized; } function mergeSnapshot( left: NormalizedRateLimitSnapshot, right: NormalizedRateLimitSnapshot, ): NormalizedRateLimitSnapshot { return { limitId: right.limitId || left.limitId, limitName: right.limitName ?? left.limitName, primary: right.primary ?? left.primary, secondary: right.secondary ?? left.secondary, }; } function assertObject(value: unknown, description: string): Record { const object = asObject(value); if (!object) throw new Error(`${description} was not an object.`); return object; } function asObject(value: unknown): Record | undefined { if (!value || typeof value !== "object" || Array.isArray(value)) return undefined; return value as Record; } function asString(value: unknown): string | undefined { if (typeof value !== "string") return undefined; return sanitizeCodexText(value, 160) || undefined; } function asNumber(value: unknown): number | undefined { if (typeof value === "number" && Number.isFinite(value)) return value; if (typeof value === "string" && value.trim()) { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : undefined; } return undefined; } function asNonnegativeInteger(value: unknown): number | undefined { const parsed = asNumber(value); if (parsed === undefined || !Number.isSafeInteger(parsed)) return undefined; return Math.max(0, parsed); }