This commit is contained in:
2026-08-23 02:12:08 +08:00
commit 5987b2a1f2
19 changed files with 4174 additions and 0 deletions
+966
View File
@@ -0,0 +1,966 @@
# Kimi API(月之暗面 Moonshot
> 来源(官方文档站 `.md` 原文,采集于 2026-08-13):
> - API 参考:[overview](https://platform.kimi.com/docs/api/overview.md)、[chat](https://platform.kimi.com/docs/api/chat.md)、[models-overview](https://platform.kimi.com/docs/api/models-overview.md)、[list-models](https://platform.kimi.com/docs/api/list-models.md)、[errors](https://platform.kimi.com/docs/api/errors.md)、[estimate](https://platform.kimi.com/docs/api/estimate.md)、[balance](https://platform.kimi.com/docs/api/balance.md)、[batch-create](https://platform.kimi.com/docs/api/batch-create.md)
> - 指南:[models](https://platform.kimi.com/docs/models.md)、[k3-quickstart](https://platform.kimi.com/docs/guide/kimi-k3-quickstart.md)、[k2.7-code-quickstart](https://platform.kimi.com/docs/guide/kimi-k2-7-code-quickstart.md)、[thinking](https://platform.kimi.com/docs/guide/use-thinking-models.md)、[reasoning-effort](https://platform.kimi.com/docs/guide/use-reasoning-effort.md)、[partial](https://platform.kimi.com/docs/guide/use-partial-mode-feature-of-kimi-api.md)、[caching](https://platform.kimi.com/docs/guide/use-context-caching-feature-of-kimi-api.md)、[tool-choice](https://platform.kimi.com/docs/guide/use-tool-choice.md)、[dynamic-tools](https://platform.kimi.com/docs/guide/use-dynamic-tool-loading.md)、[tool-calls](https://platform.kimi.com/docs/guide/use-kimi-api-to-complete-tool-calls.md)、[web-search](https://platform.kimi.com/docs/guide/use-web-search.md)、[official-tools](https://platform.kimi.com/docs/guide/use-official-tools.md)、[response_format](https://platform.kimi.com/docs/guide/response_format.md)、[streaming](https://platform.kimi.com/docs/guide/utilize-the-streaming-output-feature-of-kimi-api.md)、[vision](https://platform.kimi.com/docs/guide/use-kimi-vision-model.md)、[file-qa](https://platform.kimi.com/docs/guide/use-kimi-api-for-file-based-qa.md)、[batch](https://platform.kimi.com/docs/guide/use-batch-api.md)、[claude-code](https://platform.kimi.com/docs/guide/claude-code-kimi.md)、[troubleshooting](https://platform.kimi.com/docs/guide/troubleshooting.md)
> - 计价与限流:[limits](https://platform.kimi.com/docs/pricing/limits.md)、[chat-k3](https://platform.kimi.com/docs/pricing/chat-k3.md)
> - 完整索引:https://platform.kimi.com/docs/llms.txt OpenAPIhttps://platform.kimi.com/docs/openapi.json
>
> **2026-08-13 用真实 API key 实测校验过一遍**(约 60 次请求,覆盖两个入口)。实测与文档冲突处均以实测为准。
> 实测环境:中国站 `api.moonshot.cn`,账号档位 `msh-gid: free`Tier 03 RPM / 1 并发),流式 `system_fingerprint` = `fpv0_489ccb45`。
>
> **标注约定**:✅ 文档记载且实测确认;❌ **实测推翻文档**;🆕 文档未记载、实测发现;⚠️ 未实测或存疑。
Kimi 对外声称 **OpenAI 协议兼容**OpenAI SDK 换 `base_url` 即可用。但实测下来它是本项目已调研的几家里**"最容易在第一个请求就 400"** 的一家:
1. **新模型把采样参数写死并硬 400**——`temperature` 不是 1.0 直接报错,实测确认不是静默忽略(§3)。带默认 `temperature=0.7` 的 OpenAI 客户端在 K3 上开箱即挂。
2. **同一模型在两个入口行为相反**——OpenAI 入口对 `temperature=0.7` 报 400Anthropic 入口 200 静默忽略(§3.2)。
3. **参数集按模型分裂成四套**——合法请求体取决于 `model` 值(§3、§4)。
4. **文档与实现大面积脱节**——实测推翻了 8 条文档明确写下的规则,其中包括"K3 思考恒开不可关"这样的核心设定(§14)。**照文档写代码会同时踩到"文档说能实际不能"和"文档说不能实际能"两个方向。**
5. **它其实是两个产品**——`platform`(本文 §1–§16)和 **Kimi For Coding**`api.kimi.com/coding`,见 **§17**)是**两套独立的服务栈**:账号体系、模型名、鉴权、限流、端点、id 格式全都不同,key 互不通用。**中转站必须当作两个上游分别配置。**
---
## 1. 端点、域名与鉴权
### 1.1 双站隔离 ✅
| 产品 / 站点 | 控制台 | API base URL | Key 前缀 |
| --- | --- | --- | --- |
| platform 中国站 | `platform.kimi.com` | `https://api.moonshot.cn/v1` | `sk-` |
| platform 国际站 | `platform.kimi.ai` | `https://api.moonshot.ai/v1` | `sk-` |
| **Kimi For Coding**(另一个产品,见 §17) | — | `https://api.kimi.com/coding/v1` | **`sk-kimi-`** |
两站账号、余额、API Key 完全独立,拿错区域的 key 返回 401。中转站的上游配置必须把 key 和 base URL 绑成一对。
**Kimi For Coding 与 platform 的 key 双向不通用**(实测)——且两边的 401 文案不同,可用来判断打错了哪个上游:
```
coding key → api.moonshot.cn : {"message":"Invalid Authentication","type":"invalid_authentication_error"}
platform key → api.kimi.com/coding : {"message":"The API Key appears to be invalid or may have expired. Please verify your credentials and try again.","type":"invalid_authentication_error"}
```
⚠️ **域名分层容易记错**:文档站/控制台已迁到 `kimi.com``platform.moonshot.cn``platform.kimi.com` 301),但 **API 数据面仍是 `api.moonshot.cn` / `api.moonshot.ai`**,没有迁到 `api.kimi.com`
### 1.2 端点清单 ✅(均实测可用)
| 方法 | 路径 | 用途 |
| --- | --- | --- |
| POST | `/v1/chat/completions` | 对话补全 |
| GET | `/v1/models` | 模型列表 |
| POST | `/v1/tokenizers/estimate-token-count` | 官方 token 计数 |
| GET | `/v1/users/me/balance` | 余额查询 |
| POST/GET/DELETE | `/v1/files``/v1/files/{id}``/v1/files/{id}/content` | 文件管理 |
| POST/GET | `/v1/batches``/v1/batches/{id}``/v1/batches/{id}/cancel` | 批处理 |
鉴权统一 `Authorization: Bearer <api_key>` ✅。**没有智谱那种 JWT 历史包袱**,这点比多数国产厂商干净。
🆕 **非 chat 端点带业务包装层**`/v1/users/me/balance``/v1/tokenizers/estimate-token-count` 返回的**不是**裸对象,而是
```json
{"code":0,"data":{"total_tokens":87},"scode":"0x0","status":true}
```
即 HTTP 200 外面还套了一层 `code`/`scode`/`status`。而 `/v1/chat/completions``/v1/models` **没有**这层包装(直接 OpenAI 结构)。**同一个 API 里两种响应信封并存**——中转站的响应解析器不能一刀切。(对比 `china-providers.md` §0 里 MiniMax `base_resp` 那种"HTTP 200 藏业务错误",Kimi 是同一思路的半吊子版本:只在辅助端点上有。)
🆕 **未记载的调试响应头**(每个响应都带):
```
msh-request-id: 7b768543-96db-11f1-8fa0-624fe5b4e31b
msh-org-id: org-xxxxxxxx
msh-project-id: proj-xxxxxxxx
msh-uid: xxxxxxxxxxxxxxxxxxxx
msh-gid: free ← 账号档位,可用来自动探测 Tier
msh-trace-mode: on
```
`msh-request-id` 是报障时官方要的 request_id。⚠️ 但 `msh-org-id` / `msh-uid` / `msh-project-id` 是**账号标识**——中转站若原样透传响应头给下游,等于把自己的组织 ID 广播出去。**应当在响应头白名单里剥掉 `msh-*`**`msh-request-id` 可保留或改名)。
### 1.3 Anthropic 兼容入口 ✅(实测可用,但与 OpenAI 入口行为不一致)
```
ANTHROPIC_BASE_URL="https://api.moonshot.cn/anthropic"
ANTHROPIC_AUTH_TOKEN="<kimi api key>" # 注意是 AUTH_TOKEN 不是 API_KEY
```
实测结论:
| 项 | 结果 |
| --- | --- |
| `POST /anthropic/v1/messages` + `x-api-key` | ✅ 200 |
| `POST /anthropic/v1/messages` + `Authorization: Bearer` | ✅ 200**两种鉴权都接受** |
| `POST /anthropic/messages`(漏 `v1`) | ❌ 404,且**掉到网关层**(见下) |
| 不带 `anthropic-version` 头 | 🆕 200**不强制** |
| `temperature: 0.7` | 🆕 **200 静默忽略**(OpenAI 入口同样请求是 400 |
| `top_p: 0.5` | 🆕 200 静默忽略 |
| `tool_choice: {"type":"tool","name":...}` | ✅ 400 `tool_choice 'specified' is incompatible with thinking enabled`(与 OpenAI 入口一致) |
| `thinking: {"type":"enabled","budget_tokens":2000}` | ✅ 200 接受 |
| `system` + `cache_control: {"type":"ephemeral"}` | 🆕 200 接受但 `cache_creation_input_tokens: 0`,**未实际建缓存**(自动缓存机制接管,见 §10) |
| `model: "kimi-k3[1m]"` | ❌ **404**(见下) |
> **两个入口对采样参数的处理相反,但对 `tool_choice` 的限制一致**——不是"Anthropic 入口更宽松",而是**选择性地不一致**。中转站不能假设"打不通就换个入口试试"。
🆕 **Anthropic 入口的 `usage` 同时并存两套字段**
```json
"usage": {
"input_tokens": 0, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 88,
"output_tokens": 151, "output_tokens_details": {"thinking_tokens": 135},
"service_tier": "standard", "inference_geo": "not_available",
"prompt_tokens": 88, "cached_tokens": 88, "completion_tokens": 151, "total_tokens": 239
}
```
Anthropic 风格(`input_tokens`/`cache_read_input_tokens`/`output_tokens`)和 OpenAI 风格(`prompt_tokens`/`cached_tokens`/`completion_tokens`)**同时存在**,且语义不同:缓存命中时 `input_tokens` 变成 **0**(Anthropic 语义:不含缓存部分),而 `prompt_tokens` 仍是 **88**(OpenAI 语义:含缓存部分)。**中转站按哪套字段计费,账单会差一个缓存量。**
🆕 **thinking block 没有 `signature` 字段**——Anthropic 官方的 thinking block 带加密签名,Kimi 这个是纯明文 `{"type":"thinking","thinking":"..."}`。好处是不用当 opaque blob 搬运,坏处是**下游若按 Anthropic 规范校验 signature 会失败**。
**`kimi-k3[1m]` 是错的**——官方 Claude Code 配置页把 `ANTHROPIC_MODEL` 等六个变量全设成 `kimi-k3[1m]`,但实测**两个入口都 404**
```
OpenAI 入口: {"error":{"message":"Not found the model kimi-k3[1m] or Permission denied","type":"resource_not_found_error"}}
Anthropic 入口: {"error":{"type":"resource_not_found_error","message":"Not found the model kimi-k3[1m] or Permission denied"},"request_id":"...","type":"error"}
```
`[1m]` 是 Claude Code 客户端侧的上下文变体后缀,Kimi 服务端并不认。**正确的模型名是 `kimi-k3`。** 官方文档这段配置照抄会直接失败。
🆕 **三种错误格式并存**(这是中转站错误处理的隐藏坑):
```jsonc
// 1. OpenAI 入口 API 级错误
{"error":{"message":"...","type":"invalid_request_error"}}
// 2. Anthropic 入口 API 级错误 —— 多了 request_id 和顶层 type
{"error":{"type":"resource_not_found_error","message":"..."},"request_id":"...","type":"error"}
// 3. 网关级错误(路径不存在时)—— 完全不同的结构,中文消息,且回显 UA 和 URL
{"code":5,"error":"url.not_found","message":"没找到对象","method":"POST","scode":"0x5","status":false,"ua":"curl/8.7.1","url":"/anthropic/messages"}
```
第三种连 `error` 字段的类型都变了(从 object 变成 string)。**中转站解析上游错误时若假设 `error` 恒为 object,遇到网关级错误会抛类型异常。**
✅ 已知能力缺口:该端点**不支持 WebFetch**Claude Code 里报 `temporarily unavailable`)。
---
## 2. 模型与能力矩阵
### 2.1 在售模型 ✅
| 模型 | 上下文(实测 `/v1/models` | 默认 max output | 思考 | 视觉 | 视频 |
| --- | --- | --- | --- | --- | --- |
| `kimi-k3` | 1,048,576 | 131,072(上限 1,048,576 | 恒开但**可关**(§4.1 | ✓ | ✓ |
| `kimi-k2.7-code` | 262,144 | 32,768 | **不可关** | ✓ | ✓ |
| `kimi-k2.7-code-highspeed` | 262,144 | 32,768 | **不可关** | ✓ | ✓ |
| `kimi-k2.6` | 262,144 | ⚠️ 未记载 | 可关 | ✓ | ✓ |
| `kimi-k2.5` | 262,144 | ⚠️ 未记载 | 可关 | ✓ | ✓ |
| `moonshot-v1-{8k,32k,128k}` | 8192/32768/131072 | ⚠️ 未记载 | 无 | ✗ | ✗ |
| `moonshot-v1-{8k,32k,128k}-vision-preview` | 同上 | ⚠️ 未记载 | 无 | ✓ | ✗ |
| 🆕 `moonshot-v1-auto` | 131072 | — | 无 | ✗ | ✗ |
🆕 **`moonshot-v1-auto` 文档完全没提**,但 `/v1/models` 里有且可调用。它是**自动路由模型**:请求 `moonshot-v1-auto`**响应的 `model` 字段返回 `moonshot-v1-8k`**(实测两次都是)。
> 这是 DeepSeek `deepseek-chat` → `deepseek-v4-flash` 静默映射的同类问题:**请求模型名 ≠ 响应模型名 ≠ 账单模型名**。中转站若按响应 `model` 字段记账,用户请求 auto 会看到一个自己没点过的模型名;若按请求名记账,又对不上实际计费。**建议两个都记。**
`kimi-k2.7-code-highspeed` 是同模型高速档(约 180 tok/s),能力相同——可当作同一逻辑模型的 QoS 变体。
⚠️ 文档提到 moonshot-v1 系列"8 月 31 日下线"但未给年份,而 `/v1/models` 仍在返回。需持续关注。
### 2.2 已下线模型:无别名兜底 ✅(实测确认)
```
kimi-k2 → 404 Not found the model kimi-k2 or Permission denied
kimi-latest → 404 Not found the model kimi-latest or Permission denied
```
与 DeepSeek 把旧名静默映射到新模型**相反**,Kimi 是直接 404。中转站若面向存量客户需自建旧名映射表,并显式告知用户(别学 DeepSeek 静默换模型导致账单对不上)。
### 2.3 `/v1/models` 的真实返回:比文档丰富得多 🆕
文档给的示例只有 7 个字段,实测返回:
```json
{
"id": "kimi-k3", "object": "model", "created": 1786362236, "owned_by": "moonshot",
"permission": [{"created":0,"id":"","object":"","organization":"moonshot","group":"moonshot","is_blocking":false}],
"root": "", "parent": "",
"supports_image_in": true, "supports_video_in": true, "supports_reasoning": true,
"supports_dynamic_tools": true,
"think_efforts": {"support": true, "valid_efforts": ["low","high","max"], "default_effort": "max"},
"reasoning_efforts": {"support": true, "valid_efforts": ["low","high","max"], "default_effort": "max"},
"supports_thinking_type": "only",
"context_length": 1048576
}
```
- `permission[]` / `root` / `parent`**OpenAI 早期 `/models` 的遗留字段**(现已从 OpenAI 移除),Kimi 还留着,且内容全是空壳。
- 🆕 `think_efforts``reasoning_efforts` **内容完全相同的两个字段**——显然是改名过程中的兼容遗留,两个都在返回。
- 🆕 `supports_dynamic_tools`(仅 K3 为 true)、`supports_thinking_type: "only"`(仅 K3 有此字段)——**这两个字段是判断能力的最可靠来源**,比文档准。
- ❌ 文档示例里 `kimi-k2.5``context_length` 写的是 `128000`,实测是 **262144**
> 这是少见的"上游自带能力矩阵",中转站可以自动同步能力表而非硬编码。但注意 `supports_thinking_type: "only"` 的值是字符串 `"only"` 而非布尔,且**只有 K3 有这个字段**——解析时要容忍字段缺失。
---
## 3. 采样参数:新模型写死且**硬 400**(实测确认,本文件最大的坑)
### 3.1 OpenAI 入口:逐个报错 ✅
实测逐参数验证,**全部是 400,不是静默忽略**:
| 请求 | 实际响应 |
| --- | --- |
| `kimi-k3` + `temperature: 0.7` | `400 invalid temperature: only 1 is allowed for this model` |
| `kimi-k3` + `top_p: 0.5` | `400 invalid top_p: only 0.95 is allowed for this model` |
| `kimi-k3` + `n: 2` | `400 invalid n: only 1 is allowed for this model` |
| `kimi-k3` + `presence_penalty: 1` | `400 invalid presence_penalty: only 0 is allowed for this model` |
| `kimi-k2.7-code` + `temperature: 0.7` | `400 invalid temperature: only 1 is allowed for this model` |
| `moonshot-v1-8k` + `temperature: 1.5` | `400 Invalid request: temperature must not be greater than 1.000000` |
| `moonshot-v1-8k` + `temperature: 0.7` | ✅ 200 |
| 参数 | `moonshot-v1` | `kimi-k3` / `kimi-k2.7-code` |
| --- | --- | --- |
| `temperature` | 01,默认 **0** | 固定 `1.0`**传其他值 400** |
| `top_p` | 01,默认 1 | 固定 `0.95`**传其他值 400** |
| `n` | 15,默认 1 | 固定 `1`**传其他值 400** |
| `presence_penalty` / `frequency_penalty` | 22,默认 0 | 固定 `0`**传其他值 400** |
**三条独立的怪癖:**
1. **`temperature` 默认值跨代反转**moonshot-v1 默认 **0**,新模型固定 **1.0**。"同一家 API,不传 temperature 时的行为随模型跳变"。中转站若为对齐 OpenAI 而主动补默认 `temperature`,在 v1 上改变原有行为,在 K3 上直接 400。**结论:绝不给未传的参数补默认值。**
2. **`temperature` 上限是 1 而非 2**OpenAI 是 2)。
3. 🆕 **`n > 1``temperature` 存在隐藏耦合**——在 moonshot-v1 上传 `n: 3` 而不动 temperature
```
400 Invalid request: n should not be greater than 1 when temperature is less than or equal to 1e-5
```
因为 v1 的 temperature 默认是 0**要用 `n > 1` 就必须同时把 temperature 设为 > 0**。文档从未提及这个约束。中转站转发 `n>1` 请求时得连带检查 temperature,否则报出的错会让用户完全摸不着头脑。
✅ 其他约束实测确认:`stop` 数组最多 5 个——传 6 个报 `400 stop array too long. Expected an array with maximum length 5, but got an array with length 6 instead`(错误信息质量不错)。
🆕 **未知参数静默忽略**:传 `"foo_bar": 123` → 200 正常返回。即"未知字段宽松、已知字段严格"——这与多数厂商一致,但和它对已知参数的严苛形成反差。
### 3.2 Anthropic 入口:同样的参数被静默忽略 🆕
```
OpenAI 入口 kimi-k3 + temperature=0.7 → 400
Anthropic 入口 kimi-k3 + temperature=0.7 → 200(正常返回,参数被忽略)
Anthropic 入口 kimi-k3 + top_p=0.5 → 200
```
**同一个模型、同一个参数、两个入口结果相反。** 这是 DeepSeek "三入口行为不一致"的翻版(见 `deepseek.md`)。
> 中转站落地建议:为 Kimi 上游维护**按模型的参数白名单**,对新模型剥离 `temperature/top_p/n/presence_penalty/frequency_penalty` 后再转发。**这是本文件最该优先实现的一条规则**——它同时解决了两个入口的差异(剥离后两边行为一致)。
### 3.3 `max_tokens` vs `max_completion_tokens` ✅(实测厘清)
两个名字都能用。同时传时 **`max_completion_tokens` 优先**
```
{"max_tokens": 1500, "max_completion_tokens": 30} → completion_tokens: 30, finish_reason: "length"
```
文档的参数表只列 `max_completion_tokens`,正文却反复用 `max_tokens`,两者是新旧别名关系(文档未声明)。中转站两个都要能发,且注意优先级。
---
## 4. 思考模型:文档与实现脱节最严重的一节
| 模型 | 开关参数 | 能否关闭 | 实测 |
| --- | --- | --- | --- |
| `kimi-k3` | `reasoning_effort``low`/`high`/`max`,默认 `max`**和** `thinking.type` | ❌ **文档说不能,实测能** | 见 §4.1 |
| `kimi-k2.7-code` | `thinking.type` | ✅ 确实不能 | `400 invalid thinking: only type=enabled is allowed for this model` |
| `kimi-k2.6` | `thinking.type``enabled`/`disabled` | ✅ 能 | 200`reasoning_content: ""` |
| `kimi-k2.5` | `thinking.type` | ⚠️ 未测 | — |
### 4.1 ❌ K3 的思考**可以**关闭(推翻文档)
文档(K3 quickstart、thinking 指南、troubleshooting 三处)都说"K3 始终开启思考模式,不可关闭,只能调 `reasoning_effort`"。实测:
```jsonc
// 请求
{"model":"kimi-k3","messages":[{"role":"user","content":"说:好"}],"max_tokens":1500,"thinking":{"type":"disabled"}}
// 响应 200
{"choices":[{"message":{"role":"assistant","content":"好"},"finish_reason":"stop"}],
"usage":{"prompt_tokens":21,"completion_tokens":11,"total_tokens":32}}
```
- **没有 `reasoning_content` 字段**(不是空字符串,是字段不存在)。
- **`prompt_tokens` 从 88 掉到 21**。同一条 user 消息,思考开启时 88、关闭时 21——差值约 67 token 是**思考模式注入的隐藏 system prompt**。这也解释了为什么 `estimate-token-count` 对四个汉字的消息返回 87:**官方 tokenizer 把这段隐藏 prompt 也算进去了**。
🆕 **`thinking.disabled` 优先于 `reasoning_effort`**:两个同时传(`thinking:{"type":"disabled"}` + `reasoning_effort:"max"`)→ 思考仍然关闭,`prompt_tokens: 21`。
> 对中转站的影响是正面的:**"关闭思考"这个语义在 K3 上是可表达的**,能力降级表里不必再写"无法关闭"。但因为文档明说不行,这条**属于未文档化行为,随时可能变**——建议在实现里做成可开关的特性,并在上游探测时验证。
### 4.2 ❌ `reasoning_effort` 的枚举校验形同虚设(推翻文档)
`/v1/models` 明确声明 `valid_efforts: ["low","high","max"]`,但:
```
reasoning_effort: "medium" → 200 正常思考(OpenAI 的枚举值)
reasoning_effort: "banana" → 200 正常思考(完全非法的值)
```
**任意字符串都被接受并静默忽略。** 好消息是中转站不必做 `minimal`/`medium` → `low`/`high` 的枚举映射(不会 400);坏消息是**用户传错值不会有任何反馈**,只会默默跑在默认的 `max` 档(最贵)。中转站若要给用户可预期的成本,应当自己校验枚举并拒绝非法值。
🆕 `kimi-k2.6` 传 `reasoning_effort: "low"` → 200,但仍正常思考(`reasoning_tokens: 151`)——同样是被忽略。
### 4.3 `reasoning_content` 的回传要求 ✅ 部分推翻
- 响应里思考内容在 `choices[].message.reasoning_content`**明文**,无签名)。
- ✅ 流式中 `reasoning_content` **确实先于 `content` 出现**(实测 186 个 chunk 全是 reasoning_content 后才出 content)。`content` 开始即代表思考结束,是可靠的状态机切换信号。
- ✅ `reasoning_content` 计入 `completion_tokens`,并单独报在 `completion_tokens_details.reasoning_tokens`。
- ❌ **文档说"K3/K2.7-code 多轮缺少 `reasoning_content` 会报错"——实测不报错**:
```
多轮 assistant 消息只带 content + tool_calls、不带 reasoning_content → 200 正常继续
```
中转站不必强制存档回放 `reasoning_content`(虽然存了对模型质量更好)。这条推翻降低了实现负担——**但也意味着 Preserved Thinking 的实际语义比文档描述的松**,⚠️ 建议在长对话+多步工具链场景再验一次。
### 4.4 `max_tokens` 太小导致空 `content` ✅(实测确认)
```jsonc
{"model":"kimi-k3","max_completion_tokens":30,...}
→ 200 {"message":{"content":"","reasoning_content":"The user asked..."},"finish_reason":"length"}
```
token 全烧在思考上,`content` 为空字符串。官方建议 `max_tokens >= 16000`。
> **"200 OK + 空 content"是 Kimi 的正常返回,不能当上游故障重试**——重试只会再烧一遍思考 token。应原样透传让下游看到 `finish_reason: "length"`。
---
## 5. Partial Mode(前缀续写)✅
```json
{"role": "assistant", "content": "[\"红\",", "partial": true, "name": "可选"}
```
实测 200,返回 `" \"绿\", \"蓝\"]"`——**响应不含前缀**,需客户端自己拼。
| 维度 | Kimi | DeepSeek | Anthropic |
| --- | --- | --- | --- |
| 字段名 | **`partial: true`** | `prefix: true` | 无字段,末条 assistant 即前缀 |
| 端点 | 正式端点 | 需 `/beta` | 正式端点(Claude 4.6+ 已禁用) |
**同一能力三家三个名字**——中转站 IR 里应有单一 `prefill` 标志(见 `api-design.md`),落地时翻译成对应字段。
❌ **`partial` + `response_format: json_object` 不冲突**(推翻 `chat.md` 里那条警告):
```
{"partial":true,"content":"{"} + response_format:{"type":"json_object"} → 200,正常续写出完整 JSON
```
这是实践中最常用的组合(前缀写 `{` 强制 JSON 开头),可以放心用。
⚠️ `name` 字段是 Kimi 独有语义:OpenAI 的 `name` 是"多说话人区分"Kimi 在 partial 场景当"人设锚点"。转发时不要想当然丢弃。
⚠️ 与思考叠加的陷阱(文档提示,未实测):K3 思考开启时若 `max_tokens` 太小,会在思考阶段截断导致 `content` 为空,**下一次续写等于从零重来**。
---
## 6. 工具调用
### 6.1 与 OpenAI 的差异 ✅/❌
`tools[] / tool_calls[] / role:"tool"` 结构、流式 `delta.tool_calls[].index` 累积规则均与 OpenAI 相同。差异:
1. ❌ **`role: "tool"` 消息的 `name` 字段实际可选**(推翻文档)。文档说"必须提供 `tool_call_id` 和 `name`,Kimi 才能正确匹配",实测只给 `tool_call_id` 不给 `name` → **200 正常**。中转站从 Anthropic 入口转换时不必回查函数名(Anthropic 的 `tool_result` 只有 `tool_use_id`)。
2. ✅ **`tool_call_id` 必须严格匹配**:传一个不存在的 id →
```
400 Invalid request: tool_call_id is not found
```
🆕 注意错误信息里 `tool_call_id` 和 `is not found` 之间是**两个空格**——本该填 id 值的位置是空的,是个字符串拼接 bug。**中转站不要指望从这条错误信息里解析出是哪个 id 出的问题。**
3. ✅ `finish_reason: "tool_calls"` 时 `content` 可能非空(Kimi 会解释为什么调用)。按"有 tool_calls 就忽略 content"写的下游会丢内容。
4. 🆕 **`tool_calls[].id` 不是随机串,而是 `{函数名}_{序号}`**
```json
{"index":0,"id":"get_weather_0","type":"function","function":{"name":"get_weather",...}}
```
OpenAI 是 `call_abc123...` 的不透明随机串。这个格式**可预测且与函数名耦合**。风险有两层:中转站若按 id 反查函数名会"碰巧能用",形成隐式依赖,换上游即崩;且多轮对话里**同名函数会产生重复 id**(第二轮又是 `get_weather_0`),若中转站用 id 做全局去重键会误判。**建议中转站重写 tool_call id 为自己的不透明 id 并维护映射表。**
5. ✅ `finish_reason` 枚举为 `stop` / `length` / `tool_calls`**没有国产厂商常见的 `sensitive`**——内容安全走 400 `content_filter`,不污染枚举。
### 6.2 `tool_choice` 与思考的互斥 ✅,但有一个危险的例外 🆕
| 模型 / 条件 | `required` | 具体函数 |
| --- | --- | --- |
| `kimi-k3`(思考开) | ✅ **200 正常工作** | ❌ 400 `tool_choice 'specified' is incompatible with thinking enabled` |
| `kimi-k3`(思考关) | ⚠️ 未测 | 🆕 **200,但见下** |
| `kimi-k2.7-code`(思考不可关) | ❌ 400 `tool_choice 'required' is incompatible with thinking enabled` | ❌ 400 |
| `kimi-k2.6`(思考关) | ⚠️ 未测 | ✅ 200 正常调用工具 |
| Anthropic 入口 `kimi-k3` | ⚠️ 未测 | ❌ 400(同 OpenAI 入口) |
- ✅ K3 思考开启时 `required` **可用**(文档未明说,实测正常返回 tool_calls);但 K2.7-code 的 `required` **报错**,错误信息是"与思考不兼容"。同样是"思考开启",两个模型对 `required` 的判定不同。
- 🆕 **最危险的一条**:K3 关闭思考 + `tool_choice` 指定函数 → **HTTP 200,但模型没有调用工具,而是直接编造了结果**:
```jsonc
// 请求:tool_choice 强制 get_weatherthinking disabled
// 响应 200,无 tool_callsfinish_reason: "stop"
{"content":"北京现在的天气情况如下:\n- **天气**:晴\n- **温度**28°C\n- **湿度**45%\n- **风向**:南风..."}
```
**`tool_choice` 被静默无视,模型直接幻觉出一份完整的天气数据。** 这比 400 危险得多——下游代码等着解析 `tool_calls`,拿到的却是一段看起来很真的假数据。**中转站不应把"关闭思考"作为绕过 `tool_choice` 限制的变通方案**,那样只是把显式失败变成静默错误。
> 结论:思考开启时无法强制指定函数,这个缺口**无法用参数改写安全绕过**。能力降级表建议**显式报错**而非降级。
### 6.3 动态工具加载(K3 独有)✅ 实测确认
```json
{"role": "system", "tools": [{"type":"function","function":{...}}]}
```
- 这条 system 消息**没有 `content` 字段**——OpenAI 协议里不存在的消息形状。实测 K3 上 200 正常,模型正确调用了工具。
- ✅ `kimi-k2.6` 上 → `400 Invalid request: tokenization failed`(一个极不直观的报错,实测确认)。
- 消息位置决定工具从哪轮起可见;与顶层 `tools` 可共存。
- 官方指出顶层 `tools` 不影响前缀缓存,动态工具因进了消息历史所以会影响。
> 中转站风险:这条消息**无法用 OpenAI/Anthropic 语义表达**。若做严格消息 schema 校验("system 必须有 content")会把合法请求拒掉。**IR 里要给消息保留 passthrough 扩展字段,别做白名单式裁剪。**
### 6.4 内置工具:两套并存 ✅/⚠️
**a`builtin_function` 协议**(实测确认):
```json
{"type": "builtin_function", "function": {"name": "$web_search"}}
```
实测响应:
```json
{"tool_calls":[{"id":"t-web_search-6a7d5fde","type":"builtin_function",
"function":{"name":"$web_search","arguments":"{\"search_result\":{\"search_id\":\"dda31b08...\"},\"usage\":{\"total_tokens\":6674}}"}}]}
```
- 🆕 `tool_calls[].type` 回传的是 **`builtin_function`**,不是 OpenAI 枚举里的 `function`。**下游若按 OpenAI 枚举做校验会拒掉这个响应。**
- 🆕 id 格式又是另一套:`t-web_search-{短hash}`(与普通工具的 `get_weather_0` 不同)。
- ✅ 反直觉之处确认:**模型内部已执行搜索**(`arguments` 里直接带 `search_id` 和 `usage.total_tokens: 6674`),但客户端仍要把 `arguments` **原样**塞回 `role: "tool"` 消息再发一次。即"假装自己执行了"。
- 🆕 注意首轮的 `usage.total_tokens` 只有 93,而 arguments 内嵌的 `usage.total_tokens` 是 **6674**——**搜索消耗的 token 不在响应的 usage 里**,要从 arguments 里解析。中转站按响应 usage 计费会漏掉绝大部分搜索成本。
- ✅ 计费:每次搜索按次收费,搜索结果计入下一轮 `prompt_tokens`。
**b)官方工具(Formula API)** ⚠️ 未实测 —— 另一套体系,12 个工具:`convert`、`web-search`、`rethink`、`random-choice`、`mew`、`memory`、`excel`、`date`、`base64`、`fetch`、`quickjs`、`code-runner`。走标准 `function` 协议 + Formula URI`moonshot/{tool-name}:latest`),搜索类返回 `encrypted_output``----MOONSHOT ENCRYPTED BEGIN----...`)需原样回传。
⚠️ **`web-search`(连字符,官方工具)与 `$web_search`(美元符号,builtin_function)是两个不同的东西**,文档没交代关系。已实测的是后者。
> 中转站建议:内置工具**不要跨厂商翻译**(OpenAI `web_search_preview`、Anthropic `web_search_20250305`、Kimi `$web_search` 的执行模型和计费都不同),按 opaque 透传,并在文档标明"内置工具与上游绑定"。
---
## 7. 结构化输出 ✅(文档为准,部分实测)
`response_format` 支持 `text` / `json_object` / `json_schema`。
```json
{"type": "json_schema", "json_schema": {"name": "...", "strict": true, "schema": {...}}}
```
- ✅ `json_object` 实测可用,且可与 partial 叠加(§5)。
- ⚠️ `strict: true` 走 token 级约束解码,要求 schema 符合 **MFJSMoonshot Flavored JSON Schema**——**自造方言**,与 OpenAI Structured Outputs 子集不是同一套。文档未给完整规则页。**未实测。**
- ⚠️ 文档罕见坦诚地承认支持度**按模型分层**:`kimi-k2.7-code` 最全(`anyOf`/`oneOf`/`$ref`/`additionalProperties`);`kimi-k3` 稳定;**`kimi-k2.6` 复杂 schema 不稳定——`$ref` 可能返回 Markdown 代码块、`oneOf` 可能被忽略**。
- ✅ JSON Mode 只保证生成 JSON **Object**(不会返回顶层数组/标量)。
> 建议:`json_schema` **不做跨家 schema 转换**,透传 + 记录失败率。K2.6 可能吐 Markdown 围栏意味着"保证返回合法 JSON"的承诺需要中转站自己剥围栏——那属于改写响应体,需明确取舍。
---
## 8. 流式输出:`usage` 位置**由 `include_usage` 决定** ❌(推翻文档)
文档说 usage "只出现在最后一个 chunk 的 `choices[0].usage`OpenAI SDK 的 `chunk.usage` 返回 None"。实测发现**两种模式并存,取决于是否传 `stream_options.include_usage`**
**不传 `include_usage`**Kimi 私有行为):
```jsonc
// 最后一个 chunk —— usage 嵌在 choices[0] 里,与 finish_reason 同层
data: {"id":"...","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop",
"usage":{"prompt_tokens":88,"completion_tokens":211,"total_tokens":299,
"completion_tokens_details":{"reasoning_tokens":186}}}],"system_fingerprint":"fpv0_489ccb45"}
data: [DONE]
```
**传 `stream_options: {"include_usage": true}`**(与 OpenAI 完全一致):
```jsonc
// 独立的最后一个 chunk —— choices 为空数组,usage 在顶层
data: {"id":"...","object":"chat.completion.chunk","choices":[],
"usage":{"prompt_tokens":88,"completion_tokens":182,"total_tokens":270,"cached_tokens":88,
"completion_tokens_details":{"reasoning_tokens":166},"prompt_tokens_details":{"cached_tokens":88}}}
data: [DONE]
```
> **中转站结论(比文档的推论简单得多):总是显式传 `stream_options.include_usage = true`**,就能拿到 OpenAI 标准位置的 usage,不必改写响应体。代价从"改写响应"降为"改写请求"。
>
> 两个附带差异:① 不传时**也有** usage(OpenAI 是不传就完全没有),所以"不传就没计费数据"的假设在 Kimi 上不成立;② **只有传了 `include_usage` 的那个 chunk 才带 `cached_tokens`**——不传时的 `choices[0].usage` 里没有缓存字段,会漏掉缓存计费信息。
其余流式行为 ✅ 与 OpenAI 一致:`data:` 前缀、`chat.completion.chunk`、**增量** delta(不是阿里那种全量累积)、`role` 只在首 chunk、中途有 `"delta":{}` 空 chunk、`data: [DONE]` 收尾。官方要求**以 `[DONE]` 判断结束而非 `finish_reason`**。
🆕 每个 chunk 都带 `system_fingerprint`(如 `fpv0_489ccb45`),**非流式响应里没有这个字段**——两种模式的响应字段集不一致。
### 自动断线重连 ⚠️
官方那篇《自动断线重连》**只是应用层 try/except 重试循环**(最多 100 次、间隔 1s),**没有任何协议级续传**——没有断点 offset,没有 resume id,重连就是整个请求重发。标题容易让人误以为有 resumable stream。中转站不要指望它做流式续传。
---
## 9. 多模态:**不支持 http(s) 图片 URL** ✅(实测确认)
```
{"type":"image_url","image_url":{"url":"https://statics.moonshot.cn/.../logo.png"}}
→ 400 Invalid request: unsupported image url: https://statics.moonshot.cn/.../logo.png
```
注意这个 URL 还是 Kimi 自家域名的图片,同样被拒。只接受两种形式:
- base64 data URI`data:image/png;base64,...`
- **Kimi 自有文件协议:`ms://<file_id>`**(先经 `/v1/files` 上传)
> 与 OpenAI(接受公网 URL)和 Anthropic`source.type: "url"`)的硬性差异。中转站要么替用户下载转 base64(引入出网、体积、超时、**SSRF** 风险),要么显式报错。**建议默认报错,把"自动下载转码"做成可选开关**——代下载会让中转站变成任意 URL 的抓取代理。
其他 ✅(文档,未逐条实测):
- `image_url` 既接受 `{"url": "..."}` 对象也接受**直接字符串**,比 OpenAI 宽松。
- 请求体上限 **100MB**;图片张数无硬限,受体积约束。建议图片 ≤4K、视频 ≤1080p。
- **`content` 必须是真数组,不能是序列化后的 JSON 字符串**——文档特意警告"序列化成字符串后各模型行为不可预测"。中转站做消息归一化时若曾把 content 压成字符串(OpenAI 单文本场景的常见优化),多模态消息上必须禁用。
- 视频输入(`video_url`)仅 K2.5/K2.6/K2.7-code/K3 支持——**OpenAI 协议里没有的模态**,跨家映射无对应物。
---
## 10. Context Caching:全自动 ✅,但 256 token 阈值 ❌
⚠️ **历史怪癖已消失**:Kimi 早期的显式 Cache 对象 API 已被移除,现行是**全自动前缀缓存**,无需创建或引用 cache id。(`china-providers.md` §3 原先标注的"需查证"由此作废。)
❌ **文档说"prompt tokens > 256 才能命中前缀缓存"——实测不成立**:
```
prompt_tokens: 88 → cached_tokens: 88 (命中)
prompt_tokens: 21 → cached_tokens: 21 (命中,仅 21 token
```
21 token 的请求也完整命中。实际阈值远低于 256,或该限制已取消。
🆕 **`cached_tokens` 在响应里出现两次**(同一个值):
```json
"usage": {"prompt_tokens":88, "completion_tokens":211, "total_tokens":299,
"cached_tokens": 88, // 扁平(Kimi 私有)
"prompt_tokens_details": {"cached_tokens": 88}} // 嵌套(OpenAI 兼容)
```
文档只提到扁平那个。**中转站直接用 `prompt_tokens_details.cached_tokens` 即可对齐 OpenAI,不需要做字段映射**(比文档描述的情况好)。同理输出侧有 `completion_tokens_details.reasoning_tokens`。
⚠️ 但注意**首次请求的响应里完全没有缓存字段**(未命中时字段不出现,而非返回 0)——解析时要容忍缺失。
✅ 生命周期由系统管理,TTL 未公开。✅ 计价差异极大(K3):**命中 ¥2 / 1M,未命中 ¥20 / 1M**,输出 ¥100 / 1M。
> **中转站影响(被低估的一条)**:10 倍输入价差意味着"保持前缀稳定"是硬性成本要求。任何对 prompt 前缀的改写——注入 system 提示、重排消息、规范化空白、动态插入时间戳——都会让缓存失效,成本瞬间 10 倍。**规则:对 Kimi 上游的请求,前缀必须逐字节稳定。** 同理动态工具进消息历史会影响缓存,顶层 `tools` 不会(§6.3)。
🆕 **`prompt_cache_key` 实测被接受**(200,不报错),但因为缓存本来就自动命中,无法从外部观测它是否真的分裂了缓存命名空间。⚠️ 语义仍未知,文档的 Context Caching 指南完全没提这个参数。
🆕 **Anthropic 入口的 `cache_control: {"type":"ephemeral"}` 被接受但不生效**:返回 `cache_creation_input_tokens: 0`。自动缓存机制接管了,显式缓存断点是空操作。
---
## 11. 文件、Batch 与辅助端点
### 11.1 文件问答 ✅(文档,未实测)
流程是"上传 → 抽取 → **把抽取出的文本当作 system 消息塞进 messages**",官方强调放的是**文件内容而不是 file id**。模型侧没有"引用 file id 做问答"的通道(`ms://` 只用于多模态图片/视频块)。
- 单用户最多 **1000 个文件**,单文件 **100MB**,空文件报错。
- ⚠️ **`purpose` 取值文档自相矛盾**:错误码页说"仅接受 `file-extract`"batch 创建页要求 `purpose="batch"`。至少两个合法值。
- 抽取行为:文本原样抽;图片走 OCR(**无文字则失败**);PDF 直接抽文本,纯图 PDF 走 OCR。
- 文件类 API **限时免费**。
### 11.2 Batch API ✅(文档,未实测)
- `POST /v1/batches`,字段 `input_file_id` / `endpoint`(仅 `/v1/chat/completions`/ `completion_window` / `metadata`(≤16 对,key ≤64 字符,value ≤512 字符)。
- ⚠️ `completion_window` 取值两处不一致:API 页说"最小 12h 最大 7d",指南页说 `24h`/`3d`/`7d`。
- **省 40%** 推理费。**仅支持 `kimi-k2.6` / `kimi-k2.5`,不支持 `kimi-k3`**。
- batch 请求体里**不得包含** `temperature` / `top_p` / `n` / 两个 penalty——§3 那条规则在 batch 上变成"不许出现"而非"必须等于固定值"。
- JSONL 每行需 `custom_id`(文件内唯一)/ `method`(固定 POST/ `url`(固定 `/v1/chat/completions`/ `body`,文件 ≤100MB。
- 支持多模态 batchbase64 或 `ms://`)。
### 11.3 官方 tokenizer ✅(实测)
```
POST /v1/tokenizers/estimate-token-count
{"model":"kimi-k3","messages":[{"role":"user","content":"你好世界"}]}
→ {"code":0,"data":{"total_tokens":87},"scode":"0x0","status":true}
```
🆕 **四个汉字算出 87 token**——因为它把 §4.1 那段思考模式的隐藏 system prompt(约 67 token)也算进去了,与实际 chat 请求的 `prompt_tokens: 88` 基本吻合。
> 这其实是好事:**官方 tokenizer 的口径与计费口径一致**,中转站做成本预估/限流应当用它,而不是套 tiktoken(Kimi 不是 OpenAI 词表)。但要知道它包含隐藏开销,别拿它的结果去和"用户可见文本长度"做对比,会显得离谱。代价是每次预估多一次网络往返。
### 11.4 余额 ✅(实测)
```
GET /v1/users/me/balance
→ {"code":0,"data":{"available_balance":15,"voucher_balance":15,"cash_balance":0},"scode":"0x0","status":true}
```
`cash_balance` **可为负数表示欠费**`available_balance <= 0` 时无法调用推理 API。可作上游健康检查与余额预警(对应 DeepSeek 的 `/user/balance`,路径和字段名都不同)。
❌ **文档说"K3 需最低充值 ¥10 解锁,注册代金券不解锁 K3"——不成立**:实测账号 `cash_balance: 0`、余额全是代金券,K3 调用完全正常。
---
## 12. 错误码与限流
### 12.1 错误体 ✅/🆕
OpenAI 入口:`{"error": {"message": "...", "type": "..."}}`——**没有 OpenAI 的 `code` / `param` 字段**。中转站若要输出严格 OpenAI 错误结构需补 `code: null` / `param: null`。
另有 Anthropic 入口格式和网关格式,共三种,见 §1.3。
### 12.2 错误码表 ✅(打勾项为实测触发)
| HTTP | `type` | 触发 |
| --- | --- | --- |
| 400 | `invalid_request_error` | ✅ 参数越界/固定值不符/schema 错/`tokenization failed`/`unsupported image url`/`tool_call_id is not found` |
| 400 | `content_filter` | 内容安全拦截 |
| 401 | `invalid_authentication_error` / `incorrect_api_key_error` | 鉴权头格式错 / key 无效(**含拿错区域的 key** |
| 403 | `permission_denied_error` | API 未开通 / 越权 / IP 不在组织白名单 |
| 404 | `resource_not_found_error` | ✅ 模型不存在(下线模型、`kimi-k3[1m]` |
| 429 | `engine_overloaded_error` | ✅ **上游过载**——退避重试 |
| 429 | `exceeded_current_quota_error` | **余额不足/欠费**——重试无用 |
| 429 | `rate_limit_reached_error` | ✅ **并发/RPM/TPM/TPD 超限**——降频 |
| 499 | `client_closed_request` | 客户端提前断开 |
| 500 | `server_error` / `unexpected_output` | 服务端错误 |
| 503 | `server_unavailable` | 扩缩容/维护 |
| 504 | — | **900 秒无响应**;官方建议长请求开 `stream: true` |
**429 必须按 `type` 分流** ✅——三种 429 处置完全相反:过载要退避、欠费重试纯属浪费、限流要降并发。只看状态码的通用重试逻辑在欠费时会打爆上游。实测两种 429 都遇到了,`engine_overloaded_error` 在正常低频调用下也会偶发。
🆕 **429 的错误信息泄漏账号标识**:
```
Your account org-xxxxxxxx<ak-xxxxxxxxxxxxxxxx> request reached organization max RPM: 3, please try again after 1 seconds
```
**org id 和 ak idAPI key 的标识部分)直接出现在 message 里。** 中转站若把上游错误原样透传给下游,等于把自己的账号标识和 key 标识广播给所有用户。**这是必须在错误改写层拦截的信息泄漏点**,比 §1.2 的响应头更严重(响应头容易被整体过滤,错误 message 往往被原样转发)。
其他:
- ✅ **499 是非标准状态码**nginx 系)。中转站的 HTTP 客户端和监控要能处理,别当 4xx 计入用户失败率。
- ✅ **504 的 900 秒超时**——中转站自身的上游超时必须 ≥900s,否则会在 Kimi 还在算时先断开,浪费已消耗的 token。
- ✅ 官方提醒:**OpenAI SDK 的自动重试会把 1 次请求放大成 2–3 次真实调用**,吃掉 RPM 配额。中转站应关掉 SDK 层重试,自己实现按 type 分流的重试。
### 12.3 限流响应头 🆕(文档完全未记载)
实测 429 响应头:
```
retry-after: 1
x-retry-after: 1
msh-cooldown-seconds: 1
```
**同一个信息重复了三份**(标准头 + 两个私有头),但**没有任何 `x-ratelimit-*` 头**——即**无法在请求成功时得知剩余配额**,只能等撞到 429 才知道。
> 中转站的配额管理因此只能靠本地计数 + 429 反馈,做不了"提前避让"。`retry-after` 是标准头,直接用它做退避即可。
### 12.4 限流分档 ✅
按累计充值金额定档(代金券不算):
| Tier | 累计充值 | 并发 | RPM | TPM | TPD |
| --- | --- | --- | --- | --- | --- |
| 0 | ¥0 | 1 | 3 | 500K | 1.5M |
| 1 | ¥50 | 50 | 200 | 2M | 无限 |
| 2 | ¥100 | 100 | 500 | 3M | 无限 |
| 3 | ¥500 | 200 | 5K | 3M | 无限 |
| 4 | ¥5K | 400 | 5K | 4M | 无限 |
| 5 | ¥20K | 1K | 10K | 5M | 无限 |
✅ **Tier 0 的 3 RPM 是真实且严格的**——本次实测全程必须每请求间隔 21 秒,任何并发都立刻 429。联调时若发现大量 `rate_limit_reached_error`,先看档位而不是查代码。
🆕 响应头 `msh-gid` 直接给出档位(实测为 `free`),**中转站可用它自动探测上游档位并据此设置本地并发上限**,无需人工配置。
---
## 13. 计价(K3,人民币)✅
| 项 | 单价 / 1M tokens |
| --- | --- |
| 输入(缓存命中) | ¥2 |
| 输入(缓存未命中) | ¥20 |
| 输出 | ¥100 |
- 1M = 1,000,000。无按上下文长度分档。
- **输出是命中输入的 50 倍**。加上思考默认 `max` 档、隐藏 system prompt、Preserved ThinkingK3 的实际输出 token 量显著高于同类模型——实测一句"说:好"能烧掉 **310 个 completion tokens**(其中 287 是 reasoning),而关闭思考后只要 **11 个**。
- **同一请求关不关思考差 28 倍输出成本**,这是 §4.1"K3 思考实际可关"这条发现的直接价值。
- Batch 省 40%(不支持 K3);联网搜索按次另计费,且**搜索 token 不在响应 usage 里**(§6.4)。
---
## 14. 实测推翻的文档结论(速查)
| # | 文档说法 | 实测结果 |
| --- | --- | --- |
| 1 | K3 思考恒开、不可关闭、不使用 `thinking` 参数 | ❌ `thinking:{"type":"disabled"}` 生效,`prompt_tokens` 88→21,无 `reasoning_content` |
| 2 | 流式 usage 只在 `choices[0].usage` | ❌ 传 `include_usage:true` 时在顶层 + `choices:[]`,与 OpenAI 一致 |
| 3 | `reasoning_effort` 仅接受 `low`/`high`/`max` | ❌ `"medium"`、`"banana"` 都返回 200,非法值静默忽略 |
| 4 | K3/K2.7-code 多轮缺 `reasoning_content` 会报错 | ❌ 200 正常继续 |
| 5 | `role:"tool"` 消息必须带 `name` | ❌ 不带也 200 |
| 6 | Partial Mode 与 `json_object` 冲突 | ❌ 200 正常工作 |
| 7 | 缓存需 prompt > 256 token 才命中 | ❌ 21 token 也命中 |
| 8 | K3 需最低充值 ¥10、代金券不解锁 | ❌ 纯代金券账号正常调用 K3 |
| 9 | Claude Code 用 `kimi-k3[1m]` | ❌ 两个入口都 404,正确名是 `kimi-k3` |
| 10 | `kimi-k2.5` context_length 128000(示例) | ❌ 实际 262144 |
**文档说不能、实际能**(1、3、4、5、6、7、8)意味着照文档写会**过度限制**;**文档说能、实际不能**(9)意味着照抄配置会**直接失败**。两个方向都有。
---
## 15. 仍待验证(下一轮实测)
1. **`json_schema` + `strict: true` 的 MFJS 实际约束**(§7)——本轮未测,是结构化输出的核心。
2. **K2.6 复杂 schema 吐 Markdown 围栏**的复现条件(§7)。
3. **官方工具(Formula API)与 `$web_search` 的关系**(§6.4)。
4. **`prompt_cache_key` 是否真的分裂缓存命名空间**(§10)——需要构造缓存命中/未命中对照。
5. **文件上传 `purpose` 的合法枚举全集**、Batch 端到端(§11.1、§11.2)。
6. **K3 关闭思考后 `tool_choice: "required"` 的行为**(§6.2)——指定函数已知会静默幻觉,`required` 未测。
7. **长对话 + 多步工具链下 `reasoning_content` 是否真的可省**(§4.3)——短对话已验证可省,长链路需复验。
8. **国际站 `api.moonshot.ai`** 的行为是否与国内站一致(本轮只测了国内站)。
9. **`kimi-k2.5`** 的思考开关与 max output(本轮未覆盖该模型)。
---
## 16. 中转站接入 checklist
**必须做的请求改写**
- [ ] 按模型剥离 `temperature`/`top_p`/`n`/`presence_penalty`/`frequency_penalty`(§3)——**最高优先级**,同时消除两个入口的行为差异。
- [ ] 流式请求一律补 `stream_options.include_usage = true`(§8)——用改写请求换取 OpenAI 标准的 usage 位置和缓存字段。
- [ ] thinking 参数按模型分派:K3 可用 `reasoning_effort` 或 `thinking`K2.7-code 只能 `enabled`K2.6/K2.5 用 `thinking`(§4)。
- [ ] 自行校验 `reasoning_effort` 枚举(上游不校验,非法值会静默跑最贵档)(§4.2)。
- [ ] `prefill` → `partial: true`(§5)。
- [ ] `n > 1` 时连带检查 `temperature > 0`,否则上游报的错无法理解(§3.1)。
- [ ] 模型名 `kimi-k3[1m]` 之类的方括号后缀要拒绝或剥离(§1.3)。
**必须做的响应/错误改写**
- [ ] **剥掉 `msh-org-id` / `msh-uid` / `msh-project-id` 响应头**(§1.2,账号标识泄漏)。
- [ ] **过滤 429 错误 message 里的 `org-xxx<ak-xxx>`**(§12.2,账号+key 标识泄漏)。
- [ ] 重写 `tool_calls[].id`(上游是 `{函数名}_{序号}`,多轮会重复)(§6.1)。
- [ ] 错误体补 `code`/`param` 以对齐 OpenAI;解析上游错误时容忍 `error` 字段可能是 string(网关格式)(§12.1、§1.3)。
- [ ] 非 chat 端点要剥掉 `{"code","scode","status","data"}` 业务包装(§1.2)。
- [ ] `tool_calls[].type` 可能是 `builtin_function`,不要按 OpenAI 枚举拒绝(§6.4)。
**必须保持不动的**
- [ ] prompt 前缀逐字节稳定,不注入、不重排、不规范化空白(§10,**10 倍价差**)。
- [ ] `content` 数组不要压成字符串(§9)。
- [ ] 内置工具与 `encrypted_output` 按 opaque 透传(§6.4)。
- [ ] 不给未传参数补默认值(§3.1)。
- [ ] 消息保留 passthrough 扩展字段(动态工具的 `{"role":"system","tools":[...]}` 无 content)(§6.3)。
**能力降级表**
- [ ] 思考开启时**无法强制调用指定函数**——应**显式报错**,不要靠关闭思考绕过(§6.2,会静默幻觉出假数据)。
- [ ] K2.7-code 思考不可关、`required` 也不可用(§4、§6.2)。
- [ ] 不支持 http(s) 图片 URL(§9)。
- [ ] 新模型不支持 `n > 1`(§3.1)。
- [ ] 动态工具加载仅 K3(§6.3)。
**运维配置**
- [ ] 上游超时 ≥900s(§12.2)。
- [ ] 关闭 SDK 层自动重试,按 error `type` 分流(过载退避 / 欠费停止 / 限流降并发)(§12.2)。
- [ ] 用 `retry-after` 头做退避;**没有 `x-ratelimit-*`,配额只能本地计数**(§12.3)。
- [ ] 用响应头 `msh-gid` 自动探测账号档位并设置本地并发上限(§12.4)。
- [ ] key 与区域 base URL 绑定成对(§1.1)。
- [ ] "200 OK + 空 content + `finish_reason: length`" 不触发重试(§4.4)。
- [ ] 用 `/v1/tokenizers/estimate-token-count` 做预估(口径含隐藏 system prompt,与计费一致)(§11.3)。
**Kimi For Coding 走单独一套配置**(§17):模型别名表、鉴权、限流、错误改写都不能与 platform 共用。
---
## 17. Kimi For Coding:同一家公司的另一套栈 ✅(2026-08-13 实测)
> Base URL`https://api.kimi.com/coding/v1`(根路径 `https://api.kimi.com/coding/` 返回 `{"message":"Welcome to the Kimi For Coding API!"}`
> Key 形如 `sk-kimi-...`。面向 Claude Code / 编码 Agent 的**订阅制**产品,不是 platform 的一个套餐。
**底层推理栈相同**(流式 `system_fingerprint` 两边都是 `fpv0_489ccb45`),**但接入层是完全独立的两套实现**。官方 troubleshooting 只用一句"API Platform keys ≠ Kimi Code keys"带过,实际差异远不止鉴权。
### 17.1 差异总表
| 维度 | platform`api.moonshot.cn` | Kimi For Coding`api.kimi.com/coding` |
| --- | --- | --- |
| Key 前缀 | `sk-` | **`sk-kimi-`** |
| 账号体系 | 组织/项目/余额 | **个人账号**(见 §17.4 |
| 模型名 | `kimi-k3`、`kimi-k2.7-code`、`moonshot-v1-*` | **`k3`、`k3-256k`、`kimi-for-coding`、`kimi-for-coding-highspeed`** |
| 模型名校验 | 严格,未知名 404 | ❌ **几乎不校验**(见 §17.3) |
| OpenAI 入口 | `/v1/chat/completions` | `/v1/chat/completions` |
| Anthropic 入口 | `/anthropic/v1/messages` | **`/v1/messages`**(与 chat 同前缀,无 `/anthropic` |
| thinking `signature` | ❌ 无 | ✅ **有真实签名** |
| Anthropic usage 字段 | 混入 OpenAI 字段(双套并存) | **纯 Anthropic 字段** |
| Anthropic 响应 id | `chatcmpl-...`(不规范) | **`msg_...`**(规范) |
| SSE 事件分隔 | `event: message_start`(有空格) | `event:message_start`**无空格** |
| `count_tokens` | ❌ 无 | ✅ `POST /v1/messages/count_tokens` |
| `estimate-token-count` | ✅ 有 | ❌ 404 |
| 余额查询 | ✅ `/v1/users/me/balance` | ❌ **404,无余额端点** |
| 账号信息端点 | ❌ 无 | 🆕 **`/v1/me`(返回手机号等 PII** |
| `tool_calls[].id` | `get_weather_0`(可预测、会重复) | **`tool_jfSEsjJ6HvGswLR2r8XaBjnc`**(随机串,规范) |
| `$web_search` 的 id | `t-web_search-{hash}` | `tool_{随机串}` |
| 响应 id | `chatcmpl-6a7d5c23...`(十六进制) | `chatcmpl-JfEbxqHjKWC5fADqUduALJfG`(大小写混合) |
| 限流 | Tier 0 = **3 RPM / 1 并发** | ✅ **并发 10 全部 200**,无可观测的 RPM 限制 |
| 调试响应头 | `msh-request-id` / `msh-org-id` / `msh-uid` / `msh-gid` | **`x-trace-id`**、`server-timing`、`server: nginx`**无 `msh-*`** |
| 网关错误 | 中文 `{"code":5,"error":"url.not_found","message":"没找到对象"}` | 统一 API 格式 `{"error":{"message":"The requested resource was not found","type":"resource_not_found_error"}}` |
| `/v1/models` 字段 | `owned_by`/`permission`/`root`/`parent` | **`display_name`/`created_at`/`type`** + 分页字段 `first_id`/`last_id`/`has_more` |
| `reasoning_effort` 默认 | `max` | **`high`**(省钱档) |
**行为一致的部分**(说明共用推理层):采样参数硬 400(`invalid temperature: only 1 is allowed for this model` 逐字相同)、`tool_choice` 指定函数与思考互斥的 400、K3 `thinking:{"type":"disabled"}` 可关(`prompt_tokens` 同样 21)、partial mode、`$web_search` 的 builtin_function 协议、流式 usage 由 `include_usage` 控制、`system_fingerprint` 值相同。
🆕 但**同一个校验的错误文案两边不同**,说明校验逻辑各写了一遍:
```
n=2 platform : invalid n: only 1 is allowed for this model
n=2 coding : Your request body contains invalid value for param n
```
### 17.2 模型列表(实测 `GET /coding/v1/models`
| id | display_name | context | 实测备注 |
| --- | --- | --- | --- |
| `kimi-for-coding` | K2.7 Coding | 262144 | **未知模型名的 fallback 目标**(§17.3.1);无隐藏 system prompt |
| `kimi-for-coding-highspeed` | K2.7 Coding Highspeed | 262144 | ⚠️ 与上一行**实测无法区分**(指纹、上限、速度全同) |
| `k3` | K3 | 1048576 | `default_effort: "high"`;带 79 token 隐藏 system prompt |
| `k3-256k` | K3-256k | 262144 | 实测是**网关层给 `k3` 加的 256K 限制**,非独立模型;`supports_video_in: false` |
四个模型全部 `supports_thinking_type: "only"`。**没有 moonshot-v1 系列**。
> ⚠️ 注意 `k3-256k` 是**同模型的短上下文 SKU**——这与 platform 用 `moonshot-v1-8k/32k/128k` 切窗口是同一思路,但命名规则又不一样。中转站的别名表要为两个上游各维护一份。
### 17.3 ❌ 最危险的差异:`model` 字段几乎不校验
实测 Kimi For Coding 对未知模型名**不报错,而是照常返回 200 并把请求的名字原样回显**:
```jsonc
{"model":"gpt-4", ...} → 200,响应 "model":"gpt-4",但内容带 reasoning_content(实际是 Kimi 在跑)
{"model":"claude-sonnet-4-5",...} → 200,响应 "model":"claude-sonnet-4-5"
{"model":"moonshot-v1-8k", ...} → 200,响应 "model":"moonshot-v1-8k"(该模型在此产品里根本不存在)
{"model":"foobar123", ...} → 200,响应 "model":"foobar123"
{"model":"", ...} → 200,响应 "model":""(空字符串也接受)
```
**唯一被拒的是带方括号的名字**,而且报错方式很离谱:
```
{"model":"k3[1m]"} → HTTP 401
{"error":{"message":"Your model id does not exist, recognized as other:k3[1m]. Please set model id as `k3`.",
"type":"invalid_authentication_error"}}
```
**模型不存在却返回 401 + `invalid_authentication_error`**——状态码和错误类型都与语义不符(platform 同样情况返回的是 404 `resource_not_found_error`)。中转站若按 401 触发"key 失效"逻辑去刷新凭据或告警,会被这个错误误导。
> **对中转站的三重后果**:
> 1. **无法用模型名做路由校验**——上游不会告诉你名字写错了,只会默默用 fallback 模型跑完并收钱。
> 2. **响应的 `model` 字段不可信**——它是请求值的回显,不是实际推理的模型。platform 那边至少 `moonshot-v1-auto` 会回显真实模型名(§2.1),这边连这个都没有。
> 3. **下游可能被误导**:用户传 `gpt-4` 拿到 200 和一个 `"model":"gpt-4"` 的响应,会以为真的调用了 GPT-4。**中转站必须自己维护白名单并拒绝未知模型名**,不能依赖上游校验。
### 17.3.1 反推:模型名确实参与路由,未知名 fallback 到 `kimi-for-coding` ✅
响应只回显请求值,无法直接观测实际模型。用**两个独立的侧信道**反推,结论一致。
**方法一:上下文上限边界。** 构造 270,015 token 的 prompt(经 `count_tokens` 校准,介于 262144 与 1048576 之间),分别打各模型名:
| 请求的 model | 结果 |
| --- | --- |
| `k3` | ✅ **200 正常返回**(真的吃下了 270K) |
| `k3-256k` | `k3-256k supports only 256K context.` |
| `kimi-for-coding` | `Invalid request: Your request exceeded model token limit: 262144 (requested: 270016)` |
| `gpt-4` | **同上,逐字相同** |
| `foobar123` | **同上,逐字相同** |
**方法二:隐藏 system prompt 的 token 指纹。** §4.1 发现思考模式会注入一段隐藏 system prompt,各模型注入量不同,可作指纹。同一句 `"说:好"`
| 请求的 model | `prompt_tokens` |
| --- | --- |
| `k3` | **89** |
| `k3-256k` | **89** |
| `kimi-for-coding` | **10** |
| `kimi-for-coding-highspeed` | **10** |
| `gpt-4` / `foobar123` / `""` / `claude-sonnet-4-5` | **10** |
两个方法交叉印证:
- ✅ **模型名确实参与路由**——`k3` 走 1M 上下文且带 79 token 的隐藏 system prompt`kimi-for-coding` 系列走 256K 且**无隐藏 prompt**。二者是两条不同的推理路径。
- ✅ **所有无法识别的模型名(含空字符串)一律 fallback 到 `kimi-for-coding` 系列****不是** `k3`。
- ✅ `k3-256k` 是**网关层对 `k3` 加的 256K 限制**,不是独立模型:它的隐藏 prompt 指纹与 `k3` 相同(89),但超限错误是一条**人工写的独立文案**(不含 `requested:` 数字),与 `kimi-for-coding` 那条模型层通用错误明显不同源。
> **中转站可直接用的结论**:用户若传了拼错的模型名,实际跑的是 K2.7 Coding(256K),而不是他以为的 K3——**上下文能力从 1M 悄悄降到 256K**,长上下文任务会在毫无提示的情况下超限失败。这使"自建模型名白名单"从建议变成必需。
⚠️ 仍无法区分的是 `kimi-for-coding` 与 `kimi-for-coding-highspeed`:两者 token 指纹相同(均为 10)、上下文上限相同(262144)、**实测输出速度也无差异**33.2 vs 34.9 tok/s,重复一轮后 highspeed 反而更慢)。官方宣称 highspeed 约 180 tok/s**实测两者都只有约 33 tok/s**——要么高速档在当前负载/订阅层级下未生效,要么二者本就是同一后端。
### 17.4 🆕 `/coding/v1/me` 返回个人身份信息(隐私暴露面)
platform 只有余额端点(返回金额),Kimi For Coding 则有一个 `/v1/me`
```jsonc
{
"user_id": "...", "global_id": "...",
"nickname": "登月者XXXX",
"avatar": "https://avatar.moonshot.cn/avatar/default/...",
"phone": {"country_code": "86", "number": "1XX****XXXX"}, // ← 部分脱敏的手机号(实测返回真实号码的首三位+后四位)
"status": "USER_STATUS_NORMAL", "region": "REGION_CN",
"user_level": 25, "user_level_name": "Allegretto",
"domain": 1, "domain_name": "DOMAIN_NEXUS",
"created_time": "...", "last_login_time": "..."
}
```
> **任何持有这个 key 的人都能读到账号主人的手机号(部分脱敏)、昵称、注册与最后登录时间。** 对中转站的含义:
> - **把 Kimi For Coding 的 key 交给第三方使用 = 交出账号主人的部分身份信息**,风险等级远高于 platform 的 key(那边最多泄漏余额)。多租户中转站尤其要注意。
> - 中转站**不应把 `/v1/me` 透传给下游**。若要做上游健康检查,用它是可行的(无余额端点可用),但响应必须裁剪,只保留 `status`。
> - 反过来,这也是**唯一能确认 coding key 是否有效**的轻量端点(没有余额接口)。
### 17.5 计费与配额:中转站看不见
- ❌ **没有余额/用量端点**`/balance`、`/usage`、`/quota`、`/subscription` 全部 404)。订阅制产品,用量只能在网页端看。
- 因此**中转站无法对 Kimi For Coding 做余额预警或成本核算**——只能自己按 usage 累加统计,且没有官方口径可对账。这与 platform(有 `/v1/users/me/balance`,可做健康检查与预警)是运维层面的实质差别。
- 限流宽松(10 并发实测无 429),但**限额规则未公开**,撞到限制时的错误类型未知(本轮未触发)。⚠️ 中转站应做好"上游可能在没有任何预警的情况下拒绝服务"的准备。
### 17.6 Anthropic 入口质量反而更高
Kimi For Coding 的 `/coding/v1/messages` 是**本次调研里最接近 Anthropic 官方规范的实现**
- ✅ 规范的事件序列:`message_start` → `content_block_start` → `content_block_delta` ×N → `content_block_stop` → `message_delta` → `message_stop`
- ✅ `msg_` 前缀的 id、纯 Anthropic 的 usage 字段、**thinking block 带 `signature`**
- ✅ 有 `POST /v1/messages/count_tokens`(返回 `{"input_tokens":9}`
- ✅ `x-api-key` 和 `Authorization: Bearer` 都接受,`anthropic-version` 头非必需
对比 platform 的 `/anthropic/v1/messages`id 是 `chatcmpl-`、usage 混两套字段、thinking 无 signature、无 count_tokens。
> **中转站若要做 Anthropic 格式的上游,Kimi For Coding 明显是更合适的一侧。**
### 17.6.1 反推:`signature` 是**装饰品**,服务端不校验 ✅
Anthropic 真机会校验 thinking 的 `signature`(篡改会报错),这是"推理内容不可跨家搬运"结论的根基(见 README §2 第 2 条)。实测 Kimi For Coding 的 signature(长达 12,946 字符,看起来很像真的):
| 多轮回传时对 thinking block 的处理 | 结果 |
| --- | --- |
| 原样回传 | 200 |
| **signature 全部替换成 `XXXX...`**(等长) | **200** |
| **完全删除 `signature` 字段** | **200** |
| **保留原 signature,但把 `thinking` 文本换成伪造内容** | **200** |
**四种情况全部正常返回,没有任何校验。**
> 这条对中转站是**减负**Kimi For Coding 的 thinking block **不必当 opaque blob 搬运**,可以自由丢弃 `signature`、截断甚至改写 thinking 内容——与 platform 那侧(明文无签名)的处理方式可以统一。
>
> 但反过来它是个**安全提示**:这个 signature 只是长得像密码学签名,实际不提供任何完整性保证。**中转站不要把它当作"上游已验证推理内容"的证据**,也不要在自己的对外 API 里模仿这种"假签名"——下游若据此信任内容,就是被误导。
>
> ⚠️ 注意方向性:这条结论**只适用于 Kimi**。转发到 Anthropic 真机时,signature 仍必须原样搬运。
### 17.7 结论:必须分开处理
**是的,必须当作两个完全独立的上游。** 具体到实现:
| 层面 | 要点 |
| --- | --- |
| 上游配置 | 独立的 base URL + key + 模型别名表;key 前缀(`sk-` vs `sk-kimi-`)可用于自动识别配错 |
| 模型路由 | **中转站自建白名单**——coding 侧不校验模型名,**错名静默 fallback 到 `kimi-for-coding`(256K),上下文能力从 1M 悄悄降级**(§17.3.1 |
| thinking | coding 侧的 `signature` 不参与校验,可自由丢弃/改写(§17.6.1);转发到 Anthropic 真机时则必须原样搬运 |
| 响应处理 | coding 侧的 `model` 字段是回显值,**不可用于记账**;platform 侧 `moonshot-v1-auto` 也有类似问题 |
| 错误处理 | coding 侧模型不存在返回 **401**,不能触发凭据刷新逻辑;两侧错误文案不同 |
| 限流 | platform 需按 Tier 严格限速(Tier 0 = 3 RPM);coding 侧宽松但规则不透明 |
| 运维 | platform 用 `/v1/users/me/balance` 做健康检查;coding 只能用 `/v1/me`**响应需裁剪,含 PII** |
| Anthropic 格式 | 优先用 coding 侧;但其 thinking `signature` 必须 opaque 搬运,platform 侧则不用 |
| 隐私 | coding key 泄漏 = 泄漏账号主人手机号等信息,**不可作为共享凭据下发** |