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 f91947ca9..74020a4ec 100644 --- a/packages/pi-coding-agent/src/core/auth-storage.test.ts +++ b/packages/pi-coding-agent/src/core/auth-storage.test.ts @@ -263,6 +263,74 @@ describe("AuthStorage — areAllCredentialsBackedOff", () => { }); }); +// ─── mismatched oauth credential for non-OAuth provider (#2083) ─────────────── + +describe("AuthStorage — oauth credential for non-OAuth provider (#2083)", () => { + it("returns undefined when openrouter has type:oauth (no registered OAuth provider)", async () => { + // Simulates the bug: OpenRouter credential stored as type:"oauth" + // but OpenRouter is not a registered OAuth provider. + const storage = inMemory({ + openrouter: { + type: "oauth", + access_token: "sk-or-v1-fake", + refresh_token: "rt-fake", + expires: Date.now() + 3_600_000, + }, + }); + + // Before the fix, getApiKey returns undefined because + // resolveCredentialApiKey calls getOAuthProvider("openrouter") → null → undefined. + // The key in the oauth credential is never extracted. + const key = await storage.getApiKey("openrouter"); + // After the fix, the oauth credential with an unrecognised provider + // should be skipped, and getApiKey should fall through to env / fallback. + assert.equal(key, undefined); + }); + + it("falls through to env var when openrouter has type:oauth credential", async () => { + const storage = inMemory({ + openrouter: { + type: "oauth", + access_token: "sk-or-v1-fake", + refresh_token: "rt-fake", + expires: Date.now() + 3_600_000, + }, + }); + + // Simulate OPENROUTER_API_KEY being set via env + const origEnv = process.env.OPENROUTER_API_KEY; + try { + process.env.OPENROUTER_API_KEY = "sk-or-v1-env-key"; + const key = await storage.getApiKey("openrouter"); + assert.equal(key, "sk-or-v1-env-key"); + } finally { + if (origEnv === undefined) { + delete process.env.OPENROUTER_API_KEY; + } else { + process.env.OPENROUTER_API_KEY = origEnv; + } + } + }); + + it("falls through to fallback resolver when openrouter has type:oauth credential", async () => { + const storage = inMemory({ + openrouter: { + type: "oauth", + access_token: "sk-or-v1-fake", + refresh_token: "rt-fake", + expires: Date.now() + 3_600_000, + }, + }); + + storage.setFallbackResolver((provider) => + provider === "openrouter" ? "sk-or-v1-fallback" : undefined, + ); + + const key = await storage.getApiKey("openrouter"); + assert.equal(key, "sk-or-v1-fallback"); + }); +}); + // ─── getAll truncation ──────────────────────────────────────────────────────── describe("AuthStorage — getAll()", () => { diff --git a/packages/pi-coding-agent/src/core/auth-storage.ts b/packages/pi-coding-agent/src/core/auth-storage.ts index c632090a7..5ae286177 100644 --- a/packages/pi-coding-agent/src/core/auth-storage.ts +++ b/packages/pi-coding-agent/src/core/auth-storage.ts @@ -756,9 +756,12 @@ export class AuthStorage { if (credentials.length > 0) { const index = this.selectCredentialIndex(providerId, credentials, sessionId); if (index >= 0) { - return this.resolveCredentialApiKey(providerId, credentials[index]); + const resolved = await this.resolveCredentialApiKey(providerId, credentials[index]); + if (resolved) return resolved; + // Credential unresolvable (e.g. type:"oauth" for a non-OAuth provider) — + // fall through to env / fallback instead of returning undefined (#2083) } - // All credentials backed off - fall through to env/fallback + // All credentials backed off or unresolvable - fall through to env/fallback } // Fall back to environment variable