|
1 |
+# Kimi (Moonshot AI, international platform) — provider research for PolyLLM |
|
2 |
+ |
|
3 |
+Last documentation audit: **2026-09-08** |
|
4 |
+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. |
|
5 |
+ |
|
6 |
+> 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. |
|
7 |
+ |
|
8 |
+--- |
|
9 |
+ |
|
10 |
+## 1. Base URL, auth, headers |
|
11 |
+ |
|
12 |
+| Item | Value | |
|
13 |
+|---|---| |
|
14 |
+| 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) | |
|
15 |
+| Auth | `Authorization: Bearer <KIMI_API_KEY>` | |
|
16 |
+| 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)** | |
|
17 |
+| Optional request header | `X-Msh-Request-Nonce` (UUID v4) → response headers `Msh-Request-Timestamp` + `Msh-Request-Signature` for signature verification (docs) | |
|
18 |
+| 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`). | |
|
19 |
+| 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. | |
|
20 |
+| Token counting | `POST /v1/tokenizers/estimate-token-count` `{model, messages}` → `{"code":0,"data":{"total_tokens":93},...}` **(probed)**; accepts image/video parts (docs). | |
|
21 |
+ |
|
22 |
+## 2. SDK recommendation (TypeScript / Node) |
|
23 |
+ |
|
24 |
+- **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. |
|
25 |
+- **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. |
|
26 |
+ |
|
27 |
+## 3. Endpoints |
|
28 |
+ |
|
29 |
+| Endpoint | Status | Notes | |
|
30 |
+|---|---|---| |
|
31 |
+| `POST /v1/chat/completions` | **primary** | OpenAI-compatible; all 4 models; `reasoning_content`; builtin `$web_search`. **(probed)** | |
|
32 |
+| `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)** | |
|
33 |
+| `POST /anthropic/v1/messages` | active | Anthropic SDK compatible, kimi-k3 only, `output_tokens_details.thinking_tokens` (docs; not probed) | |
|
34 |
+| `GET /v1/models`, `GET /v1/models/{id}` | active | Rich flags, see §12. **(probed)** | |
|
35 |
+| `GET /v1/users/me/balance` | active | §1 **(probed)** | |
|
36 |
+| `POST /v1/tokenizers/estimate-token-count` | active | §1 **(probed)** | |
|
37 |
+| `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) | |
|
38 |
+| `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 | |
|
39 |
+| `POST /v1/batch/*` | active | 60 % of list price (docs) | |
|
40 |
+ |
|
41 |
+## 4. Chat Completions request/response (probed shapes) |
|
42 |
+ |
|
43 |
+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). |
|
44 |
+ |
|
45 |
+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). |
|
46 |
+ |
|
47 |
+Non-streaming response **(probed, kimi-k2.6)**: |
|
48 |
+ |
|
49 |
+```json |
|
50 |
+{ "id": "chatcmpl-6a9fabafa11938d17f75e852", "object": "chat.completion", "created": 1788849073, "model": "kimi-k2.6", |
|
51 |
+ "choices": [{ "index": 0, "finish_reason": "stop", |
|
52 |
+ "message": { "role": "assistant", "content": "Two plus two equals four.", |
|
53 |
+ "reasoning_content": "The user is asking a simple math question…" } }], |
|
54 |
+ "usage": { "prompt_tokens": 20, "completion_tokens": 78, "total_tokens": 98, |
|
55 |
+ "completion_tokens_details": { "reasoning_tokens": 70 } } } |
|
56 |
+``` |
|
57 |
+ |
|
58 |
+- **`completion_tokens` INCLUDES reasoning tokens** (78 = 70 reasoning + 8 visible); `total_tokens = prompt + completion`. Bill reasoning at the output price. |
|
59 |
+- 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`). |
|
60 |
+- `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".** |
|
61 |
+- Hidden system prompt: ~17 prompt tokens overhead on K2.x, **~92–98 on kimi-k3** (25 when thinking disabled) **(probed)**. |
|
62 |
+- Non-thinking K2.6 responses still carry `reasoning_content: ""` when tools are called **(probed)**. |
|
63 |
+ |
|
64 |
+## 5. Streaming protocol (chat completions) **(probed on all 4 models)** |
|
65 |
+ |
|
66 |
+SSE `text/event-stream`, `data: {json}` lines only (no `event:` field), terminated by `data: [DONE]` (docs insist: end on `[DONE]`, not on `finish_reason`). |
|
67 |
+ |
|
68 |
+```json |
|
69 |
+data: {"id":"chatcmpl-…","object":"chat.completion.chunk","created":1788849079,"model":"kimi-k2.6", |
|
70 |
+ "choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}],"system_fingerprint":"fpv0_7f4e9b4b"} |
|
71 |
+data: {…"choices":[{"index":0,"delta":{"reasoning_content":"The"},"finish_reason":null}]…} |
|
72 |
+… |
|
73 |
+data: {…"choices":[{"index":0,"delta":{"content":"Bon"},"finish_reason":null}]…} |
|
74 |
+data: {…"choices":[{"index":0,"delta":{},"finish_reason":"stop", |
|
75 |
+ "usage":{"prompt_tokens":17,"completion_tokens":96,"total_tokens":113,"cached_tokens":17, |
|
76 |
+ "completion_tokens_details":{"reasoning_tokens":86},"prompt_tokens_details":{"cached_tokens":17}}}]…} |
|
77 |
+data: {…"choices":[],"usage":{…same…}} ← only with stream_options.include_usage |
|
78 |
+data: [DONE] |
|
79 |
+``` |
|
80 |
+ |
|
81 |
+- 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)**. |
|
82 |
+- **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`. |
|
83 |
+- 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>`. |
|
84 |
+ |
|
85 |
+## 6. Tool / function calling **(probed on all 4 models)** |
|
86 |
+ |
|
87 |
+- 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)**. |
|
88 |
+- `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)**. |
|
89 |
+- 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". |
|
90 |
+- **`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. |
|
91 |
+- Dynamic tool loading (K3 only): `{role:"system", tools:[…]}` message *without* `content` injects tools mid-conversation; on other models → tokenization error (docs). |
|
92 |
+ |
|
93 |
+## 7. Structured output **(probed)** |
|
94 |
+ |
|
95 |
+- `response_format: {type:"json_object"}` → valid JSON object on all 4 models (mention JSON in the prompt; only JSON *objects*, never arrays — docs). |
|
96 |
+- `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.** |
|
97 |
+- Thinking models still emit `reasoning_content`; parse only `message.content`. A too-small `max_tokens` truncates the JSON (`finish_reason: length`). |
|
98 |
+ |
|
99 |
+## 8. Reasoning controls **(probed matrix)** |
|
100 |
+ |
|
101 |
+| Model | Thinking | `thinking` param | `reasoning_effort` | `reasoning_content` | |
|
102 |
+|---|---|---|---|---| |
|
103 |
+| 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`) | |
|
104 |
+| 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|high|max` (no visible effect), `none` → disables thinking** | yes; `""` when disabled | |
|
105 |
+| 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|high|max` accepted (no visible effect); **`none` → 400** (same message) | yes | |
|
106 |
+| kimi-k2.7-code-highspeed | always on | same as k2.7-code | same as k2.7-code | yes | |
|
107 |
+ |
|
108 |
+- `/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). |
|
109 |
+- Responses API: `reasoning: {effort}` → `{type:"reasoning", summary:[{type:"summary_text", text}]}` output item (full text, not a summary) **(probed)**. |
|
110 |
+ |
|
111 |
+## 9. Sampling & other parameters — support matrix **(probed, chat completions)** |
|
112 |
+ |
|
113 |
+| Param | k3 | k2.7-code | k2.7-code-highspeed | k2.6 | |
|
114 |
+|---|---|---|---|---| |
|
115 |
+| `temperature` | **only `1`** (0, 0.5, 1.5, 2 → 400) | only `1` | only `1` | only `1` with thinking (docs: 0.6 fixed when disabled) | |
|
116 |
+| `top_p` | **only `0.95`** | only `0.95` | only `0.95` | only `0.95` | |
|
117 |
+| `n` | only `1` | only `1` | only `1` | only `1` | |
|
118 |
+| `presence_penalty` / `frequency_penalty` | **only `0`** | only `0` | only `0` | only `0` | |
|
119 |
+| `max_tokens` / `max_completion_tokens` | ✓ (both) | ✓ | ✓ | ✓ | |
|
120 |
+| `stop` (≤ 5) | ✓ (6 → 400 `stop array too long…maximum length 5`) | ✓ | ✓ | ✓ | |
|
121 |
+| `seed` | ✓ accepted (undocumented) | ✓ | ✓ | ✓ | |
|
122 |
+| `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** | ✓ | |
|
123 |
+| `response_format` json_object / json_schema | ✓ / ✓ | ✓ / ✓ | ✓ / ✓ | ✓ / **unreliable** | |
|
124 |
+| `thinking` | see §8 | enabled only | enabled only | ✓ | |
|
125 |
+| `reasoning_effort` | ✓ | tolerated | tolerated | tolerated | |
|
126 |
+| `tool_choice: required` | ✓ | 400 (thinking) | 400 (thinking) | 400 (thinking) | |
|
127 |
+| `tool_choice: {function}` | 400 (thinking) | 400 | 400 | 400 (✓ when thinking disabled) | |
|
128 |
+| `parallel_tool_calls` | ✓ | ✓ | ✓ | ✓ | |
|
129 |
+| `developer` role | 400 | 400 | 400 | 400 | |
|
130 |
+| unknown params | ignored | ignored | ignored | ignored | |
|
131 |
+ |
|
132 |
+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.** |
|
133 |
+ |
|
134 |
+## 10. Modalities, context, output limits |
|
135 |
+ |
|
136 |
+- **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. |
|
137 |
+- 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). |
|
138 |
+- **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)**. |
|
139 |
+- **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. |
|
140 |
+- 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). |
|
141 |
+ |
|
142 |
+## 11. Web search & official tools **(probed)** |
|
143 |
+ |
|
144 |
+Three different mechanisms exist; pricing $0.005 per successful search call + result tokens billed as prompt tokens (docs `/pricing/tools`). |
|
145 |
+ |
|
146 |
+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. |
|
147 |
+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). |
|
148 |
+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). |
|
149 |
+ |
|
150 |
+**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. |
|
151 |
+ |
|
152 |
+## 12. Model listing & pricing **(docs + probed)** |
|
153 |
+ |
|
154 |
+`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. |
|
155 |
+ |
|
156 |
+| Model | ctx | in / cache-hit / out ($/M) | Batch (60 %) | Notes | |
|
157 |
+|---|---|---|---|---| |
|
158 |
+| 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 | |
|
159 |
+| kimi-k2.7-code | 262 144 | **0.95 / 0.19 / 4.00** | 0.57 / 0.114 / 2.40 | coding, thinking always on | |
|
160 |
+| kimi-k2.7-code-highspeed | 262 144 | **1.90 / 0.38 / 8.00** | — | same model, ~180–260 tok/s, exactly 2× price | |
|
161 |
+| kimi-k2.6 | 262 144 | **0.95 / 0.16 / 4.00** | 0.57 / 0.10 / 2.40 | thinking switchable, cheapest cache hits | |
|
162 |
+ |
|
163 |
+"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. |
|
164 |
+ |
|
165 |
+## 13. Prompt caching |
|
166 |
+ |
|
167 |
+- **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. |
|
168 |
+- 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. |
|
169 |
+ |
|
170 |
+## 14. Errors, rate limits, retries |
|
171 |
+ |
|
172 |
+Error body: OpenAI-style `{"error":{"message":"…","type":"…"}}` (no `code` observed; docs mention an optional `code`). |
|
173 |
+ |
|
174 |
+| Case **(probed)** | HTTP | body | |
|
175 |
+|---|---|---| |
|
176 |
+| Invalid key | **401** | `{"error":{"message":"Invalid Authentication","type":"invalid_authentication_error"}}` | |
|
177 |
+| No Authorization header | 401 | `{"error":{"message":"Incorrect API key provided","type":"incorrect_api_key_error"}}` | |
|
178 |
+| Unknown / retired model | **404** | `{"error":{"message":"Not found the model kimi-k99 or Permission denied","type":"resource_not_found_error"}}` | |
|
179 |
+| Fixed param violated | 400 | `invalid temperature: only 1 is allowed for this model` (`invalid_request_error`) | |
|
180 |
+| `developer` role | 400 | `Invalid request: role 'developer' is not allowed` | |
|
181 |
+| Context overflow | 400 | `Invalid request: Your request exceeded model token limit: 262144 (requested: 540015)` | |
|
182 |
+| Empty/malformed messages | 400 | `Invalid request: messages must not be empty` / `…field in the request (expected type []object) is illegal…` | |
|
183 |
+| Public image URL | 400 | `Invalid request: unsupported image url: …` | |
|
184 |
+| tool_choice vs thinking | 400 | `tool_choice 'required' is incompatible with thinking enabled` / `tool_choice 'specified' is incompatible with thinking enabled` | |
|
185 |
+| Builtin replay on K3 | 400 | `Invalid request: tokenization failed` | |
|
186 |
+| 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** | |
|
187 |
+| Engine busy | 429 | `{"error":{"message":"The engine is currently overloaded, please try again later","type":"engine_overloaded_error"}}` | |
|
188 |
+| 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) | |
|
189 |
+| Server | 500 / 503 / 504 | `server_error`, `server_unavailable`, gateway timeout → use streaming (docs) | |
|
190 |
+ |
|
191 |
+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). |
|
192 |
+ |
|
193 |
+**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). |
|
194 |
+ |
|
195 |
+## 15. Lifecycle / naming |
|
196 |
+ |
|
197 |
+- 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. |
|
198 |
+- 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). |
|
199 |
+- 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). |
|
200 |
+- 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. |
|
201 |
+ |
|
202 |
+## 16. Exact streaming code that worked |
|
203 |
+ |
|
204 |
+```ts |
|
205 |
+import OpenAI from "openai"; |
|
206 |
+const client = new OpenAI({ apiKey: process.env.KIMI_API_KEY, baseURL: "https://api.moonshot.ai/v1", timeout: 600_000, maxRetries: 0 }); |
|
207 |
+ |
|
208 |
+const stream = await client.chat.completions.create({ |
|
209 |
+ model: "kimi-k2.6", |
|
210 |
+ messages: [{ role: "user", content: "Weather in Montreal? Use the tool." }], |
|
211 |
+ tools: [{ type: "function", function: { name: "get_weather", parameters: { type: "object", properties: { city: { type: "string" } }, required: ["city"] } } }], |
|
212 |
+ stream: true, |
|
213 |
+ stream_options: { include_usage: true }, |
|
214 |
+ max_tokens: 16_000, // must cover reasoning + answer |
|
215 |
+ ...({ thinking: { type: "enabled" } } as any), // K2.6 only; K3 uses reasoning_effort: "low"|"high"|"max" |
|
216 |
+}); |
|
217 |
+const acc = { reasoning: "", content: "", toolCalls: [] as any[] }; |
|
218 |
+for await (const chunk of stream) { |
|
219 |
+ const d = (chunk.choices[0]?.delta ?? {}) as any; |
|
220 |
+ if (d.reasoning_content) acc.reasoning += d.reasoning_content; // thinking pane |
|
221 |
+ if (d.content) acc.content += d.content; |
|
222 |
+ for (const tc of d.tool_calls ?? []) { // fragmented by index |
|
223 |
+ const t = (acc.toolCalls[tc.index] ??= { id: "", type: "function", function: { name: "", arguments: "" } }); |
|
224 |
+ if (tc.id) t.id = tc.id; if (tc.function?.name) t.function.name += tc.function.name; |
|
225 |
+ if (tc.function?.arguments) t.function.arguments += tc.function.arguments; |
|
226 |
+ } |
|
227 |
+ if (chunk.choices[0]?.finish_reason) onFinish(chunk.choices[0].finish_reason); // stop | length | tool_calls |
|
228 |
+ if (chunk.usage) onUsage(chunk.usage); // final choices: [] chunk; reasoning_tokens inside completion_tokens |
|
229 |
+} |
|
230 |
+// replay: { role:"assistant", content: acc.content || null, reasoning_content: acc.reasoning, tool_calls: acc.toolCalls } |
|
231 |
+``` |
|
232 |
+ |
|
233 |
+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. |
|
234 |
+ |
|
235 |
+## 17. Probe results table |
|
236 |
+ |
|
237 |
+| # | Probe | Model(s) | Result | |
|
238 |
+|---|---|---|---| |
|
239 |
+| 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 | |
|
240 |
+| 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 | |
|
241 |
+| 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 | |
|
242 |
+| 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) | |
|
243 |
+| 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: ""` | |
|
244 |
+| 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 | |
|
245 |
+| 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 | |
|
246 |
+| 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 | |
|
247 |
+| 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 | |
|
248 |
+| 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 | |
|
249 |
+ |
|
250 |
+## Documentation pages used (all fetched 2026-09-08; every `platform.moonshot.ai` URL redirected 301 to `platform.kimi.ai`) |
|
251 |
+ |
|
252 |
+- 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 |
|
253 |
+- 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 |
|
254 |
+- 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 |
|
255 |
+- https://platform.kimi.ai/docs/pricing/chat · …/pricing/chat-k3 · …/pricing/chat-k27-code · …/pricing/chat-k26 · …/pricing/tools · …/pricing/batch · …/pricing/limits |
|
256 |
+- Redirected (301 → platform.kimi.ai): all `/docs/*` paths of platform.moonshot.ai given in the brief. `https://platform.kimi.ai/sitemap.xml` only lists 2 pages; use `llms.txt` instead. Not probed: `/anthropic/v1/messages`, files upload, batch. |