feat: persist Antigravity model list across restarts

The antigravity provider list lived only in a module-scoped variable
initialized from ANTG_PROVIDER_MODELS, so /agy:refresh-models updated the
running process but the list silently reset to the static defaults on
/reload or restart.

- add ~/.config/agy-pi/models-cache.json (mode 0600, atomic write) storing
  version, refreshedAt, project and the fetched model definitions
- load the cache at startup; fall back to ANTG_PROVIDER_MODELS when the file
  is missing, unreadable, corrupt or has an incompatible version
- persist the live catalog in /agy:refresh-models and report the file path
- add /agy:models-reset to drop the cache and restore the defaults

Verified: extension loads with no cache (11 defaults), a valid cache is
honored and registered, corrupt/invalid/version-mismatched caches fall back
silently, and /agy:models-reset deletes the cache and re-registers the
defaults end-to-end over RPC.
This commit is contained in:
Matteo Benedetto
2026-09-13 18:53:10 +02:00
parent 4920c4c0ba
commit 16fba66723
+96 -4
View File
@@ -1598,9 +1598,75 @@ const ANTG_PROVIDER_MODELS: AntgProviderModelDef[] = [
{ id: "gpt-oss-120b-medium", name: "GPT-OSS 120B (Medium)", reasoning: false, images: false, contextWindow: 262144, maxTokens: 32768 },
];
// Modelli attualmente registrati nel provider (statici all'avvio, aggiornabili
// con /agy:refresh-models dal catalogo vivo fetchAvailableModels).
let antgRegisteredModels: AntgProviderModelDef[] = ANTG_PROVIDER_MODELS;
// ---------------------------------------------------------------------------
// Cache persistente dei modelli (~/.config/agy-pi/models-cache.json)
// Il catalogo vivo scaricato da /agy:refresh-models viene salvato qui, così la
// lista sopravvive al riavvio dell'estensione. Se il file è assente, illeggibile
// o corrotto si riparte da ANTG_PROVIDER_MODELS senza errori fatali.
// ---------------------------------------------------------------------------
const MODELS_CACHE_FILE = path.join(CONFIG_DIR, "models-cache.json");
const MODELS_CACHE_VERSION = 1;
interface AntgModelsCache {
version: number;
refreshedAt: string;
project?: string;
models: AntgProviderModelDef[];
}
function isAntgModelDef(value: any): value is AntgProviderModelDef {
return (
value !== null &&
typeof value === "object" &&
typeof value.id === "string" &&
value.id.trim() !== "" &&
typeof value.contextWindow === "number" &&
value.contextWindow > 0 &&
typeof value.maxTokens === "number" &&
value.maxTokens > 0
);
}
function loadModelsCache(): AntgModelsCache | null {
try {
const raw = JSON.parse(fs.readFileSync(MODELS_CACHE_FILE, "utf8"));
if (raw?.version !== MODELS_CACHE_VERSION) return null;
if (!Array.isArray(raw.models) || raw.models.length === 0) return null;
if (!raw.models.every(isAntgModelDef)) return null;
return raw as AntgModelsCache;
} catch {
return null; // assente, illeggibile o corrotto → default
}
}
function saveModelsCache(models: AntgProviderModelDef[], projectId?: string) {
try {
fs.mkdirSync(CONFIG_DIR, { recursive: true });
const tmp = `${MODELS_CACHE_FILE}.tmp`;
fs.writeFileSync(
tmp,
JSON.stringify(
{
version: MODELS_CACHE_VERSION,
refreshedAt: new Date().toISOString(),
project: projectId,
models,
},
null,
2,
),
{ mode: 0o600 },
);
fs.renameSync(tmp, MODELS_CACHE_FILE); // scrittura atomica
} catch {
/* ignora */
}
}
// Modelli attualmente registrati nel provider: cache persistente se presente,
// altrimenti i default statici. Aggiornabili con /agy:refresh-models (che ora
// salva su disco) e ripristinabili con /agy:models-reset.
let antgRegisteredModels: AntgProviderModelDef[] = loadModelsCache()?.models ?? ANTG_PROVIDER_MODELS;
// Configurazione del provider "antigravity" (riusata all'avvio e al refresh).
function antgProviderConfig(models: AntgProviderModelDef[]) {
@@ -2618,10 +2684,11 @@ export default function agyExtension(pi: ExtensionAPI) {
if (!models.length) throw new Error("Il catalogo non ha restituito modelli utilizzabili");
antgRegisteredModels = models;
pi.registerProvider("antigravity", antgProviderConfig(models));
saveModelsCache(models, pid);
ctx.ui.setStatus("agy:refresh-models", "");
const lines = models.map((m) => ` ${m.id}${m.name}${m.reasoning ? " (thinking)" : ""}`).join("\n");
ctx.ui.notify(
`✅ Provider antigravity aggiornato: ${models.length} modelli (project ${pid})\n${lines}`,
`✅ Provider antigravity aggiornato: ${models.length} modelli (project ${pid})\n${lines}\n\n💾 Lista salvata in ${MODELS_CACHE_FILE} (persistente al riavvio).`,
"info",
);
} catch (e: any) {
@@ -2631,6 +2698,31 @@ export default function agyExtension(pi: ExtensionAPI) {
},
});
// =========================================================================
// Comando: /agy:models-reset — rimuove la cache persistente dei modelli e
// ripristina la lista statica di default.
// =========================================================================
pi.registerCommand("agy:models-reset", {
description:
"Rimuove la cache persistente dei modelli (~/.config/agy-pi/models-cache.json) e ri-registra il provider con la lista di default.",
handler: async (_args, ctx) => {
let removed = false;
try {
removed = fs.existsSync(MODELS_CACHE_FILE);
fs.rmSync(MODELS_CACHE_FILE, { force: true });
} catch (e: any) {
ctx.ui.notify(`Errore rimozione cache modelli: ${e.message}`, "error");
return;
}
antgRegisteredModels = ANTG_PROVIDER_MODELS;
pi.registerProvider("antigravity", antgProviderConfig(ANTG_PROVIDER_MODELS));
ctx.ui.notify(
`♻️ ${removed ? "Cache rimossa" : "Nessuna cache presente"}: provider antigravity ripristinato a ${ANTG_PROVIDER_MODELS.length} modelli di default.`,
"info",
);
},
});
// =========================================================================
// Comandi interattivi
// =========================================================================