From 6f0395ec79f9eefe51575486279f44e595aeee2b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 26 Aug 2026 07:05:40 -0700 Subject: [PATCH] fix(llama-cpp): skip failed models during automatic setup (#130136) --- .../src/external-server/setup.test.ts | 22 +++++ .../llama-cpp/src/external-server/setup.ts | 89 +++++++------------ 2 files changed, 54 insertions(+), 57 deletions(-) diff --git a/extensions/llama-cpp/src/external-server/setup.test.ts b/extensions/llama-cpp/src/external-server/setup.test.ts index eeba372f24bd..ce07291f42b2 100644 --- a/extensions/llama-cpp/src/external-server/setup.test.ts +++ b/extensions/llama-cpp/src/external-server/setup.test.ts @@ -247,6 +247,11 @@ describe("llama-server setup", () => { ], expected: "llama-cpp/meta-llama/Llama-3.3-8B", }, + { + name: "does not recommend a server when every model has failed", + models: [{ id: "google/gemma-4-27b", status: "unloaded", failed: true }], + expected: null, + }, { name: "returns no candidate for an empty model catalog", models: [], @@ -851,4 +856,21 @@ describe("llama-server setup", () => { ); expect(ctx.runtime.exit).toHaveBeenCalledWith(1); }); + + it("rejects failed-only implicit setup while preserving an explicitly selected model", async () => { + const discovery = successfulDiscovery(); + discovery.models = discovery.models.map((model) => ({ ...model, failed: true })); + discoverMock.mockResolvedValue(discovery); + + const implicit = nonInteractiveContext(); + await expect(validateLlamaServerNonInteractive(implicit)).resolves.toBe(false); + expect(implicit.runtime.error).toHaveBeenCalledWith( + "No llama-server text models were found at http://localhost:8080.", + ); + expect(removeProviderAuthProfilesWithLockMock).not.toHaveBeenCalled(); + expect(upsertAuthProfileWithLockMock).not.toHaveBeenCalled(); + + const explicit = nonInteractiveContext({ customModelId: "qwen/model:Q4_K_M" }); + await expect(validateLlamaServerNonInteractive(explicit)).resolves.toBe(true); + }); }); diff --git a/extensions/llama-cpp/src/external-server/setup.ts b/extensions/llama-cpp/src/external-server/setup.ts index 2b7c0d2b9d58..81c03973f28b 100644 --- a/extensions/llama-cpp/src/external-server/setup.ts +++ b/extensions/llama-cpp/src/external-server/setup.ts @@ -38,8 +38,7 @@ import { resolveLlamaServerEndpoint } from "./endpoint.js"; import { buildLlamaServerProviderConfig } from "./models.js"; function selectSetupModelId(discovery: Extract) { - const healthy = discovery.models.filter((model) => !model.failed); - const candidates = healthy.length > 0 ? healthy : discovery.models; + const candidates = discovery.models.filter((model) => !model.failed); const ready = candidates.filter( (model) => model.status === "loaded" || model.status === "sleeping", ); @@ -224,28 +223,33 @@ async function removeDefaultAuthProfile(agentDir?: string): Promise { } } -async function discoverForSetup(params: { - config: OpenClawConfig; - baseUrl: string; - env?: NodeJS.ProcessEnv; - signal?: AbortSignal; -}): Promise { - const providerConfig = params.config.models?.providers?.[LLAMA_CPP_PROVIDER_ID]; - const headers = await resolveLlamaServerProviderHeaders({ - config: params.config, - env: params.env, - headers: providerConfig?.headers, - }); - const resolvedApiKey = !hasLlamaServerAuthorizationHeader(headers) - ? await resolveLlamaServerRuntimeApiKey({ config: params.config }) - : undefined; - return await discoverLlamaServer({ - baseUrl: params.baseUrl, - apiKey: resolvedApiKey, - headers, - signal: params.signal, - cacheTtlMs: 0, - }); +async function discoverForSetup( + ctx: ProviderAppGuidedSetupContext, +): Promise | null> { + const provider = ctx.config.models?.providers?.[LLAMA_CPP_PROVIDER_ID]; + if (provider?.localService) { + return null; + } + try { + const headers = await resolveLlamaServerProviderHeaders({ + config: ctx.config, + env: ctx.env, + headers: provider?.headers, + }); + const apiKey = !hasLlamaServerAuthorizationHeader(headers) + ? await resolveLlamaServerRuntimeApiKey({ config: ctx.config }) + : undefined; + const discovery = await discoverLlamaServer({ + baseUrl: provider?.baseUrl ?? LLAMA_SERVER_DEFAULT_ORIGIN, + apiKey, + headers, + signal: ctx.signal, + cacheTtlMs: 0, + }); + return discovery.kind === "success" ? discovery : null; + } catch { + return null; + } } async function discoverWithAccess(params: { @@ -267,23 +271,8 @@ async function discoverWithAccess(params: { export async function detectLlamaServerSetup( ctx: ProviderAppGuidedSetupContext, ): Promise<{ modelRef: string; detail?: string } | null> { - const provider = ctx.config.models?.providers?.[LLAMA_CPP_PROVIDER_ID]; - if (provider?.localService) { - return null; - } - const baseUrl = provider?.baseUrl ?? LLAMA_SERVER_DEFAULT_ORIGIN; - let discovery: LlamaServerDiscoveryResult; - try { - discovery = await discoverForSetup({ - config: ctx.config, - baseUrl, - env: ctx.env, - signal: ctx.signal, - }); - } catch { - return null; - } - if (discovery.kind !== "success") { + const discovery = await discoverForSetup(ctx); + if (!discovery) { return null; } const modelId = selectSetupModelId(discovery); @@ -300,22 +289,8 @@ export async function detectLlamaServerSetup( export async function prepareLlamaServerSetup( ctx: ProviderAppGuidedSetupContext & { modelRef: string }, ): Promise { - const provider = ctx.config.models?.providers?.[LLAMA_CPP_PROVIDER_ID]; - if (provider?.localService) { - return null; - } - let discovery: LlamaServerDiscoveryResult; - try { - discovery = await discoverForSetup({ - config: ctx.config, - baseUrl: provider?.baseUrl ?? LLAMA_SERVER_DEFAULT_ORIGIN, - env: ctx.env, - signal: ctx.signal, - }); - } catch { - return null; - } - if (discovery.kind !== "success") { + const discovery = await discoverForSetup(ctx); + if (!discovery) { return null; } const prefix = `${LLAMA_CPP_PROVIDER_ID}/`;