import type { ModelsDevCatalog, ModelsDevMetadata } from "./types.ts"; import { withNetworkTimeout } from "./network.ts"; export const MODELS_DEV_URL = "https://models.dev/api.json"; function isMetadata(value: unknown): value is ModelsDevMetadata { return !!value && typeof value === "object" && typeof (value as { id?: unknown }).id === "string"; } /** * The models.dev fields this package reads. Everything else the API returns * (descriptions, release dates, tool-call flags, ...) is dropped at parse time: * it is never consulted, and it was two thirds of a ~7.5 MB cache that every * startup read and parsed. */ const METADATA_FIELDS = [ "id", "sourceProvider", "name", "reasoning", "reasoning_options", "thinkingLevelMap", "modalities", "limit", "cost", ] as const satisfies ReadonlyArray; function pruneMetadata(metadata: ModelsDevMetadata): ModelsDevMetadata { const pruned: Partial = {}; for (const field of METADATA_FIELDS) { if (metadata[field] !== undefined) (pruned as Record)[field] = metadata[field]; } return pruned as ModelsDevMetadata; } export function parseModelsDevCatalog(payload: unknown): ModelsDevCatalog { if (!payload || typeof payload !== "object" || Array.isArray(payload)) { throw new Error("models.dev catalog must be a JSON object"); } const record = payload as Record; const catalog: ModelsDevCatalog = {}; for (const [key, value] of Object.entries(record)) { const provider = value as { models?: unknown }; if (provider && typeof provider === "object" && provider.models && typeof provider.models === "object") { for (const [modelId, metadata] of Object.entries(provider.models as Record)) { if (!isMetadata(metadata)) continue; const canonicalId = metadata.id.includes("/") ? metadata.id : `${key}/${modelId}`; const catalogKey = canonicalId.startsWith(`${key}/`) ? canonicalId : `${key}/${canonicalId}`; catalog[catalogKey] = pruneMetadata({ ...metadata, id: canonicalId, sourceProvider: key }); } continue; } if (isMetadata(value)) { catalog[key] = pruneMetadata(value); } } if (Object.keys(catalog).length === 0 && Object.keys(record).length > 0) { throw new Error("models.dev catalog contained no valid models"); } return catalog; } export async function fetchModelsDevCatalog(timeoutMs?: number, signal?: AbortSignal): Promise { return withNetworkTimeout(async (reqSignal) => { const response = await fetch(MODELS_DEV_URL, { headers: { Accept: "application/json" }, signal: reqSignal }); if (!response.ok) { throw new Error(`models.dev fetch failed: HTTP ${response.status} ${response.statusText}`); } return parseModelsDevCatalog(await response.json()); }, timeoutMs, "models.dev fetch", signal); } export function hasSourceProviderMetadata(catalog: ModelsDevCatalog): boolean { return Object.values(catalog).every((metadata) => typeof metadata.sourceProvider === "string"); }