Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/fixes/10096-kimi-coding-apikey-save.md
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ export default function AddApiKeyModal({
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
provider,
provider: provider === "kimi-coding" ? "kimi-coding-apikey" : provider,
entries: parsed.entries.map((e) => ({
name: e.name,
apiKey: e.apiKey,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,49 @@ describe("conn-modals (Phase 1c extraction)", () => {
);
});

it("AddApiKeyModal remaps Kimi Code bulk API-key additions to the admitted provider id", async () => {
const fetchMock = vi.fn(() =>
Promise.resolve({
ok: true,
json: async () => ({ success: 1, failed: 0, total: 1, errors: [] }),
text: async () => "",
} as Response)
);
vi.stubGlobal("fetch", fetchMock);
const c = renderModal(
<AddApiKeyModal
isOpen={true}
provider="kimi-coding"
providerName="Kimi Code"
isCompatible={false}
onSave={vi.fn().mockResolvedValue(undefined)}
onClose={vi.fn()}
/>
);

const bulkTab = Array.from(c.querySelectorAll("button")).find(
(button) => button.textContent === "providers.bulkTabBulkAdd"
);
act(() => bulkTab!.click());
const bulkInput = c.querySelector<HTMLTextAreaElement>("textarea");
setTextareaValue(bulkInput!, "main|sk-kimi-test");
const submitButton = Array.from(c.querySelectorAll("button")).find(
(button) => button.textContent === "providers.bulkAddAllKeys"
);
await act(async () => {
submitButton!.click();
await Promise.resolve();
await Promise.resolve();
});

expect(fetchMock).toHaveBeenCalledWith(
"/api/providers/bulk",
expect.objectContaining({
body: expect.stringContaining('"provider":"kimi-coding-apikey"'),
})
);
});

it("AddApiKeyModal does not infer a regional provider selection", () => {
const c = renderModal(
<AddApiKeyModal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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();
Expand Down
37 changes: 37 additions & 0 deletions tests/unit/bug-10096-kimi-coding-apikey-save.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
Loading