TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1import type OpenAI from "openai";2import { type PolyModel, modelKey } from "@/lib/ai/core/types";3import { createOpenAICompatAdapter } from "../shared/openai-compat/factory";4import { KIMI_CATALOG } from "./catalog";56/**7 * Kimi (Moonshot AI, international platform) — OpenAI-compatible at https://api.moonshot.ai/v1.8 * `GET /v1/models/{id}` exposes `supports_image_in`, `supports_video_in`, `supports_reasoning`,9 * `think_efforts.valid_efforts`, `supports_thinking_type` ("only" = cannot be disabled) and10 * `context_length` — used live; pricing comes from the catalog (docs/provider-research/kimi.md).11 * Verified quirks: temperature must be 1 and top_p 0.95 (400 otherwise), thinking streams as12 * `delta.reasoning_content` and must be replayed on assistant turns, `json_schema` is not enforced13 * (json_object + prompt instead).14 */15interface KimiModel {16 id: string;17 created?: number;18 context_length?: number;19 supports_image_in?: boolean;20 supports_video_in?: boolean;21 supports_reasoning?: boolean;22 supports_dynamic_tools?: boolean;23 think_efforts?: { support?: boolean; valid_efforts?: string[]; default_effort?: string };24 reasoning_efforts?: { support?: boolean; valid_efforts?: string[]; default_effort?: string };25 supports_thinking_type?: string; // "only" | "optional" | …26}2728export function normalizeKimiModel(m: KimiModel): PolyModel | null {29 if (!m.id.startsWith("kimi") && !m.id.startsWith("moonshot")) return null;30 const cat = KIMI_CATALOG.get(m.id);31 const reasoning = m.supports_reasoning ?? true;32 const efforts = m.reasoning_efforts?.valid_efforts ?? m.think_efforts?.valid_efforts ?? ["low", "high", "max"];33 const alwaysThinking = m.supports_thinking_type === "only";34 return {35 key: modelKey("kimi", m.id),36 id: m.id,37 provider: "kimi",38 displayName: cat?.displayName ?? prettify(m.id),39 family: cat?.family ?? (m.id.startsWith("kimi-k3") ? "Kimi K3" : "Kimi K2"),40 capabilities: {41 text: true,42 vision: m.supports_image_in ?? true,43 audioInput: false,44 audioOutput: false,45 imageGeneration: false,46 video: m.supports_video_in ?? false,47 reasoning,48 tools: true,49 structuredOutput: true,50 streaming: true,51 files: false,52 webSearch: false, // `$web_search` builtin exists but its round trip is not stable enough to expose yet53 ...(cat?.capabilityOverrides ?? {}),54 },55 limits: { contextTokens: m.context_length ?? cat?.limits?.contextTokens, maxOutputTokens: cat?.limits?.maxOutputTokens ?? 32_768 },56 parameters: {57 temperature: false, // only 1 accepted58 topP: false, // only 0.95 accepted59 topK: false,60 maxTokens: true,61 stop: true,62 seed: false,63 frequencyPenalty: false, // only 0 accepted64 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 thinkingBudget: false,68 ...(cat?.parameters ?? {}),69 },70 status: cat?.status ?? "active",71 pricing: cat?.pricing ?? null,72 metadata: {73 jsonSchema: false,74 jsonWordRequired: true,75 ...(cat?.metadata ?? {}),76 alwaysThinking,77 defaultReasoningEffort: m.reasoning_efforts?.default_effort ?? m.think_efforts?.default_effort,78 dynamicTools: m.supports_dynamic_tools,79 createdAt: m.created ? new Date(m.created * 1000).toISOString() : undefined,80 sortWeight: cat?.sortWeight ?? (m.id.startsWith("kimi-k3") ? 100 : m.id.includes("2.7-code-highspeed") ? 88 : m.id.includes("2.7") ? 90 : 85),81 },82 };83}8485function prettify(id: string) {86 return id.replace(/^kimi-/, "Kimi ").replace(/-code/, " Code").replace(/-highspeed/, " High-speed").replace(/\bk(\d)/, "K$1");87}8889async function list(_client: OpenAI, apiKey: string, signal?: AbortSignal): Promise<PolyModel[]> {90 const headers = { Authorization: `Bearer ${apiKey}` };91 const res = await fetch("https://api.moonshot.ai/v1/models", { headers, signal });92 if (!res.ok) {93 const body = (await res.json().catch(() => ({}))) as { error?: { message?: string; type?: string } };94 throw Object.assign(new Error(body.error?.message ?? `HTTP ${res.status}`), { status: res.status, error: body, headers: res.headers });95 }96 const data = (await res.json()) as { data: { id: string }[] };97 const details = await Promise.all(98 data.data.map(async (m) => {99 const r = await fetch(`https://api.moonshot.ai/v1/models/${encodeURIComponent(m.id)}`, { headers, signal }).catch(() => null);100 return r && r.ok ? ((await r.json()) as KimiModel) : ({ id: m.id } as KimiModel);101 }),102 );103 return details.map(normalizeKimiModel).filter((m): m is PolyModel => Boolean(m));104}105106export const kimiAdapter = createOpenAICompatAdapter({107 id: "kimi",108 name: "Kimi (Moonshot AI)",109 baseURL: "https://api.moonshot.ai/v1",110 keyDocsUrl: "https://platform.moonshot.ai/console/api-keys",111 keyPrefixHint: "sk-",112 listModels: list,113 useMaxTokens: true,114 messageOptions: { inlineFiles: true, replayReasoningContent: true },115 tweakParams: (params, settings, req) => {116 const p = params as unknown as Record<string, unknown>;117 const meta = (req.modelInfo?.metadata ?? {}) as Record<string, unknown>;118 delete p.temperature; // only the default (1) is accepted119 delete p.top_p; // only 0.95 is accepted120 delete p.seed;121 delete p.frequency_penalty; // only 0 is accepted122 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" };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";136 },137 refineError: (status, code, message) => {138 if (status === 401 || code === "invalid_authentication_error") return "INVALID_API_KEY";139 if (status === 403 && /balance|quota|insufficient/i.test(message)) return "INSUFFICIENT_CREDITS";140 if (status === 404) return "MODEL_NOT_FOUND";141 if (code === "exceeded_current_quota_error" || code === "rate_limit_reached_error" || code === "engine_overloaded_error") return "RATE_LIMITED";142 if (code === "content_filter") return "CONTENT_REJECTED";143 if (/only 1 is allowed|only 0.95 is allowed/i.test(message)) return "INVALID_PARAMETER";144 return undefined;145 },146});147