/** * Quota Capability Contract (DESIGN.md §13, ADR-0001). * * Defines the normalized Provider-quota Interface shared by every * Provider that reports plan usage. Each Adapter maps its Provider * response shape into named quota categories with current and optional * weekly windows, optional counts, a remaining percentage, and a reset * time so callers do not need Provider-specific knowledge. * * Normalization rules (DESIGN.md §13): * - Percentages are REMAINING percentages clamped to 0..100. * - A valid explicit remaining percentage wins; otherwise derive * `(remaining / limit) * 100` from finite nonnegative counts where * used is not greater than limit. * - A Provider that publishes an exact `remaining` without a limit * (unknown-limit window — GitHub #49) reports that value verbatim; * `used`, `limit`, and `remainingPercent` are omitted rather than * inferred or fabricated. * - A Provider that reports a known `used` with an explicit remaining * percentage but NO limit (used-only window — GitHub #99; Tavily's * unlimited key publishes `key.limit: null`) keeps the observed * `used` alongside the explicit percentage; `limit` and `remaining` * are omitted rather than fabricated. Additive under QuotaDashboard * schema v1. * - A Provider whose counts are invalid but which publishes an exact * `remaining` (historically Z.AI's cumulative `currentValue` past * the window cap — GitHub #109; the zai adapter now rejects such * entries whole — #191) has `remaining` published verbatim next to * any explicit remaining percentage; counts are omitted rather than * derived from contradicting fields. * - Invalid optional counts are omitted together (not set to zero). * - A category that has neither a valid percentage, nor valid counts, * nor an explicit remaining is rejected with `QUOTA_ERROR`. * - Nonempty names, finite values, and ISO dates are mandatory. * * This module imports only Provider identity types and shared errors; * it imports no Provider transport and no Provider Adapter. */ import type { ProviderId } from "../providers/types.js"; import { type ScoutlineErrorCode } from "../lib/errors.js"; export interface QuotaWindow { durationSeconds?: number; used?: number; limit?: number; remaining?: number; /** * Remaining share of the window, 0..100. OPTIONAL since #49: a * Provider that reports an exact `remaining` but no limit (Jina's * `X-RateLimit-Remaining-*` headers) has no honest percentage — the * field is omitted rather than fabricated against an inferred tier. */ remainingPercent?: number; resetsAt?: string; } export interface QuotaCategory { name: string; unit: "requests" | "tokens" | "credits" | "USD"; current: QuotaWindow; weekly?: QuotaWindow; /** * Per-tool consumption inside this category (GitHub #191). Additive * OPTIONAL field under schema version 1 — the PB-T5 precedent: a * Provider that publishes a breakdown (Z.AI's `usageDetails`, whose * `modelCode` names the tool and `usage` counts the calls) populates * it; a Provider with none simply omits the field, so every * pre-#191 consumer (TTY renderer, snapshot round-trip, envelope) * keeps working unchanged and no schema-version bump is needed. * * Entries are published in Provider order with non-positive and * unnamed entries dropped, so a present array is always observably * non-empty — an absent field and an empty-breakdown provider are * deliberately indistinguishable (neither says "these tools used * nothing"). The rows are informational window detail: they are * carried independently of whether `current` is populated, so a * category whose window was rejected as inconsistent still reports * which tools consumed the budget. */ toolUsage?: readonly { readonly tool: string; readonly usage: number; }[]; } export interface ProviderQuotaSuccess { provider: ProviderId; status: "ok"; plan?: string; categories: QuotaCategory[]; /** * Optional provider-authored caveat(s) the quota command surfaces to * the user alongside the dashboard. A generic, provider-neutral * channel: a Provider that needs to flag a caveat about its quota * numbers (e.g. Brave reports a rate-limit window, NOT spend or * credits consumed under metered billing) populates this field; the * command renders each entry to stderr without branching on provider * identity. Additive and backward-compatible — Providers with no * caveat simply omit the field. */ warnings?: readonly string[]; /** * Source + freshness label (PB-T5 — Plan B). Additive under schema * version 1: when omitted (the pre-PB-T5 caller path), the row is a * direct live probe whose freshness is implicit (the dashboard was * just built). When the dashboard reads PB-T1's snapshot, this field * carries the source ("snapshot" vs "live" fallback) and the * authoritative flag so a user can correlate a selection pick with * the data that drove it without misattributing it to fresher data * than it is. * * Freshness is judged solely from `observedAt` — the snapshot's * ground-truth clock — never from `locallyUpdatedAt` (PB-T2's local * decrement never resets the staleness clock). */ readonly quotaSource?: QuotaSourceLabel; } export interface ProviderQuotaFailure { provider: ProviderId; status: "error"; error: { code: ScoutlineErrorCode; message: string; help?: string; }; } /** * A configured Provider that advertises no `quota` Capability (PB-T5 — * Plan B). Today only Exa matches this row in `all-providers` mode: it * is configured and capable inventory, but has no quota endpoint to * probe. The dashboard emits this variant with **zero adapter/transport * calls** — no descriptor.create(), no quota.invoke(), no fallback to a * live probe. The variant is additive under schema version 1: every * existing consumer (TTY renderer, exit-code computation, warnings * loop) handles `status` via fall-through, so the new `"none"` value * cannot break a pre-PB-T5 caller. * * Single-Provider (`--provider `) mode still throws * `UnsupportedCapabilityError` when the pinned Provider lacks `quota` — * the user explicitly asked for one Provider's quota, so emitting a * no-signal row would hide the user error. The no-signal row appears * only in `all-providers` mode (the default). */ export interface ProviderQuotaNone { readonly provider: ProviderId; readonly status: "none"; readonly reason: "no-capability"; } /** * Where a {@link ProviderQuotaSuccess} row's data came from (PB-T5). * Carried as a flat sub-object so consumers that don't read it pay * nothing. See {@link ProviderQuotaSuccess.quotaSource}. */ export interface QuotaSourceLabel { /** * `"snapshot"` — read from PB-T1's `state.json` and judged fresh. * `"live"` — the snapshot was stale/missing/corrupt, so the * dashboard fell back to a live probe (and awaited the write-through * to the snapshot before returning). */ readonly source: "snapshot" | "live"; /** Epoch-ms the underlying observation was made (`observedAt`). */ readonly observedAt: number; /** * Whether `observedAt` is within the authoritative staleness * threshold (`DEFAULT_QUOTA_STALE_THRESHOLD_MS`, 10 min). Selection * (PB-T4) treats non-authoritative rows as eligible-but-neutral; the * dashboard surfaces the same flag so a user can correlate a * selection pick with the data that drove it. A `"live"` row is * always authoritative (just observed); a `"snapshot"` row is * authoritative iff `observedAt` is within the threshold. */ readonly authoritative: boolean; } export interface QuotaDashboard { schemaVersion: 1; effectiveProvider: ProviderId; providers: Array; } export interface QuotaCapability { invoke(): Promise; } /** * Inputs to {@link buildQuotaWindow}. Every field is optional except * that at least one of `explicitRemainingPercent`, a valid count set * (`used` + `limit`), or an explicit finite nonnegative `remaining` * must be present, otherwise the window is unrecoverable and * `QUOTA_ERROR` is thrown. */ export interface QuotaWindowInputs { durationSeconds?: number; used?: number; limit?: number; resetsAtEpochMs?: number; /** * A Provider-supplied REMAINING percentage (already in remaining * terms, not used terms). A finite value wins over count-derived * derivation and is then clamped to 0..100. */ explicitRemainingPercent?: number; /** * A Provider-supplied EXACT remaining count. Two distinct uses: * (GitHub #49) a window whose limit is unknown and neither an * explicit percentage nor a valid count set is present — the built * window carries `remaining` verbatim and omits `used`, `limit`, and * `remainingPercent`; and (GitHub #109 — historically Z.AI's * cumulative `currentValue` past the window cap; the zai adapter now * rejects such entries whole — #191, though the branch stays for * other Providers) a window whose counts are invalid — `remaining` * is published verbatim next to any explicit remaining percentage, * counts omitted. Nothing is ever inferred from tier tables or * fabricated as a percentage. */ remaining?: number; } /** * Build a normalized {@link QuotaWindow} from Provider inputs. * * Resolution order: * 1. A finite explicit remaining percentage wins (then clamped). * 2. Otherwise derive the percentage from valid counts. * 3. Otherwise, when neither is available but an explicit finite * nonnegative `remaining` is supplied (unknown-limit window, * GitHub #49), publish that value verbatim with `used`, `limit`, * and `remainingPercent` omitted — never inferred, never * fabricated. * 4. Otherwise throw `QUOTA_ERROR` — the category is unrecoverable. * * Used-only windows (#99): an explicit percentage combined with a * finite nonnegative `used` and NO `limit` (Provider cannot or does * not publish a ceiling, e.g. Tavily's unlimited key) keeps the * observed `used` next to the percentage; `limit` and `remaining` * are omitted rather than fabricated. Inputs that supply an invalid * count pair keep the omitted-together discipline — this branch never * rescues a partially invalid set. * * Invalid optional counts are omitted together; valid counts populate * `used`, `limit`, and a derived `remaining`. `durationSeconds` and * `resetsAt` are included only when finite/ISO-valid. */ export declare function buildQuotaWindow(inputs: QuotaWindowInputs): QuotaWindow; /** * Map a thrown error into a normalized {@link ProviderQuotaFailure}. The * caller is responsible for recursive redaction before the failure * crosses an outward boundary (all-provider quota does this in P4-03). */ export declare function quotaFailureFromError(provider: ProviderId, error: unknown): ProviderQuotaFailure; //# sourceMappingURL=quota.d.ts.map