/** * Quota normalization — capability/category mapping + authority-aware * scoring (Plan B — PB-T3). * * Derives a remaining score per `(provider, capability)` from the **raw * category snapshot** persisted by PB-T1, so PB-T4 can rank providers * without learning any Provider's category schema. Pure derivation only: * takes a {@link QuotaState} as input, returns scored/ranked results, * never touches disk or transport. * * Two review fixes shape the contract: * * 1. **Map raw categories, not a pre-derived remaining.** Each Provider * normalizes its native quota payload into named {@link QuotaCategory} * entries (PB-T1). This module declares which category name governs * which `(provider, capability)` pair, then reads that category's * `current.remainingPercent` as the score. The score is already * normalized 0..100 by `buildQuotaWindow`; this module does NOT * re-derive or re-clamp. * * 2. **Separate authority from score.** The "neutral 50 yet never * fullest" contradiction is resolved by representing unknown * authority explicitly rather than encoding it as a numeric score. * Providers with a real credit/token signal form a **known tier**, * ranked by score; providers without a signal (Brave rate-limit, * Exa none) form an **unknown tier**, ranked after every known * provider. The unknown tier never wins over a healthy known * provider, even a low-scored one. The one exception is the #97 * KNOWN_EXHAUSTED demotion: a known provider whose capability-mapped * category reads 0% on a snapshot observed within the 24h * {@link QUOTA_EXHAUSTION_DEMOTION_HORIZON_MS} is demoted into the * unknown tier by the ranker and placed strictly below every * natural unknown. Demotion is ranking-only — the scorer still * returns `authority:"known", score:0` — and runs only when the * caller supplies a clock (`ScoreOptions.now`); without one the * checks are skipped and the pre-#97 ordering holds. * * Boundary rules: * - Imports the quota contract types from `capabilities/quota.js`, * provider identity/capability types from `providers/types.js`, and * PB-T1's `QuotaState` from `lib/quota-store.js`. No provider * transport, no command presentation, no `MainDependencies`. * - **Pure module.** No disk I/O, no transport, no * `process.stderr.write`. Warning output is returned to the caller * through an `onWarning` callback; the scoring result itself never * observes the outside world. * - **Fail-open on drift.** If a mapped category name is absent from * the live snapshot (provider renamed it), degrade to the * provider-level fallback, then to **unknown** — never hard-fail. * Each degradation emits a structured warning so drift is visible. * * Relation to other Plan B tickets: * - PB-T1: this module reads `QuotaState` (the persisted snapshot); * it does not write. * - PB-T2: consumption events advance `locallyUpdatedAt` and adjust * category estimates. This module reads the resulting category * `current` window; the locally-decremented value is the score. * - PB-T4: consumes `rankProvidersForCapability` and applies the * selection algorithm. This module never selects. */ import type { ProviderCapability, ProviderId } from "../providers/types.js"; import type { QuotaState } from "./quota-store.js"; /** * The authority-aware score for a single `(provider, capability)` pair. * * - `authority: "known"` — the provider exposes a real credit/token * signal and the mapped category was found in the snapshot. `score` * is the category's `current.remainingPercent` (already normalized * 0..100 by PB-T1). `category` is the matched category name (after * alias resolution / fallback), so PB-T4 can surface which pool was * ranked. * - `authority: "unknown"` — no authoritative signal is available * (provider has no spend quota; snapshot missing; mapped category * absent and no fallback matched; percent corrupt; or — since #97 — * the ranker demoted a fresh-0% known score to KNOWN_EXHAUSTED). * `reason` is a stable, machine-readable reason code, NOT a * user-facing message. Unknown entries are eligible as fallback and * rank after every healthy known-scored provider (PB-T4 contract); * a known provider demoted to KNOWN_EXHAUSTED ranks after the * natural unknowns. * * `unknown` is NEVER encoded as a numeric score (no neutral `50`, no * `Infinity`, no `0`). This is the explicit fix for the "neutral 50 yet * never fullest" contradiction flagged in review item 12: authority is * a separate axis from score, and PB-T4 sorts the two tiers * independently. */ export type CapabilityScore = { readonly authority: "known"; readonly score: number; readonly category: string; } | { readonly authority: "unknown"; readonly reason: UnknownScoreReason; }; /** * Machine-readable reason for an unknown score. Stable across releases * so callers (PB-T4, dashboards) can branch without parsing prose. The * matching human-readable text lives in the warning metadata. */ export type UnknownScoreReason = /** * The provider is in the always-unknown authority tier by policy * (Brave rate-limit, Exa no quota capability). No category lookup * is attempted. */ "PROVIDER_NON_AUTHORITATIVE" /** * No mapping entry exists for `(provider, capability)`. Either the * capability is observational (`quota`/`diagnostics`) or the * mapping table has a gap (ticket: add a row + warn). */ | "MAPPING_MISSING" /** * The provider has no snapshot in {@link QuotaState} (never * harvested, or the entry was cleared). */ | "SNAPSHOT_MISSING" /** * The snapshot exists but its `categories` array is empty. */ | "SNAPSHOT_EMPTY" /** * None of the mapped aliases matched a category, and no * provider-level fallback matched either. Likely provider-side * rename (drift). */ | "CATEGORY_NOT_FOUND" /** * The matched category's `remainingPercent` is not a finite number * in 0..100. PB-T1's `buildQuotaWindow` is supposed to guarantee * this, but a hand-edited `state.json` could violate it; the scorer * treats corrupt input as unknown rather than synthesizing a score. */ | "PERCENT_CORRUPT" /** * Ranking-only demotion (#97): the provider scored * `authority:"known", score:0` — its capability-mapped category is * depleted — on a snapshot whose `observedAt` is within * {@link QUOTA_EXHAUSTION_DEMOTION_HORIZON_MS} of the caller's * clock. The ranker rewrites the entry into the unknown tier and * ranks it strictly below every natural unknown, so a * still-exhausted provider no longer floats to the top of the * selection order. Never produced by {@link scoreCapability} * (demotion lives in ranking, not in scoring's meaning) and never * when no clock was supplied. */ | "KNOWN_EXHAUSTED"; /** * One row of the static mapping table: for `(provider, capability)`, * which raw `QuotaCategory.name` governs the score. * * `categoryAliases` is an **ordered** list of acceptable category names. * The first alias that matches a category in the live snapshot wins. * This is the fail-open seam: when a provider renames a category, the * lookup silently misses and the scorer degrades to the * `providerFallbackCategory` (if any), then to unknown. Drift is * surfaced through a structured warning. * * Case-sensitivity follows the existing normalizers: Tavily emits * lowercase endpoint names (`search`, `crawl`, ...), Z.AI emits * `requests`/`tokens`, Firecrawl emits a case-sensitive `Credits`. The * alias list must match the normalizer's exact emission; do not * case-fold here or the mapping will hide drift. */ export interface CapabilityMappingEntry { readonly provider: ProviderId; readonly capability: ProviderCapability; readonly categoryAliases: readonly string[]; /** * Optional provider-level category used when no alias matches. * For Tavily, this is the aggregate `requests` category — every * endpoint shares one credit pool, so a missing endpoint category * degrades to the pool-level remaining. For providers with one * category per capability (Z.AI, Firecrawl), this is intentionally * absent: a missing category means real drift, not a graceful * fallback target. */ readonly providerFallbackCategory?: string; } /** * Default MiniMax model-name aliases per capability. MiniMax's * `/remains` normalizer emits one category per live `model_name` * string, and `model_name` values are arbitrary (the live schema does * NOT emit a stable `general` label — see ticket * `tests/quota-conformance.test.js:310-370` and fixtures including * `zorla-x` and `abab6.5s-chat`). This table is the documented * mapping policy: the first alias that matches a live category wins. * * Tests and PB-T4 callers can override this table via * {@link ScoreOptions.minimaxModelAliases} when a deployment uses * a different model name. Fail-open applies: an empty match degrades * to unknown + warn, never a throw. */ export declare const DEFAULT_MINIMAX_MODEL_ALIASES: Readonly>; /** * The full Vision capability set advertised by Z.AI, plus the * specialized operations MiniMax may attest. They all share the same * category mapping within their provider (Z.AI → `tokens`, MiniMax → * the VLM model alias), so the table is generated from one constant * list per provider to keep the rows in sync. */ export declare const ZAI_VISION_CAPABILITIES: readonly ProviderCapability[]; /** * Specialized MiniMax Vision operations (mirror of the descriptor's * attested set). They all share the VLM model alias. */ export declare const MINIMAX_VISION_CAPABILITIES: readonly ProviderCapability[]; /** * Z.AI capabilities that share the rolling `requests` category. * `quota` and `diagnostics` are intentionally absent — they are * observational, not selection candidates. */ export declare const ZAI_REQUEST_CAPABILITIES: readonly ProviderCapability[]; /** * Tavily endpoint-mapped capabilities. Each maps to a same-named * endpoint category with a fallback to the aggregate `requests` * pool (every Tavily endpoint bills against one credit pool). */ export declare const TAVILY_ENDPOINT_CAPABILITIES: readonly ProviderCapability[]; /** * Tavily `(capability → endpoint category name)` resolution. `reader` * is mapped to the `extract` endpoint (the Tavily `/extract` endpoint * serves reader calls). Other capabilities map to the same-named * endpoint category. */ export declare const TAVILY_CAPABILITY_TO_ENDPOINT: Readonly>; /** * Firecrawl capabilities. All four consume the shared `Credits` pool. */ export declare const FIRECRAWL_CREDIT_CAPABILITIES: readonly ProviderCapability[]; /** * The static capability→category mapping table. Built once at module * load from the per-provider capability lists so a future capability * addition only edits one constant, not 8 rows. * * Excluded by design: * - Brave: always-unknown authority (rate-limit, not spend). * - Exa: always-unknown authority (no quota capability). * - `quota`/`diagnostics` on every provider: observational, not * selection candidates. * * The table is `readonly` and exported so PB-T5/dashboards/Doctor can * render the same source of truth the scorer uses. */ export declare const CAPABILITY_MAPPINGS: readonly CapabilityMappingEntry[]; /** * Per-provider authority policy. * * - `"mapped"` — the provider exposes real credit/token signals; its * categories map to capabilities via {@link CAPABILITY_MAPPINGS} and * the score is the matched category's `remainingPercent`. * - `"always-unknown"` — the provider has no authoritative spend * signal. {@link CapabilityScore} always returns `authority:"unknown"` * with the documented reason, regardless of whether a snapshot * exists. The provider remains **eligible** for PB-T4 fallback; it * never wins over a healthy known-scored provider (since #97, a * known provider whose capability-mapped category reads 0% on a * snapshot within the 24h demotion horizon is demoted below the * unknown tier itself, so "healthy" is the operative word). * * `reason` is surfaced unchanged through the warning channel when a * score is requested for an always-unknown provider; it documents the * policy for dashboards and Doctor. */ export interface ProviderAuthorityPolicy { readonly provider: ProviderId; readonly kind: "mapped" | "always-unknown"; readonly reason: string; } /** * The static authority-policy table. Eighteen providers are explicitly * non-authoritative: * * - **Brave**: reports a rate-limit window via `X-RateLimit-*` headers, * not spend or credits consumed. Brave uses metered billing, so the * `remainingPercent` is a rate-limit signal, not a budget signal * (see `BRAVE_QUOTA_CAVEAT` in `providers/brave/quota.ts`). The * numeric window is retained for PB-T5 dashboard display; the * authority axis deliberately ignores it. * - **Exa**: advertises no `quota` capability at all. There is nothing * to map or synthesize. * - **Parallel AI, Perplexity**: neither advertises a quota * capability; there is no signal to map. * - **Jina AI**: DOES advertise a `quota` capability * (`createJinaQuotaCapability`), but its signal is a per-minute * rate-limit window (exact remaining RPM/TPM with an explicitly * unknown limit — GitHub #49), not spend or plan usage. * - **Linkup**: DOES advertise a `quota` capability * (`createLinkupQuotaCapability`), but its signal is an exact remaining * credit balance with an unknown limit (GitHub #49), not a * - **Spider.cloud**: DOES advertise a `quota` capability * (`createSpiderQuotaCapability`), but its signal is an exact * remaining credit balance with an unknown limit, not a * percentage-bounded plan signal. * - **Bocha AI**: does not advertise a `quota` capability; there is * no signal to map. * * All eighteen providers stay **eligible** for PB-T4 fallback (their * `authority:"unknown"` score sorts after every healthy known * provider), so they can still be picked when no healthy known * provider remains. Since #97 they can even outrank a mapped provider: * a known-tier provider whose capability-mapped category reads 0% on * a snapshot observed within the 24h demotion horizon is demoted below * the natural unknowns in the ranking. */ export declare const PROVIDER_AUTHORITY_POLICIES: readonly ProviderAuthorityPolicy[]; /** * A structured warning emitted by the scorer. The pure module never * writes to stderr; it returns warnings through the * {@link ScoreOptions.onWarning} callback so the caller (PB-T4 / main) * owns the rendering surface. * * `code` is stable across releases so dashboards can de-duplicate. * `message` is a single-line human-readable string suitable for stderr. */ export interface QuotaMappingWarning { readonly code: "PROVIDER_NON_AUTHORITATIVE" | "MAPPING_MISSING" | "SNAPSHOT_MISSING" | "SNAPSHOT_EMPTY" | "CATEGORY_NOT_FOUND" | "PROVIDER_FALLBACK_USED" | "PERCENT_CORRUPT" | "KNOWN_EXHAUSTED"; readonly provider: ProviderId; readonly capability: ProviderCapability; readonly message: string; } /** * Look up the authority policy for a provider. Returns `undefined` * only if the registry learns a new provider ID without a corresponding * policy row; the scorer treats undefined as `"always-unknown"` so an * unmapped provider never accidentally wins. */ export declare function getProviderAuthorityPolicy(provider: ProviderId): ProviderAuthorityPolicy | undefined; /** * Look up the static mapping entry for `(provider, capability)`. * Returns `undefined` when no row exists (observational capability, * unknown capability, or a mapping-table gap). * * For MiniMax, the returned entry has an empty `categoryAliases` * sentinel; the scorer expands it via * {@link resolveMiniMaxAliasesForCapability}. */ export declare function getCapabilityMapping(provider: ProviderId, capability: ProviderCapability): CapabilityMappingEntry | undefined; /** * Resolve the MiniMax alias list for a capability. Search returns the * `search` aliases; every MiniMax vision capability returns the * `vision.interpret-image` aliases (they all share the VLM transport). * Unknown capabilities return an empty list. */ export declare function resolveMiniMaxAliasesForCapability(capability: ProviderCapability, aliases?: Readonly>): readonly string[]; /** * The union of capability-relevant quota category names for one * provider — exactly the names the scorer can match for that provider: * the effective `categoryAliases` of every {@link CAPABILITY_MAPPINGS} * row (MiniMax rows expand through the model-alias table) plus each * row's `providerFallbackCategory` (Tavily's aggregate `requests` * pool). Account-level or informational categories a provider may also * emit (e.g. Tavily's `plan`) are deliberately absent: they are not * capability-relevant. * * Returns `undefined` when the provider has no mapping rows at all * (observational-only or always-unknown providers) — callers choose * their own fallback (doctor's availability classifier treats any * fresh category at 0% as exhausted for unmapped providers). */ export declare function getProviderQuotaCategoryNames(provider: ProviderId): ReadonlySet | undefined; /** * Options for {@link scoreCapability} and {@link rankProvidersForCapability}. */ export interface ScoreOptions { /** * Override the MiniMax model-name alias table. Production uses * {@link DEFAULT_MINIMAX_MODEL_ALIASES}; tests pass a tailored table * to assert specific match paths. */ readonly minimaxModelAliases?: Readonly>; /** * Best-effort warning sink. The scorer never calls * `process.stderr.write`; every degradation routes through this * callback. Caller owns the rendering surface. Production wires a * stderr writer; tests inject a recorder. */ readonly onWarning?: (warning: QuotaMappingWarning) => void; /** * The clock for the #97 KNOWN_EXHAUSTED demotion, as a Unix epoch * millisecond. This module stays clockless: when `now` is absent * the exhaustiveness check is skipped entirely and ranking behaves * exactly as it did before #97 (a 0% known provider outranks the * unknown tier). When supplied, a known-tier candidate whose * capability-mapped category read 0% on a snapshot observed within * {@link QUOTA_EXHAUSTION_DEMOTION_HORIZON_MS} of this clock is * demoted below the natural unknowns in * {@link rankProvidersForCapability}. Ignored by * {@link scoreCapability} — scoring's meaning is unchanged (D6). */ readonly now?: number; } /** * Derive the authority-aware score for a single `(provider, capability)` * pair from the persisted {@link QuotaState}. * * Resolution order: * 1. **Authority policy.** An `always-unknown` provider returns * `{ authority: "unknown", reason: "PROVIDER_NON_AUTHORITATIVE" }` * without consulting the snapshot. This is the Brave/Exa contract. * 2. **Mapping presence.** A `(provider, capability)` with no row in * {@link CAPABILITY_MAPPINGS} returns `reason: "MAPPING_MISSING"`. * Observational capabilities (`quota`/`diagnostics`) hit this path * by design. * 3. **Snapshot presence.** Missing snapshot → `SNAPSHOT_MISSING`; * empty `categories` array → `SNAPSHOT_EMPTY`. * 4. **Alias match.** Walk the entry's effective alias list (static * for most providers; MiniMax resolves through the alias table). * The first category whose name matches wins. * 5. **Provider-level fallback.** When no alias matches and the entry * declares a `providerFallbackCategory`, look it up. A match * emits a `PROVIDER_FALLBACK_USED` warning so drift is visible. * 6. **Unknown + warn.** No match at all → `CATEGORY_NOT_FOUND`. * 7. **Percent validation.** A matched category whose * `remainingPercent` is non-finite or outside 0..100 returns * `PERCENT_CORRUPT`. PB-T1's `buildQuotaWindow` is supposed to * guarantee this, but a hand-edited `state.json` could violate * it; the scorer treats corrupt input as unknown rather than * synthesizing a score. * * This function is total: it never throws. Every failure path returns * a typed unknown score; the warning channel explains why. */ export declare function scoreCapability(state: QuotaState, provider: ProviderId, capability: ProviderCapability, options?: ScoreOptions): CapabilityScore; /** * A ranked provider entry. The order in the returned array is the * selection order PB-T4 walks. * * - Known-tier entries appear first, sorted by `score` descending. * Ties break by registry order (`PROVIDER_IDS` by default, or the * `registryOrder` option). A known-tier provider whose * capability-mapped category reads 0% on a snapshot within the 24h * demotion horizon (#97, `ScoreOptions.now` supplied) does not stay * here — it is demoted to the unknown tier with reason * `KNOWN_EXHAUSTED`. * - Unknown-tier entries appear after every (healthy) known entry, in * registry order. They remain eligible as fallback. * - Demoted (`KNOWN_EXHAUSTED`) entries rank strictly below every * natural unknown: natural unknowns first in registry order, then * demoted entries in registry order (D6 positioning). Without a * supplied clock no demotion happens and this band is empty. * * Optional fields are present only when meaningful: `score`/`category` * for known entries; `reason` for unknown entries. */ export interface RankedProvider { readonly provider: ProviderId; readonly authority: "known" | "unknown"; readonly score?: number; readonly category?: string; readonly reason?: UnknownScoreReason; } /** * Rank candidate providers for a capability. * * Returns a new array (caller owns it). The ranking is deterministic * for identical inputs: same `state` + same `candidates` + same * `registryOrder` → byte-identical output order. * * Algorithm: * 1. Score each candidate via {@link scoreCapability}. * 2. Demote fresh-exhausted known entries (#97): when a clock was * supplied, a known entry whose capability-mapped category read * 0% on a snapshot observed within * {@link QUOTA_EXHAUSTION_DEMOTION_HORIZON_MS} is rewritten to * the unknown tier with reason `KNOWN_EXHAUSTED`. * 3. Partition into known and unknown tiers. * 4. Sort known tier by score desc; break ties by registry order. * 5. Sort unknown tier by registry order only. * 6. Concatenate: known first, then natural unknowns in registry * order, then demoted entries in registry order (D6 positioning — * demoted entries rank strictly below every natural unknown). * * The "5% known beats unknown" contract falls out of steps 3–6: every * healthy known entry precedes every unknown entry regardless of * score. A known provider at 5% remaining still ranks above a * non-authoritative Brave/Exa provider, because Brave/Exa are in the * unknown tier. The contract is freshness-scoped since #97: at 0% on a * fresh snapshot the provider is demoted below the unknown tier * instead — the one case where an unknown-tier provider outranks a * known one. Without a supplied clock the demotion never runs and the * pre-#97 ordering holds byte-for-byte. * * Duplicate candidates are de-duplicated in first-occurrence order * before scoring (defensive — PB-T4 builds candidate lists, and a * duplicate would otherwise double-emit warnings). */ export declare function rankProvidersForCapability(state: QuotaState, capability: ProviderCapability, candidates: readonly ProviderId[], options?: ScoreOptions & { /** * Stable registry order for tie-breaking. Defaults to * {@link PROVIDER_IDS} (the canonical production order * `[zai, minimax, tavily, exa, brave, firecrawl]`). */ readonly registryOrder?: readonly ProviderId[]; }): readonly RankedProvider[]; //# sourceMappingURL=quota-mapping.d.ts.map