feat(llm): 重构 types.rs 为完整的 OpenAI 兼容类型系统
将 `types.rs` 拆分为模块化目录,所有类型派生 `Serialize/Deserialize`, 并新增 `OpenaiChatChunk`、`Role` 扩展等 30+ 缺失类型 消除对 `cycle/usage.rs` 的反向依赖,`Usage`/`CostTracker` 移至 `types/usage.rs`
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
# 方案:重构 `types.rs` 为完整的 OpenAI 兼容 API 类型系统
|
||||
|
||||
## 1. 现状分析
|
||||
|
||||
### 当前问题
|
||||
|
||||
| 问题 | 详细 |
|
||||
|------|------|
|
||||
| **无 serde** | 所有类型只有 `Debug + Clone`,无 `Serialize/Deserialize`,迫使 `OpenaiProvider` 手动构建 JSON(354 行中约 200 行是序列化代码) |
|
||||
| **请求参数不全** | `ChatRequest` 只支持 `model, messages, system_prompt, tools, max_tokens, temperature, extra_body`,缺失 streaming、response_format、tool_choice、stop、reasoning_effort 等 30+ 参数 |
|
||||
| **响应类型太薄** | `ChatResponse` 只返回 `message + usage + stop_reason`,缺失 `id, created, model, choices` 数组、`logprobs`、`system_fingerprint` 等 |
|
||||
| **无流式支持** | 无 `ChatCompletionChunk` 类型,无法处理 SSE 流式响应 |
|
||||
| **反向依赖** | `types.rs` 引用 `cycle::usage::Usage`,造成模块间反向依赖 |
|
||||
| **手动解析易出错** | `parse_response()` 从 `Value` 中逐字段解析,逻辑脆弱,不支持复杂嵌套类型 |
|
||||
|
||||
### OpenAI API 参考文档覆盖范围
|
||||
|
||||
已完整阅读文档(2177 行),涵盖了完整的请求参数(35+ 个顶层参数)和响应结构。
|
||||
|
||||
## 2. 新类型系统设计
|
||||
|
||||
### 架构
|
||||
|
||||
将 `types.rs` 重构为 Rust 新风格模块目录(符合项目已有惯例),按功能领域拆分:
|
||||
|
||||
```
|
||||
src/llm/
|
||||
├── types/
|
||||
│ ├── mod.rs # 模块根:re-exports + 基础枚举/共用类型
|
||||
│ ├── request.rs # 请求参数(ChatCompletionRequest 等)
|
||||
│ ├── response.rs # 响应类型(ChatCompletionResponse + ChatCompletionChunk)
|
||||
│ ├── message.rs # 消息类型(6 种角色消息 + content parts)
|
||||
│ ├── tool.rs # 工具定义 + 工具调用
|
||||
│ ├── usage.rs # Token 用量(从 cycle/usage.rs 移入,消除反向依赖)
|
||||
│ └── shared.rs # 共用枚举(ReasoningEffort, ServiceTier, ResponseFormat 等)
|
||||
```
|
||||
|
||||
同时,将 `cycle/usage.rs` 中的 `Usage` 和 `CostTracker` **移到** `types/usage.rs`,`cycle/usage.rs` 保留 `pub use` 兼容 re-export。
|
||||
|
||||
### 核心决策
|
||||
|
||||
| 决策 | 选择 | 理由 |
|
||||
|------|------|------|
|
||||
| **序列化方式** | 全部类型 derive `Serialize, Deserialize` | 消除手动 JSON 构建,让 provider 直接 `.json(&req)` / `.json::<Res>()` |
|
||||
| **类型风格** | 直接映射 OpenAI API JSON 形状 | 一目了然,与 API 文档 1:1 对应,调试方便 |
|
||||
| **命名策略** | 添加 `OpenAI` 前缀(如 `OpenaiChatRequest`) | 明确标注为 OpenAI 兼容类型 |
|
||||
| **字段命名** | `#[serde(rename_all = "snake_case")]` | OpenAI API 使用 snake_case |
|
||||
| **可选字段** | `#[serde(skip_serializing_if = "Option::is_none")]` | 不序列化 None 字段,保持请求体干净 |
|
||||
| **默认值** | `#[serde(default)]` | 反序列化时缺失字段用默认值 |
|
||||
| **后向兼容** | 通过类型别名保持 `ChatRequest`/`ChatResponse` 等名称可用 | LlmProvider/LlmCycle 接口不变 |
|
||||
| **泛化策略** | Anthropic 是独立体系,暂不纳入当前设计 | 保持当前类型系统专注 OpenAI,Provider 层做转换 |
|
||||
|
||||
### 关键类型设计原则
|
||||
|
||||
- **`OpenaiChatRequest`**:统一结构体(不拆分 NonStreaming/Streaming),包含 `stream: Option<bool>` 字段,所有字段均为 `Option`,build 时 `skip_serializing_if`
|
||||
- **`OpenaiChatResponse`**:直接对应 `ChatCompletion`(完整响应),保留完整 choices 数组等所有字段
|
||||
- **`OpenaiChatChunk`**:对应流式 chunk,`object = "chat.completion.chunk"`
|
||||
- **消息系统**:用单个 `OpenaiChatMessage` enum 覆盖 6 种角色消息类型(Developer/System/User/Assistant/Tool/Function),每种内部使用对应 struct
|
||||
- **Content parts**:`OpenaiContentPart` enum 覆盖 text/image_url/input_audio/file/refusal
|
||||
|
||||
## 3. 完整类型清单
|
||||
|
||||
### `types/mod.rs` — 共用类型
|
||||
```
|
||||
Role → enum { Developer, System, User, Assistant, Tool, Function }
|
||||
FinishReason → enum { Stop, Length, ToolCalls, ContentFilter, FunctionCall }
|
||||
ServiceTier → enum { Auto, Default, Flex, Scale, Priority }
|
||||
Modality → enum { Text, Audio }
|
||||
ImageDetail → enum { Auto, Low, High }
|
||||
AudioFormat → enum { Wav, Mp3, Aac, Flac, Opus, Pcm16 }
|
||||
Voice → struct { id: String } 或预定义枚举
|
||||
SearchContextSize → enum { Low, Medium, High }
|
||||
StopSequence → enum { Single(String), Multiple(Vec<String>) }
|
||||
Verbosity → enum { Low, Medium, High }
|
||||
```
|
||||
|
||||
### `types/request.rs` — 请求参数
|
||||
```
|
||||
OpenaiChatRequest → struct (35+ 字段,所有 OpenAI 参数)
|
||||
ResponseFormat → enum { Text, JsonObject { .. }, JsonSchema { .. } }
|
||||
ToolChoice → enum { None, Auto, Required, Named { .. }, AllowedTools { .. } }
|
||||
StreamOptions → struct { include_usage, include_obfuscation }
|
||||
AudioParam → struct { format, voice }
|
||||
PredictionContent → struct { type, content }
|
||||
WebSearchOptions → struct { search_context_size, user_location }
|
||||
UserLocation → struct { type, approximate: Approximate }
|
||||
Approximate → struct { city, country, region, timezone }
|
||||
FunctionCallOption → struct { name } // deprecated
|
||||
FunctionDefinition → struct { name, description, parameters, strict }
|
||||
OpenaiTool → enum { Function { .. }, Custom { .. } }
|
||||
```
|
||||
|
||||
### `types/response.rs` — 响应类型
|
||||
```
|
||||
OpenaiChatResponse → struct { id, object, created, model, choices, usage, system_fingerprint, service_tier }
|
||||
Choice → struct { index, message, finish_reason, logprobs }
|
||||
OpenaiChatMessage → struct { content, refusal, role, tool_calls, function_call, audio, annotations }
|
||||
OpenaiChatChunk → struct { id, object, created, model, choices, usage, system_fingerprint, service_tier }
|
||||
ChunkChoice → struct { index, delta, logprobs, finish_reason }
|
||||
Delta → struct { role, content, tool_calls, function_call }
|
||||
Logprobs → struct { content, refusal }
|
||||
TokenLogprob → struct { token, bytes, logprob, top_logprobs }
|
||||
TopLogprob → struct { token, bytes, logprob }
|
||||
Annotation → struct { type, url_citation }
|
||||
URLCitation → struct { end_index, start_index, title, url }
|
||||
OpenaiAudio → struct { id, data, expires_at, transcript }
|
||||
FunctionCall → struct { name, arguments }
|
||||
OpenaiToolCall → enum { Function { id, function, type }, Custom { id, custom, type } }
|
||||
```
|
||||
|
||||
### `types/message.rs` — 消息类型
|
||||
```
|
||||
OpenaiChatMessage → enum (覆盖 6 种角色消息)
|
||||
DeveloperMessage → struct { content, role, name }
|
||||
SystemMessage → struct { content, role, name }
|
||||
UserMessage → struct { content, role, name }
|
||||
AssistantMessage → struct { content, refusal, role, name, tool_calls, function_call, audio }
|
||||
ToolMessage → struct { content, role, tool_call_id }
|
||||
FunctionMessage → struct { content, role, name }
|
||||
OpenaiContentPart → enum
|
||||
OpenaiContentPartText → struct { type, text }
|
||||
OpenaiContentPartImage → struct { type, image_url: ImageURL }
|
||||
OpenaiContentPartInputAudio → struct { type, input_audio: InputAudio }
|
||||
OpenaiContentPartFile → struct { type, file: FileData }
|
||||
OpenaiContentPartRefusal → struct { type, refusal }
|
||||
ImageURL → struct { url, detail }
|
||||
InputAudio → struct { data, format }
|
||||
FileData → struct { file_data, file_id, filename }
|
||||
```
|
||||
|
||||
### `types/tool.rs` — 工具类型
|
||||
```
|
||||
OpenaiToolDefinition → struct { name, description, parameters, strict }
|
||||
(保留 ToolDefinition 别名保持后向兼容,重定义为包含所有字段)
|
||||
OpenaiToolCall (在请求中使用) → 见 response.rs 中的定义
|
||||
```
|
||||
|
||||
### `types/usage.rs` — Token 用量
|
||||
```
|
||||
Usage → struct { prompt_tokens, completion_tokens, total_tokens,
|
||||
completion_tokens_details, prompt_tokens_details }
|
||||
CompletionTokensDetails → struct { reasoning_tokens, audio_tokens,
|
||||
accepted_prediction_tokens, rejected_prediction_tokens }
|
||||
PromptTokensDetails → struct { audio_tokens, cached_tokens }
|
||||
CostTracker → 从 cycle/usage.rs 移入(累计追踪器)
|
||||
```
|
||||
|
||||
### 删除的旧类型
|
||||
- `ContentBlock` → 被 `OpenaiContentPart` 替代(更准确的 OpenAI API 命名)
|
||||
- `StopReason` → 被 `FinishReason` 替代(与 API 命名一致)
|
||||
- `Message` → 被 `OpenaiChatMessage` 替代
|
||||
|
||||
### 类型别名(后向兼容)
|
||||
```
|
||||
ChatRequest = OpenaiChatRequest
|
||||
ChatResponse = OpenaiChatResponse
|
||||
Message = OpenaiChatMessage
|
||||
ContentBlock = OpenaiContentPart
|
||||
ToolDefinition = OpenaiToolDefinition
|
||||
Role = Role(保持不变,但扩展变体)
|
||||
StopReason = FinishReason
|
||||
```
|
||||
|
||||
## 4. 对其他模块的影响
|
||||
|
||||
### `provider/openai.rs`
|
||||
- **大幅简化**:`build_request_body()` → 直接 `serde_json::to_value(&request)`
|
||||
- `parse_response()` 中 100+ 行手动解析 → 直接 `serde_json::from_value::<OpenaiChatResponse>()`
|
||||
- `serialize_messages()`, `serialize_message()`, `serialize_content_block()`, `serialize_tool()` → **全部删除**
|
||||
- 新增 `chat_stream()` 方法返回 `OpenaiChatChunk` 流
|
||||
- 需要适配新类型的字段名变更(如 `Usage` 中 `input_tokens` → `prompt_tokens`)
|
||||
|
||||
### `provider.rs` (trait)
|
||||
- 接口保持不变,继续使用 `ChatRequest`/`ChatResponse` 类型别名
|
||||
- 调整 `Usage` 类型引用路径
|
||||
|
||||
### `cycle.rs`
|
||||
- `CycleConfig` 扩展支持更多请求参数(至少增加 `tools, tool_choice, response_format, stop, reasoning_effort, seed` 等)
|
||||
- `LlmCycle::submit()` 构建 `ChatRequest` 时使用新类型
|
||||
- `response.usage` 字段类型变更(新 `Usage` 含更多字段)
|
||||
- 此时不添加流式支持
|
||||
|
||||
### `cycle/usage.rs`
|
||||
- `Usage` 结构体**被移走**到 `types/usage.rs`
|
||||
- `cycle/usage.rs` 保留 `pub use crate::llm::types::usage::{Usage, CostTracker};` 作为兼容性 re-export
|
||||
- `CostTracker` 逻辑不变
|
||||
|
||||
### `error.rs`
|
||||
- 无明显变更,错误类型和映射逻辑不变
|
||||
|
||||
## 5. 实施步骤
|
||||
|
||||
### Phase 1: 基础设施
|
||||
```
|
||||
1. [准备] 在 Cargo.toml 中确认 serde 依赖(已有 serde = "1",features = ["derive"])
|
||||
2. [创建] 新建 src/llm/types/ 目录
|
||||
```
|
||||
|
||||
### Phase 2: 类型定义(按依赖顺序)
|
||||
```
|
||||
3. [usage.rs] 从 cycle/usage.rs 迁移 Usage + CostTracker
|
||||
4. [shared.rs] 定义 Role, FinishReason, ServiceTier, Modality, ImageDetail, StopSequence, ResponseFormat
|
||||
5. [message.rs] 定义 OpenaiChatMessage(6种角色)+ OpenaiContentPart + ImageURL + InputAudio
|
||||
6. [tool.rs] 定义 OpenaiToolDefinition + OpenaiToolCall + FunctionCall
|
||||
7. [request.rs] 定义 OpenaiChatRequest(35+ 字段)+ ToolChoice + StreamOptions
|
||||
8. [response.rs] 定义 OpenaiChatResponse + OpenaiChatChunk + Choice + Delta + Logprobs
|
||||
```
|
||||
|
||||
### Phase 3: 模块组装
|
||||
```
|
||||
9. [mod.rs] 创建模块根,re-export 所有类型 + 别名(ChatRequest = OpenaiChatRequest 等)
|
||||
10. [usage.rs] 更新 cycle/usage.rs 为 pub use re-export
|
||||
11. [删除] 删除旧 src/llm/types.rs
|
||||
```
|
||||
|
||||
### Phase 4: Provider 适配
|
||||
```
|
||||
12. [provider/openai.rs] 重写为 serde 序列化(删除 ~200 行手动代码)
|
||||
13. [cycle.rs] 适配新类型字段(prompt_tokens vs input_tokens)
|
||||
```
|
||||
|
||||
### Phase 5: 验证
|
||||
```
|
||||
14. [编译] cargo check 确保编译通过
|
||||
15. [检查] cargo clippy 确保无警告
|
||||
16. [测试] cargo test 确保测试通过
|
||||
```
|
||||
|
||||
## 6. 验证方式
|
||||
|
||||
- `cargo check` — 编译通过
|
||||
- `cargo clippy` — 无警告
|
||||
- `cargo test` — 所有测试通过(如果有集成测试,可能需要调整)
|
||||
- 检查 `OpenaiProvider` 代码量减少(预期从 354 行降至 ~150 行)
|
||||
- 手动验证序列化输出是否符合 OpenAI API 格式
|
||||
|
||||
## 7. 注意事项
|
||||
|
||||
1. **Break change**: 某些类型名称变化(如 `StopReason` → `FinishReason`),项目处于早期阶段,可接受
|
||||
2. **后向兼容**: 通过类型别名保持旧名称可用,接口层无需修改
|
||||
3. **Anthropic 处理**: Anthropic 是独立体系,不在当前设计中泛化,单独实现 Provider
|
||||
4. **异步流**: `chat_stream()` 的签名需要仔细设计(`Pin<Box<dyn Stream<Item = Result<OpenaiChatChunk, LlmError>>>>` 或自定义类型)
|
||||
5. **CostTracker 不变**: 虽然 Usage 变复杂了,但 CostTracker 只累计 input/output token 数,逻辑不变
|
||||
Reference in New Issue
Block a user