UNPKG

@earendil-works/pi-coding-agent

Version:

Coding agent CLI with read, bash, edit, write tools and session management

115 lines 5.5 kB
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