# kosong：LLM 抽象层

> kosong 的 provider 适配、finish reason 归一化与 thinking 档位，以及它在 v0.43 里的新位置：v2 引擎改用自带的 human/llm 请求库。

- 作者：David（道雾轩）
- 专栏：Kimi Code 源码解读（https://daiw.org/manual/kimi-code.md）
- 最后更新：2026-09-19
- 原文：https://daiw.org/manual/kimi-code/kosong
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

`packages/kosong`（马来语「空」）是 LLM / provider 抽象层，`src/` 约 9200 行，其中约 6900 行是 provider 适配。它**没有任何 workspace 内部依赖**，只依赖 Anthropic、OpenAI、Google 三家官方 SDK 与 zod、zod-to-json-schema，可以独立拿出来用。

## 位置变了

v0.29 时，kosong 是 v1 引擎发请求的那一层。v0.43 里：

- **agent-core-v2 不依赖 kosong**。它的 `package.json` 里没有 kosong，import 边界检查还会直接拦下任何引用（`packages/agent-core-v2/scripts/check-import-boundaries.mjs:135`）：

```js
    if (KOSONG_PATH_RE.test(specifier)) {
      violations.push({
        file: absFile,
        line,
        message: `the kosong kernel is deleted (${specifier}) — request/provider code lives in #human/llm, the v2 compatibility boundary is #/llm-adapter`,
      });
```

- **用 kosong 的是 `packages/node-sdk`**：SDK 的公开类型（`ContentPart`、`ThinkingEffort`、`ToolCall`）、provider 配置类型、错误类（`APIStatusError` 等）和 models.dev 目录解析都来自这里。

也就是说，kosong 仍是仓库里那个可独立使用的抽象层，但引擎发请求走的是 v2 自己的 `src/human/llm/`。下面先讲 kosong 本身，再做对照。

## 结构

```text
packages/kosong/src/
├── provider.ts        ChatProvider 接口与核心类型（274 行）
├── message.ts         消息与 ContentPart 模型（253 行）
├── generate.ts        生成流程（365 行）
├── catalog.ts         models.dev 风格的模型元数据（509 行）
├── errors.ts          错误分类（622 行）
└── providers/
    ├── anthropic.ts            1334 行
    ├── openai-responses.ts     1228 行
    ├── google-genai.ts          998 行
    ├── openai-legacy.ts         723 行  (Chat Completions)
    ├── kimi.ts                  682 行
    ├── kimi-errors.ts            87 行  (v0.29 之后新增)
    └── ...
```

`createProvider()` 按 `type` 分派：`anthropic`、`openai`（Chat Completions）、`openai_responses`、`google-genai` / `vertexai`、`kimi`。`ContentPart` 仍是 `TextPart | ThinkPart | ImageURLPart | AudioURLPart | VideoURLPart`，推理内容与视频输入都是一等类型。

## FinishReason 归一化

```ts
export type FinishReason =
  | 'completed'
  | 'tool_calls'
  | 'truncated'
  | 'filtered'
  | 'paused'
  | 'other';
