115 lines
5.4 KiB
JavaScript
115 lines
5.4 KiB
JavaScript
import { VERSION } from "../config.js";
|
|
import { getPiUserAgent } from "../utils/pi-user-agent.js";
|
|
const DEFAULT_CATALOG_BASE_URL = "https://pi.dev";
|
|
export const REMOTE_CATALOG_REFRESH_INTERVAL_MS = 4 * 60 * 60 * 1000;
|
|
function mergeModels(baseline, dynamic) {
|
|
const merged = [...baseline];
|
|
for (const model of dynamic) {
|
|
const index = merged.findIndex((entry) => entry.id === model.id);
|
|
if (index >= 0)
|
|
merged[index] = model;
|
|
else
|
|
merged.push(model);
|
|
}
|
|
return merged;
|
|
}
|
|
function parseCatalog(providerId, value) {
|
|
const entries = Array.isArray(value)
|
|
? value
|
|
: typeof value === "object" && value !== null && "models" in value && Array.isArray(value.models)
|
|
? value.models
|
|
: typeof value === "object" && value !== null
|
|
? Object.values(value)
|
|
: undefined;
|
|
if (!entries)
|
|
throw new Error(`Invalid model catalog for provider "${providerId}"`);
|
|
return entries
|
|
.filter((entry) => typeof entry === "object" && entry !== null && "id" in entry)
|
|
.map((model) => ({ ...model, provider: providerId }));
|
|
}
|
|
function remoteModels(entry, localGeneratedAt) {
|
|
if (!entry)
|
|
return [];
|
|
if (localGeneratedAt !== undefined && (entry.lastModified === undefined || entry.lastModified <= localGeneratedAt)) {
|
|
return [];
|
|
}
|
|
return entry.models;
|
|
}
|
|
/** Add a persisted pi.dev catalog overlay to a static built-in provider. */
|
|
export function withRemoteCatalog(provider, catalogBaseUrl = DEFAULT_CATALOG_BASE_URL, localGeneratedAt) {
|
|
let dynamicModels = [];
|
|
let inflightRefresh;
|
|
return {
|
|
...provider,
|
|
getModels: () => mergeModels(provider.getModels(), dynamicModels),
|
|
refreshModels: (context) => {
|
|
inflightRefresh ??= (async () => {
|
|
try {
|
|
const stored = await context.store.read();
|
|
dynamicModels = remoteModels(stored, localGeneratedAt).filter((model) => model.provider === provider.id);
|
|
if (!context.allowNetwork || context.signal?.aborted)
|
|
return;
|
|
if (!context.force &&
|
|
stored?.checkedAt !== undefined &&
|
|
stored.lastModified !== undefined &&
|
|
Date.now() - stored.checkedAt < REMOTE_CATALOG_REFRESH_INTERVAL_MS) {
|
|
return;
|
|
}
|
|
// Only revalidate when a cached body backs the validator, so a 304 can never
|
|
// leave the overlay empty.
|
|
const validator = stored?.models.length ? stored.etag : undefined;
|
|
const url = new URL(`/api/models/providers/${encodeURIComponent(provider.id)}`, catalogBaseUrl);
|
|
const response = await fetch(url, {
|
|
headers: {
|
|
accept: "application/json",
|
|
"User-Agent": getPiUserAgent(VERSION),
|
|
...(validator ? { "if-none-match": validator } : {}),
|
|
},
|
|
signal: context.signal,
|
|
});
|
|
if (context.signal?.aborted)
|
|
return;
|
|
const checkedAt = Date.now();
|
|
// Unchanged: dynamicModels already holds the stored overlay, so only the
|
|
// freshness window moves.
|
|
if (response.status === 304 && stored) {
|
|
await context.store.write({ ...stored, checkedAt });
|
|
return;
|
|
}
|
|
if (response.status === 404 || response.status === 501) {
|
|
await context.store.write({
|
|
...(stored ?? { models: [] }),
|
|
checkedAt,
|
|
lastModified: 0,
|
|
etag: undefined,
|
|
});
|
|
return;
|
|
}
|
|
if (!response.ok) {
|
|
// Transient failure: the cached body and its validator stay valid, so keep the
|
|
// etag and let the next refresh revalidate instead of downloading the catalog.
|
|
await context.store.write({ ...(stored ?? { models: [] }), checkedAt });
|
|
throw new Error(`Model catalog request failed for ${provider.id}: ${response.status}`);
|
|
}
|
|
const refreshed = parseCatalog(provider.id, await response.json());
|
|
const lastModified = Date.parse(response.headers.get("last-modified") ?? "");
|
|
if (context.signal?.aborted)
|
|
return;
|
|
const entry = {
|
|
models: refreshed,
|
|
checkedAt,
|
|
lastModified: Number.isNaN(lastModified) ? 0 : lastModified,
|
|
etag: response.headers.get("etag") ?? undefined,
|
|
};
|
|
dynamicModels = remoteModels(entry, localGeneratedAt);
|
|
await context.store.write(entry);
|
|
}
|
|
finally {
|
|
inflightRefresh = undefined;
|
|
}
|
|
})();
|
|
return inflightRefresh;
|
|
},
|
|
};
|
|
}
|
|
//# sourceMappingURL=remote-catalog-provider.js.map
|