From 49acad863dff8baac453fdc44960155dbd6430e2 Mon Sep 17 00:00:00 2001 From: lsmir2 Date: Wed, 17 Jun 2026 04:28:41 +0800 Subject: [PATCH 1/4] feat(alibaba-coding-plan): add endpoint selection for China/International/Custom Allow users to select the API endpoint during Alibaba Coding Plan login: - Option 1: International endpoint (coding-intl.dashscope.aliyuncs.com) [default] - Option 2: China endpoint (coding.dashscope.aliyuncs.com) - Option 3: Custom URL This fixes the issue where users with China-region API keys could not authenticate because the validation was hardcoded to use the international endpoint. Closes #317 --- .../ai/src/registry/alibaba-coding-plan.ts | 70 ++++++- .../test/alibaba-endpoint-selection.test.ts | 192 ++++++++++++++++++ 2 files changed, 252 insertions(+), 10 deletions(-) create mode 100644 packages/ai/test/alibaba-endpoint-selection.test.ts diff --git a/packages/ai/src/registry/alibaba-coding-plan.ts b/packages/ai/src/registry/alibaba-coding-plan.ts index f23f5b54e..2d728bb57 100644 --- a/packages/ai/src/registry/alibaba-coding-plan.ts +++ b/packages/ai/src/registry/alibaba-coding-plan.ts @@ -1,19 +1,63 @@ -import { validateOpenAICompatibleApiKey } from "./api-key-validation"; -import type { OAuthController, OAuthLoginCallbacks } from "./oauth/types"; +import * as apiKeyValidation from "./api-key-validation"; +import type { OAuthController, OAuthCredentials, OAuthLoginCallbacks } from "./oauth/types"; import type { ProviderDefinition } from "./types"; -const AUTH_URL = "https://modelstudio.console.alibabacloud.com/"; -const API_BASE_URL = "https://coding-intl.dashscope.aliyuncs.com/v1"; +const DEFAULT_AUTH_URL = "https://modelstudio.console.alibabacloud.com/"; +const CHINA_AUTH_URL = "https://dashscope.console.aliyun.com/"; +const DEFAULT_API_BASE_URL = "https://coding-intl.dashscope.aliyuncs.com/v1"; +const CHINA_API_BASE_URL = "https://coding.dashscope.aliyuncs.com/v1"; const VALIDATION_MODEL = "qwen3.5-plus"; -export async function loginAlibabaCodingPlan(options: OAuthController): Promise { +export async function loginAlibabaCodingPlan(options: OAuthController): Promise { if (!options.onPrompt) { throw new Error("Alibaba Coding Plan login requires onPrompt callback"); } + // Ask which endpoint to use + const endpointChoice = await options.onPrompt({ + message: `Select Alibaba Coding Plan endpoint: +1. International (coding-intl.dashscope.aliyuncs.com) [default] +2. China mainland (coding.dashscope.aliyuncs.com) +3. Custom URL + +Enter 1, 2, or 3`, + placeholder: "1", + }); + + // Check for abort after endpoint selection (Escape returns "") + if (options.signal?.aborted) { + throw new Error("Login cancelled"); + } + + const choice = endpointChoice.trim(); + let baseUrl: string; + let authUrl: string; + let instructions: string; + if (choice === "2") { + baseUrl = CHINA_API_BASE_URL; + authUrl = CHINA_AUTH_URL; + instructions = "Copy your API key from the Alibaba Cloud DashScope console (China mainland)"; + } else if (choice === "3") { + const customUrl = await options.onPrompt({ + message: "Enter custom base URL", + placeholder: "https://your-proxy.com/v1", + }); + const trimmedUrl = customUrl.trim().replace(/\/+$/, ""); + if (!trimmedUrl) { + throw new Error("Custom URL is required for option 3"); + } + baseUrl = trimmedUrl; + authUrl = DEFAULT_AUTH_URL; + instructions = "Copy your API key from the Alibaba Cloud DashScope console"; + } else { + baseUrl = DEFAULT_API_BASE_URL; + authUrl = DEFAULT_AUTH_URL; + instructions = "Copy your API key from the Alibaba Cloud DashScope console (International)"; + } + options.onAuth?.({ - url: AUTH_URL, - instructions: "Copy your API key from the Alibaba Cloud DashScope console", + url: authUrl, + instructions, }); const apiKey = await options.onPrompt({ @@ -31,19 +75,25 @@ export async function loginAlibabaCodingPlan(options: OAuthController): Promise< } options.onProgress?.("Validating API key..."); - await validateOpenAICompatibleApiKey({ + await apiKeyValidation.validateOpenAICompatibleApiKey({ provider: "Alibaba Coding Plan", apiKey: trimmed, - baseUrl: API_BASE_URL, + baseUrl, model: VALIDATION_MODEL, signal: options.signal, }); - return trimmed; + return { + access: trimmed, + refresh: trimmed, + expires: 0, + enterpriseUrl: baseUrl, + }; } export const alibabaCodingPlanProvider = { id: "alibaba-coding-plan", name: "Alibaba Coding Plan", login: (cb: OAuthLoginCallbacks) => loginAlibabaCodingPlan(cb), + getApiKey: (credentials) => credentials.access, } as const satisfies ProviderDefinition; diff --git a/packages/ai/test/alibaba-endpoint-selection.test.ts b/packages/ai/test/alibaba-endpoint-selection.test.ts new file mode 100644 index 000000000..b6b59912c --- /dev/null +++ b/packages/ai/test/alibaba-endpoint-selection.test.ts @@ -0,0 +1,192 @@ +import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test"; +import type { OAuthController } from "../src/registry/oauth/types"; +import * as apiKeyValidation from "../src/registry/api-key-validation"; + +describe("alibaba-coding-plan endpoint selection", () => { + let validateSpy: ReturnType; + let alibabaModule: typeof import("../src/registry/alibaba-coding-plan"); + + beforeEach(async () => { + // Import the module + alibabaModule = await import("../src/registry/alibaba-coding-plan"); + + // Spy on the validate function from the namespace + validateSpy = spyOn(apiKeyValidation, "validateOpenAICompatibleApiKey").mockResolvedValue(undefined); + }); + + afterEach(() => { + validateSpy.mockRestore(); + }); + + it("option 1 uses international endpoint and auth URL", async () => { + let capturedAuth: { url: string; instructions?: string } | undefined; + const options: OAuthController = { + onAuth: (info) => { capturedAuth = info; }, + onProgress: () => {}, + onPrompt: async (prompt) => { + if (prompt.message.includes("Select Alibaba")) return "1"; + if (prompt.message.includes("Paste your")) return "sk-test-key"; + return ""; + }, + }; + + const result = await alibabaModule.loginAlibabaCodingPlan(options); + + expect(result.access).toBe("sk-test-key"); + expect(result.refresh).toBe("sk-test-key"); + expect(result.enterpriseUrl).toBe("https://coding-intl.dashscope.aliyuncs.com/v1"); + expect(capturedAuth?.url).toBe("https://modelstudio.console.alibabacloud.com/"); + expect(capturedAuth?.instructions).toContain("International"); + + expect(validateSpy).toHaveBeenCalledWith({ + provider: "Alibaba Coding Plan", + apiKey: "sk-test-key", + baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1", + model: "qwen3.5-plus", + signal: undefined, + }); + }); + + it("option 2 uses China endpoint and auth URL", async () => { + let capturedAuth: { url: string; instructions?: string } | undefined; + const options: OAuthController = { + onAuth: (info) => { capturedAuth = info; }, + onProgress: () => {}, + onPrompt: async (prompt) => { + if (prompt.message.includes("Select Alibaba")) return "2"; + if (prompt.message.includes("Paste your")) return "sk-cn-key"; + return ""; + }, + }; + + const result = await alibabaModule.loginAlibabaCodingPlan(options); + + expect(result.access).toBe("sk-cn-key"); + expect(result.refresh).toBe("sk-cn-key"); + expect(result.enterpriseUrl).toBe("https://coding.dashscope.aliyuncs.com/v1"); + expect(capturedAuth?.url).toBe("https://dashscope.console.aliyun.com/"); + expect(capturedAuth?.instructions).toContain("China mainland"); + + expect(validateSpy).toHaveBeenCalledWith({ + provider: "Alibaba Coding Plan", + apiKey: "sk-cn-key", + baseUrl: "https://coding.dashscope.aliyuncs.com/v1", + model: "qwen3.5-plus", + signal: undefined, + }); + }); + + it("option 3 prompts for custom URL and uses it", async () => { + let capturedAuth: { url: string; instructions?: string } | undefined; + const options: OAuthController = { + onAuth: (info) => { capturedAuth = info; }, + onProgress: () => {}, + onPrompt: async (prompt) => { + if (prompt.message.includes("Select Alibaba")) return "3"; + if (prompt.message.includes("custom base URL")) return "https://my-proxy.com/v1"; + if (prompt.message.includes("Paste your")) return "sk-custom-key"; + return ""; + }, + }; + + const result = await alibabaModule.loginAlibabaCodingPlan(options); + + expect(result.access).toBe("sk-custom-key"); + expect(result.refresh).toBe("sk-custom-key"); + expect(result.enterpriseUrl).toBe("https://my-proxy.com/v1"); + expect(capturedAuth?.url).toBe("https://modelstudio.console.alibabacloud.com/"); + + expect(validateSpy).toHaveBeenCalledWith({ + provider: "Alibaba Coding Plan", + apiKey: "sk-custom-key", + baseUrl: "https://my-proxy.com/v1", + model: "qwen3.5-plus", + signal: undefined, + }); + }); + + it("empty input defaults to international endpoint and auth URL", async () => { + const options: OAuthController = { + onAuth: () => {}, + onProgress: () => {}, + onPrompt: async (prompt) => { + if (prompt.message.includes("Select Alibaba")) return ""; + if (prompt.message.includes("Paste your")) return "sk-test-key"; + return ""; + }, + }; + + const result = await alibabaModule.loginAlibabaCodingPlan(options); + + expect(result.enterpriseUrl).toBe("https://coding-intl.dashscope.aliyuncs.com/v1"); + }); + + it("strips trailing slashes from custom URL", async () => { + const options: OAuthController = { + onAuth: () => {}, + onProgress: () => {}, + onPrompt: async (prompt) => { + if (prompt.message.includes("Select Alibaba")) return "3"; + if (prompt.message.includes("custom base URL")) return "https://my-proxy.com/v1///"; + if (prompt.message.includes("Paste your")) return "sk-test-key"; + return ""; + }, + }; + + const result = await alibabaModule.loginAlibabaCodingPlan(options); + + expect(result.enterpriseUrl).toBe("https://my-proxy.com/v1"); + }); + + it("throws error when custom URL is empty", async () => { + const options: OAuthController = { + onAuth: () => {}, + onProgress: () => {}, + onPrompt: async (prompt) => { + if (prompt.message.includes("Select Alibaba")) return "3"; + if (prompt.message.includes("custom base URL")) return ""; + return ""; + }, + }; + + await expect(alibabaModule.loginAlibabaCodingPlan(options)).rejects.toThrow( + "Custom URL is required for option 3" + ); + }); + + it("throws error when API key is empty", async () => { + const options: OAuthController = { + onAuth: () => {}, + onProgress: () => {}, + onPrompt: async (prompt) => { + if (prompt.message.includes("Select Alibaba")) return "1"; + if (prompt.message.includes("Paste your")) return ""; + return ""; + }, + }; + + await expect(alibabaModule.loginAlibabaCodingPlan(options)).rejects.toThrow( + "API key is required" + ); + }); + + it("checks abort signal after endpoint selection", async () => { + const controller = new AbortController(); + const options: OAuthController = { + onAuth: () => {}, + onProgress: () => {}, + onPrompt: async (prompt) => { + if (prompt.message.includes("Select Alibaba")) { + controller.abort(); + return ""; + } + return ""; + }, + signal: controller.signal, + }; + + await expect(alibabaModule.loginAlibabaCodingPlan(options)).rejects.toThrow( + "Login cancelled" + ); + }); +}); From 4513980fce70b8a42d53e369f8a13cd849e3e3ef Mon Sep 17 00:00:00 2001 From: lsmir2 Date: Wed, 17 Jun 2026 05:44:32 +0800 Subject: [PATCH 2/4] fix(alibaba-coding-plan): use stored enterpriseUrl for API requests - Add alibaba-coding-plan to needsStructuredApiKey in getOAuthApiKey - Parse JSON apiKey in createRequestSetup to extract token and baseUrl - Add tests for JSON apiKey generation and parsing - Matches github-copilot pattern for enterprise URL handling --- .../ai/src/providers/openai-completions.ts | 13 +++ packages/ai/src/registry/oauth/index.ts | 21 +++-- .../test/alibaba-endpoint-selection.test.ts | 82 +++++++++++++++---- 3 files changed, 91 insertions(+), 25 deletions(-) diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index ee4e02fb2..eb1f70666 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -1281,6 +1281,19 @@ async function createRequestSetup( copilotPremiumRequests = copilot.premiumRequests; baseUrl = resolveGitHubCopilotBaseUrl(model.baseUrl, rawApiKey) ?? model.baseUrl; } + if (model.provider === "alibaba-coding-plan") { + try { + const parsed = JSON.parse(rawApiKey); + if (typeof parsed?.token === "string") { + apiKey = parsed.token; + } + if (typeof parsed?.enterpriseUrl === "string") { + baseUrl = parsed.enterpriseUrl; + } + } catch { + // Not JSON — use raw apiKey and catalog baseUrl + } + } // Azure OpenAI requires /deployments/{id}/chat/completions?api-version=YYYY-MM-DD. // The generic openai-completions path adds neither, producing silent 404s. let azureDefaultQuery: Record | undefined; diff --git a/packages/ai/src/registry/oauth/index.ts b/packages/ai/src/registry/oauth/index.ts index a5d2e581d..82cb38f66 100644 --- a/packages/ai/src/registry/oauth/index.ts +++ b/packages/ai/src/registry/oauth/index.ts @@ -136,17 +136,20 @@ export async function getOAuthApiKey( } // For providers that need request-time credential metadata, return JSON. const needsStructuredApiKey = - provider === "github-copilot" || provider === "google-gemini-cli" || provider === "google-antigravity"; + provider === "github-copilot" || + provider === "google-gemini-cli" || + provider === "google-antigravity" || + provider === "alibaba-coding-plan"; const apiKey = needsStructuredApiKey ? JSON.stringify({ - token: creds.access, - enterpriseUrl: creds.enterpriseUrl, - projectId: creds.projectId, - refreshToken: creds.refresh, - expiresAt: creds.expires, - email: creds.email, - accountId: creds.accountId, - }) + token: creds.access, + enterpriseUrl: creds.enterpriseUrl, + projectId: creds.projectId, + refreshToken: creds.refresh, + expiresAt: creds.expires, + email: creds.email, + accountId: creds.accountId, + }) : creds.access; return { newCredentials: creds, apiKey }; } diff --git a/packages/ai/test/alibaba-endpoint-selection.test.ts b/packages/ai/test/alibaba-endpoint-selection.test.ts index b6b59912c..f4edd2618 100644 --- a/packages/ai/test/alibaba-endpoint-selection.test.ts +++ b/packages/ai/test/alibaba-endpoint-selection.test.ts @@ -1,16 +1,14 @@ import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test"; -import type { OAuthController } from "../src/registry/oauth/types"; +import type { OAuthController, OAuthCredentials } from "../src/registry/oauth/types"; import * as apiKeyValidation from "../src/registry/api-key-validation"; +import { loginAlibabaCodingPlan, alibabaCodingPlanProvider } from "../src/registry/alibaba-coding-plan"; +import { getOAuthApiKey } from "../src/registry/oauth/index"; +import type { Mock } from "bun:test"; describe("alibaba-coding-plan endpoint selection", () => { - let validateSpy: ReturnType; - let alibabaModule: typeof import("../src/registry/alibaba-coding-plan"); + let validateSpy: Mock, ReturnType>; - beforeEach(async () => { - // Import the module - alibabaModule = await import("../src/registry/alibaba-coding-plan"); - - // Spy on the validate function from the namespace + beforeEach(() => { validateSpy = spyOn(apiKeyValidation, "validateOpenAICompatibleApiKey").mockResolvedValue(undefined); }); @@ -30,7 +28,7 @@ describe("alibaba-coding-plan endpoint selection", () => { }, }; - const result = await alibabaModule.loginAlibabaCodingPlan(options); + const result = await loginAlibabaCodingPlan(options); expect(result.access).toBe("sk-test-key"); expect(result.refresh).toBe("sk-test-key"); @@ -59,7 +57,7 @@ describe("alibaba-coding-plan endpoint selection", () => { }, }; - const result = await alibabaModule.loginAlibabaCodingPlan(options); + const result = await loginAlibabaCodingPlan(options); expect(result.access).toBe("sk-cn-key"); expect(result.refresh).toBe("sk-cn-key"); @@ -89,7 +87,7 @@ describe("alibaba-coding-plan endpoint selection", () => { }, }; - const result = await alibabaModule.loginAlibabaCodingPlan(options); + const result = await loginAlibabaCodingPlan(options); expect(result.access).toBe("sk-custom-key"); expect(result.refresh).toBe("sk-custom-key"); @@ -116,7 +114,7 @@ describe("alibaba-coding-plan endpoint selection", () => { }, }; - const result = await alibabaModule.loginAlibabaCodingPlan(options); + const result = await loginAlibabaCodingPlan(options); expect(result.enterpriseUrl).toBe("https://coding-intl.dashscope.aliyuncs.com/v1"); }); @@ -133,7 +131,7 @@ describe("alibaba-coding-plan endpoint selection", () => { }, }; - const result = await alibabaModule.loginAlibabaCodingPlan(options); + const result = await loginAlibabaCodingPlan(options); expect(result.enterpriseUrl).toBe("https://my-proxy.com/v1"); }); @@ -149,7 +147,7 @@ describe("alibaba-coding-plan endpoint selection", () => { }, }; - await expect(alibabaModule.loginAlibabaCodingPlan(options)).rejects.toThrow( + await expect(loginAlibabaCodingPlan(options)).rejects.toThrow( "Custom URL is required for option 3" ); }); @@ -165,7 +163,7 @@ describe("alibaba-coding-plan endpoint selection", () => { }, }; - await expect(alibabaModule.loginAlibabaCodingPlan(options)).rejects.toThrow( + await expect(loginAlibabaCodingPlan(options)).rejects.toThrow( "API key is required" ); }); @@ -185,8 +183,60 @@ describe("alibaba-coding-plan endpoint selection", () => { signal: controller.signal, }; - await expect(alibabaModule.loginAlibabaCodingPlan(options)).rejects.toThrow( + await expect(loginAlibabaCodingPlan(options)).rejects.toThrow( "Login cancelled" ); }); }); + +describe("alibaba-coding-plan JSON apiKey", () => { + it("getOAuthApiKey returns JSON with token and enterpriseUrl", async () => { + const credentials = { + "alibaba-coding-plan": { + access: "sk-test-key", + refresh: "refresh-token", + expires: Date.now() + 3600000, + enterpriseUrl: "https://coding.dashscope.aliyuncs.com/v1", + }, + }; + const result = await getOAuthApiKey("alibaba-coding-plan", credentials); + expect(result).not.toBeNull(); + const parsed = JSON.parse(result!.apiKey); + expect(parsed.token).toBe("sk-test-key"); + expect(parsed.enterpriseUrl).toBe("https://coding.dashscope.aliyuncs.com/v1"); + }); + + it("JSON apiKey parsing extracts token for Bearer header", () => { + const rawApiKey = JSON.stringify({ + token: "sk-bearer-token", + enterpriseUrl: "https://custom.endpoint.com/v1", + }); + const parsed = JSON.parse(rawApiKey); + const apiKey = typeof parsed?.token === "string" ? parsed.token : rawApiKey; + expect(apiKey).toBe("sk-bearer-token"); + }); + + it("JSON apiKey parsing extracts enterpriseUrl for baseUrl", () => { + const rawApiKey = JSON.stringify({ + token: "sk-test", + enterpriseUrl: "https://china.dashscope.aliyuncs.com/v1", + }); + const parsed = JSON.parse(rawApiKey); + const baseUrl = typeof parsed?.enterpriseUrl === "string" ? parsed.enterpriseUrl : undefined; + expect(baseUrl).toBe("https://china.dashscope.aliyuncs.com/v1"); + }); + + it("non-JSON apiKey falls back to raw value", () => { + const rawApiKey = "sk-plain-key"; + let apiKey = rawApiKey; + try { + const parsed = JSON.parse(rawApiKey); + if (typeof parsed?.token === "string") { + apiKey = parsed.token; + } + } catch { + // Not JSON — use raw apiKey + } + expect(apiKey).toBe("sk-plain-key"); + }); +}); From cc210c374c354651db2a7b117a2abde49c857a62 Mon Sep 17 00:00:00 2001 From: lsmir2 Date: Wed, 17 Jun 2026 06:27:49 +0800 Subject: [PATCH 3/4] fix(alibaba-coding-plan): address Codex review feedback - Set expires to Number.MAX_SAFE_INTEGER to prevent OAuth credential expiry check failure - Replace banned ReturnType<> with Mock pattern in tests - Flatten endpoint selection prompt to single line to avoid TUI row accounting issues Fixes review comments on PR #2802 --- packages/ai/src/registry/alibaba-coding-plan.ts | 9 ++------- packages/ai/test/alibaba-endpoint-selection.test.ts | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/ai/src/registry/alibaba-coding-plan.ts b/packages/ai/src/registry/alibaba-coding-plan.ts index 2d728bb57..92937d828 100644 --- a/packages/ai/src/registry/alibaba-coding-plan.ts +++ b/packages/ai/src/registry/alibaba-coding-plan.ts @@ -15,12 +15,7 @@ export async function loginAlibabaCodingPlan(options: OAuthController): Promise< // Ask which endpoint to use const endpointChoice = await options.onPrompt({ - message: `Select Alibaba Coding Plan endpoint: -1. International (coding-intl.dashscope.aliyuncs.com) [default] -2. China mainland (coding.dashscope.aliyuncs.com) -3. Custom URL - -Enter 1, 2, or 3`, + message: "Select Alibaba Coding Plan endpoint: 1=International (default), 2=China, 3=Custom — enter 1, 2, or 3", placeholder: "1", }); @@ -86,7 +81,7 @@ Enter 1, 2, or 3`, return { access: trimmed, refresh: trimmed, - expires: 0, + expires: Number.MAX_SAFE_INTEGER, enterpriseUrl: baseUrl, }; } diff --git a/packages/ai/test/alibaba-endpoint-selection.test.ts b/packages/ai/test/alibaba-endpoint-selection.test.ts index f4edd2618..9ad9ed51a 100644 --- a/packages/ai/test/alibaba-endpoint-selection.test.ts +++ b/packages/ai/test/alibaba-endpoint-selection.test.ts @@ -6,7 +6,7 @@ import { getOAuthApiKey } from "../src/registry/oauth/index"; import type { Mock } from "bun:test"; describe("alibaba-coding-plan endpoint selection", () => { - let validateSpy: Mock, ReturnType>; + let validateSpy: Mock; beforeEach(() => { validateSpy = spyOn(apiKeyValidation, "validateOpenAICompatibleApiKey").mockResolvedValue(undefined); From e264d8b1000f80ccb7dd1a367036ebea48414dd4 Mon Sep 17 00:00:00 2001 From: lsmir2 Date: Wed, 17 Jun 2026 08:47:17 +0800 Subject: [PATCH 4/4] fix(auth-storage): replace legacy api_key rows on OAuth login upgrade When a user who previously logged in (stored as api_key credential) re-logs in with the new version (returns OAuthCredentials with enterpriseUrl), the old api_key row was not replaced by #upsertOAuthCredential. AuthStorage.getApiKey() prioritizes api_key over OAuth, so the old row was matched, returned a raw key without JSON wrapper, and the enterpriseUrl (selected endpoint) was lost. Changed to set() which calls replaceAuthCredentialsForProvider() to replace ALL existing credentials for the provider. Related to #2802 --- packages/ai/src/auth-storage.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/ai/src/auth-storage.ts b/packages/ai/src/auth-storage.ts index 34168bafe..0c56cec96 100644 --- a/packages/ai/src/auth-storage.ts +++ b/packages/ai/src/auth-storage.ts @@ -1786,7 +1786,10 @@ export class AuthStorage { return; } const newCredential: OAuthCredential = { type: "oauth", ...result }; - await this.#upsertOAuthCredential(def.storeCredentialsAs ?? provider, newCredential); + // Use set() instead of #upsertOAuthCredential to replace ALL existing credentials + // (including legacy api_key rows from older versions) with the new OAuth credential. + // This ensures getApiKey() doesn't match an old api_key row before the new OAuth row. + await this.set(def.storeCredentialsAs ?? provider, newCredential); } /**