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%
34.4 KB

# Kimi (Moonshot AI, international platform) — provider research for PolyLLM

Last documentation audit: 2026-09-08 Probes executed 2026-09-08 with a real key against https://api.moonshot.ai/v1 (scripts in research/kimi/, raw outputs in research/kimi/out/). Everything marked (probed) was observed live; everything marked (docs) comes from the pages listed at the end.

Heads-up: every https://platform.moonshot.ai/docs/... URL in the brief now 301-redirects to https://platform.kimi.ai/docs/... (the platform was rebranded "Kimi Open Platform"). The API host did not move: https://api.moonshot.ai/v1 is the only documented base URL (api.kimi.ai → 404, api.moonshot.cn is the separate China platform whose keys are not interchangeable). The full doc index is https://platform.kimi.ai/docs/llms.txt; the sitemap.xml only lists two marketing pages.


# 1. Base URL, auth, headers

Item Value
REST base URL https://api.moonshot.ai/v1 (OpenAI Chat Completions + Responses). Anthropic-compatible Messages API at https://api.moonshot.ai/anthropic (/v1/messages, kimi-k3 only) (docs)
Auth Authorization: Bearer <KIMI_API_KEY>
Content type application/json. Malformed field → 400 {"error":{"message":"Invalid request: the \messages` field in the request (expected type []object) is illegal, and string is not acceptable","type":"invalid_request_error"}}` (probed)
Optional request header X-Msh-Request-Nonce (UUID v4) → response headers Msh-Request-Timestamp + Msh-Request-Signature for signature verification (docs)
Response headers (probed) msh-request-id, x-msh-trace-id, msh-gid (account tier, e.g. enterprise-tier-2), msh-org-id, msh-project-id, msh-uid, msh-context-cache-token-saved: <n> (cache hit size), msh-context-cache-token-boosted-nvme. No x-ratelimit-* and no retry-after headers, even on 429. Behind Cloudflare (cf-ray).
Key validation GET /v1/models (200 with a valid key, 401 invalid_authentication_error with a bad one) or GET /v1/users/me/balance → {"code":0,"data":{"available_balance":54.78,"voucher_balance":4.78,"cash_balance":50},"scode":"0x0","status":true} (probed) — cheap "validate key" + balance display.
Token counting POST /v1/tokenizers/estimate-token-count {model, messages} → {"code":0,"data":{"total_tokens":93},...} (probed); accepts image/video parts (docs).

# 2. SDK recommendation (TypeScript / Node)

  • No official Moonshot/Kimi JS SDK (npm moonshot, kimi-ai, @moonshot-ai/sdk, @moonshotai/sdk, kimi-sdk, moonshotai, @ai-sdk/moonshot, @ai-sdk/kimi do not exist / are unrelated — checked 2026-09-08). Docs recommend the official OpenAI SDK (Python/Node) with base_url swapped, or the Anthropic SDK against /anthropic. All doc samples are curl + Python; no JS samples.
  • Recommendation for PolyLLM: OpenAI SDK openai@7.10.0 with baseURL: "https://api.moonshot.ai/v1", maxRetries: 0 (the SDK's default 2 retries count against the 100 RPM quota — docs warn about this), long timeout. Kimi-specific fields (thinking, reasoning_effort non-OpenAI values, partial) must be passed as extra body fields (cast). Smoke test (probed): client.chat.completions.create({..., stream:true, thinking:{type:"enabled"}} as any) streams delta.reasoning_content + tool_calls fine.

# 3. Endpoints

Endpoint Status Notes
POST /v1/chat/completions primary OpenAI-compatible; all 4 models; reasoning_content; builtin $web_search. (probed)
POST /v1/responses active Docs: kimi-k3 only — kimi-k2.6 also answered 200 (probed). Stateless (store/previous_response_id/conversation always false/null, docs). Server-side web_search tool. SSE with event: lines. usage.input_tokens_details.cache_write_tokens. (probed)
POST /anthropic/v1/messages active Anthropic SDK compatible, kimi-k3 only, output_tokens_details.thinking_tokens (docs; not probed)
GET /v1/models, GET /v1/models/{id} active Rich flags, see §12. (probed)
GET /v1/users/me/balance active §1 (probed)
POST /v1/tokenizers/estimate-token-count active §1 (probed)
POST/GET/DELETE /v1/files, /v1/files/{id}/content active purposes file-extract, image, video, batch; 100 MB/file, 1 000 files, 10 GB; extraction "temporarily free"; new ids file_… since 2026-08-31; images/videos referenced as ms://<file_id> (docs)
GET /v1/formulas, GET /v1/formulas/{ns}/{name}[:latest]/tools, POST /v1/formulas/{uri}/fibers active "Official tools" (web-search, fetch, convert, code-runner, quickjs, excel, memory, …). (probed) §11
POST /v1/batch/* active 60 % of list price (docs)

# 4. Chat Completions request/response (probed shapes)

Request fields (docs API ref + OpenAPI + probes): model, messages, max_completion_tokens (default 131 072 on K3 / 32 768 on K2.x, max 1 048 576 on K3) — max_tokens accepted as alias (probed), stream, stream_options.include_usage, stop (≤ 5 strings, ≤ 32 bytes each), response_format (text|json_object|json_schema), tools, tool_choice, parallel_tool_calls, thinking ({type:"enabled"|"disabled", keep?: "all"|null}), reasoning_effort (low|high|max, K3), logprobs/top_logprobs (0–20), prediction ({type:"content", content}), prompt_cache_key, safety_identifier, partial (on the last assistant message), temperature/top_p/n/presence_penalty/frequency_penalty (all fixed, §9), seed (accepted, undocumented).

Roles: system, user, assistant, tool. developer → 400 Invalid request: role 'developer' is not allowed on all 4 models (probed) — map developer→system in the adapter. Unknown top-level params (foo_bar) are silently ignored (probed). Tool messages: {role:"tool", tool_call_id, name, content} (docs include name; K3 builtin round trip failed without it — see §11).

Non-streaming response (probed, kimi-k2.6):

json
{ "id": "chatcmpl-6a9fabafa11938d17f75e852", "object": "chat.completion", "created": 1788849073, "model": "kimi-k2.6",
  "choices": [{ "index": 0, "finish_reason": "stop",
    "message": { "role": "assistant", "content": "Two plus two equals four.",
                 "reasoning_content": "The user is asking a simple math question…" } }],
  "usage": { "prompt_tokens": 20, "completion_tokens": 78, "total_tokens": 98,
             "completion_tokens_details": { "reasoning_tokens": 70 } } }
  • completion_tokens INCLUDES reasoning tokens (78 = 70 reasoning + 8 visible); total_tokens = prompt + completion. Bill reasoning at the output price.
  • On a cache hit usage gains cached_tokens at the top level AND prompt_tokens_details.cached_tokens (same value) (probed). No system_fingerprint in non-streamed responses (present in every stream chunk, e.g. fpv0_7f4e9b4b).
  • finish_reason: stop, length, tool_calls (probed). max_tokens caps reasoning + content together: max_tokens: 200 on K2.6/K3 → finish_reason: "length" with empty content after 199 reasoning tokens (probed). Docs recommend max_tokens ≥ 16000 for thinking models. Adapter rule: never send a small max_tokens to a thinking model; treat length with empty content as "thinking truncated".
  • Hidden system prompt: ~17 prompt tokens overhead on K2.x, ~92–98 on kimi-k3 (25 when thinking disabled) (probed).
  • Non-thinking K2.6 responses still carry reasoning_content: "" when tools are called (probed).

# 5. Streaming protocol (chat completions) (probed on all 4 models)

SSE text/event-stream, data: {json} lines only (no event: field), terminated by data: [DONE] (docs insist: end on [DONE], not on finish_reason).

json
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","created":1788849079,"model":"kimi-k2.6",
       "choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}],"system_fingerprint":"fpv0_7f4e9b4b"}
data: {…"choices":[{"index":0,"delta":{"reasoning_content":"The"},"finish_reason":null}]…}
…
data: {…"choices":[{"index":0,"delta":{"content":"Bon"},"finish_reason":null}]…}
data: {…"choices":[{"index":0,"delta":{},"finish_reason":"stop",
        "usage":{"prompt_tokens":17,"completion_tokens":96,"total_tokens":113,"cached_tokens":17,
                 "completion_tokens_details":{"reasoning_tokens":86},"prompt_tokens_details":{"cached_tokens":17}}}]…}
data: {…"choices":[],"usage":{…same…}}          ← only with stream_options.include_usage
data: [DONE]
  • Delta keys observed: role, content, reasoning_content, tool_calls. First chunk is always {"role":"assistant","content":""}; reasoning deltas precede content deltas; K3 may emit content text before tool_calls ("I'll check the weather…") (probed).
  • Usage is ALWAYS present inside the finish chunk's choice (choices[0].usage, non-standard) even without stream_options; include_usage: true additionally appends the standard choices: [] chunk with top-level usage (probed). The OpenAI SDK only surfaces the latter → keep sending include_usage: true.
  • Tool-call deltas (OpenAI-standard fragmenting): first fragment {"index":0,"id":"get_weather_0","type":"function","function":{"name":"get_weather","arguments":""}}, then {"index":0,"function":{"arguments":"{\""}} … (6–8 fragments for one call) → concatenate by index, finish_reason: "tool_calls" (probed). Tool-call ids are <function_name>_<index> (get_weather_0) — not unique across turns; builtin ids are t-web_search-<hex>.

# 6. Tool / function calling (probed on all 4 models)

  • Format: OpenAI nested {type:"function", function:{name, description, parameters}}; names [A-Za-z0-9_-], must be unique per request. Parallel calls possible (model returns several tool_calls); parallel_tool_calls: true|false accepted everywhere with no error (probed).
  • tool_choice: auto (default), none, required, {type:"function",function:{name}}. With thinking enabled: required → 400 tool_choice 'required' is incompatible with thinking enabled on K2.6 / K2.7-code / K2.7-code-highspeed (allowed on K3); forcing a specific function → 400 tool_choice 'specified' is incompatible with thinking enabled on all four (works on K2.6 with thinking.type: "disabled") (probed).
  • Round trip (assistant message with tool_calls + {role:"tool", tool_call_id, name, content}) works on every model (probed). Docs: forgetting the assistant message → "tool_call_id not found".
  • reasoning_content replay: docs say to always send back the complete assistant message (incl. reasoning_content) for thinking models; K2.7-code has Preserved Thinking always on, K2.6 only with thinking.keep: "all". Probed: omitting reasoning_content, or sending "", did NOT produce an error on any of the 4 models — the round trip completed with 200 and a correct answer. So it is a quality requirement, not a protocol one. Adapter: store and replay reasoning_content on assistant messages with tool_calls (cheap: it is billed as prompt tokens), but do not fail if it is missing.
  • Dynamic tool loading (K3 only): {role:"system", tools:[…]} message without content injects tools mid-conversation; on other models → tokenization error (docs).

# 7. Structured output (probed)

  • response_format: {type:"json_object"} → valid JSON object on all 4 models (mention JSON in the prompt; only JSON objects, never arrays — docs).
  • response_format: {type:"json_schema", json_schema:{name, strict:true, schema}} ("Moonshot Flavored JSON Schema" MFJS spec): valid, schema-conformant JSON on K3, K2.7-code, K2.7-code-highspeed. kimi-k2.6 with thinking enabled ignored the schema and returned prose twice (probed, 3 000 max_tokens); with thinking: disabled it returned valid JSON preceded by ~300 blank lines. Docs themselves say K2.6 "occasionally behaves unstably with complex schemas". Adapter: mark K2.6 structured output as unreliable / validate client-side; prefer K3 or K2.7-code.
  • Thinking models still emit reasoning_content; parse only message.content. A too-small max_tokens truncates the JSON (finish_reason: length).

# 8. Reasoning controls (probed matrix)

Model Thinking thinking param reasoning_effort reasoning_content
kimi-k3 always on (docs); /models: supports_thinking_type: "only", think_efforts: low/high/max, default max {type:"enabled"} ✓, {type:"enabled",keep:"all"} ✓, {type:"disabled"} → 200 with NO reasoning (undocumented, prompt overhead drops 92→25 tokens) low/high/max (docs, default max) ✓; medium accepted; none → 200 with no reasoning (undocumented) yes (non-stream + delta.reasoning_content)
kimi-k2.6 on by default, switchable enabled (default, temp 1.0) / disabled (temp 0.6) / enabled+keep:"all" ✓ not documented for K2.6 but **accepted: `low medium
kimi-k2.7-code always on only {type:"enabled"[,keep:"all"]}; disabled → 400 invalid thinking: only type=enabled is allowed for this model `low medium
kimi-k2.7-code-highspeed always on same as k2.7-code same as k2.7-code yes
  • /v1/models exposes reasoning_efforts.valid_efforts only for K3 — drive the UI from that. No thinking-budget parameter. Reasoning tokens are inside completion_tokens and billed as output. thinking.keep: "all" keeps historical reasoning in context (extra prompt tokens).
  • Responses API: reasoning: {effort} → {type:"reasoning", summary:[{type:"summary_text", text}]} output item (full text, not a summary) (probed).

# 9. Sampling & other parameters — support matrix (probed, chat completions)

Param k3 k2.7-code k2.7-code-highspeed k2.6
temperature only 1 (0, 0.5, 1.5, 2 → 400) only 1 only 1 only 1 with thinking (docs: 0.6 fixed when disabled)
top_p only 0.95 only 0.95 only 0.95 only 0.95
n only 1 only 1 only 1 only 1
presence_penalty / frequency_penalty only 0 only 0 only 0 only 0
max_tokens / max_completion_tokens ✓ (both) ✓ ✓ ✓
stop (≤ 5) ✓ (6 → 400 stop array too long…maximum length 5) ✓ ✓ ✓
seed ✓ accepted (undocumented) ✓ ✓ ✓
logprobs/top_logprobs 400 invalid logprobs: only false is allowed for this model ✓ (logprobs returned) 429 engine_overloaded_error on every attempt (8/8) — treat as unsupported ✓
response_format json_object / json_schema ✓ / ✓ ✓ / ✓ ✓ / ✓ ✓ / unreliable
thinking see §8 enabled only enabled only ✓
reasoning_effort ✓ tolerated tolerated tolerated
tool_choice: required ✓ 400 (thinking) 400 (thinking) 400 (thinking)
tool_choice: {function} 400 (thinking) 400 400 400 (✓ when thinking disabled)
parallel_tool_calls ✓ ✓ ✓ ✓
developer role 400 400 400 400
unknown params ignored ignored ignored ignored

Exact rejection text: {"error":{"message":"invalid temperature: only 1 is allowed for this model","type":"invalid_request_error"}} (same pattern for top_p: only 0.95, n: only 1, presence_penalty: only 0, frequency_penalty: only 0, logprobs: only false). Adapter rule: do not expose temperature/top_p/penalties/n for Kimi at all (or send only the fixed values); expose stop, max_tokens, thinking (K2.6), reasoning_effort (K3), JSON modes, tools.

# 10. Modalities, context, output limits

  • Input: text + image + video on all 4 models (supports_image_in / supports_video_in: true in /models; vision probed OK on all 4 with a 32×32 PNG data URL). Output: text only. No audio, no image generation.
  • Image parts: {type:"image_url", image_url:{url:"data:image/png;base64,…"}} and the string form {type:"image_url", image_url:"data:…"} both accepted (probed); {type:"video_url", video_url:{url}} for video (docs). Public HTTP(S) URLs are rejected: 400 Invalid request: unsupported image url: https://… (probed) — the adapter must download and inline as base64 (or upload via /v1/files purpose image and reference ms://<file_id>). Formats: jpeg/png/gif/webp/bmp/heic/heif (animated gif/webp billed as video), video mp4/mpeg/mov/avi/x-flv/mpg/webm/wmv/3gpp; recommended ≤ 4K images, ≤ 1080p video, request body ≤ 100 MB (docs). A 32×32 PNG cost ≈ 7 prompt tokens; a 2×2 PNG was accepted but effectively ignored (0 extra tokens, K3 said "I don't see an image") (probed). Image tokens are billed as normal input tokens (no separate image price on the pricing pages).
  • Context windows (/models.context_length, matches docs): kimi-k3 1 048 576; kimi-k2.6 / k2.7-code / k2.7-code-highspeed 262 144. Overflow → 400 Invalid request: Your request exceeded model token limit: 262144 (requested: 540015) (probed).
  • Max output: docs — K3 default max_completion_tokens 131 072, max 1 048 576; K2.x default 32 768. The API does not validate the cap: max_tokens: 300000 on K2.x and 2000000 on K3 returned 200 (probed) — the adapter must clamp itself. Docs' 504 advice: use streaming for long generations.
  • Files: /v1/files purpose file-extract → GET /v1/files/{id}/content text that you put in a system message (no native file attachment in chat); "temporarily free" (docs).

# 11. Web search & official tools (probed)

Three different mechanisms exist; pricing $0.005 per successful search call + result tokens billed as prompt tokens (docs /pricing/tools).

  1. Builtin $web_search on chat completions — tools:[{type:"builtin_function", function:{name:"$web_search"}}]. The search is executed server-side during step 1: the model returns finish_reason: "tool_calls" with {"id":"t-web_search-6a9faef21a9e","type":"builtin_function","function":{"name":"$web_search","arguments":"{\"search_result\":{\"search_id\":\"7dd6…\"},\"usage\":{\"total_tokens\":7247}}"}} (streamed in 2 chunks: header with arguments:"", then the full arguments string) and usage.completion_tokens: 1. Step 2 = echo the assistant message verbatim + {role:"tool", tool_call_id, name:"$web_search", content: <arguments verbatim>}. Works on kimi-k2.6 and kimi-k2.7-code (K2.6: prompt_tokens 7 337, answer with Yahoo Finance URL inline; K2.7-code: 9 275) (probed). Broken on kimi-k3: step 2 → 400 Invalid request: tokenization failed whatever we replayed (verbatim, without/with empty reasoning_content, with/without reasoning_effort); changing type to "function" gets a 200 but the search content is not injected (265 prompt tokens, model says the tool returned nothing). No citation structure: the URL appears only in the answer text.
  2. Official tools ("formulas") — the documented path for K3: GET /v1/formulas/moonshot/web-search:latest/tools → [{type:"function", function:{name:"web_search", parameters:{query, classes?: [all|academic|social|library|finance|code|ecommerce|medical]}}}]; pass them as normal tools; when the model calls web_search run POST /v1/formulas/moonshot/web-search:latest/fibers {name:"web_search", arguments:"<json string>"} → {id:"fiber-…", object:"fiber", status:"succeeded", context:{input, encrypted_output:"----MOONSHOT ENCRYPTED BEGIN----…----MOONSHOT ENCRYPTED END----", references}}; send encrypted_output as the tool message content → 200 on K3 (step 2 prompt 1 594 tokens, answer with source URL) (probed). 12 public formulas listed by GET /v1/formulas (fetch, convert, web-search, code-runner, quickjs, excel, memory, date, base64, rethink, random-choice, mew); only web-search is billed. context.references may hold structured sources (not inspected).
  3. Responses API tools:[{type:"web_search"}] on kimi-k3 — fully server-side, one request: events response.web_search_call.in_progress/searching/completed, output items web_search_call (action:{type:"search", query}), reasoning, message; annotations: [] (no url_citation), URL only inline; usage 7 888 input tokens for one search (probed). Options allowed_domains (≤ 100), one web_search tool per request; search_context_size/user_location unsupported (docs).

Adapter recommendation: web search = capability of K2.6 / K2.7-code via $web_search (simple 2-step, chat completions) and K3 via formulas or Responses; do not advertise $web_search on K3. Docs for K2.6 also say web search is incompatible with thinking on that model — probed OK with thinking on.

# 12. Model listing & pricing (docs + probed)

GET /v1/models (probed) fields: id, object, created, owned_by:"moonshot", permission[], root, parent, context_length, supports_image_in, supports_video_in, supports_reasoning, plus on K3 only: supports_dynamic_tools: true, think_efforts / reasoning_efforts: {support:true, valid_efforts:["low","high","max"], default_effort:"max"}, supports_thinking_type: "only". No pricing, no max output in the API.

Model ctx in / cache-hit / out ($/M) Batch (60 %) Notes
kimi-k3 1 048 576 3.00 / 0.30 / 15.00 not listed flagship, 2.8 T params, reasoning_effort, dynamic tools, Responses + Messages APIs
kimi-k2.7-code 262 144 0.95 / 0.19 / 4.00 0.57 / 0.114 / 2.40 coding, thinking always on
kimi-k2.7-code-highspeed 262 144 1.90 / 0.38 / 8.00 — same model, ~180–260 tok/s, exactly 2× price
kimi-k2.6 262 144 0.95 / 0.16 / 4.00 0.57 / 0.10 / 2.40 thinking switchable, cheapest cache hits

"Cache miss" price == normal input price; no cache write fee, no storage fee. Web search $0.005/call. Prices exclude taxes. File extraction free (temporary). Retired (404 resource_not_found_error) (probed): kimi-k2.5, moonshot-v1-* (2026-08-31), kimi-k2-*-preview (2026-05-25), kimi-k2-thinking, kimi-latest (2026-01-28), kimi-thinking-preview (2025-11-11). No aliases exist.

# 13. Prompt caching

  • Automatic on every model, prefix-based, no configuration; only requests whose prompt exceeds 256 tokens are cached (docs). Reported as usage.cached_tokens + usage.prompt_tokens_details.cached_tokens and header msh-context-cache-token-saved (probed: 821-token prompt → second call cached_tokens: 512, i.e. 256-token blocks; the ~17/92-token hidden prefix is cached on the very first call). Cache-hit price ≈ 10–17 % of input. prompt_cache_key (chat + Responses) improves hit rate for similar requests (docs). No TTL documented.
  • Provider-side state: none — Responses API is stateless here (store: false, previous_response_id: null, docs + probed echo). Nothing to opt out of for a BYOK app.

# 14. Errors, rate limits, retries

Error body: OpenAI-style {"error":{"message":"…","type":"…"}} (no code observed; docs mention an optional code).

Case (probed) HTTP body
Invalid key 401 {"error":{"message":"Invalid Authentication","type":"invalid_authentication_error"}}
No Authorization header 401 {"error":{"message":"Incorrect API key provided","type":"incorrect_api_key_error"}}
Unknown / retired model 404 {"error":{"message":"Not found the model kimi-k99 or Permission denied","type":"resource_not_found_error"}}
Fixed param violated 400 invalid temperature: only 1 is allowed for this model (invalid_request_error)
developer role 400 Invalid request: role 'developer' is not allowed
Context overflow 400 Invalid request: Your request exceeded model token limit: 262144 (requested: 540015)
Empty/malformed messages 400 Invalid request: messages must not be empty / …field in the request (expected type []object) is illegal…
Public image URL 400 Invalid request: unsupported image url: …
tool_choice vs thinking 400 tool_choice 'required' is incompatible with thinking enabled / tool_choice 'specified' is incompatible with thinking enabled
Builtin replay on K3 400 Invalid request: tokenization failed
Concurrency/RPM 429 {"error":{"message":"Organization Rate limit exceeded, please try again after 1 seconds","type":"rate_limit_reached_error"}} and …request reached organization max RPM: 100, please try again after 1 seconds — no retry-after header
Engine busy 429 {"error":{"message":"The engine is currently overloaded, please try again later","type":"engine_overloaded_error"}}
Content filter / quota 400 / 429 content_filter "The request was rejected because it was considered high risk"; exceeded_current_quota_error "Account balance is insufficient" (docs)
Server 500 / 503 / 504 server_error, server_unavailable, gateway timeout → use streaming (docs)

Rate limits (docs /pricing/limits, per user/organization, computed with max_completion_tokens): Tier0 ($1 paid) 1 concurrent / 3 RPM / 500k TPM / 1.5M TPD; Tier1 ($10) 15 / 100 / 2M / ∞; Tier2 ($20) 40 / 100 / 3M; Tier3 ($100) 50 / 200 / 3M; Tier4 ($1 000) 60 / 200 / 4M; Tier5 ($3 000) 100 / 300 / 5M. Our key (msh-gid: enterprise-tier-2) hit the 100 RPM ceiling during the parameter matrix (probed). Interrupted 429 requests are not charged (docs). Risk-control throttling is permanent (docs).

Retry/timeout recommendation: retry rate_limit_reached_error / engine_overloaded_error / 5xx with jittered backoff starting at ~1–3 s (the message says "after 1 seconds"; no header), cap attempts (engine_overloaded persisted 8× for highspeed+logprobs — surface it); never retry 400/401/404; disable SDK auto-retries (they eat RPM). Use streaming for anything long (504 risk) and a read timeout ≥ 5 min (K3 max effort). Map 401 (invalid_authentication_error / incorrect_api_key_error) to the app's invalid-credentials state; map 404 resource_not_found_error to "model unavailable for this key/region" (the same error is used for a China-platform key on the international host).

# 15. Lifecycle / naming

  • 4 active models, no aliases, no -latest. K2.6 = general (Apr 2026 era), K2.7-code / -highspeed = coding, K3 = flagship (1M ctx). Migration guidance: everything retired → kimi-k3.
  • Docs domain renamed to platform.kimi.ai (2026); API host unchanged. Keys are platform-specific (international vs .cn vs Kimi Code) — the same 401/404 errors are returned for a wrong-platform key (docs troubleshooting).
  • Partial mode ({role:"assistant", content:"prefix", partial:true} as last message) works (probed on K2.6 non-thinking; output continues the prefix); with thinking models also replay reasoning_content and give a large max_tokens (docs).
  • K3 believed the current date was 2026-04-25 in a formula-tool answer (system-prompt date), while Responses web_search answered with the real date — do not rely on the model's date.

# 16. Exact streaming code that worked

ts
import OpenAI from "openai";
const client = new OpenAI({ apiKey: process.env.KIMI_API_KEY, baseURL: "https://api.moonshot.ai/v1", timeout: 600_000, maxRetries: 0 });

const stream = await client.chat.completions.create({
  model: "kimi-k2.6",
  messages: [{ role: "user", content: "Weather in Montreal? Use the tool." }],
  tools: [{ type: "function", function: { name: "get_weather", parameters: { type: "object", properties: { city: { type: "string" } }, required: ["city"] } } }],
  stream: true,
  stream_options: { include_usage: true },
  max_tokens: 16_000,                       // must cover reasoning + answer
  ...({ thinking: { type: "enabled" } } as any),   // K2.6 only; K3 uses reasoning_effort: "low"|"high"|"max"
});
const acc = { reasoning: "", content: "", toolCalls: [] as any[] };
for await (const chunk of stream) {
  const d = (chunk.choices[0]?.delta ?? {}) as any;
  if (d.reasoning_content) acc.reasoning += d.reasoning_content;      // thinking pane
  if (d.content) acc.content += d.content;
  for (const tc of d.tool_calls ?? []) {                              // fragmented by index
    const t = (acc.toolCalls[tc.index] ??= { id: "", type: "function", function: { name: "", arguments: "" } });
    if (tc.id) t.id = tc.id; if (tc.function?.name) t.function.name += tc.function.name;
    if (tc.function?.arguments) t.function.arguments += tc.function.arguments;
  }
  if (chunk.choices[0]?.finish_reason) onFinish(chunk.choices[0].finish_reason); // stop | length | tool_calls
  if (chunk.usage) onUsage(chunk.usage);   // final choices: [] chunk; reasoning_tokens inside completion_tokens
}
// replay: { role:"assistant", content: acc.content || null, reasoning_content: acc.reasoning, tool_calls: acc.toolCalls }

Raw fetch SSE (research/kimi/lib.ts → rawSSE): split on \n\n, take data: lines, JSON.parse, stop at [DONE]. For /v1/responses the stream has event: <name> + data: pairs; the JSON type field equals the event name.

# 17. Probe results table

# Probe Model(s) Result
00 GET /models, /models/{id}, /users/me/balance, POST /tokenizers/estimate-token-count — all 200; 4 models with context_length, image/video/reasoning flags, K3 think_efforts; balance 54.79 USD; estimate 93 tokens
01a tiny chat completion max_tokens: 200 4 models all 200; reasoning_content on all 4; completion_tokens includes reasoning_tokens; K3 7.1 s, highspeed 1.0 s
01b streaming with/without include_usage 4 models data: only; role→reasoning_content→content; usage in finish chunk's choice always + choices: [] chunk with include_usage; cached_tokens top-level and in prompt_tokens_details; K2.6/K3 hit length at 200 tokens inside reasoning
02 param matrix (38 variants) 4 models §9: temperature/top_p/n/penalties fixed (400 otherwise); stop ≤ 5; seed ok; developer 400; logprobs K3 400 / highspeed 429; thinking.disabled ok on K2.6 and K3, 400 on K2.7; reasoning_effort medium/none accepted on K3/K2.6, none 400 on K2.7; tool_choice required 400 on K2.x, function 400 on all (thinking); json modes ok; unknown params ignored; hit 100 RPM (429 bodies captured)
03 streamed function call round trip; replay with / without / empty reasoning_content 4 models (+K2.6 thinking off) fragmented tool_calls deltas, ids get_weather_0, finish_reason: tool_calls; all three replay variants 200 on all models; K2.6 non-thinking returns reasoning_content: ""
04 json_object, json_schema strict, vision 32×32 / 2×2 PNG (object + string image_url), public URL 4 models json_object valid on 4/4; json_schema valid on K3/K2.7/K2.7-hs, prose on K2.6; vision 200 on 4/4, ≈7 tokens per 32×32, 2×2 ignored; public URL 400
05 invalid key / no auth / unknown + 5 retired models / malformed / missing messages / oversize max_tokens / 540k-token prompt / forced tool w/o thinking / cache / partial mixed 401 ×2, 404 ×6, 400 ×2, oversize max_tokens accepted (K2.x 300k, K3 2M), overflow 400 with exact limit, forced tool ok on K2.6 non-thinking, cached_tokens: 512 on 2nd call, partial mode continues prefix
06/06b $web_search builtin, streamed then variants kimi-k3 step 1 ok (server-side search, search_id, 7.2k search tokens); step 2 400 tokenization failed for verbatim / no reasoning / empty reasoning / no name; type:"function" → 200 but no search content
07 Responses API web_search (stream) + plain; Responses on K2.6; $web_search on K2.6 + K2.7-code; GET /formulas* k3, k2.6, k2.7-code Responses: 16 event types, web_search_call item, annotations: [], 7 888 input tokens; K2.6 Responses 200; $web_search round trip 200 on K2.6 (7 337 prompt tokens) and K2.7-code step 1 ok; formulas list 12 tools, web-search declaration fetched
08 K3 $web_search default effort; K2.7-code $web_search step 2; formulas fiber round trip on K3; K2.6 json_schema thinking on/off; highspeed logprobs; OpenAI SDK smoke test mixed K3 builtin still 400; K2.7-code step 2 200 (9 275 prompt tokens); fiber succeeded with encrypted_output → K3 answer with URL (200); K2.6 schema prose (thinking) / JSON after blank lines (no thinking); highspeed logprobs 429 ×8; SDK streams reasoning_content + tool call + usage

# Documentation pages used (all fetched 2026-09-08; every platform.moonshot.ai URL redirected 301 to platform.kimi.ai)