# 01 - LLM API 层：lib/ai

## 这一阶段做了什么？

`lib/ai` 是整个项目的地基。它的职责是：**用一套统一的接口，对接各种不同的 LLM 提供商**（Anthropic、OpenAI、Google 等）。

本阶段实现了核心类型和架构骨架，但还没有对接具体的 LLM 服务商（那是后续的事）。

> **参考**：pi-mono 的 ai 包共 34 个文件（10 核心 + 17 Provider 实现 + 7 工具），支持 24+ LLM 提供商和 500+ 模型定义。本阶段聚焦于类型基础和架构骨架，具体 Provider 实现留待后续。

## 要解决什么问题？

调用不同的 LLM 就像用不同的充电线——接口长得不一样，但你只是想充电。`lib/ai` 做的就是提供统一接口：

- 定义通用的消息格式（用户消息、助手消息、工具结果）
- 定义通用的流式响应格式（LLM 的回复是逐字流出的）
- 定义 Provider 注册机制（注册新服务商只需要一个函数调用）
- 提供统一的 `stream()` 和 `complete()` 接口

## 核心概念

### 消息（Message）

一次 LLM 对话由多条消息组成。用户说的话是 `UserMessage`，AI 回复是 `AssistantMessage`，工具执行结果是 `ToolResultMessage`：

```moonbit
enum Message {
  User(UserMessage)             // 用户发的消息
  Assistant(AssistantMessage)   // AI 的回复
  ToolResult(ToolResultMessage) // 工具执行后返回的结果
}
```

其中 `AssistantMessage` 包含完整的响应元数据：内容块列表、API 标识、Provider 标识、模型 ID、Token 用量和费用、停止原因、错误消息、时间戳。

`ToolResultMessage` 包含工具输出内容、对应的工具调用 ID、工具名称、以及执行是否出错。

### 内容块（ContentBlock）

一条消息可以包含多种内容：文本、思考过程、图片、工具调用。用 `enum` 区分：

```moonbit
enum ContentBlock {
  Text(String)                // 普通文本
  Thinking(String)            // AI 的"思考过程"（某些模型支持）
  Image(ImageContent)         // 图片（base64 编码，支持 jpeg/png/gif/webp）
  ToolCall(ToolCall)          // AI 要求调用某个工具（包含 id、name、arguments）
}
```

### 停止原因（StopReason）

AI 回复结束的原因有好几种，用 `enum` 表示：

```moonbit
enum StopReason {
  Stop       // 正常结束
  Length     // 达到最大 token 限制
  ToolUse    // AI 要求调用工具，等待工具结果
  Error      // 出错了
  Aborted    // 被用户中断
}
```

### 用量与费用（Usage / CostBreakdown）

每次 LLM 调用都会统计 token 用量和对应费用：

```moonbit
struct Usage {
  input : Int           // 输入 token 数
  output : Int          // 输出 token 数
  cache_read : Int      // 从缓存读取的 token 数
  cache_write : Int     // 写入缓存的 token 数
  total_tokens : Int    // 总 token 数
  cost : CostBreakdown  // 费用明细（按输入/输出/缓存分类，单位美元）
}
```

### 模型（Model）

```moonbit
struct Model {
  id : String               // 模型标识，如 "claude-sonnet-4-20250514"
  name : String             // 显示名称
  api : ApiId               // 使用的 API 协议
  provider : ProviderId     // 所属供应商
  base_url : String?        // 自定义端点（自托管或代理场景）
  reasoning : Bool          // 是否支持推理/思维链
  input_modalities : Array[InputModality]  // 支持的输入模态（文本/图片）
  cost : ModelCost          // 价格（每百万 token，美元）
  context_window : Int      // 上下文窗口大小
  max_tokens : Int          // 最大输出 token 数
}
```

`ApiId` 和 `ProviderId` 是用 struct 包装的字符串，用来区分不同的 API 和 Provider：

```moonbit
struct ApiId(String)        // 如 "anthropic-messages"、"openai-chat-completions"
struct ProviderId(String)   // 如 "anthropic"、"openai"
```

已知的 API 和 Provider 用常量定义（如 `api_anthropic_messages`、`provider_openai` 等）。

模型费用通过 `calculate_cost(model, usage)` 计算，`models_are_equal(a, b)` 基于 id + provider 判断模型是否等价。

### 对话上下文（Context）

```moonbit
struct Context {
  system_prompt : String?       // 系统提示词
  messages : Array[Message]     // 对话历史消息列表
  tools : Array[Tool]           // 可用工具列表
}
```

其中 `Tool` 定义了可供 LLM 调用的外部工具：

```moonbit
struct Tool {
  name : String         // 工具名称（LLM 通过此名称调用）
  description : String  // 功能描述
  parameters : Json     // 参数的 JSON Schema
}
```

### 流式请求配置（StreamOptions）

```moonbit
struct StreamOptions {
  temperature : Double?              // 采样温度
  max_tokens : Int?                  // 最大输出 token 数
  top_p : Double?                    // 核采样参数
  api_key : String?                  // API 密钥（覆盖默认配置）
  cache_retention : CacheRetention?  // 缓存策略（None/Short/Long）
  retry_count : Int?                 // 失败重试次数
  retry_delay_ms : Int?              // 重试间隔（毫秒）
  thinking_mode : ThinkingMode?      // 思维链模式（Disabled/Enabled/Adaptive）
  thinking_budget_tokens : Int?      // 思维链 token 预算
  reasoning : ThinkingLevel?         // 推理深度（Minimal/Low/Medium/High/XHigh）
}
```

所有字段都是可选的，`StreamOptions::default()` 创建一个全 None 的默认值。

### 流式事件（AssistantMessageEvent）

