diff --git a/packages/coding-agent/src/extensions/llama/client.ts b/packages/coding-agent/src/extensions/llama/client.ts index 3233295a8..67be757d8 100644 --- a/packages/coding-agent/src/extensions/llama/client.ts +++ b/packages/coding-agent/src/extensions/llama/client.ts @@ -158,6 +158,13 @@ export function llamaInferenceUrl(serverUrl: string): string { return `${normalizeLlamaServerUrl(serverUrl)}/v1`; } +function connectionErrorDetail(error: unknown): string { + const cause = error instanceof Error ? error.cause : undefined; + if (cause instanceof Error && cause.message) return cause.message; + if (cause && typeof cause === "object" && "code" in cause && typeof cause.code === "string") return cause.code; + return error instanceof Error ? error.message : String(error); +} + export class LlamaClient { readonly serverUrl: string; private readonly apiKey: string | undefined; @@ -167,13 +174,25 @@ export class LlamaClient { this.apiKey = apiKey; } + /** Fetches from the server, naming the server and the network-level cause when the connection fails. */ + private async fetchServer(path: string, init: RequestInit): Promise { + try { + return await fetch(`${this.serverUrl}${path}`, init); + } catch (error) { + if (init.signal?.aborted) throw error; + throw new Error(`Could not reach llama.cpp server at ${this.serverUrl}: ${connectionErrorDetail(error)}`, { + cause: error, + }); + } + } + private async request(path: string, init: RequestInit = {}): Promise { const headers = new Headers(init.headers); if (init.body !== undefined) headers.set("Content-Type", "application/json"); if (this.apiKey) headers.set("Authorization", `Bearer ${this.apiKey}`); const timeout = AbortSignal.timeout(15_000); const signal = init.signal ? AbortSignal.any([init.signal, timeout]) : timeout; - const response = await fetch(`${this.serverUrl}${path}`, { ...init, headers, signal }); + const response = await this.fetchServer(path, { ...init, headers, signal }); let payload: unknown; try { payload = await response.json(); @@ -229,7 +248,7 @@ export class LlamaClient { async watch(onEvent: (event: LlamaModelEvent) => void, signal?: AbortSignal): Promise { const headers = new Headers(); if (this.apiKey) headers.set("Authorization", `Bearer ${this.apiKey}`); - const response = await fetch(`${this.serverUrl}/models/sse`, { headers, signal }); + const response = await this.fetchServer("/models/sse", { headers, signal }); if (!response.ok || !response.body) throw new Error(`llama.cpp SSE returned HTTP ${response.status}`); const reader = response.body.getReader(); const decoder = new TextDecoder(); diff --git a/packages/coding-agent/test/llama-extension.test.ts b/packages/coding-agent/test/llama-extension.test.ts index f211d6210..ce7faa453 100644 --- a/packages/coding-agent/test/llama-extension.test.ts +++ b/packages/coding-agent/test/llama-extension.test.ts @@ -59,6 +59,19 @@ describe("llama.cpp extension", () => { expect(() => normalizeLlamaServerUrl("file:///tmp/llama")).toThrow("http or https"); }); + // #11745: a refused connection showed only "fetch failed" + it("names the server and the cause when it cannot be reached", async () => { + const server = createServer(); + server.listen(0, "127.0.0.1"); + await once(server, "listening"); + const port = (server.address() as AddressInfo).port; + await new Promise((resolve) => server.close(() => resolve())); + + await expect(new LlamaClient(`http://127.0.0.1:${port}`).list()).rejects.toThrow( + `Could not reach llama.cpp server at http://127.0.0.1:${port}: connect ECONNREFUSED 127.0.0.1:${port}`, + ); + }); + it("exposes loaded and sleeping models with router metadata", () => { const controller = createLlamaProvider(); controller.setCatalog(