diff --git a/studio/frontend/src/features/chat/api/chat-adapter.ts b/studio/frontend/src/features/chat/api/chat-adapter.ts index 61d71b641a..f87229d78f 100644 --- a/studio/frontend/src/features/chat/api/chat-adapter.ts +++ b/studio/frontend/src/features/chat/api/chat-adapter.ts @@ -437,6 +437,9 @@ function waitForModelReady(abortSignal?: AbortSignal): Promise { * without selecting one. Prefers GGUF (picks smallest cached variant), * falls back to smallest cached safetensors model. */ +// Cap cascade so broken cached repos can't spam /api/inference/load. +const MAX_AUTO_LOAD_ATTEMPTS = 3; + async function autoLoadSmallestModel(): Promise<{ loaded: boolean; blockedByTrustRemoteCode: boolean; @@ -451,6 +454,7 @@ async function autoLoadSmallestModel(): Promise<{ }); let blockedByTrustRemoteCode = false; let hadNonTrustFailure = false; + let loadAttempts = 0; async function canAutoLoad(payload: { model_path: string; @@ -481,6 +485,7 @@ async function autoLoadSmallestModel(): Promise<{ if (ggufRepos.length > 0) { const sorted = [...ggufRepos].sort((a, b) => a.size_bytes - b.size_bytes); for (const repo of sorted) { + if (loadAttempts >= MAX_AUTO_LOAD_ATTEMPTS) break; try { const variants = await listGgufVariants(repo.repo_id); const downloaded = variants.variants @@ -498,6 +503,7 @@ async function autoLoadSmallestModel(): Promise<{ ) { continue; } + loadAttempts += 1; const loadResp = await loadModel({ model_path: repo.repo_id, hf_token: hfToken, @@ -560,6 +566,7 @@ async function autoLoadSmallestModel(): Promise<{ if (modelRepos.length > 0) { const sorted = [...modelRepos].sort((a, b) => a.size_bytes - b.size_bytes); for (const repo of sorted) { + if (loadAttempts >= MAX_AUTO_LOAD_ATTEMPTS) break; try { if ( !(await canAutoLoad({ @@ -571,6 +578,7 @@ async function autoLoadSmallestModel(): Promise<{ ) { continue; } + loadAttempts += 1; const sfLoadResp = await loadModel({ model_path: repo.repo_id, hf_token: hfToken, @@ -616,6 +624,17 @@ async function autoLoadSmallestModel(): Promise<{ } } + // Cap also gates the default download so the total /api/inference/load + // budget across cached + fallback is MAX_AUTO_LOAD_ATTEMPTS, not +1. + if (loadAttempts >= MAX_AUTO_LOAD_ATTEMPTS) { + toast.dismiss(toastId); + return { + loaded: false, + blockedByTrustRemoteCode: + blockedByTrustRemoteCode && !hadNonTrustFailure, + }; + } + // No cached models found — try downloading a small default GGUF toast("Downloading a small model…", { id: toastId, @@ -634,6 +653,7 @@ async function autoLoadSmallestModel(): Promise<{ toast.dismiss(toastId); return { loaded: false, blockedByTrustRemoteCode }; } + loadAttempts += 1; const loadResp = await loadModel({ model_path: "unsloth/gemma-4-E2B-it-GGUF", hf_token: hfToken,