Studio: bump llama-server prefill timeout to 300s + warm RAG embedder when RAG turns on

This commit is contained in:
Roland Tannous 2026-05-26 21:24:31 +04:00
commit 38e22c15d7
4 changed files with 57 additions and 2 deletions

View file

@ -4203,9 +4203,12 @@ class LlamaCppBackend:
# without triggering a retry storm. Cancel during both
# prefill and streaming is handled by the watcher thread
# which closes the response, unblocking any httpx read.
# 300 s headroom for large models (30B+) re-prefilling after
# a tool call that returned a long result (e.g. RAG chunks
# with images) — prior 120 s was tripping on Gemma-4-31B.
prefill_timeout = httpx.Timeout(
connect = 30,
read = 120.0,
read = 300.0,
write = 10,
pool = 10,
)

View file

@ -435,6 +435,32 @@ def get_rag_defaults(
return _load_rag_defaults()
@router.post("/warmup")
def warmup_rag_embedder(
current_subject: str = Depends(get_current_subject),
) -> dict:
"""Preload the configured default embedder so the first retrieval is warm.
Called from the frontend when the user enables the RAG pill moves the
cold-load latency (Qwen3-VL-Embedding-2B is ~4 GB) out of the first
chat-completion path, where a 30s+ load can race the llama-server
prefill timeout.
"""
from utils.rag.config import resolve_embedder
defaults = _load_rag_defaults()
model_name = defaults.embedding_model or resolve_embedder(
defaults.mode,
defaults.chunking_strategy,
)
try:
embeddings.get_embedder(model_name)
except Exception as exc: # noqa: BLE001
logger.warning("RAG warmup failed for %s: %s", model_name, exc)
return {"ok": False, "model": model_name, "error": str(exc)}
return {"ok": True, "model": model_name}
@router.put("/defaults", response_model = RagDefaults)
def set_rag_defaults(
payload: UpdateRagDefaultsRequest,

View file

@ -657,6 +657,17 @@ export const useChatRuntimeStore = create<ChatRuntimeStore>((set, get) => ({
nextState.ragToolEnabled = true;
saveBool(CHAT_RAG_TOOL_ENABLED_KEY, true);
}
// After hydration, if RAG is on (persisted or just migrated),
// warm the embedder so the first message doesn't pay the
// cold load inline.
if (
nextState.ragToolEnabled === true ||
(nextState.ragToolEnabled === undefined && state.ragToolEnabled)
) {
void import("@/features/rag/api/rag-api")
.then((m) => m.warmupRagEmbedder())
.catch(() => {});
}
return nextState;
});
} catch {
@ -869,8 +880,16 @@ export const useChatRuntimeStore = create<ChatRuntimeStore>((set, get) => ({
return { toolsEnabled };
}),
setRagToolEnabled: (ragToolEnabled) =>
set(() => {
set((state) => {
saveBool(CHAT_RAG_TOOL_ENABLED_KEY, ragToolEnabled);
// Warmup on off→on transitions: kick the backend to preload the
// embedder so the user's first RAG-using message doesn't pay the
// cold-start (~30s for Qwen3-VL-Embedding-2B) inline. Fire-and-forget.
if (ragToolEnabled && !state.ragToolEnabled) {
void import("@/features/rag/api/rag-api")
.then((m) => m.warmupRagEmbedder())
.catch(() => {});
}
return { ragToolEnabled };
}),
setCodeToolsEnabled: (codeToolsEnabled) =>

View file

@ -304,6 +304,13 @@ export async function setRagDefaults(
return parseJsonOrThrow<RagDefaults>(response);
}
/** Preload the configured embedder on the backend. Long-running (cold
* load can take 30s+). Fire-and-forget: failure is non-fatal because
* the first real query will lazy-load again. */
export async function warmupRagEmbedder(): Promise<void> {
await authFetch("/api/rag/warmup", { method: "POST" });
}
// --- Search ---
export async function search(req: SearchRequest): Promise<SearchHit[]> {