SPB Git

spb/polyllm

Public

Kimi: apply audit (pricing, output caps, per-model thinking knobs, json_schema per model, tool_choice fallback)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Sep 8, 2026) parent d07d171 Browse files

4 changed files +47 −28

modified docs/provider-test-matrix.md +10 −10
@@ -22,13 +22,13 @@ Test models: openai: gpt-5.4-mini / reasoning gpt-5.4-mini; anthropic: claude-ha
22 22
23 ## Notes 23 ## Notes
24 24
25−- openrouter: models: 323 (openai/gpt-6-astra, openai/gpt-6-astra-pro, inclusionai/ling-3.0-flash-sante:free, qwen/qwen3.8-max-0902, meta/muse-spark-1.3…) 25+- kimi: models: 4 (kimi-k3, kimi-k2.7-code, kimi-k2.7-code-highspeed, kimi-k2.6…)
26−- openrouter: text(openai/gpt-5.4-nano): "OK" deltas=1 usage={"inputTokens":11,"outputTokens":5,"cachedInputTokens":0,"reasoningTokens":0,"totalTokens":16} finish=stop 26+- kimi: text(kimi-k2.6): "OK" deltas=68 usage={"inputTokens":12,"outputTokens":71,"cachedInputTokens":12,"reasoningTokens":68,"totalTokens":83} finish=stop
27−- openrouter: system: "Arr! Hello there, matey, cheers!" 27+- kimi: system: "Arr hello to ye matey."
28−- openrouter: vision: "Red" 28+- kimi: vision: "Red"
29−- openrouter: tools: [{"n":"calculator","a":{"expression":"1234*5678"}}] finish=tool-calls 29+- kimi: tools: [{"n":"calculator","a":{"expression":"1234 * 5678"}}] finish=tool-calls
30−- openrouter: structured: {"city":"Ottawa","country":"Canada"} 30+- kimi: structured: {"city":"Ottawa","country":"Canada"}
31−- openrouter: reasoning(openai/gpt-5.4-nano): text="16" reasoningChars=838 reasoningTokens=565 31+- kimi: reasoning(kimi-k3): text="16" reasoningChars=709 reasoningTokens=289
32−- openrouter: invalid key → INVALID_API_KEY (401) 32+- kimi: invalid key → INVALID_API_KEY (401)
33−- openrouter: unknown model → INVALID_PARAMETER: definitely-not-a-model-xyz is not a valid model ID 33+- kimi: unknown model → MODEL_NOT_FOUND: Not found the model definitely-not-a-model-xyz or Permission denied
34−- openrouter: long: 468 words, 541 deltas 34+- kimi: long: 410 words, 1641 deltas
modified src/lib/ai/providers/kimi/catalog.ts +16 −2
@@ -1,4 +1,18 @@
1 import type { CatalogEntry } from "@/lib/ai/registry/catalog"; 1 import type { CatalogEntry } from "@/lib/ai/registry/catalog";
2 2
3−/** Kimi documented augmentation — filled from docs/provider-research/kimi.md (2026-09-08). */ 3+/**
4−export const KIMI_CATALOG = new Map<string, CatalogEntry>(); 4+ * Kimi documented augmentation — docs/provider-research/kimi.md (audited 2026-09-08).
5+ * Pricing USD/M (input / cache-hit / output) from the pricing page; max output per docs
6+ * (K3 131 072 default, K2.x 32 768 — the API does not validate it, so we clamp client-side).
7+ * Thinking controls: K2.6 `thinking:{type}`; K2.7-code(-highspeed) fixed (disable → 400); K3 `reasoning_effort` low|high|max.
8+ * `json_schema` is enforced on K3 / K2.7 but not on K2.6 (prose came back twice).
9+ */
10+const price = (i: number, c: number, o: number) => ({ inputPerMillion: i, cachedInputPerMillion: c, outputPerMillion: o, source: "kimi-pricing-page", asOf: "2026-09-08" });
11+const caps = { text: true, vision: true, audioInput: false, audioOutput: false, imageGeneration: false, video: true, reasoning: true, tools: true, structuredOutput: true, streaming: true, files: false, webSearch: false };
12+
13+export const KIMI_CATALOG = new Map<string, CatalogEntry>([
14+ ["kimi-k3", { displayName: "Kimi K3", family: "Kimi K3", capabilities: caps, limits: { maxOutputTokens: 131_072 }, pricing: price(3, 0.3, 15), status: "active", sortWeight: 100, metadata: { thinkingKnob: "reasoning_effort", jsonSchema: true, minOutputForReasoning: 8192 } }],
15+ ["kimi-k2.7-code", { displayName: "Kimi K2.7 Code", family: "Kimi K2", capabilities: caps, limits: { maxOutputTokens: 32_768 }, pricing: price(0.95, 0.19, 4), status: "active", sortWeight: 90, metadata: { thinkingKnob: "fixed", jsonSchema: true, minOutputForReasoning: 8192, toolChoiceRequired: false } }],
16+ ["kimi-k2.7-code-highspeed", { displayName: "Kimi K2.7 Code High-speed", family: "Kimi K2", capabilities: caps, limits: { maxOutputTokens: 32_768 }, pricing: price(1.9, 0.38, 8), status: "active", sortWeight: 88, metadata: { thinkingKnob: "fixed", jsonSchema: true, minOutputForReasoning: 8192, toolChoiceRequired: false } }],
17+ ["kimi-k2.6", { displayName: "Kimi K2.6", family: "Kimi K2", capabilities: caps, limits: { maxOutputTokens: 32_768 }, pricing: price(0.95, 0.16, 4), status: "active", sortWeight: 85, metadata: { thinkingKnob: "thinking", jsonSchema: false, minOutputForReasoning: 8192, toolChoiceRequired: false } }],
18+]);
modified src/lib/ai/providers/kimi/index.ts +20 −15
@@ -52,7 +52,7 @@ export function normalizeKimiModel(m: KimiModel): PolyModel | null {
52 webSearch: false, // `$web_search` builtin exists but its round trip is not stable enough to expose yet 52 webSearch: false, // `$web_search` builtin exists but its round trip is not stable enough to expose yet
53 ...(cat?.capabilityOverrides ?? {}), 53 ...(cat?.capabilityOverrides ?? {}),
54 }, 54 },
55− limits: { contextTokens: m.context_length ?? cat?.limits?.contextTokens, maxOutputTokens: cat?.limits?.maxOutputTokens }, 55+ limits: { contextTokens: m.context_length ?? cat?.limits?.contextTokens, maxOutputTokens: cat?.limits?.maxOutputTokens ?? 32_768 },
56 parameters: { 56 parameters: {
57 temperature: false, // only 1 accepted 57 temperature: false, // only 1 accepted
58 topP: false, // only 0.95 accepted 58 topP: false, // only 0.95 accepted
@@ -60,19 +60,19 @@ export function normalizeKimiModel(m: KimiModel): PolyModel | null {
60 maxTokens: true, 60 maxTokens: true,
61 stop: true, 61 stop: true,
62 seed: false, 62 seed: false,
63− frequencyPenalty: true, 63+ frequencyPenalty: false, // only 0 accepted
64− presencePenalty: true, 64+ presencePenalty: false,
65− reasoningEffort: reasoning, 65+ reasoningEffort: reasoning && (cat?.metadata?.thinkingKnob ?? (m.reasoning_efforts?.support ? "reasoning_effort" : "thinking")) !== "fixed",
66− reasoningEffortLevels: reasoning ? [...(alwaysThinking ? [] : ["none"]), ...efforts] : undefined, 66+ reasoningEffortLevels: reasoning ? ((cat?.metadata?.thinkingKnob ?? "thinking") === "fixed" ? undefined : m.reasoning_efforts?.support ? efforts : ["none", "high"]) : undefined,
67 thinkingBudget: false, 67 thinkingBudget: false,
68 ...(cat?.parameters ?? {}), 68 ...(cat?.parameters ?? {}),
69 }, 69 },
70 status: cat?.status ?? "active", 70 status: cat?.status ?? "active",
71 pricing: cat?.pricing ?? null, 71 pricing: cat?.pricing ?? null,
72 metadata: { 72 metadata: {
73− ...(cat?.metadata ?? {}),
74 jsonSchema: false, 73 jsonSchema: false,
75 jsonWordRequired: true, 74 jsonWordRequired: true,
75+ ...(cat?.metadata ?? {}),
76 alwaysThinking, 76 alwaysThinking,
77 defaultReasoningEffort: m.reasoning_efforts?.default_effort ?? m.think_efforts?.default_effort, 77 defaultReasoningEffort: m.reasoning_efforts?.default_effort ?? m.think_efforts?.default_effort,
78 dynamicTools: m.supports_dynamic_tools, 78 dynamicTools: m.supports_dynamic_tools,
@@ -118,16 +118,21 @@ export const kimiAdapter = createOpenAICompatAdapter({
118 delete p.temperature; // only the default (1) is accepted 118 delete p.temperature; // only the default (1) is accepted
119 delete p.top_p; // only 0.95 is accepted 119 delete p.top_p; // only 0.95 is accepted
120 delete p.seed; 120 delete p.seed;
121− if (req.modelInfo?.capabilities.reasoning && settings.reasoningEffort) { 121+ delete p.frequency_penalty; // only 0 is accepted
122− const e = settings.reasoningEffort; 122+ delete p.presence_penalty;
123− if (e === "none" && meta.alwaysThinking !== true) p.thinking = { type: "disabled" }; 123+ const knob = (meta.thinkingKnob as string | undefined) ?? "thinking";
124− else { 124+ const effort = settings.reasoningEffort;
125− const levels = (req.modelInfo.parameters.reasoningEffortLevels ?? ["low", "high", "max"]).filter((l) => l !== "none"); 125+ if (req.modelInfo?.capabilities.reasoning && effort && knob !== "fixed") {
126− const wanted = e === "minimal" ? "low" : e === "medium" ? (levels.includes("medium") ? "medium" : "low") : e === "xhigh" ? (levels.includes("xhigh") ? "xhigh" : "high") : e === "none" ? levels[0] : e; 126+ if (knob === "reasoning_effort") {
127− p.thinking = { type: "enabled" }; 127+ // K3: low | high | max (medium accepted); "none" is undocumented → map to low.
128− p.reasoning_effort = levels.includes(wanted) ? wanted : levels[levels.length - 1]; 128+ p.reasoning_effort = effort === "none" || effort === "minimal" || effort === "low" ? "low" : effort === "max" ? "max" : effort === "medium" ? "medium" : "high";
129− } 129+ } else if (effort === "none") p.thinking = { type: "disabled" };
130+ else p.thinking = { type: "enabled" };
130 } 131 }
132+ // Reasoning consumes max_tokens; the API does not validate the cap, so clamp and raise tiny caps.
133+ if (req.modelInfo?.capabilities.reasoning && effort !== "none" && typeof params.max_tokens === "number" && params.max_tokens < Number(meta.minOutputForReasoning ?? 8192)) params.max_tokens = Number(meta.minOutputForReasoning ?? 8192);
134+ // Forced/required tool choice → 400 with thinking on K2.x and forced functions everywhere → fall back to auto.
135+ if (params.tool_choice && params.tool_choice !== "none" && (typeof params.tool_choice === "object" || meta.toolChoiceRequired === false)) params.tool_choice = "auto";
131 }, 136 },
132 refineError: (status, code, message) => { 137 refineError: (status, code, message) => {
133 if (status === 401 || code === "invalid_authentication_error") return "INVALID_API_KEY"; 138 if (status === 401 || code === "invalid_authentication_error") return "INVALID_API_KEY";
modified tests/unit/compat-providers.test.ts +1 −1
@@ -59,7 +59,7 @@ describe("id-only listings", () => {
59 expect(normalizeDeepSeekModel({ id: "text-embedding" })).toBeNull(); 59 expect(normalizeDeepSeekModel({ id: "text-embedding" })).toBeNull();
60 expect(normalizeKimiModel({ id: "kimi-k3" })?.family).toBe("Kimi K3"); 60 expect(normalizeKimiModel({ id: "kimi-k3" })?.family).toBe("Kimi K3");
61 expect(normalizeKimiModel({ id: "kimi-k2.6" })?.parameters.temperature).toBe(false); // only temperature=1 is accepted 61 expect(normalizeKimiModel({ id: "kimi-k2.6" })?.parameters.temperature).toBe(false); // only temperature=1 is accepted
62− expect(normalizeKimiModel({ id: "kimi-k3", supports_thinking_type: "only", reasoning_efforts: { valid_efforts: ["low", "high", "max"] } })?.parameters.reasoningEffortLevels).toEqual(["low", "high", "max"]); 62+ expect(normalizeKimiModel({ id: "kimi-k3", supports_thinking_type: "only", reasoning_efforts: { support: true, valid_efforts: ["low", "high", "max"] } })?.parameters.reasoningEffortLevels).toEqual(["low", "high", "max"]);
63 expect(normalizeCerebrasModel({ id: "gpt-oss-120b" })?.parameters.reasoningEffortLevels).toEqual(["low", "medium", "high"]); 63 expect(normalizeCerebrasModel({ id: "gpt-oss-120b" })?.parameters.reasoningEffortLevels).toEqual(["low", "medium", "high"]);
64 }); 64 });
65 }); 65 });
66 66
Menu

Theme