SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%

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 16 days ago (Sep 8, 2026) parent d07d171

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 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…)
26 - openrouter: text(openai/gpt-5.4-nano): "OK" deltas=1 usage={"inputTokens":11,"outputTokens":5,"cachedInputTokens":0,"reasoningTokens":0,"totalTokens":16} finish=stop
27 - openrouter: system: "Arr! Hello there, matey, cheers!"
28 - openrouter: vision: "Red"
29 - openrouter: tools: [{"n":"calculator","a":{"expression":"1234*5678"}}] finish=tool-calls
30 - openrouter: structured: {"city":"Ottawa","country":"Canada"}
31 - openrouter: reasoning(openai/gpt-5.4-nano): text="16" reasoningChars=838 reasoningTokens=565
32 - openrouter: invalid key → INVALID_API_KEY (401)
33 - openrouter: unknown model → INVALID_PARAMETER: definitely-not-a-model-xyz is not a valid model ID
34 - openrouter: long: 468 words, 541 deltas
25 +- kimi: models: 4 (kimi-k3, kimi-k2.7-code, kimi-k2.7-code-highspeed, kimi-k2.6…)
26 +- kimi: text(kimi-k2.6): "OK" deltas=68 usage={"inputTokens":12,"outputTokens":71,"cachedInputTokens":12,"reasoningTokens":68,"totalTokens":83} finish=stop
27 +- kimi: system: "Arr hello to ye matey."
28 +- kimi: vision: "Red"
29 +- kimi: tools: [{"n":"calculator","a":{"expression":"1234 * 5678"}}] finish=tool-calls
30 +- kimi: structured: {"city":"Ottawa","country":"Canada"}
31 +- kimi: reasoning(kimi-k3): text="16" reasoningChars=709 reasoningTokens=289
32 +- kimi: invalid key → INVALID_API_KEY (401)
33 +- kimi: unknown model → MODEL_NOT_FOUND: Not found the model definitely-not-a-model-xyz or Permission denied
34 +- kimi: long: 410 words, 1641 deltas
modified src/lib/ai/providers/kimi/catalog.ts +16 −2
@@ -1,4 +1,18 @@
1 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). */
4 export const KIMI_CATALOG = new Map<string, CatalogEntry>();
3 +/**
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 52 webSearch: false, // `$web_search` builtin exists but its round trip is not stable enough to expose yet
53 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 56 parameters: {
57 57 temperature: false, // only 1 accepted
58 58 topP: false, // only 0.95 accepted
@@ -60,19 +60,19 @@ export function normalizeKimiModel(m: KimiModel): PolyModel | null {
60 60 maxTokens: true,
61 61 stop: true,
62 62 seed: false,
63 frequencyPenalty: true,
64 presencePenalty: true,
65 reasoningEffort: reasoning,
66 reasoningEffortLevels: reasoning ? [...(alwaysThinking ? [] : ["none"]), ...efforts] : undefined,
63 + frequencyPenalty: false, // only 0 accepted
64 + presencePenalty: false,
65 + reasoningEffort: reasoning && (cat?.metadata?.thinkingKnob ?? (m.reasoning_efforts?.support ? "reasoning_effort" : "thinking")) !== "fixed",
66 + reasoningEffortLevels: reasoning ? ((cat?.metadata?.thinkingKnob ?? "thinking") === "fixed" ? undefined : m.reasoning_efforts?.support ? efforts : ["none", "high"]) : undefined,
67 67 thinkingBudget: false,
68 68 ...(cat?.parameters ?? {}),
69 69 },
70 70 status: cat?.status ?? "active",
71 71 pricing: cat?.pricing ?? null,
72 72 metadata: {
73 ...(cat?.metadata ?? {}),
74 73 jsonSchema: false,
75 74 jsonWordRequired: true,
75 + ...(cat?.metadata ?? {}),
76 76 alwaysThinking,
77 77 defaultReasoningEffort: m.reasoning_efforts?.default_effort ?? m.think_efforts?.default_effort,
78 78 dynamicTools: m.supports_dynamic_tools,
@@ -118,16 +118,21 @@ export const kimiAdapter = createOpenAICompatAdapter({
118 118 delete p.temperature; // only the default (1) is accepted
119 119 delete p.top_p; // only 0.95 is accepted
120 120 delete p.seed;
121 if (req.modelInfo?.capabilities.reasoning && settings.reasoningEffort) {
122 const e = settings.reasoningEffort;
123 if (e === "none" && meta.alwaysThinking !== true) p.thinking = { type: "disabled" };
124 else {
125 const levels = (req.modelInfo.parameters.reasoningEffortLevels ?? ["low", "high", "max"]).filter((l) => l !== "none");
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;
127 p.thinking = { type: "enabled" };
128 p.reasoning_effort = levels.includes(wanted) ? wanted : levels[levels.length - 1];
129 }
121 + delete p.frequency_penalty; // only 0 is accepted
122 + delete p.presence_penalty;
123 + const knob = (meta.thinkingKnob as string | undefined) ?? "thinking";
124 + const effort = settings.reasoningEffort;
125 + if (req.modelInfo?.capabilities.reasoning && effort && knob !== "fixed") {
126 + if (knob === "reasoning_effort") {
127 + // K3: low | high | max (medium accepted); "none" is undocumented → map to low.
128 + p.reasoning_effort = effort === "none" || effort === "minimal" || effort === "low" ? "low" : effort === "max" ? "max" : effort === "medium" ? "medium" : "high";
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 137 refineError: (status, code, message) => {
133 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 59 expect(normalizeDeepSeekModel({ id: "text-embedding" })).toBeNull();
60 60 expect(normalizeKimiModel({ id: "kimi-k3" })?.family).toBe("Kimi K3");
61 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 63 expect(normalizeCerebrasModel({ id: "gpt-oss-120b" })?.parameters.reasoningEffortLevels).toEqual(["low", "medium", "high"]);
64 64 });
65 65 });
66 66