@earendil-works/pi-coding-agent
Version:
Coding agent CLI with read, bash, edit, write tools and session management
115 lines • 5.5 kB
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