mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(ai): revert kimi-coding switch to Chat Completions
Reverts b06c1f2. The pi.dev model catalog is served to every client
since 0.80.7, so released clients received kimi-coding models with
api openai-completions and baseUrl https://api.kimi.com/coding/v1.
Their kimi-coding provider only implements Anthropic Messages and
ignores model.api, so requests went to /coding/v1/v1/messages and
failed with 404. The change will re-land together with a bump of the
catalog's minimum pi version.
refs #10720, refs #10985
This commit is contained in:
parent
4a5d372915
commit
6a1f799b67
9 changed files with 99 additions and 113 deletions
|
|
@ -10,10 +10,6 @@
|
|||
|
||||
- Added cache-preserving thinking level changes for GPT-6 models on the openai provider: with an API key, the level is sent as configuration_update input items instead of changing reasoning.effort, controlled by compat.supportsMidConvoEffort ([#10853](https://github.com/earendil-works/pi/issues/10853))
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed the `kimi-coding` provider to use OpenAI Chat Completions at `https://api.kimi.com/coding/v1`, the protocol used by the official Kimi Code client. K3, K3-256K, and Kimi For Coding now accept tools added mid-conversation without invalidating the prompt cache. `models.json` overrides of the `kimi-coding` base URL must now include `/v1` ([#10720](https://github.com/earendil-works/pi/issues/10720))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed tool-call argument parsing dropping fields when a string contains an invalid `\u` escape such as `\usepackage` ([#10868](https://github.com/earendil-works/pi/issues/10868))
|
||||
|
|
|
|||
|
|
@ -343,21 +343,13 @@ const KIMI_K3_COST = {
|
|||
cacheRead: 0.3,
|
||||
cacheWrite: 0,
|
||||
} as const;
|
||||
// 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,
|
||||
// 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 },
|
||||
};
|
||||
const OPENROUTER_KIMI_K3_MODEL_IDS = new Set(["moonshotai/kimi-k3", "~moonshotai/kimi-latest"]);
|
||||
|
||||
|
|
@ -942,7 +934,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. Kimi Coding sets these flags in createKimiCodingModels().
|
||||
// plain system text in place.
|
||||
function applyOpenAICompletionsTranscriptMetadata(model: Model<Api>): void {
|
||||
if (model.api !== "openai-completions") return;
|
||||
const isKimiK3 =
|
||||
|
|
@ -1777,58 +1769,6 @@ 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...");
|
||||
|
|
@ -2432,7 +2372,48 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
|
|||
|
||||
// Process Kimi For Coding models
|
||||
if (data["kimi-code-plan-global"]?.models) {
|
||||
models.push(...createKimiCodingModels(data["kimi-code-plan-global"].models as Record<string, ModelsDevModel>));
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Process Moonshot AI models
|
||||
|
|
@ -2440,6 +2421,14 @@ 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 } : {};
|
||||
|
|
@ -2454,7 +2443,12 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
|
|||
if (m.tool_call !== true) continue;
|
||||
|
||||
const isKimiK3 = modelId === "kimi-k3";
|
||||
const compat = { ...(isKimiK3 ? KIMI_K3_OPENAI_COMPAT : KIMI_OPENAI_COMPAT) };
|
||||
const compat = isKimiK3 ? { ...moonshotCompat } : moonshotCompat;
|
||||
if (isKimiK3) {
|
||||
compat.requiresReasoningContentOnAssistantMessages = true;
|
||||
compat.thinkingFormat = "openai";
|
||||
compat.supportsReasoningEffort = true;
|
||||
}
|
||||
models.push({
|
||||
id: modelId,
|
||||
name: m.name || modelId,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { openAICompletionsApi } from "../api/openai-completions.lazy.ts";
|
||||
import { anthropicMessagesApi } from "../api/anthropic-messages.lazy.ts";
|
||||
import { envApiKeyAuth, lazyOAuth } from "../auth/helpers.ts";
|
||||
import { loadKimiCodingOAuth } from "../auth/oauth/load.ts";
|
||||
import { createProvider, type Provider } from "../models.ts";
|
||||
import { KIMI_CODING_MODELS } from "./kimi-coding.models.ts";
|
||||
|
||||
export function kimiCodingProvider(): Provider<"openai-completions"> {
|
||||
export function kimiCodingProvider(): Provider<"anthropic-messages"> {
|
||||
return createProvider({
|
||||
id: "kimi-coding",
|
||||
name: "Kimi For Coding",
|
||||
baseUrl: "https://api.kimi.com/coding/v1",
|
||||
baseUrl: "https://api.kimi.com/coding",
|
||||
auth: {
|
||||
apiKey: envApiKeyAuth("Kimi API key", ["KIMI_API_KEY"]),
|
||||
oauth: lazyOAuth({
|
||||
|
|
@ -19,6 +19,6 @@ export function kimiCodingProvider(): Provider<"openai-completions"> {
|
|||
}),
|
||||
},
|
||||
models: Object.values(KIMI_CODING_MODELS),
|
||||
api: openAICompletionsApi(),
|
||||
api: anthropicMessagesApi(),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ const EXPECTED_CURRENT_ADAPTIVE_THINKING_MODELS = [
|
|||
"fireworks/accounts/fireworks/models/deepseek-v4p1-flash",
|
||||
"fireworks/accounts/fireworks/models/gpt-oss-120b",
|
||||
"fireworks/accounts/fireworks/models/qwen3p8-max",
|
||||
"kimi-coding/kimi-for-coding",
|
||||
"kimi-coding/k3",
|
||||
"kimi-coding/kimi-for-coding-highspeed",
|
||||
"opencode/claude-opus-4-8",
|
||||
"opencode/claude-opus-5",
|
||||
"vercel-ai-gateway/anthropic/claude-opus-4.8",
|
||||
|
|
@ -38,7 +41,9 @@ describe("Anthropic adaptive thinking model metadata", () => {
|
|||
// Regression for #9323: Fireworks uses catalog effort metadata and
|
||||
// verified fallbacks, not a fixed set of adaptive model names.
|
||||
modelId.startsWith("fireworks/") ||
|
||||
/(opus[-.](4[-.][678]|5)|sonnet[-.]4[-.]6|sonnet[-.]5|haiku[-.]5[-.]5|fable[-.]5)/.test(modelId),
|
||||
/(opus[-.](4[-.][678]|5)|sonnet[-.]4[-.]6|sonnet[-.]5|haiku[-.]5[-.]5|fable[-.]5|kimi-coding\/)/.test(
|
||||
modelId,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -134,4 +134,13 @@ describe("Anthropic empty thinking signature compat", () => {
|
|||
{ type: "text", text: "internal reasoning" },
|
||||
]);
|
||||
});
|
||||
|
||||
it.each(["k3"] as const)("allows empty signatures for Kimi Coding %s", async (modelId) => {
|
||||
const model = getModel("kimi-coding", modelId);
|
||||
expect(model.compat?.allowEmptySignature).toBe(true);
|
||||
|
||||
const payload = await capturePayload(model, makeContext(" ", "internal reasoning", "kimi-coding", modelId));
|
||||
const assistant = payload.messages?.find((message) => message.role === "assistant");
|
||||
expect(assistant?.content).toEqual([{ type: "thinking", thinking: "internal reasoning", signature: "" }]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -89,6 +89,20 @@ describe("Anthropic forceAdaptiveThinking compat override", () => {
|
|||
expect(payload.output_config).toEqual({ effort: "xhigh" });
|
||||
});
|
||||
|
||||
it.each([
|
||||
["kimi-for-coding", "medium", "medium"],
|
||||
["k3", "max", "max"],
|
||||
["kimi-for-coding-highspeed", "medium", "medium"],
|
||||
] as const)(
|
||||
"uses adaptive thinking effort without a token budget for Kimi Coding %s",
|
||||
async (modelId, reasoning, effort) => {
|
||||
const payload = await capturePayload(getModel("kimi-coding", modelId), { reasoning });
|
||||
|
||||
expect(payload.thinking).toEqual({ type: "adaptive", display: "summarized" });
|
||||
expect(payload.output_config).toEqual({ effort });
|
||||
},
|
||||
);
|
||||
|
||||
it("allows built-in adaptive models to opt out with compat.forceAdaptiveThinking false", async () => {
|
||||
const model: Model<"anthropic-messages"> = {
|
||||
...getModel("anthropic", "claude-opus-4-8"),
|
||||
|
|
|
|||
|
|
@ -1041,7 +1041,7 @@ describe("Generate E2E Tests", () => {
|
|||
});
|
||||
|
||||
describe.skipIf(!process.env.KIMI_API_KEY)(
|
||||
"Kimi For Coding Provider (kimi-for-coding via OpenAI Completions)",
|
||||
"Kimi For Coding Provider (kimi-for-coding via Anthropic Messages)",
|
||||
() => {
|
||||
const llm = getModel("kimi-coding", "kimi-for-coding");
|
||||
|
||||
|
|
|
|||
|
|
@ -209,12 +209,6 @@ describe("getSupportedThinkingLevels", () => {
|
|||
expect(getSupportedThinkingLevels(model!)).toEqual(["low", "high", "max"]);
|
||||
});
|
||||
|
||||
it("excludes off for always-thinking Kimi Coding Highspeed", () => {
|
||||
const model = getModel("kimi-coding", "kimi-for-coding-highspeed");
|
||||
expect(model).toBeDefined();
|
||||
expect(getSupportedThinkingLevels(model!)).not.toContain("off");
|
||||
});
|
||||
|
||||
it("includes only high for OpenCode Grok Build", () => {
|
||||
const model = getModel("opencode", "grok-build-0.1");
|
||||
expect(model).toBeDefined();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Type } from "typebox";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { getModel, streamSimple } from "../src/compat.ts";
|
||||
import { streamSimple } from "../src/compat.ts";
|
||||
import type { Api, Context, Model, Tool } from "../src/types.ts";
|
||||
|
||||
class PayloadCaptured extends Error {}
|
||||
|
|
@ -339,32 +339,6 @@ describe("transcript system messages", () => {
|
|||
);
|
||||
});
|
||||
|
||||
type KimiPayload = {
|
||||
tools?: Array<{ function?: { name: string } }>;
|
||||
messages: Array<{ role: string; tools?: Array<{ function?: { name: string } }> }>;
|
||||
};
|
||||
|
||||
// #10720: Kimi Coding uses Chat Completions so K3-style models keep added tools out of the top-level list.
|
||||
test.each(["k3", "k3-256k", "kimi-for-coding"] as const)("anchors additions for Kimi Coding %s", async (modelId) => {
|
||||
const payload = await capturePayload<KimiPayload>(getModel("kimi-coding", modelId), additionContext);
|
||||
|
||||
expect(payload.tools?.map((value) => value.function?.name)).toEqual(["base_tool"]);
|
||||
expect(payload.messages.find((message) => message.tools)?.tools?.map((value) => value.function?.name)).toEqual([
|
||||
"late_tool",
|
||||
]);
|
||||
});
|
||||
|
||||
// #10720: Kimi Code Highspeed rejects tool-bearing system messages.
|
||||
test("keeps Kimi Coding Highspeed additions in top-level tools", async () => {
|
||||
const payload = await capturePayload<KimiPayload>(
|
||||
getModel("kimi-coding", "kimi-for-coding-highspeed"),
|
||||
additionContext,
|
||||
);
|
||||
|
||||
expect(payload.tools?.map((value) => value.function?.name)).toEqual(["base_tool", "late_tool"]);
|
||||
expect(payload.messages.some((message) => message.tools !== undefined)).toBe(false);
|
||||
});
|
||||
|
||||
test("keeps Kimi K2 system text inline without dynamic tool messages", async () => {
|
||||
const model: Model<"openai-completions"> = {
|
||||
...modelBase,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue