feat: add API-key Kimi Coding provider path (#463)

* feat: add api-key Kimi Coding provider support

* fix(kimi-coding): honor apikey auth header in executor

Ensure DefaultExecutor sends x-api-key for kimi-coding-apikey at runtime
and deduplicate shared kimi coding config blocks in registry and models
config to reduce drift between oauth and apikey variants.

---------

Co-authored-by: OmniRoute Agent <agent@omniroute.local>
This commit is contained in:
Jefferson Nunn
2026-03-18 23:48:26 -05:00
committed by GitHub
parent 3d0c8d8d45
commit f89f40778f
7 changed files with 98 additions and 22 deletions

View File

@@ -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<string, RegistryEntry> = {
@@ -559,16 +575,9 @@ export const REGISTRY: Record<string, RegistryEntry> = {
"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<string, RegistryEntry> = {
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: {

View File

@@ -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;

View File

@@ -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<string, ProviderModelsConfigEntry> = {
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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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(

View File

@@ -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;
}
});