From 1ef6ba16f92ed2a55c9a292843a8fa3828f18e40 Mon Sep 17 00:00:00 2001 From: Nils Reeh Date: Mon, 13 Apr 2026 14:30:32 +0200 Subject: [PATCH] fix(pi-coding-agent): skip localhost dummy key when fallback resolver provides a configured key Custom OpenAI-compatible providers running on localhost (e.g. a local proxy) with an explicit apiKey in models.json received 'local-no-key-needed' during compaction instead of their configured key, causing 401 errors. The localhost shortcut in AuthStorage.getApiKey() was unconditional. Normal dispatch calls getApiKeyForProvider() which skips the baseUrl check entirely, so the fallback resolver was reached and the real key was used. Compaction calls getApiKey(model) which passes baseUrl, hitting the shortcut first. Closes #4106 --- .../src/core/auth-storage.test.ts | 38 +++++++++++++++++++ .../pi-coding-agent/src/core/auth-storage.ts | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/pi-coding-agent/src/core/auth-storage.test.ts b/packages/pi-coding-agent/src/core/auth-storage.test.ts index 819cc092b..646162f2b 100644 --- a/packages/pi-coding-agent/src/core/auth-storage.test.ts +++ b/packages/pi-coding-agent/src/core/auth-storage.test.ts @@ -531,3 +531,41 @@ describe("AuthStorage — getEarliestBackoffExpiry", () => { assert.equal(expiry, nearExpiry, "should return the nearest (smallest) expiry"); }); }); + +// ─── localhost baseUrl shortcut ──────────────────────────────────────────────── + +describe("AuthStorage — localhost baseUrl shortcut", () => { + it("returns 'local-no-key-needed' for localhost provider with no configured key", async () => { + const storage = inMemory({}); + const key = await storage.getApiKey("ollama", undefined, { baseUrl: "http://localhost:11434" }); + assert.equal(key, "local-no-key-needed"); + }); + + it("returns 'local-no-key-needed' for 127.0.0.1 provider with no configured key", async () => { + const storage = inMemory({}); + const key = await storage.getApiKey("custom", undefined, { baseUrl: "http://127.0.0.1:8080/v1" }); + assert.equal(key, "local-no-key-needed"); + }); + + it("returns configured key from fallback resolver for localhost custom provider (#4106)", async () => { + // Regression test: compaction called getApiKey(model) where model.baseUrl is localhost. + // The localhost shortcut must NOT override an explicitly configured apiKey from models.json. + const storage = inMemory({}); + storage.setFallbackResolver((provider) => + provider === "cliproxy" ? "sk-real-proxy-key" : undefined, + ); + + const key = await storage.getApiKey("cliproxy", undefined, { baseUrl: "http://localhost:8317/v1" }); + assert.equal(key, "sk-real-proxy-key"); + }); + + it("returns configured key from fallback resolver when baseUrl uses 127.0.0.1 (#4106)", async () => { + const storage = inMemory({}); + storage.setFallbackResolver((provider) => + provider === "myproxy" ? "sk-myproxy-key" : undefined, + ); + + const key = await storage.getApiKey("myproxy", undefined, { baseUrl: "http://127.0.0.1:9000/v1" }); + assert.equal(key, "sk-myproxy-key"); + }); +}); diff --git a/packages/pi-coding-agent/src/core/auth-storage.ts b/packages/pi-coding-agent/src/core/auth-storage.ts index 02e2f3103..c604fc801 100644 --- a/packages/pi-coding-agent/src/core/auth-storage.ts +++ b/packages/pi-coding-agent/src/core/auth-storage.ts @@ -819,7 +819,7 @@ export class AuthStorage { */ async getApiKey(providerId: string, sessionId?: string, options?: { baseUrl?: string }): Promise { // If the model has a local baseUrl, return a dummy key to avoid auth blocking - if (options?.baseUrl) { + if (options?.baseUrl && !this.fallbackResolver?.(providerId)) { try { const hostname = new URL(options.baseUrl).hostname; if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0" || hostname === "::1") {