LLM 的回复是逐字流出的，每个"chunk"就是一个事件。流的生命周期为：`Start → (Text|Thinking|ToolCall)* → Done|Error`，其中 Text/Thinking/ToolCall 各自遵循 `Start → Delta* → End` 的子生命周期：

```moonbit
enum AssistantMessageEvent {
  Start(AssistantMessage)           // 流开始，携带初始助手消息骨架
  TextStart(Int)                    // 文本块开始（参数为内容块索引）
  TextDelta(Int, String)            // 文本增量片段
  TextEnd(Int, String)              // 文本块结束，携带完整文本
  ThinkingStart(Int)                // 思维链开始
  ThinkingDelta(Int, String)        // 思维链增量片段
  ThinkingEnd(Int, String)          // 思维链结束，携带完整思维内容
  ToolCallStart(Int, ToolCall)      // 工具调用开始
  ToolCallDelta(Int, String)        // 工具调用参数增量（JSON 片段）
  ToolCallEnd(Int, ToolCall)        // 工具调用结束，携带完整 ToolCall
  Done(StopReason, AssistantMessage)   // 流正常结束
  Error(StopReason, AssistantMessage)  // 流因错误结束
}
```

辅助函数：
- `is_terminal(event)` —— 判断是否为终止事件（Done 或 Error）
- `final_message(event)` —— 从终止事件中提取最终的 AssistantMessage

### Provider 注册机制

不需要定义 trait，而是直接注册函数：

```moonbit
// 注册一个 Provider
register_provider(api_id, stream_fn)

// 用的时候查找
get_provider(api_id) -> stream_fn?

// 调用统一的流式接口
stream(model, context, options, handler) -> Unit!StreamError

// complete 是 stream 的便捷包装，收集完所有事件后返回最终消息
complete(model, context, options) -> AssistantMessage!StreamError
```

## 文件结构

```
lib/ai/
├── moon.pkg           # 包配置
├── types.mbt          # 核心类型：ContentBlock, Message, StopReason, Usage
├── model.mbt          # Model, ApiId, ProviderId, 成本计算
├── context.mbt        # Context（对话上下文）, Tool（工具定义）, StreamOptions
├── event.mbt          # 流式事件类型，终止判断，最终消息提取
├── provider.mbt       # Provider 注册表，stream，complete
├── types_test.mbt     # 测试
├── model_test.mbt     # 测试
├── event_test.mbt     # 测试
└── provider_test.mbt  # 测试
```

## 设计决策

### 为什么用回调而不是 async/await？

pi-mono 用 `AsyncIterable` 做流式响应。MoonBit 的 async 还在实验阶段，所以本阶段用回调模式：

```moonbit
pub(all) struct EventHandler((AssistantMessageEvent) -> Unit)

pub fn stream(model, context, options, handler) -> Unit!StreamError
```

Provider 通过调用 `handler.0(event)` 推送事件。等 MoonBit async 成熟后可以迁移为 channel/iterator 模式。

### 为什么用 `pub(all)` 而不是 `pub`？

MoonBit 的 `pub` 只允许外部读取，不允许外部构造。下游包（agent、coding_agent）需要构造这些类型，所以用 `pub(all)`。

### 为什么 Provider 注册用函数而不是 trait？

和 pi-mono 保持一致。直接存储 stream 函数更简单灵活，不需要额外的类型层次。注册表用 `Array[ProviderEntry]` 实现（线性查找，API 数量少无性能问题）。若同一 API 重复注册，会覆盖旧的。

`complete()` 是 `stream()` 的便捷包装，内部通过回调收集终止事件中的最终消息。

## 核心抽象映射总览

本阶段实现的每个 TypeScript 概念到 MoonBit 的映射：

| 概念 | TypeScript | MoonBit |
|------|-----------|---------|
| 消息 | `UserMessage \| AssistantMessage \| ToolResultMessage` | `enum Message` |
| 内容块 | `TextContent \| ThinkingContent \| ImageContent \| ToolCall` | `enum ContentBlock` |
| 停止原因 | `"stop" \| "length" \| "toolUse" \| "error" \| "aborted"` | `enum StopReason` |
| 流事件 | `AssistantMessageEvent`（可辨识联合） | `enum AssistantMessageEvent` |
| 模型 | `Model<TApi>` interface | `struct Model` |
| API 标识 | `type Api = string & {}` | `struct ApiId(String)` |
| Provider 标识 | `type KnownProvider = string` | `struct ProviderId(String)` |
| 工具 | `interface Tool` | `struct Tool` |
| 上下文 | `interface Context` | `struct Context` |
| 流选项 | `interface StreamOptions` | `struct StreamOptions` |
| Provider 注册 | `registerApiProvider()` + map | `register_provider()` + Array |

## 和 pi-mono 的差异

本阶段只建骨架，没有搬过来的一些东西：

| 没做的 | 为什么 |
|--------|--------|
| 17 个 Provider 实现 | 先搭类型基础，后续按需添加 |
| 500+ 模型数据库 | 先定义少量已知模型，按需扩展 |
| OAuth 认证 | 暂时不需要 |
| 输入验证（TypeBox + AJV） | 等 agent 阶段再做 |
| 溢出检测（19 种 Provider 错误模式） | 暂不实现 |
| 懒加载 `createLazyStream()` | MoonBit 的包管理不需要 |

## 测试覆盖

共 15 个测试，全部通过：

- **types_test.mbt**：StopReason 相等性、Usage 零值、Message/ToolResult 构造、ImageMimeType
- **model_test.mbt**：ApiId/ProviderId 标识符相等、成本计算、模型相等性判断
- **event_test.mbt**：终止事件判断（Done 和 Error）、final_message 提取
- **provider_test.mbt**：Provider 注册/查找、未注册 API 错误、stream 委托、complete 收集
