mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(coding-agent): name the llama.cpp server and cause when it cannot be reached (#11779)
When `/login llama.cpp` could not connect to the server URL, pi showed only `Error: Failed to save API key for llama.cpp: fetch failed`. That happens on a refused connection, a wrong port, or `https://` against a plain-HTTP server. Node's `fetch` keeps the real reason in `error.cause`, and pi dropped it, so the user had nothing to go on. `LlamaClient` now sends its requests, including the `/models/sse` watch, through a small `fetchServer` helper. When `fetch` fails at the network level, the helper throws `Could not reach llama.cpp server at <url>: <cause>` and keeps the original error as `cause`. Aborts and timeouts are rethrown unchanged. HTTP error responses are handled as before. The same message also improves `/llama` and the model catalog refresh, because they use the same client. **Testing** - Added a test in `test/llama-extension.test.ts` that calls `LlamaClient.list()` against a closed local port and expects the server URL and `connect ECONNREFUSED ...` in the error. It fails without the change (`got 'fetch failed'`) and passes with it. The whole file passes (16 tests). - `npm run check` passes. - Manual check, `/login llama.cpp` with `http://127.0.0.1:8099` (nothing listening): - Before: `Error: Failed to save API key for llama.cpp: fetch failed` - After: `Error: Failed to save API key for llama.cpp: Could not reach llama.cpp server at http://127.0.0.1:8099: connect ECONNREFUSED 127.0.0.1:8099` Fixes #11745 Requested by @badlogic Co-authored-by: triagent <triagent@localhost>
This commit is contained in:
parent
2af8a42f5f
commit
ea0f49a365
2 changed files with 34 additions and 2 deletions
|
|
@ -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<Response> {
|
||||
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<unknown> {
|
||||
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<void> {
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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<void>((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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue