diff --git a/changelog.d/fixes/10096-kimi-coding-apikey-save.md b/changelog.d/fixes/10096-kimi-coding-apikey-save.md new file mode 100644 index 0000000000..2b5f1bb8b6 --- /dev/null +++ b/changelog.d/fixes/10096-kimi-coding-apikey-save.md @@ -0,0 +1 @@ +- fix(dashboard): remap unified Kimi Code card API-key save to the admitted `kimi-coding-apikey` connection id, fixing 400 "Invalid provider" on Save (#10096) diff --git a/src/app/(dashboard)/dashboard/providers/[id]/hooks/useApiKeySave.ts b/src/app/(dashboard)/dashboard/providers/[id]/hooks/useApiKeySave.ts index 7e5354bc6d..07732aefe6 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/hooks/useApiKeySave.ts +++ b/src/app/(dashboard)/dashboard/providers/[id]/hooks/useApiKeySave.ts @@ -32,6 +32,19 @@ type UseApiKeySaveParams = { t: ProviderMessageTranslator; }; +// Issue #10096: the unified Kimi Code dashboard card shares one page/providerId +// ("kimi-coding") between OAuth and API-key auth. "kimi-coding" is an +// OAuth-primary managed id and is NOT an admitted API-key/dual-auth connection +// id (see isManagedProviderConnectionId in src/lib/providers/catalog.ts), so +// posting it here 400s with "Invalid provider". The dedicated managed +// API-key id "kimi-coding-apikey" IS admitted — remap only the POST payload +// so the saved connection lands under the correct managed id. The OAuth flow +// (handleOAuthSuccess in ProviderDetailPageClient.tsx) does not go through +// this hook, so it keeps posting "kimi-coding" unchanged. +export function resolveApiKeySaveProviderId(providerId: string): string { + return providerId === "kimi-coding" ? "kimi-coding-apikey" : providerId; +} + export function useApiKeySave({ providerId, fetchConnections, @@ -48,7 +61,10 @@ export function useApiKeySave({ const res = await fetch("/api/providers", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ provider: providerId, ...formData }), + body: JSON.stringify({ + provider: resolveApiKeySaveProviderId(providerId), + ...formData, + }), }); if (res.ok) { const connectionData = await res.json(); diff --git a/tests/unit/bug-10096-kimi-coding-apikey-save.test.ts b/tests/unit/bug-10096-kimi-coding-apikey-save.test.ts new file mode 100644 index 0000000000..54bfda78e7 --- /dev/null +++ b/tests/unit/bug-10096-kimi-coding-apikey-save.test.ts @@ -0,0 +1,37 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +// Issue #10096: Kimi Code API key validates OK but Save returns 400 "Invalid provider". +// +// Root cause: the unified Kimi Code dashboard card's API-key branch posted +// provider: "kimi-coding" (an OAuth-primary managed id, NOT an admitted +// API-key connection id) to POST /api/providers, which the backend rejects. +// The dedicated managed API-key id "kimi-coding-apikey" IS admitted. +// +// Fix: resolveApiKeySaveProviderId() in useApiKeySave.ts remaps the posted +// provider id to "kimi-coding-apikey" for the API-key save flow only, while +// the OAuth flow (which never calls this hook) keeps posting "kimi-coding". + +const { isManagedProviderConnectionId } = await import("../../src/lib/providers/catalog.ts"); +const { resolveApiKeySaveProviderId } = await import( + "../../src/app/(dashboard)/dashboard/providers/[id]/hooks/useApiKeySave.ts" +); + +test("Kimi Code API-key save flow remaps to the admitted managed API-key id", () => { + assert.equal( + resolveApiKeySaveProviderId("kimi-coding"), + "kimi-coding-apikey", + "the unified Kimi Code card's API-key save flow must post kimi-coding-apikey, not kimi-coding" + ); + assert.equal( + isManagedProviderConnectionId(resolveApiKeySaveProviderId("kimi-coding")), + true, + "the remapped id must be an admitted managed provider connection id (POST /api/providers accepts it)" + ); +}); + +test("resolveApiKeySaveProviderId leaves every other provider id untouched", () => { + assert.equal(resolveApiKeySaveProviderId("openai"), "openai"); + assert.equal(resolveApiKeySaveProviderId("kimi-coding-apikey"), "kimi-coding-apikey"); + assert.equal(resolveApiKeySaveProviderId("qoder"), "qoder"); +});