{"version":3,"file":"remote-catalog-provider.d.ts","sourceRoot":"","sources":["../../src/core/remote-catalog-provider.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAgC,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAKpF,eAAO,MAAM,kCAAkC,QAAqB,CAAC;AAwCrE,4EAA4E;AAC5E,wBAAgB,iBAAiB,CAChC,QAAQ,EAAE,QAAQ,EAClB,cAAc,GAAE,MAAiC,EACjD,eAAe,CAAC,EAAE,GAAG,GACnB,QAAQ,CAiEV","sourcesContent":["import { stat } from \"node:fs/promises\";\nimport type { Api, Model, ModelsStoreEntry, Provider } from \"@earendil-works/pi-ai\";\nimport { VERSION } from \"../config.ts\";\nimport { getPiUserAgent } from \"../utils/pi-user-agent.ts\";\n\nconst DEFAULT_CATALOG_BASE_URL = \"https://pi.dev\";\nexport const REMOTE_CATALOG_REFRESH_INTERVAL_MS = 4 * 60 * 60 * 1000;\n\nfunction mergeModels(baseline: readonly Model<Api>[], dynamic: readonly Model<Api>[]): Model<Api>[] {\n\tconst merged = [...baseline];\n\tfor (const model of dynamic) {\n\t\tconst index = merged.findIndex((entry) => entry.id === model.id);\n\t\tif (index >= 0) merged[index] = model;\n\t\telse merged.push(model);\n\t}\n\treturn merged;\n}\n\nfunction parseCatalog(providerId: string, value: unknown): Model<Api>[] {\n\tconst entries = Array.isArray(value)\n\t\t? value\n\t\t: typeof value === \"object\" && value !== null && \"models\" in value && Array.isArray(value.models)\n\t\t\t? value.models\n\t\t\t: typeof value === \"object\" && value !== null\n\t\t\t\t? Object.values(value)\n\t\t\t\t: undefined;\n\tif (!entries) throw new Error(`Invalid model catalog for provider \"${providerId}\"`);\n\treturn entries\n\t\t.filter((entry): entry is Model<Api> => typeof entry === \"object\" && entry !== null && \"id\" in entry)\n\t\t.map((model) => ({ ...model, provider: providerId }));\n}\n\nfunction remoteModels(\n\tentry: ModelsStoreEntry | undefined,\n\tlocalLastModified: number | undefined,\n): readonly Model<Api>[] {\n\tif (!entry) return [];\n\tif (\n\t\tlocalLastModified !== undefined &&\n\t\t(entry.lastModified === undefined || entry.lastModified <= localLastModified)\n\t) {\n\t\treturn [];\n\t}\n\treturn entry.models;\n}\n\n/** Add a persisted pi.dev catalog overlay to a static built-in provider. */\nexport function withRemoteCatalog(\n\tprovider: Provider,\n\tcatalogBaseUrl: string = DEFAULT_CATALOG_BASE_URL,\n\tlocalCatalogUrl?: URL,\n): Provider {\n\tlet dynamicModels: readonly Model<Api>[] = [];\n\tlet inflightRefresh: Promise<void> | undefined;\n\n\treturn {\n\t\t...provider,\n\t\tgetModels: () => mergeModels(provider.getModels(), dynamicModels),\n\t\trefreshModels: (context) => {\n\t\t\tinflightRefresh ??= (async () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst localLastModified = localCatalogUrl\n\t\t\t\t\t\t? await stat(localCatalogUrl).then(\n\t\t\t\t\t\t\t\t(value) => value.mtimeMs,\n\t\t\t\t\t\t\t\t() => undefined,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t: undefined;\n\t\t\t\t\tconst stored = await context.store.read();\n\t\t\t\t\tdynamicModels = remoteModels(stored, localLastModified).filter(\n\t\t\t\t\t\t(model) => model.provider === provider.id,\n\t\t\t\t\t);\n\t\t\t\t\tif (!context.allowNetwork || context.signal?.aborted) return;\n\t\t\t\t\tif (\n\t\t\t\t\t\t!context.force &&\n\t\t\t\t\t\tstored?.checkedAt !== undefined &&\n\t\t\t\t\t\tstored.lastModified !== undefined &&\n\t\t\t\t\t\tDate.now() - stored.checkedAt < REMOTE_CATALOG_REFRESH_INTERVAL_MS\n\t\t\t\t\t) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst url = new URL(`/api/models/providers/${encodeURIComponent(provider.id)}`, catalogBaseUrl);\n\t\t\t\t\tconst response = await fetch(url, {\n\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\taccept: \"application/json\",\n\t\t\t\t\t\t\t\"User-Agent\": getPiUserAgent(VERSION),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tsignal: context.signal,\n\t\t\t\t\t});\n\t\t\t\t\tif (context.signal?.aborted) return;\n\t\t\t\t\tconst checkedAt = Date.now();\n\t\t\t\t\tif (response.status === 404 || response.status === 501) {\n\t\t\t\t\t\tawait context.store.write({ ...(stored ?? { models: [] }), checkedAt, lastModified: 0 });\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tif (!response.ok) {\n\t\t\t\t\t\tawait context.store.write({ ...(stored ?? { models: [] }), checkedAt });\n\t\t\t\t\t\tthrow new Error(`Model catalog request failed for ${provider.id}: ${response.status}`);\n\t\t\t\t\t}\n\t\t\t\t\tconst refreshed = parseCatalog(provider.id, await response.json());\n\t\t\t\t\tconst lastModified = Date.parse(response.headers.get(\"last-modified\") ?? \"\");\n\t\t\t\t\tif (context.signal?.aborted) return;\n\t\t\t\t\tconst entry = {\n\t\t\t\t\t\tmodels: refreshed,\n\t\t\t\t\t\tcheckedAt,\n\t\t\t\t\t\tlastModified: Number.isNaN(lastModified) ? 0 : lastModified,\n\t\t\t\t\t};\n\t\t\t\t\tdynamicModels = remoteModels(entry, localLastModified);\n\t\t\t\t\tawait context.store.write(entry);\n\t\t\t\t} finally {\n\t\t\t\t\tinflightRefresh = undefined;\n\t\t\t\t}\n\t\t\t})();\n\t\t\treturn inflightRefresh;\n\t\t},\n\t};\n}\n"]}