```

`provider.ts:76` 上方的注释列了对照：`completed` 对应 OpenAI 的 `stop`、Anthropic 的 `end_turn` / `stop_sequence`、Gemini 的 `STOP`；`truncated` 对应 `length`、`max_tokens`、`MAX_TOKENS`、`max_output_tokens`；`paused` 是 Anthropic 特有的 `pause_turn`。最值得记住的一句：

> Note that the OpenAI Responses API and Google GenAI report `'completed'` here; only the Chat Completions–style providers and Anthropic surface a dedicated value.

**Responses API 与 Gemini 带着工具调用也报 `completed`**，所以判断要不要执行工具不能只看 finish reason。两个逃生舱照旧：`rawFinishReason` 保留原始字符串，`null` 表示 provider 根本没给。

## ThinkingEffort

```ts
export type ThinkingEffort = 'off' | 'on' | (string & {});
```

`(string & {})` 让联合类型既保留 `'off'` / `'on'` 的字面量提示，又允许任意字符串：只有开关的模型用 `'on'`，分档模型用自己声明的档位（`low`、`high`、`max`）。兼容与回退在到达适配器之前就已解决，适配器只做翻译。

`MaxCompletionTokensOptions` 也还在：Chat Completions 系的 provider 用 `usedContextTokens` 与 `maxContextTokens` 把输出上限收紧到剩余窗口。

## Kimi 的例外出口

```ts
// Kimi provider: exported so callers can narrow a `ChatProvider` to the Kimi
// backend (instanceof) and apply Kimi-specific request params (generation
// kwargs, `thinking.keep` extra body).
export { KimiChatProvider } from './providers/kimi';
export type { ExtraBody, GenerationKwargs, KimiOptions, ThinkingConfig } from './providers/kimi';
export { classifyKimiQuotaError } from './providers/kimi-errors';
```

抽象层故意开的口子：调用方可以 `instanceof KimiChatProvider` 收窄类型，使用 Kimi 专属参数。新增的 `classifyKimiQuotaError` 把「额度或余额耗尽」从普通 429 限流里分出来，`isRetryableGenerateError()` 对它直接返回 `false`——在充值之前，重试永远不会成功。

## 对照：v2 的 human/llm

v2 的请求库在 `packages/agent-core-v2/src/human/llm/`，设计说明见包内 `packages/agent-core-v2/docs/zh/llm.md`。概念一脉相承，结构换了一套：

| 关注点 | kosong | v2 |
| --- | --- | --- |
| 协议适配 | 每家一个 `ChatProvider` 类 | 四个协议基座（openai、openai-responses、anthropic、google-genai），各含 contract、format、lower、trait、requester 等模块 |
| 厂商定制 | 子类与构造参数 | 按协议类型化的 trait（`OpenAITrait`、`AnthropicTrait` 等）|
| provider 定义 | `createProvider(config)` 分派到类 | `createProvider()` 声明协议、连接与错误转换；Kimi 声明了 openai、anthropic、openai_responses 三种协议 |
| 输出 | 回调 | 可序列化事件流：`llm.sent`、`llm.streaming.part`、`llm.done`、`llm.failed.*` |
| 重试 | 只提供 `isRetryableGenerateError()`，由调用方重试 | 纯函数策略，由 turn 状态机驱动（见[上一篇](https://daiw.org/manual/kimi-code/turn-convergence)）|
| 消息转换 | 各适配器内联 | Pattern Rewriter：有序、独立的 pattern 改写消息区间后再 lowering |

原样搬过来的也不少：`FinishReason` 与 `ThinkingEffort` 在 `human/llm/finish-reason.ts`、`human/llm/thinking.ts` 里是同样的定义；`UNKNOWN_CAPABILITY` 与能力矩阵思路一致；输出上限收紧挪到 `llm-adapter/model/completion-budget.ts`，同样带着 `usedContextTokens` 与 `maxContextTokens`。

`src/llm-adapter/` 是 v2 其余代码与 human 内核之间的兼容边界：基于类的错误（`APIContextOverflowError` 等）、token 估算、模型目录与请求器实现都在这里。

## 能力与目录

「模型有什么能力」仍是一份可更新的数据：kosong 的 `catalog.ts` 解析 models.dev 格式，`node-sdk` 的 `fetchCatalog` / `loadBuiltInCatalog` 负责拉取与兜底，`apps/kimi-code/src/built-in-catalog.ts` 是内置副本。同一段 agent 代码要在视觉、thinking、上下文窗口差很多的模型之间跑，只能在运行时查能力，不能硬编码。

## 那些小文件

跨 provider 的麻烦在 kosong 的 `providers/` 下各有一个小文件：`merge-user-messages.ts`（有的 provider 不允许连续 user 消息）、`tool-call-id.ts`（ID 格式与长度各家不同）、`reasoning-key.ts`（推理内容字段名不同）、`request-auth.ts`、`chat-completions-stream.ts`、`anthropic-profile.ts`。v2 把同类问题收进协议基座，例如 `human/llm/toolCallIdNormalizer.ts` 专门把流式响应里重复的 tool call id 重映射成唯一 id。

下一篇：[上下文与投影](https://daiw.org/manual/kimi-code/context)。
