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 tohttps://platform.kimi.ai/docs/...(the platform was rebranded "Kimi Open Platform"). The API host did not move:https://api.moonshot.ai/v1is the only documented base URL (api.kimi.ai→ 404,api.moonshot.cnis the separate China platform whose keys are not interchangeable). The full doc index ishttps://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/kimido not exist / are unrelated — checked 2026-09-08). Docs recommend the official OpenAI SDK (Python/Node) withbase_urlswapped, or the Anthropic SDK against/anthropic. All doc samples are curl + Python; no JS samples. - Recommendation for PolyLLM: OpenAI SDK
openai@7.10.0withbaseURL: "https://api.moonshot.ai/v1",maxRetries: 0(the SDK's default 2 retries count against the 100 RPM quota — docs warn about this), longtimeout. Kimi-specific fields (thinking,reasoning_effortnon-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)streamsdelta.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):
{ "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_tokensINCLUDES reasoning tokens (78 = 70 reasoning + 8 visible);total_tokens = prompt + completion. Bill reasoning at the output price.- On a cache hit usage gains
cached_tokensat the top level ANDprompt_tokens_details.cached_tokens(same value) (probed). Nosystem_fingerprintin non-streamed responses (present in every stream chunk, e.g.fpv0_7f4e9b4b). finish_reason:stop,length,tool_calls(probed).max_tokenscaps reasoning + content together:max_tokens: 200on K2.6/K3 →finish_reason: "length"with emptycontentafter 199 reasoning tokens (probed). Docs recommendmax_tokens ≥ 16000for thinking models. Adapter rule: never send a small max_tokens to a thinking model; treatlengthwith 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).
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 emitcontenttext beforetool_calls("I'll check the weather…") (probed). - Usage is ALWAYS present inside the finish chunk's choice (
choices[0].usage, non-standard) even withoutstream_options;include_usage: trueadditionally appends the standardchoices: []chunk with top-levelusage(probed). The OpenAI SDK only surfaces the latter → keep sendinginclude_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 byindex,finish_reason: "tool_calls"(probed). Tool-call ids are<function_name>_<index>(get_weather_0) — not unique across turns; builtin ids aret-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 severaltool_calls);parallel_tool_calls: true|falseaccepted everywhere with no error (probed). tool_choice:auto(default),none,required,{type:"function",function:{name}}. With thinking enabled:required→ 400tool_choice 'required' is incompatible with thinking enabledon K2.6 / K2.7-code / K2.7-code-highspeed (allowed on K3); forcing a specific function → 400tool_choice 'specified' is incompatible with thinking enabledon all four (works on K2.6 withthinking.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_contentreplay: 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 withthinking.keep: "all". Probed: omittingreasoning_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 replayreasoning_contenton assistant messages withtool_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 withoutcontentinjects 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); withthinking: disabledit 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 onlymessage.content. A too-smallmax_tokenstruncates 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/modelsexposesreasoning_efforts.valid_effortsonly for K3 — drive the UI from that. No thinking-budget parameter. Reasoning tokens are insidecompletion_tokensand 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: truein/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: 400Invalid request: unsupported image url: https://…(probed) — the adapter must download and inline as base64 (or upload via/v1/filespurposeimageand referencems://<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 → 400Invalid request: Your request exceeded model token limit: 262144 (requested: 540015)(probed). - Max output: docs — K3 default
max_completion_tokens131 072, max 1 048 576; K2.x default 32 768. The API does not validate the cap:max_tokens: 300000on K2.x and2000000on K3 returned 200 (probed) — the adapter must clamp itself. Docs' 504 advice: use streaming for long generations. - Files:
/v1/filespurposefile-extract→GET /v1/files/{id}/contenttext 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).
- Builtin
$web_searchon chat completions —tools:[{type:"builtin_function", function:{name:"$web_search"}}]. The search is executed server-side during step 1: the model returnsfinish_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 witharguments:"", then the full arguments string) andusage.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 → 400Invalid request: tokenization failedwhatever we replayed (verbatim, without/with emptyreasoning_content, with/withoutreasoning_effort); changingtypeto"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. - 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 callsweb_searchrunPOST /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}}; sendencrypted_outputas the tool message content → 200 on K3 (step 2 prompt 1 594 tokens, answer with source URL) (probed). 12 public formulas listed byGET /v1/formulas(fetch, convert, web-search, code-runner, quickjs, excel, memory, date, base64, rethink, random-choice, mew); only web-search is billed.context.referencesmay hold structured sources (not inspected). - Responses API
tools:[{type:"web_search"}]on kimi-k3 — fully server-side, one request: eventsresponse.web_search_call.in_progress/searching/completed, output itemsweb_search_call(action:{type:"search", query}),reasoning,message;annotations: [](no url_citation), URL only inline; usage 7 888 input tokens for one search (probed). Optionsallowed_domains(≤ 100), one web_search tool per request;search_context_size/user_locationunsupported (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_tokensand headermsh-context-cache-token-saved(probed: 821-token prompt → second callcached_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 replayreasoning_contentand give a largemax_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
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)
- https://platform.kimi.ai/docs/introduction · https://platform.kimi.ai/docs/llms.txt (index) · https://platform.kimi.ai/docs/models · https://platform.kimi.ai/docs/platform-changelog · https://platform.kimi.ai/docs/openapi.json
- https://platform.kimi.ai/docs/api/overview · https://platform.kimi.ai/docs/api/chat · https://platform.kimi.ai/docs/api/models-overview (Model Parameter Reference) · https://platform.kimi.ai/docs/api/responses · https://platform.kimi.ai/docs/api/messages · https://platform.kimi.ai/docs/api/list-models · https://platform.kimi.ai/docs/api/estimate · https://platform.kimi.ai/docs/api/balance · https://platform.kimi.ai/docs/api/files-upload · https://platform.kimi.ai/docs/api/errors
- https://platform.kimi.ai/docs/guide/ · …/guide/kimi-k3-quickstart · …/guide/kimi-k2-6-quickstart · …/guide/kimi-k2-7-code-quickstart · …/guide/use-thinking-models · …/guide/use-reasoning-effort · …/guide/engage-in-multi-turn-conversations-using-kimi-api · …/guide/utilize-the-streaming-output-feature-of-kimi-api · …/guide/use-json-mode-feature-of-kimi-api · …/guide/response_format · …/guide/use-partial-mode-feature-of-kimi-api · …/guide/use-kimi-vision-model · …/guide/use-context-caching-feature-of-kimi-api · …/guide/use-kimi-api-to-complete-tool-calls · …/guide/use-tool-choice · …/guide/use-dynamic-tool-loading · …/guide/kimi-k3-tool-calling-best-practice · …/guide/use-web-search · …/guide/use-official-tools · …/guide/troubleshooting · …/guide/auto-reconnect
- https://platform.kimi.ai/docs/pricing/chat · …/pricing/chat-k3 · …/pricing/chat-k27-code · …/pricing/chat-k26 · …/pricing/tools · …/pricing/batch · …/pricing/limits
- Redirected (301 → platform.kimi.ai): all
/docs/*paths of platform.moonshot.ai given in the brief.https://platform.kimi.ai/sitemap.xmlonly lists 2 pages; usellms.txtinstead. Not probed:/anthropic/v1/messages, files upload, batch.