// SPDX-License-Identifier: GPL-3.0-or-later /** * Shared contracts for the live-pricing system. * * The design goal is a multi-provider layer: supporting a new catalog means * implementing one `CatalogAdapter` (fetch + map to LiveModel). Every lifecycle * concern — cache, TTL, conditional requests, persistence, best-effort * fallback, merging onto a curated catalog — lives in sync.ts / merge.ts and is * reused unchanged. */ /** Price in USD per million tokens, matching Pi's `Model["cost"]`. */ export interface ModelCostRates { input: number; output: number; cacheRead: number; cacheWrite: number; } export interface ModelCostTier extends ModelCostRates { /** This tier applies once total input usage exceeds this token count. */ inputTokensAbove: number; } export interface ModelCost extends ModelCostRates { /** Request-wide pricing tiers; the highest matching threshold wins. */ tiers?: ModelCostTier[]; } /** Mirrors Pi's `ModelThinkingLevel`. */ export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; /** Mirrors Pi's `ThinkingLevelMap`: null marks a level as unsupported. */ export type ThinkingLevelMap = Partial>; /** * A model as accepted by Pi's `ProviderConfigInput["models"]`. * * `compat`, `thinkingLevelMap` and `samplingParams` are carried through * verbatim so that merging onto Pi's curated catalog preserves metadata that no * public pricing endpoint exposes (cache-control format, thinking-level * mapping, per-model sampling defaults). */ export interface LiveModel { id: string; name: string; reasoning: boolean; input: ("text" | "image")[]; contextWindow: number; maxTokens: number; cost: ModelCost; thinkingLevelMap?: ThinkingLevelMap; samplingParams?: Record; compat?: Record; api?: string; baseUrl?: string; } /** A fetched catalog. `null` from a fetch means 304 Not Modified. */ export interface FetchedCatalog { models: LiveModel[]; fetchedAt: number; etag?: string; } /** * Contract for a remote catalog source. * * Two integration modes are supported, chosen by whether `baseModels` is set: * * - **Merge mode** (`baseModels` present, e.g. OpenRouter): the adapter * overrides a provider Pi already ships. Only prices are taken from the * live catalog; every other field stays curated. Models absent from the * live feed keep their existing definition, and a failed or partial fetch * contributes nothing at all, so the user's catalog can never shrink. * * - **Standalone mode** (`baseModels` absent, e.g. DeepInfra): the adapter * registers a provider Pi does not know about, and the live feed is the only * source of truth. */ export interface CatalogAdapter { readonly providerId: string; readonly providerName: string; readonly baseUrl: string; /** Pi `Api` id used for models that don't carry their own. */ readonly api: string; /** * Fetch the catalog. Implementations must pass `signal` to blocking I/O and * may send `ifNoneMatch` as `If-None-Match`, returning `null` on HTTP 304. */ readonly fetch: (signal: AbortSignal, ifNoneMatch?: string) => Promise; /** * Last-resort curated catalog to merge live prices onto, and the flag that * marks this adapter as merge mode. sync.ts prefers the catalog it captures * from Pi's live registry, which — unlike anything an adapter can reach — also * carries the user's `models.json` models. */ readonly baseModels?: () => readonly LiveModel[]; }