mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
feat(ai): use Chat Completions for the kimi-coding provider
Switch kimi-coding to OpenAI Chat Completions at api.kimi.com/coding/v1, the protocol the official Kimi Code client uses. K3, K3-256K, and Kimi For Coding accept tools declared in later system messages, so tools loaded mid-conversation (e.g. by tool_search) keep the prompt cache. Highspeed rejects tool-bearing system messages and keeps added tools top-level. closes #10720
This commit is contained in:
parent
87f9a36419
commit
b06c1f2cf1
9 changed files with 113 additions and 99 deletions
|
|
@ -343,13 +343,21 @@ const KIMI_K3_COST = {
|
|||
cacheRead: 0.3,
|
||||
cacheWrite: 0,
|
||||
} as const;
|
||||
// Kimi Coding is subscription-backed, so models.dev reports zero cost. Use the
|
||||
// equivalent Moonshot API rates to estimate the value of subscription usage.
|
||||
const KIMI_CODING_IMPLIED_COSTS: Record<string, Model<Api>["cost"]> = {
|
||||
k3: KIMI_K3_COST,
|
||||
"kimi-for-coding": { input: 0.95, output: 4, cacheRead: 0.19, cacheWrite: 0 },
|
||||
"kimi-for-coding-highspeed": { input: 1.9, output: 8, cacheRead: 0.38, cacheWrite: 0 },
|
||||
"kimi-k2-thinking": { input: 0.6, output: 2.5, cacheRead: 0.15, cacheWrite: 0 },
|
||||
// Kimi Chat Completions settings shared by Moonshot and Kimi Coding.
|
||||
const KIMI_OPENAI_COMPAT: OpenAICompletionsCompat = {
|
||||
supportsStore: false,
|
||||
supportsDeveloperRole: false,
|
||||
supportsReasoningEffort: false,
|
||||
maxTokensField: "max_tokens",
|
||||
supportsStrictMode: false,
|
||||
thinkingFormat: "deepseek",
|
||||
};
|
||||
// Kimi K3-style models take reasoning_effort and need earlier reasoning sent back.
|
||||
const KIMI_K3_OPENAI_COMPAT: OpenAICompletionsCompat = {
|
||||
...KIMI_OPENAI_COMPAT,
|
||||
requiresReasoningContentOnAssistantMessages: true,
|
||||
thinkingFormat: "openai",
|
||||
supportsReasoningEffort: true,
|
||||
};
|
||||
const OPENROUTER_KIMI_K3_MODEL_IDS = new Set(["moonshotai/kimi-k3", "~moonshotai/kimi-latest"]);
|
||||
|
||||
|
|
@ -918,7 +926,7 @@ function applyOpenAIToolSearchMetadata(model: Model<Api>): void {
|
|||
// tool-bearing system messages. Kimi K3 accepts both forms; Fireworks and OpenCode pass
|
||||
// its tool-bearing form through. GitHub Copilot forwards K3 text but silently drops its
|
||||
// tool-bearing message. DeepSeek V4 Pro and OpenAI models behind OpenRouter also accept
|
||||
// plain system text in place.
|
||||
// plain system text in place. Kimi Coding sets these flags in createKimiCodingModels().
|
||||
function applyOpenAICompletionsTranscriptMetadata(model: Model<Api>): void {
|
||||
if (model.api !== "openai-completions") return;
|
||||
const isKimiK3 =
|
||||
|
|
@ -1753,6 +1761,58 @@ function processFireworksModels(provider: ModelsDevProvider | undefined): Model<
|
|||
return models;
|
||||
}
|
||||
|
||||
// Kimi Coding is subscription-backed, so models.dev reports zero cost. Use the
|
||||
// equivalent Moonshot API rates to estimate the value of subscription usage.
|
||||
const KIMI_CODING_IMPLIED_COSTS: Record<string, Model<Api>["cost"]> = {
|
||||
k3: KIMI_K3_COST,
|
||||
"kimi-for-coding": { input: 0.95, output: 4, cacheRead: 0.19, cacheWrite: 0 },
|
||||
"kimi-for-coding-highspeed": { input: 1.9, output: 8, cacheRead: 0.38, cacheWrite: 0 },
|
||||
};
|
||||
// Models whose https://api.kimi.com/coding/v1/models entry reports effort levels and
|
||||
// dynamic tool loading (tools declared in later system messages). Highspeed reports
|
||||
// neither and rejects tool-bearing system messages with a 400.
|
||||
const KIMI_CODING_K3_STYLE_MODEL_IDS = new Set(["k3", "k3-256k", "kimi-for-coding"]);
|
||||
|
||||
// Kimi Coding uses Chat Completions, the same protocol as the official Kimi Code client.
|
||||
function createKimiCodingModels(kimiModels: Record<string, ModelsDevModel>): Model<"openai-completions">[] {
|
||||
const models: Model<"openai-completions">[] = [];
|
||||
for (const [modelId, m] of Object.entries(kimiModels)) {
|
||||
if (m.tool_call !== true) continue;
|
||||
|
||||
const isK3Style = KIMI_CODING_K3_STYLE_MODEL_IDS.has(modelId);
|
||||
const impliedCost = KIMI_CODING_IMPLIED_COSTS[modelId];
|
||||
// K3-style models send reasoning_effort, so expose the effort values models.dev lists.
|
||||
const effortLevels = isK3Style && m.reasoning_options ? getEffortThinkingLevelMap(m.reasoning_options) : undefined;
|
||||
|
||||
models.push({
|
||||
id: modelId,
|
||||
name: m.name || modelId,
|
||||
api: "openai-completions",
|
||||
provider: "kimi-coding",
|
||||
baseUrl: "https://api.kimi.com/coding/v1",
|
||||
compat: {
|
||||
...(isK3Style ? KIMI_K3_OPENAI_COMPAT : KIMI_OPENAI_COMPAT),
|
||||
// All models accept system text after the conversation starts.
|
||||
supportsMidConvoSystemMessages: true,
|
||||
...(isK3Style ? { supportsMidConvoToolAdditions: true } : {}),
|
||||
},
|
||||
reasoning: m.reasoning === true,
|
||||
input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
|
||||
cost: {
|
||||
input: m.cost?.input || impliedCost?.input || 0,
|
||||
output: m.cost?.output || impliedCost?.output || 0,
|
||||
cacheRead: m.cost?.cache_read || impliedCost?.cacheRead || 0,
|
||||
cacheWrite: m.cost?.cache_write || impliedCost?.cacheWrite || 0,
|
||||
},
|
||||
contextWindow: m.limit?.context || 4096,
|
||||
maxTokens: m.limit?.output || 4096,
|
||||
// Kimi Code reports every model as always-thinking.
|
||||
thinkingLevelMap: { ...effortLevels, off: null },
|
||||
});
|
||||
}
|
||||
return models;
|
||||
}
|
||||
|
||||
async function loadModelsDevData(): Promise<Model<any>[]> {
|
||||
try {
|
||||
console.log("Fetching models from models.dev API...");
|
||||
|
|
@ -2356,48 +2416,7 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
|
|||
|
||||
// Process Kimi For Coding models
|
||||
if (data["kimi-code-plan-global"]?.models) {
|
||||
const kimiModels = data["kimi-code-plan-global"].models as Record<string, ModelsDevModel>;
|
||||
const hasCanonicalModel = Object.prototype.hasOwnProperty.call(kimiModels, "kimi-for-coding");
|
||||
|
||||
const kimiAliases = new Set(["k2p5", "k2p6", "k2p7"]);
|
||||
|
||||
for (const [modelId, model] of Object.entries(kimiModels)) {
|
||||
const m = model as ModelsDevModel;
|
||||
if (m.tool_call !== true) continue;
|
||||
// models.dev may expose versioned aliases (e.g. k2p5/k2p6/k2p7).
|
||||
// Normalize aliases to the canonical model id and drop duplicates when canonical exists.
|
||||
if (kimiAliases.has(modelId) && hasCanonicalModel) continue;
|
||||
|
||||
const normalizedId = kimiAliases.has(modelId) ? "kimi-for-coding" : modelId;
|
||||
const normalizedName = kimiAliases.has(modelId) ? "Kimi For Coding" : m.name || normalizedId;
|
||||
const isKimiK3 = normalizedId === "k3";
|
||||
const allowEmptySignature = isKimiK3 || normalizedId === "kimi-for-coding";
|
||||
const impliedCost = KIMI_CODING_IMPLIED_COSTS[normalizedId];
|
||||
|
||||
models.push({
|
||||
id: normalizedId,
|
||||
name: normalizedName,
|
||||
api: "anthropic-messages",
|
||||
provider: "kimi-coding",
|
||||
// Kimi For Coding's Anthropic-compatible API - SDK appends /v1/messages
|
||||
baseUrl: "https://api.kimi.com/coding",
|
||||
compat: {
|
||||
...(allowEmptySignature ? { allowEmptySignature: true } : {}),
|
||||
forceAdaptiveThinking: true,
|
||||
},
|
||||
reasoning: isKimiK3 || m.reasoning === true,
|
||||
input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
|
||||
cost: {
|
||||
input: m.cost?.input || impliedCost?.input || 0,
|
||||
output: m.cost?.output || impliedCost?.output || 0,
|
||||
cacheRead: m.cost?.cache_read || impliedCost?.cacheRead || 0,
|
||||
cacheWrite: m.cost?.cache_write || impliedCost?.cacheWrite || 0,
|
||||
},
|
||||
contextWindow: m.limit?.context || 4096,
|
||||
maxTokens: m.limit?.output || 4096,
|
||||
});
|
||||
recordModelsDevReasoningOptions("kimi-coding", normalizedId, m);
|
||||
}
|
||||
models.push(...createKimiCodingModels(data["kimi-code-plan-global"].models as Record<string, ModelsDevModel>));
|
||||
}
|
||||
|
||||
// Process Moonshot AI models
|
||||
|
|
@ -2405,14 +2424,6 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
|
|||
{ key: "moonshotai", provider: "moonshotai", baseUrl: "https://api.moonshot.ai/v1" },
|
||||
{ key: "moonshotai-cn", provider: "moonshotai-cn", baseUrl: "https://api.moonshot.cn/v1" },
|
||||
] as const;
|
||||
const moonshotCompat: OpenAICompletionsCompat = {
|
||||
supportsStore: false,
|
||||
supportsDeveloperRole: false,
|
||||
supportsReasoningEffort: false,
|
||||
maxTokensField: "max_tokens",
|
||||
supportsStrictMode: false,
|
||||
thinkingFormat: "deepseek",
|
||||
};
|
||||
const getMoonshotProviderModels = (key: "moonshotai" | "moonshotai-cn"): Record<string, ModelsDevModel> => {
|
||||
const providerModels = data[key]?.models as Record<string, ModelsDevModel> | undefined;
|
||||
return providerModels ? { ...providerModels } : {};
|
||||
|
|
@ -2427,12 +2438,7 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
|
|||
if (m.tool_call !== true) continue;
|
||||
|
||||
const isKimiK3 = modelId === "kimi-k3";
|
||||
const compat = isKimiK3 ? { ...moonshotCompat } : moonshotCompat;
|
||||
if (isKimiK3) {
|
||||
compat.requiresReasoningContentOnAssistantMessages = true;
|
||||
compat.thinkingFormat = "openai";
|
||||
compat.supportsReasoningEffort = true;
|
||||
}
|
||||
const compat = { ...(isKimiK3 ? KIMI_K3_OPENAI_COMPAT : KIMI_OPENAI_COMPAT) };
|
||||
models.push({
|
||||
id: modelId,
|
||||
name: m.name || modelId,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue