/** * Shared helpers for refreshing the live model catalog. */ import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent"; import type { RefreshModelsContext } from "@earendil-works/pi-ai"; import { resolveStartupApiKey } from "./auth.ts"; import { accessTokenFromCredential } from "./credentials.ts"; import { fetchAccountLimits } from "./limits.ts"; import { FALLBACK_MODELS, fetchModels, toModelConfig, type OpenferenceModelInfo, } from "./models.ts"; import { setPacingRpm, type PacingState } from "./pacing.ts"; export interface ModelCatalogState { /** Last successfully fetched live catalog (or startup list). */ lastSuccessful: OpenferenceModelInfo[]; } export function createModelCatalogState( initial: OpenferenceModelInfo[] = FALLBACK_MODELS, ): ModelCatalogState { return { lastSuccessful: initial }; } function assertNotAborted(signal?: AbortSignal): void { if (signal?.aborted) throw new Error("aborted"); } export async function buildRefreshedModels( context: RefreshModelsContext, options: { catalog: ModelCatalogState; pacingState?: PacingState; fetchModelsImpl?: typeof fetchModels; fetchLimitsImpl?: typeof fetchAccountLimits; }, ): Promise { if (!context.allowNetwork) { return options.catalog.lastSuccessful.map(toModelConfig); } assertNotAborted(context.signal); const token = accessTokenFromCredential(context.credential) ?? resolveStartupApiKey(); if (!token) return options.catalog.lastSuccessful.map(toModelConfig); const fetchModelsImpl = options.fetchModelsImpl ?? fetchModels; const fetchLimitsImpl = options.fetchLimitsImpl ?? fetchAccountLimits; try { const live = await fetchModelsImpl(token, context.signal); assertNotAborted(context.signal); if (live.length > 0) { // Commit before optional pacing so a limits failure/abort cannot rewind // a successful model fetch. options.catalog.lastSuccessful = live; } if (options.pacingState && !context.signal?.aborted) { try { const limits = await fetchLimitsImpl(token, fetch, context.signal); if (!context.signal?.aborted) { setPacingRpm(options.pacingState, limits.maxRpm); } } catch { // Pacing is best-effort; never discard a successful model catalog. } } if (live.length > 0) { return live.map(toModelConfig); } } catch (err) { if (err instanceof Error && err.message === "aborted") throw err; if (err instanceof Error && err.name === "AbortError") throw new Error("aborted"); console.warn(`[openference] refreshModels failed: ${(err as Error).message}`); } // On failure, keep the last successful catalog — never rewind to a stale // startup-only snapshot after a live refresh has succeeded. return options.catalog.lastSuccessful.map(toModelConfig); }