diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index cba697e545..7029b97281 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -78,6 +78,22 @@ interface LegacyProvider { clientVersion?: string; } +const KIMI_CODING_SHARED = { + format: "claude", + executor: "default", + baseUrl: "https://api.kimi.com/coding/v1/messages", + authHeader: "x-api-key", + headers: { + "Anthropic-Version": "2023-06-01", + "Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14", + }, + models: [ + { id: "kimi-k2.5", name: "Kimi K2.5" }, + { id: "kimi-k2.5-thinking", name: "Kimi K2.5 Thinking" }, + { id: "kimi-latest", name: "Kimi Latest" }, + ] as RegistryModel[], +} as const; + // ── Registry ────────────────────────────────────────────────────────────── export const REGISTRY: Record = { @@ -559,16 +575,9 @@ export const REGISTRY: Record = { "kimi-coding": { id: "kimi-coding", alias: "kmc", - format: "claude", - executor: "default", - baseUrl: "https://api.kimi.com/coding/v1/messages", + ...KIMI_CODING_SHARED, urlSuffix: "?beta=true", authType: "oauth", - authHeader: "x-api-key", - headers: { - "Anthropic-Version": "2023-06-01", - "Anthropic-Beta": "claude-code-20250219,interleaved-thinking-2025-05-14", - }, oauth: { clientIdEnv: "KIMI_CODING_OAUTH_CLIENT_ID", clientIdDefault: "17e5f671-d194-4dfb-9706-5516cb48c098", @@ -576,11 +585,13 @@ export const REGISTRY: Record = { refreshUrl: "https://auth.kimi.com/api/oauth/token", authUrl: "https://auth.kimi.com/api/oauth/device_authorization", }, - models: [ - { id: "kimi-k2.5", name: "Kimi K2.5" }, - { id: "kimi-k2.5-thinking", name: "Kimi K2.5 Thinking" }, - { id: "kimi-latest", name: "Kimi Latest" }, - ], + }, + + "kimi-coding-apikey": { + id: "kimi-coding-apikey", + alias: "kmca", + ...KIMI_CODING_SHARED, + authType: "apikey", }, kilocode: { diff --git a/open-sse/executors/default.ts b/open-sse/executors/default.ts index f51512c32e..fa52037ae4 100644 --- a/open-sse/executors/default.ts +++ b/open-sse/executors/default.ts @@ -54,6 +54,7 @@ export class DefaultExecutor extends BaseExecutor { break; case "glm": case "kimi-coding": + case "kimi-coding-apikey": case "minimax": case "minimax-cn": headers["x-api-key"] = credentials.apiKey || credentials.accessToken; diff --git a/src/app/api/providers/[id]/models/route.ts b/src/app/api/providers/[id]/models/route.ts index 3ddd9b0d6d..e204b0f3e6 100644 --- a/src/app/api/providers/[id]/models/route.ts +++ b/src/app/api/providers/[id]/models/route.ts @@ -28,6 +28,14 @@ type ProviderModelsConfigEntry = { parseResponse: (data: any) => any; }; +const KIMI_CODING_MODELS_CONFIG: ProviderModelsConfigEntry = { + url: "https://api.kimi.com/coding/v1/models", + method: "GET", + headers: { "Content-Type": "application/json" }, + authHeader: "x-api-key", + parseResponse: (data) => data.data || data.models || [], +}; + // Providers that return hardcoded models (no remote /models API) const STATIC_MODEL_PROVIDERS = { deepgram: () => [ @@ -134,11 +142,10 @@ const PROVIDER_MODELS_CONFIG: Record = { parseResponse: (data) => data.data || [], }, "kimi-coding": { - url: "https://api.kimi.com/coding/v1/models", - method: "GET", - headers: { "Content-Type": "application/json" }, - authHeader: "x-api-key", - parseResponse: (data) => data.data || data.models || [], + ...KIMI_CODING_MODELS_CONFIG, + }, + "kimi-coding-apikey": { + ...KIMI_CODING_MODELS_CONFIG, }, anthropic: { url: "https://api.anthropic.com/v1/models", diff --git a/src/shared/constants/config.ts b/src/shared/constants/config.ts index 17af31cb6d..1eca616107 100644 --- a/src/shared/constants/config.ts +++ b/src/shared/constants/config.ts @@ -35,6 +35,7 @@ export const PROVIDER_ENDPOINTS = { glm: "https://api.z.ai/api/anthropic/v1/messages", kimi: "https://api.moonshot.ai/v1/chat/completions", "kimi-coding": "https://api.kimi.com/coding/v1/messages", + "kimi-coding-apikey": "https://api.kimi.com/coding/v1/messages", minimax: "https://api.minimax.io/anthropic/v1/messages", "minimax-cn": "https://api.minimaxi.com/anthropic/v1/messages", openai: "https://api.openai.com/v1/chat/completions", diff --git a/src/shared/constants/providers.ts b/src/shared/constants/providers.ts index a014572e14..b8dee930f4 100644 --- a/src/shared/constants/providers.ts +++ b/src/shared/constants/providers.ts @@ -83,6 +83,15 @@ export const APIKEY_PROVIDERS = { textIcon: "KM", website: "https://kimi.moonshot.cn", }, + "kimi-coding-apikey": { + id: "kimi-coding-apikey", + alias: "kmca", + name: "Kimi Coding (API Key)", + icon: "psychology", + color: "#1E40AF", + textIcon: "KC", + website: "https://kimi.com", + }, minimax: { id: "minimax", alias: "minimax", diff --git a/tests/unit/plan3-p0.test.mjs b/tests/unit/plan3-p0.test.mjs index 51cc9e91df..eec1f63f51 100644 --- a/tests/unit/plan3-p0.test.mjs +++ b/tests/unit/plan3-p0.test.mjs @@ -7,10 +7,8 @@ import { detectFormat } from "../../open-sse/services/provider.ts"; import { shouldUseNativeCodexPassthrough } from "../../open-sse/handlers/chatCore.ts"; import { translateRequest } from "../../open-sse/translator/index.ts"; import { GithubExecutor } from "../../open-sse/executors/github.ts"; -import { - CodexExecutor, - setDefaultFastServiceTierEnabled, -} from "../../open-sse/executors/codex.ts"; +import { DefaultExecutor } from "../../open-sse/executors/default.ts"; +import { CodexExecutor, setDefaultFastServiceTierEnabled } from "../../open-sse/executors/codex.ts"; import { translateNonStreamingResponse } from "../../open-sse/handlers/responseTranslator.ts"; import { extractUsageFromResponse } from "../../open-sse/handlers/usageExtractor.ts"; import { parseSSEToResponsesOutput } from "../../open-sse/handlers/sseParser.ts"; @@ -60,6 +58,14 @@ test("GithubExecutor keeps non-codex model on /chat/completions", () => { assert.match(url, /\/chat\/completions$/); }); +test("DefaultExecutor uses x-api-key for kimi-coding-apikey", () => { + const executor = new DefaultExecutor("kimi-coding-apikey"); + const headers = executor.buildHeaders({ apiKey: "sk-kimi-test" }, true); + + assert.equal(headers["x-api-key"], "sk-kimi-test"); + assert.equal(headers.Authorization, undefined); +}); + test("CodexExecutor forces stream=true for upstream compatibility", () => { const executor = new CodexExecutor(); const transformed = executor.transformRequest( diff --git a/tests/unit/search-provider-validation.test.mjs b/tests/unit/search-provider-validation.test.mjs index f05aa51864..26ed3e97cc 100644 --- a/tests/unit/search-provider-validation.test.mjs +++ b/tests/unit/search-provider-validation.test.mjs @@ -48,3 +48,44 @@ test("serper validation still rejects unauthorized keys", async () => { globalThis.fetch = originalFetch; } }); + +test("kimi-coding-apikey validation uses Kimi Coding messages endpoint", async () => { + const originalFetch = globalThis.fetch; + const calls = []; + + globalThis.fetch = async (url, init = {}) => { + calls.push({ + url: String(url), + method: init.method || "GET", + headers: init.headers || {}, + }); + + return new Response(JSON.stringify({ ok: true }), { + status: 400, + headers: { "content-type": "application/json" }, + }); + }; + + try { + const result = await validateProviderApiKey({ + provider: "kimi-coding-apikey", + apiKey: "sk-kimi-test", + }); + + assert.equal(result.valid, true); + assert.equal(result.error, null); + assert.equal(calls.length, 1); + + assert.equal(calls[0].url, "https://api.kimi.com/coding/v1/messages"); + assert.equal(calls[0].method, "POST"); + assert.equal(calls[0].headers["x-api-key"], "sk-kimi-test"); + assert.equal(calls[0].headers["Anthropic-Version"], "2023-06-01"); + + for (const call of calls) { + assert.equal(call.url.includes("?beta=true/messages"), false); + assert.equal(call.url.includes("?beta=true/models"), false); + } + } finally { + globalThis.fetch = originalFetch; + } +});