refactor(types): request.rs 类型移入 provider/openai.rs
- 删除 types/request.rs(187 行) - 所有 OpenAI wire-format 类型迁入 provider/openai.rs,可见性 pub(crate): StreamOptions / OpenaiTool / AudioParam / PredictionContent / UserLocation / Approximate / WebSearchOptions / OpenaiChatRequest - types/mod.rs 删除 pub mod request; 与对应 re-export - convert_request 同步降级为 pub(crate) 以匹配 OpenaiChatRequest 可见性 - 公共 re-export 路径 agcore::llm::types::OpenaiChatRequest 等已删除(Breaking Change,见 CHANGELOG)
This commit is contained in:
@@ -0,0 +1,640 @@
|
||||
# Phase 13 — 热身清理 + ContextSlot fork/merge 实施方案
|
||||
|
||||
- **文档编号**:19
|
||||
- **标题**:Phase 13 — 热身清理 + ContextSlot fork/merge 实施方案
|
||||
- **日期**:2026-07-08
|
||||
- **状态**:待实施
|
||||
- **涉及模块**:agent/context、agent/session、llm/types、llm/provider/openai、llm/stream
|
||||
- **关联文档**:roadmap.md(§Phase 13)、17-phase10-contextslot.md
|
||||
- **对应**:Roadmap §Phase 13(v0.3.0 第一阶段)
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
v0.3.0 是 agcore 从"LLM 调用工具箱"升级为"多 Agent 基础系统"的关键版本。Phase 13 是 v0.3.0 的第一阶段,定位为"热身",包含两大部分:
|
||||
|
||||
- **技术债清理**:删除 Phase 0 遗留的旧 types 文件(`request.rs`、`response.rs`、`old_stream.rs`),以及已标记 `#[deprecated]` 的 `ChatResponse` 结构体
|
||||
- **ContextSlot fork/merge**:为 ContextSlot 增加分叉和合并能力,为后续 Phase 17 Checkpointer 和 Phase 18 SubAgent Dispatch 打基础
|
||||
|
||||
**依赖关系**:无(独立交付)
|
||||
|
||||
**优先级**:P0
|
||||
|
||||
**预估规模**:净减 ~200 行代码(新增 ~505 行,删除 ~704 行)
|
||||
|
||||
---
|
||||
|
||||
## 2. 需求分析
|
||||
|
||||
### 2.1 功能需求
|
||||
|
||||
1. **技术债清理**:删除 `src/llm/types/request.rs`(187 行)、`response.rs`(177 行)、`old_stream.rs`(45 行),将其中的 OpenAI wire-format 类型移入 `src/llm/provider/openai.rs`;删除 `types/mod.rs` 中的 `ChatResponse` 废弃结构体
|
||||
2. **`ContextSlot::fork`**:从现有 context slot 分支出独立的子 slot
|
||||
3. **`ContextSlot::merge`**:将子 slot 的消息合并回父 slot
|
||||
4. **`MergeStrategy`** 枚举:Append(追加)/ Replace(替换),`#[non_exhaustive]` 预留 Phase 16 Summarize 扩展
|
||||
|
||||
### 2.2 非功能需求
|
||||
|
||||
- **每步可编译**:5 个 Step 按物理文件切割,每步 `cargo build --all-targets + cargo test` 验证
|
||||
- **指定公共 API 路径保持向后兼容**:`agcore::llm::types::ToolChoice`(re-export 不变)、`crate::llm::stream::StreamEvent`(重导出保留);其余 wire-format 类型(`OpenaiChatRequest`、`OpenaiChatResponse/Chunk`、`StreamOptions` 等)移入 `provider/openai.rs` 后属 Breaking Change,详见 §4.3 CHANGELOG
|
||||
- **向后兼容的 StreamEvent 路径**:`crate::llm::stream::StreamEvent` 重导出保留,不修改 `cycle.rs` 和 `session.rs` 的 import
|
||||
|
||||
---
|
||||
|
||||
## 3. 方案设计
|
||||
|
||||
### 3.1 整体架构
|
||||
|
||||
Phase 13 分为 5 个 Step,按执行顺序排列:
|
||||
|
||||
```
|
||||
Step 13.5 (fork/merge) → Step 13.4 (ToolChoice) → Step 13.1 (request types) → Step 13.2 (response types) → Step 13.3 (cleanup)
|
||||
```
|
||||
|
||||
这种顺序的好处:
|
||||
|
||||
- **先交付价值**:13.5 是唯一有用户功能交付的 Step,先做建立节奏
|
||||
- **排序约束**:13.4 必须先于 13.1(ToolChoice 不搬走,request.rs 不能删)
|
||||
- **13.3 收尾**:删除旧文件和 `ChatResponse` 是 breaking change,放在最后
|
||||
|
||||
### 3.2 Step 13.5 — ContextSlot fork/merge
|
||||
|
||||
#### MergeStrategy 枚举
|
||||
|
||||
定义在 `src/agent/context.rs`:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub enum MergeStrategy {
|
||||
/// 子 slot 消息追加到父 slot 末尾。
|
||||
Append,
|
||||
/// 用子 slot 消息替换父 slot 内容。
|
||||
Replace,
|
||||
}
|
||||
```
|
||||
|
||||
- `#[non_exhaustive]` 保证 Phase 16 加入 `Summarize` 变体时不破坏现有代码
|
||||
- 不预埋 `Summarize` 占位变体(YAGNI 原则)
|
||||
|
||||
#### ContextSlot::fork
|
||||
|
||||
```rust
|
||||
impl ContextSlot {
|
||||
pub fn fork(&self, child_id: String, strategy: DeriveStrategy) -> ContextSlot {
|
||||
let messages = match &strategy {
|
||||
DeriveStrategy::Full => self.messages.clone(),
|
||||
DeriveStrategy::Focused(cfg) => Self::filter_focused(&self.messages, cfg),
|
||||
};
|
||||
tracing::debug!(
|
||||
parent_id = %self.id,
|
||||
child_id = %child_id,
|
||||
?strategy,
|
||||
"ContextSlot::fork"
|
||||
);
|
||||
ContextSlot {
|
||||
id: child_id,
|
||||
session_id: self.session_id.clone(),
|
||||
config: SlotConfig {
|
||||
mode: match &strategy {
|
||||
DeriveStrategy::Full => SlotMode::Full,
|
||||
DeriveStrategy::Focused(cfg) => SlotMode::Focused(cfg.clone()),
|
||||
},
|
||||
source: SlotSource::Derived {
|
||||
parent_id: self.id.clone(),
|
||||
strategy,
|
||||
},
|
||||
budget: self.config.budget.clone(),
|
||||
compact: self.config.compact,
|
||||
},
|
||||
messages,
|
||||
meta: SlotMeta::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
设计要点:
|
||||
|
||||
- 纯数据层操作,不持久化
|
||||
- 子 slot 的 `meta` 全新创建(`SlotMeta::new()`),不继承父 slot 的 message_count
|
||||
- 子 slot 的 source 记录 `parent_id`,血缘可追溯
|
||||
- 添加 `tracing::debug!` 日志,支持多 slot 交互场景的审计追踪
|
||||
|
||||
#### ContextSlot::merge
|
||||
|
||||
```rust
|
||||
impl ContextSlot {
|
||||
/// 将子 slot 的消息合并到当前 slot。
|
||||
///
|
||||
/// **注意**:本方法仅操作内存数据,不自动持久化。
|
||||
/// 调用方需在 merge 后自行调用 `self.save(&store)` 将结果写入后端存储。
|
||||
pub fn merge(&mut self, child: ContextSlot, strategy: MergeStrategy) -> Result<(), AgentError> {
|
||||
// 防御性检查
|
||||
if self.id == child.id {
|
||||
return Err(AgentError::Config("不能将 slot 合并到自身".into()));
|
||||
}
|
||||
if self.session_id != child.session_id {
|
||||
return Err(AgentError::Config("不能合并不同 session 的 slot".into()));
|
||||
}
|
||||
if matches!(self.config.mode, SlotMode::Readonly) {
|
||||
return Err(AgentError::SlotReadonly("Readonly slot 不允许合并".into()));
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
self_id = %self.id,
|
||||
child_id = %child.id,
|
||||
?strategy,
|
||||
"ContextSlot::merge"
|
||||
);
|
||||
|
||||
match strategy {
|
||||
MergeStrategy::Append => {
|
||||
let count = child.messages.len();
|
||||
self.messages.extend(child.messages);
|
||||
self.meta.message_count += count;
|
||||
}
|
||||
MergeStrategy::Replace => {
|
||||
self.messages = child.messages;
|
||||
self.meta.message_count = self.messages.len();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### AgentSession::derive_slot 重构
|
||||
|
||||
现有 `derive_slot`(session.rs:213-260)的手工复制代码改为调用 `parent.fork()`:
|
||||
|
||||
```rust
|
||||
pub async fn derive_slot(
|
||||
&mut self,
|
||||
id: impl Into<String>,
|
||||
parent_id: &str,
|
||||
strategy: DeriveStrategy,
|
||||
) -> Result<(), AgentError> {
|
||||
let slot_id = id.into();
|
||||
if self.slots.contains_key(&slot_id) {
|
||||
return Err(AgentError::SlotAlreadyExists(slot_id));
|
||||
}
|
||||
let parent = self
|
||||
.slots
|
||||
.get(parent_id)
|
||||
.ok_or_else(|| AgentError::SlotNotFound(parent_id.to_string()))?;
|
||||
let child = parent.fork(slot_id.clone(), strategy); // ← 用 fork
|
||||
child.save(&*self.resolve_store()).await?;
|
||||
self.slots.insert(slot_id, child);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
重复检查、查找父 slot 的代码不变;消息复制逻辑委托给 `fork()`。
|
||||
|
||||
#### 测试计划(新增 9 个)
|
||||
|
||||
| 测试名 | 验证点 |
|
||||
|--------|--------|
|
||||
| `fork_full_copies_messages` | fork Full 策略复制父 slot 全部消息 |
|
||||
| `fork_focused_filters_messages` | fork Focused 策略按 config 过滤 |
|
||||
| `fork_preserves_independence` | 父 slot 追加消息不影响子 slot |
|
||||
| `fork_sets_derived_source` | 子 slot source 正确记录 parent_id |
|
||||
| `merge_append_appends_messages` | Append 追加到父 slot 末尾,message_count 正确 |
|
||||
| `merge_replace_replaces_messages` | Replace 替换父 slot 消息,message_count 正确 |
|
||||
| `merge_self_rejected` | self-merge 返回 `Err` |
|
||||
| `merge_readonly_rejected` | 合并到 Readonly slot 返回 `Err` |
|
||||
| `merge_cross_session_rejected` | 跨 session 合并返回 `Err` |
|
||||
|
||||
### 3.3 Step 13.4 — ToolChoice 移入 tool.rs
|
||||
|
||||
#### 变更文件
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `src/llm/types/request.rs` | 删除 `ToolChoice` 枚举 + serde impl(~28-99 行) |
|
||||
| `src/llm/types/tool.rs` | 新增 `ToolChoice` 枚举 + serde impl(原样搬入) |
|
||||
| `src/llm/types/mod.rs` | `pub use request::{..., ToolChoice}` → `pub use tool::ToolChoice` |
|
||||
| `src/llm/types/request_v2.rs` | import 路径 `request::ToolChoice` → `tool::ToolChoice` |
|
||||
|
||||
**import 路径变化**:
|
||||
|
||||
| 当前 | 移动后 |
|
||||
|------|--------|
|
||||
| `crate::llm::types::request::ToolChoice` | `crate::llm::types::tool::ToolChoice` |
|
||||
| `crate::llm::types::ToolChoice`(通过 re-export) | `crate::llm::types::ToolChoice`(通过 tool.rs re-export,保持不变) |
|
||||
|
||||
**验证**:`cargo build --all-targets` + `cargo test` + `cargo clippy`
|
||||
|
||||
### 3.4 Step 13.1 — request.rs 类型移入 openai.rs
|
||||
|
||||
#### 变更文件
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `src/llm/types/request.rs` | **整文件删除**(187 行) |
|
||||
| `src/llm/provider/openai.rs` | 新增 `StreamOptions`、`OpenaiTool`、`AudioParam`、`PredictionContent`、`UserLocation`、`Approximate`、`WebSearchOptions`、`OpenaiChatRequest` 等类型定义 |
|
||||
| `src/llm/types/mod.rs` | 删除 `pub use request::{OpenaiChatRequest, OpenaiTool, StreamOptions}`;删除 `pub mod request;` |
|
||||
| `src/llm/provider/openai.rs` import 调整 | 原 `use crate::llm::types::request::{...}` 改为从同级 `use super::super::types::...` 或直接使用本文件内类型 |
|
||||
|
||||
**注意**:`OpenaiTool` 引用 `OpenaiToolDefinition`(定义在 `tool.rs`),移入 `openai.rs` 后需通过 `crate::llm::types::tool::OpenaiToolDefinition` 引用。`OpenaiChatRequest.messages` 字段引用 `OpenaiChatMessage`(定义在 `openai_message.rs`),路径不变。
|
||||
|
||||
**设计决策**:搬入 `openai.rs` 后的类型可见性可降级为 `pub(crate)`。它们是与 OpenAI wire-format 绑定的内部序列化类型,公共 API 消费者不应直接接触。
|
||||
|
||||
**验证**:`cargo build --all-targets` + `cargo test` + `cargo clippy`
|
||||
|
||||
### 3.5 Step 13.2 — response.rs 类型移入 openai.rs
|
||||
|
||||
#### 变更文件
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `src/llm/types/response.rs` | **整文件删除**(177 行) |
|
||||
| `src/llm/provider/openai.rs` | 新增 `TokenLogprob`、`TopLogprob`、`Logprobs`、`URLCitation`、`Annotation`、`OpenaiAudio`、`Choice`、`OpenaiChatResponse`、`Delta`、`ChunkChoice`、`OpenaiChatChunk` + `From<OpenaiChatMessage> for Delta` + `From<OpenaiChatResponse> for OpenaiChatChunk` |
|
||||
| `src/llm/types/mod.rs` | 删除 `pub use response::{...}`;删除 `pub mod response;` |
|
||||
| `src/llm/stream.rs:26` | 将 `use crate::llm::types::{OpenaiChatChunk, OpenaiToolCall}` 中的 `OpenaiChatChunk` 路径改为 `crate::llm::provider::openai::OpenaiChatChunk`(`OpenaiToolCall` 保持从 `tool.rs`) |
|
||||
|
||||
**验证**:`cargo build --all-targets` + `cargo test` + `cargo clippy`
|
||||
|
||||
### 3.6 Step 13.3 — 旧文件清理 + ChatResponse 删除
|
||||
|
||||
#### 13.3a — 删除 `old_stream.rs`
|
||||
|
||||
> **前置验证**:实施前执行 `grep -rn 'parse_chunk_stream\|map_legacy_to_ir\|LegacyToIrEventStream\|ChunkToLegacyEventStream' src/` 确认零外部调用方,记录结果到实施 commit。
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `src/llm/types/old_stream.rs` | **整文件删除**(45 行,`LegacyStreamEvent`) |
|
||||
| `src/llm/types/mod.rs` | 删除 `pub mod old_stream;` |
|
||||
| `src/llm/stream.rs` | 删除 `use crate::llm::types::old_stream::LegacyStreamEvent`;删除 `parse_chunk_stream`、`parse_chunk_stream_legacy`、`ChunkToLegacyEventStream`、`LegacyToIrEventStream`、`map_legacy_to_ir`、`empty_message_response`(~160 行死代码) |
|
||||
|
||||
**stream.rs 最终形态**:
|
||||
|
||||
```rust
|
||||
//! 流式事件系统 —— 重导出 StreamEvent 供向后兼容。
|
||||
pub use crate::llm::types::response_v2::StreamEvent;
|
||||
```
|
||||
|
||||
**为什么不全删 stream.rs**:`cycle.rs` 和 `session.rs` 的 `use crate::llm::stream::StreamEvent` 路径保持不变。全删 + 改所有 import 路径的改动量 > 收益。保留 1 行重导出就够。
|
||||
|
||||
#### 13.3b — 删除 `ChatResponse`
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `src/llm/types/mod.rs` | 删除 `ChatResponse` 结构体定义 + 两个 `#[allow(deprecated)]` `From` impl(`From<OpenaiChatResponse> for ChatResponse` 和 `From<ChatResponse> for OpenaiChatChunk`) |
|
||||
|
||||
`ChatResponse` 自 v0.1.0 起标记 `#[deprecated]`,v0.2.0-rc.1 阶段直接删除即可。删除前运行 `cargo doc --no-deps 2>&1 | grep -i 'ChatResponse'` 确认零文档引用。
|
||||
|
||||
**验证**:`cargo build --all-targets` + `cargo test` + `cargo clippy` + `cargo doc --no-deps`
|
||||
|
||||
---
|
||||
|
||||
## 4. 实现计划
|
||||
|
||||
### 4.1 实施顺序总览
|
||||
|
||||
```
|
||||
Step 13.5 ──→ Step 13.4 ──→ Step 13.1 ──→ Step 13.2 ──→ Step 13.3
|
||||
(fork/merge) (ToolChoice) (request) (response) (cleanup)
|
||||
│ │ │ │ │
|
||||
▼ ▼ ▼ ▼ ▼
|
||||
+60 行净增 -0 净增 -0 净增 -0 净增 -260 删除
|
||||
+9 个测试 import 路径 纯类型搬移 纯类型搬移 +1 行重导出
|
||||
变更
|
||||
```
|
||||
|
||||
### 4.2 各 Step 文件变更清单
|
||||
|
||||
#### Step 13.5 — ContextSlot fork/merge
|
||||
|
||||
| 操作 | 文件 | 变更说明 |
|
||||
|------|------|---------|
|
||||
| 新增 | `src/agent/context.rs` | `MergeStrategy` 枚举 + `ContextSlot::fork()` + `ContextSlot::merge()` |
|
||||
| 重构 | `src/agent/session.rs` | `derive_slot` 改为调用 `parent.fork()` |
|
||||
| 新增 | 内联测试 | 9 个新测试(fork, merge, 边界) |
|
||||
|
||||
#### Step 13.4 — ToolChoice 移动
|
||||
|
||||
| 操作 | 文件 | 变更说明 |
|
||||
|------|------|---------|
|
||||
| 删除 | `src/llm/types/request.rs` | 移除 `ToolChoice` 枚举 + serde impl |
|
||||
| 新增 | `src/llm/types/tool.rs` | 增加 `ToolChoice` 枚举 + serde impl |
|
||||
| 修改 | `src/llm/types/mod.rs` | 更新 re-export 路径 |
|
||||
| 修改 | `src/llm/types/request_v2.rs` | 更新 import 路径 |
|
||||
|
||||
#### Step 13.1 — request 类型搬移
|
||||
|
||||
| 操作 | 文件 | 变更说明 |
|
||||
|------|------|---------|
|
||||
| 删除 | `src/llm/types/request.rs` | 整文件删除(187 行) |
|
||||
| 新增 | `src/llm/provider/openai.rs` | 增加所有 OpenAI wire-format 类型 |
|
||||
| 修改 | `src/llm/types/mod.rs` | 删除 re-export + mod 声明 |
|
||||
|
||||
#### Step 13.2 — response 类型搬移
|
||||
|
||||
| 操作 | 文件 | 变更说明 |
|
||||
|------|------|---------|
|
||||
| 删除 | `src/llm/types/response.rs` | 整文件删除(177 行) |
|
||||
| 新增 | `src/llm/provider/openai.rs` | 增加所有 OpenAI wire-format 类型 + From impl |
|
||||
| 修改 | `src/llm/types/mod.rs` | 删除 re-export + mod 声明 |
|
||||
| 修改 | `src/llm/stream.rs` | 更新 `OpenaiChatChunk` import 路径 |
|
||||
|
||||
#### Step 13.3 — 旧文件清理
|
||||
|
||||
| 操作 | 文件 | 变更说明 |
|
||||
|------|------|---------|
|
||||
| 删除 | `src/llm/types/old_stream.rs` | 整文件删除(45 行) |
|
||||
| 修改 | `src/llm/types/mod.rs` | 删除 `pub mod old_stream;` + 删除 `ChatResponse` 结构体 + `From` impl |
|
||||
| 修改 | `src/llm/stream.rs` | 删除所有死代码,仅保留 `pub use` 重导出 |
|
||||
|
||||
### 4.3 回滚策略
|
||||
|
||||
所有 Step 通过 git commit 管理,回退时 `git revert <commit>` 即可。每个 Step 独立编译,回滚不会级联依赖。若 Step 13.3(`ChatResponse` 删除)导致外部编译失败,单独 revert 该 commit 即可恢复 `ChatResponse` + `old_stream.rs`。
|
||||
|
||||
### 4.4 CHANGELOG 条目
|
||||
|
||||
```markdown
|
||||
## [0.3.0] - 未发布
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
**类型路径变更(0.3.0):**
|
||||
- `agcore::llm::types::request::ToolChoice` → `agcore::llm::types::tool::ToolChoice`(公共 re-export 路径 `agcore::llm::types::ToolChoice` 保持不变)
|
||||
- `agcore::llm::types::request::StreamOptions` → `agcore::llm::provider::openai::StreamOptions`
|
||||
- `agcore::llm::types::request::OpenaiChatRequest` → `agcore::llm::provider::openai::OpenaiChatRequest`
|
||||
- `agcore::llm::types::response::OpenaiChatResponse` → `agcore::llm::provider::openai::OpenaiChatResponse`
|
||||
- `agcore::llm::types::response::OpenaiChatChunk` → `agcore::llm::provider::openai::OpenaiChatChunk`
|
||||
- 其余 `request.rs`/`response.rs` 中的 wire-format 类型(`OpenaiTool`、`AudioParam`、`Choice`、`Delta` 等)同步移入 `agcore::llm::provider::openai` 模块
|
||||
|
||||
**类型删除:**
|
||||
- `agcore::llm::types::ChatResponse` 已删除(自 v0.1.0 标记 `#[deprecated]`,请改用 `MessageResponse`)
|
||||
- `agcore::llm::types::old_stream::LegacyStreamEvent` 已删除(内部死代码)
|
||||
|
||||
### Features
|
||||
- `ContextSlot::fork(child_id, strategy)` — 从父槽派生独立的子槽(数据层操作)
|
||||
- `ContextSlot::merge(child, strategy)` — 将子槽消息合并回父槽(支持 Append/Replace)
|
||||
- `MergeStrategy` 枚举(`#[non_exhaustive]`,Phase 16 可扩展 Summarize)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 风险评估
|
||||
|
||||
| 风险 | 影响 | 概率 | 缓解措施 |
|
||||
|------|------|------|---------|
|
||||
| `ChatResponse` 被外部 crate 引用 | 编译 break | 中 — `#[deprecated]` 仅产生编译警告,外部 crate 可能通过 `#[allow(deprecated)]` 静默依赖 | CHANGELOG 明确标注语义版本(0.3.0)和迁移指引;Step 13.3 验收加入 `cargo doc --no-deps \| grep ChatResponse` 确认零引用 |
|
||||
| `StreamOptions` 等 wire-format 类型路径变更影响直接引用消费者 | 编译 break | 低(v0.2.0-rc.1,极少外部消费者使用内部类型) | CHANGELOG 完整列出所有路径变更;编译错误立即可发现 |
|
||||
| `parse_chunk_stream` 有隐藏调用方 | 编译 break | 极低(实施前执行 `grep -rn 'parse_chunk_stream\|map_legacy_to_ir\|LegacyToIrEventStream' src/` 前置验证) | Step 13.3 前运行 grep 验证并记录结果;`cargo build --all-targets` 可 100% 捕获 |
|
||||
| `#[allow(deprecated)]` 遗漏 | clippy 警告 | 低 | `cargo clippy --all-targets -- -D warnings` 验证 |
|
||||
| Step 顺序错误导致编译中间态 | 开发者体验差 | 中 | 严格按 13.5→13.4→13.1→13.2→13.3 执行;每步 `cargo build` 验证 |
|
||||
| `stream.rs` 简化后 import 断链 | 编译 break | 极低 | 保留 `pub use` 重导出路径,`cycle.rs`/`session.rs` import 不变 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 验收标准
|
||||
|
||||
### M9 里程碑(Phase 13 完成条件)
|
||||
|
||||
| # | 条件 | 验证方法 |
|
||||
|---|------|---------|
|
||||
| 1 | `request.rs`、`response.rs`、`old_stream.rs` 三个旧文件不存在 | `ls src/llm/types/` 确认 |
|
||||
| 2 | `ChatResponse` 结构体不存在 | 全局搜索 `ChatResponse` 仅保留 `openai.rs` 中 `OpenaiChatResponse` 引用 |
|
||||
| 3 | `ToolChoice` 在 `tool.rs` 中定义,公共路径 `agcore::llm::types::ToolChoice` 保持不变 | `cargo doc --no-deps` 确认类型文档 |
|
||||
| 4 | `OpenaiChatRequest`/`Response`/`Chunk` 在 `provider/openai.rs` 中定义 | 编译通过 |
|
||||
| 5 | `ContextSlot::fork()` 单元测试通过(P0 条件全部满足) | `cargo test` |
|
||||
| 6 | `ContextSlot::merge()` 单元测试通过(P0 条件全部满足) | `cargo test` |
|
||||
| 7 | `stream.rs` 只保留 `pub use` 重导出 | 文件内容确认 |
|
||||
| 8 | `cargo build --all-targets` 编译通过 | 编译验证 |
|
||||
| 9 | `cargo test --all-targets` 全绿(预期 283~285 测试) | 测试验证 |
|
||||
| 10 | `cargo clippy --all-targets -- -D warnings` 0 警告 | clippy 验证 |
|
||||
| 11 | CHANGELOG 包含 Phase 13 的 Breaking Changes 和 Features 条目 | 文件确认 |
|
||||
|
||||
### fork/merge 详细验收 P0 项
|
||||
|
||||
**fork 的 5 项 P0 条件:**
|
||||
|
||||
| # | 条件 | 优先级 |
|
||||
|---|------|--------|
|
||||
| 1 | `fork("child", Full)` 创建新 slot,消息在 fork 时刻 == 父 slot | P0 |
|
||||
| 2 | 子 slot 获得独立消息列表——父 slot 后续追加不影响子 slot | P0 |
|
||||
| 3 | 子 slot 的 source 标记为 `Derived { parent_id, strategy }` | P0 |
|
||||
| 4 | 子 slot 可独立持久化(fork + save + load roundtrip) | P0 |
|
||||
| 5 | fork 不允许重复 id(返回 `SlotAlreadyExists`)(由 `derive_slot` 编排层保证) | P0 |
|
||||
|
||||
**merge 的 5 项 P0 条件:**
|
||||
|
||||
| # | 条件 | 优先级 |
|
||||
|---|------|--------|
|
||||
| 1 | `parent.merge(child, Append)` 子消息追加到父末尾 | P0 |
|
||||
| 2 | `parent.merge(child, Replace)` 子消息替换父全量消息 | P0 |
|
||||
| 3 | merge 后父 slot 的 `meta.message_count` 正确更新 | P0 |
|
||||
| 4 | merge 不允许合并到 Readonly 目标 slot | P0 |
|
||||
| 5 | merge 不允许 self-merge(child.id == parent.id) | P0 |
|
||||
|
||||
---
|
||||
|
||||
## 参考来源
|
||||
|
||||
- Roadmap:`docs/roadmap.md` §Phase 13
|
||||
- ContextSlot 设计:`docs/17-phase10-contextslot.md`
|
||||
- 旧 StreamEvent 设计:`src/llm/stream.rs` 文件注释
|
||||
- 当前代码库:`src/llm/types/request.rs`、`src/llm/types/response.rs`、`src/llm/types/old_stream.rs`、`src/llm/types/mod.rs`、`src/llm/provider/openai.rs`、`src/agent/context.rs`、`src/agent/session.rs`
|
||||
|
||||
---
|
||||
|
||||
## 7. 实施计划
|
||||
|
||||
### 全局说明
|
||||
|
||||
**commit 策略**:每个 Step 一个独立 commit。commit message 格式:
|
||||
```
|
||||
<type>(<scope>): <中文描述>
|
||||
```
|
||||
- Step 13.5 → `feat(agent): 实现 ContextSlot fork/merge`
|
||||
- Step 13.4 → `refactor(types): ToolChoice 移入 tool.rs`
|
||||
- Step 13.1 → `refactor(types): request.rs 类型移入 provider/openai.rs`
|
||||
- Step 13.2 → `refactor(types): response.rs 类型移入 provider/openai.rs`
|
||||
- Step 13.3 → `refactor(types): 删除旧类型文件和 ChatResponse`
|
||||
|
||||
**验证命令(每步通用)**:
|
||||
```bash
|
||||
cargo build --all-targets && cargo test && cargo clippy --all-targets -- -D warnings
|
||||
```
|
||||
|
||||
**预计测试数量变化**:
|
||||
- 当前基线:277 测试(每个 Step 开始时 `cargo test` 确认)
|
||||
- Step 13.5 后:286(+9)
|
||||
- Step 13.4-13.2 后:286(无变化)
|
||||
- Step 13.3 后:285(-1,`ChatResponse` 的 `From` impl 无测试直接引用,删除后仅 `types/mod.rs` 中的 `deprecated` 注释行减少,不影响测试计数。实施前执行 `grep -rn 'ChatResponse' src/ --include='*test*' --include='*tests*'` 确认零测试引用)
|
||||
- 最终范围:285 测试
|
||||
|
||||
### Step 13.5 — ContextSlot fork/merge
|
||||
|
||||
**前置依赖**:无(纯新增,不依赖前序 Step)
|
||||
|
||||
**任务描述**:在 `agent/context.rs` 中新增 `MergeStrategy` 枚举、`ContextSlot::fork()` 方法和 `ContextSlot::merge()` 方法;重构 `agent/session.rs` 中的 `derive_slot` 改为调用 `parent.fork()`;新增 9 个内联测试覆盖 fork/merge 的 happy path 和 error path。
|
||||
|
||||
**涉及文件**:
|
||||
- `src/agent/context.rs` — 新增枚举和方法
|
||||
- `src/agent/session.rs` — 重构 derive_slot
|
||||
- `src/agent.rs` — 追加 `MergeStrategy` re-export
|
||||
|
||||
**具体操作**:
|
||||
1. 在 `context.rs` 中新增 `MergeStrategy` 枚举(Append / Replace,`#[non_exhaustive]`)
|
||||
2. 在 `context.rs` 中 `impl ContextSlot` 块内新增 `fork(&self, child_id: String, strategy: DeriveStrategy) -> ContextSlot` 方法
|
||||
3. 在 `context.rs` 中 `impl ContextSlot` 块内新增 `merge(&mut self, child: ContextSlot, strategy: MergeStrategy) -> Result<(), AgentError>` 方法(含 self-merge/cross-session/Readonly 三项防御检查 + `tracing::debug!` 日志)
|
||||
4. 在 `session.rs` 的 `derive_slot` 方法中将手工消息复制代码替换为 `parent.fork(slot_id, strategy)`
|
||||
5. 在 `agent.rs` 的 `pub use context::{...}` 列表中追加 `MergeStrategy`
|
||||
6. 在 `context.rs` 的 `#[cfg(test)] mod tests` 中新增 9 个测试用例
|
||||
|
||||
**注意**:重构后 `derive_slot` 的子 slot `budget` 从 `ContextBudget::default()` 变为继承父 slot,`compact` 从 `true` 变为继承父 slot。由于 `ContextBudget` 在 v0.2 无消费逻辑且父 slot 的 `compact` 默认也为 `true`,此变化无实际影响。验收条件中"行为不变"指对外功能行为不变(slot 消息内容、血缘关系不变)。
|
||||
|
||||
**预估工作量**:M(1-4h)
|
||||
|
||||
**风险等级**:低(纯新增,不修改已有逻辑路径)
|
||||
|
||||
**验收条件**:
|
||||
- `MergeStrategy` 枚举存在,`Append` 和 `Replace` 两个变体可用,且通过 `agcore::agent::MergeStrategy` 路径可访问
|
||||
- `ContextSlot::fork` 返回的 child 在 fork 时刻消息等于父 slot
|
||||
- fork Focused 策略按 `FocusedConfig` 过滤消息
|
||||
- 父 slot 后续追加消息不影响子 slot
|
||||
- 子 slot 的 source 正确记录 `Derived { parent_id, strategy }`
|
||||
- `parent.merge(child, Append)` 追加到父末尾,message_count 正确
|
||||
- `parent.merge(child, Replace)` 替换父全量消息,message_count 正确
|
||||
- self-merge 返回 `Err(AgentError::Config)`
|
||||
- merge 到 Readonly slot 返回 `Err(AgentError::SlotReadonly)`
|
||||
- 跨 session merge 返回 `Err(AgentError::Config)`
|
||||
- `derive_slot` 对外行为不变(slot 消息内容、血缘关系、持久化行为均不变;内部 budget/compact 继承差异无实际影响),测试全绿
|
||||
- `cargo doc --no-deps` 无 warning(验证新增公开 API 的文档注释完整)
|
||||
|
||||
**回退方式**:`git revert` 该 commit
|
||||
|
||||
### Step 13.4 — ToolChoice 移入 tool.rs
|
||||
|
||||
**前置依赖**:Step 13.5(顺序约束:必须早于 Step 13.1——若 Step 13.1 先执行会将 `ToolChoice` 与 `request.rs` 一同删除,导致本 Step 无可搬移的源)
|
||||
|
||||
**任务描述**:将 `ToolChoice` 枚举及其 serde 实现从 `types/request.rs` 搬移到 `types/tool.rs`,更新所有 import/path 引用。公共 re-export 路径 `agcore::llm::types::ToolChoice` 保持不变。
|
||||
|
||||
**涉及文件**:
|
||||
- `src/llm/types/request.rs` — 删除 ToolChoice(~28-99 行)
|
||||
- `src/llm/types/tool.rs` — 新增 ToolChoice 枚举 + serde impl
|
||||
- `src/llm/types/mod.rs` — re-export 路径从 `request` 改为 `tool`
|
||||
- `src/llm/types/request_v2.rs` — import 路径从 `request::` 改为 `tool::`
|
||||
|
||||
**具体操作**:
|
||||
1. 从 `request.rs` 复制 `ToolChoice` 枚举 + `Serialize`/`Deserialize` impl 到 `tool.rs`
|
||||
2. 从 `request.rs` 中删除 `ToolChoice` 定义
|
||||
3. 在 `mod.rs` 中将 `pub use request::{..., ToolChoice}` 改为 `pub use tool::ToolChoice`
|
||||
4. 在 `request_v2.rs` 中将 `use crate::llm::types::request::ToolChoice` 改为 `use crate::llm::types::tool::ToolChoice`
|
||||
5. 验证 `cycle.rs` 的 `use crate::llm::types::ToolChoice`(通过 re-export)路径不变
|
||||
|
||||
**预估工作量**:S(<1h)
|
||||
|
||||
**风险等级**:低(有限的 import 路径变更,编译立即可发现)
|
||||
|
||||
**验收条件**:
|
||||
- `ToolChoice` 在 `tool.rs` 中定义
|
||||
- `pub use tool::ToolChoice` 在 `mod.rs` 中
|
||||
- `request_v2.rs` 编译通过
|
||||
- `cycle.rs` 路径不变
|
||||
- `cargo build --all-targets` + `cargo test` + `cargo clippy` 全绿
|
||||
|
||||
**回退方式**:`git revert` 该 commit
|
||||
|
||||
### Step 13.1 — request.rs 类型移入 openai.rs
|
||||
|
||||
**前置依赖**:Step 13.4(ToolChoice 已移走,request.rs 剩余内容全是 OpenAI wire-format 专有类型)
|
||||
|
||||
**任务描述**:删除 `types/request.rs` 整文件,将所有剩余类型(`OpenaiChatRequest`、`StreamOptions`、`OpenaiTool`、`AudioParam`、`PredictionContent`、`UserLocation`、`Approximate`、`WebSearchOptions`)搬入 `provider/openai.rs`,更新 `mod.rs` re-export。
|
||||
|
||||
**涉及文件**:
|
||||
- `src/llm/types/request.rs` — 整文件删除
|
||||
- `src/llm/provider/openai.rs` — 新增所有类型定义
|
||||
- `src/llm/types/mod.rs` — 删除 re-export + mod 声明
|
||||
|
||||
**具体操作**:
|
||||
1. 从 `request.rs` 复制所有剩余类型定义到 `openai.rs`,可见性设为 `pub(crate)`
|
||||
2. `OpenaiTool` 内引用 `OpenaiToolDefinition`(定义在 `tool.rs`),路径改为 `crate::llm::types::tool::OpenaiToolDefinition`
|
||||
3. 删除 `openai.rs` 中原 `use crate::llm::types::request::{...}` import
|
||||
4. 从 `mod.rs` 删除 `pub use request::{OpenaiChatRequest, OpenaiTool, StreamOptions}` 和 `pub mod request;`
|
||||
5. 删除 `types/request.rs` 文件
|
||||
|
||||
**预估工作量**:M(1-4h)
|
||||
|
||||
**风险等级**:低(纯搬移 + 删除,文件内无逻辑变更)
|
||||
|
||||
**验收条件**:
|
||||
- `request.rs` 文件不存在
|
||||
- `OpenaiChatRequest` 等类型在 `openai.rs` 中定义,编译通过
|
||||
- `OpenaiTool` 通过 `crate::llm::types::tool::OpenaiToolDefinition` 正确引用
|
||||
- `cargo build --all-targets` + `cargo test` + `cargo clippy` 全绿
|
||||
|
||||
**回退方式**:`git revert` 该 commit。若 Step 13.2 也已提交,单独 revert 本 Step 可能因 `provider/openai.rs` 并发修改产生合并冲突。安全回退顺序为逆序:先 revert 13.2,再 revert 13.1。
|
||||
|
||||
### Step 13.2 — response.rs 类型移入 openai.rs
|
||||
|
||||
**前置依赖**:无(与 Step 13.1 共享 `provider/openai.rs` 和 `types/mod.rs`,但本 Step 仅追加类型定义,无覆盖操作;建议在 13.1 之后顺序执行以避免并行时的合并冲突)
|
||||
|
||||
**任务描述**:删除 `types/response.rs` 整文件,将所有类型(`OpenaiChatResponse`、`OpenaiChatChunk`、`Choice`、`Delta`、`ChunkChoice` 等 + 两个 `From` impl)搬入 `provider/openai.rs`,更新 `mod.rs` 和 `stream.rs` 的 import 路径。
|
||||
|
||||
**涉及文件**:
|
||||
- `src/llm/types/response.rs` — 整文件删除
|
||||
- `src/llm/provider/openai.rs` — 新增所有类型定义 + From impl
|
||||
- `src/llm/types/mod.rs` — 删除 re-export + mod 声明
|
||||
- `src/llm/stream.rs` — `OpenaiChatChunk` import 路径改为 `provider::openai`
|
||||
|
||||
**具体操作**:
|
||||
1. 从 `response.rs` 复制所有类型定义(含 `From` impl)到 `openai.rs`,可见性设为 `pub(crate)`
|
||||
2. 删除 `openai.rs` 中原 `use crate::llm::types::response::{...}` import
|
||||
3. 从 `mod.rs` 删除 `pub use response::{...}` 和 `pub mod response;`
|
||||
4. 在 `stream.rs:26` 将 `OpenaiChatChunk` 的 import 路径改为 `crate::llm::provider::openai::OpenaiChatChunk`(`OpenaiToolCall` 路径不变)
|
||||
5. 删除 `types/response.rs` 文件
|
||||
|
||||
**预估工作量**:M(1-4h)
|
||||
|
||||
**风险等级**:低(与 Step 13.1 模式完全相同)
|
||||
|
||||
**验收条件**:
|
||||
- `response.rs` 文件不存在
|
||||
- `OpenaiChatResponse`/`Chunk` 等类型在 `openai.rs` 中定义,编译通过
|
||||
- `stream.rs` import 路径正确
|
||||
- `cargo build --all-targets` + `cargo test` + `cargo clippy` 全绿
|
||||
|
||||
**回退方式**:`git revert` 该 commit。若 Step 13.1 和本 Step 均已提交,安全回退顺序为逆序:先 revert 本 Step,再 revert 13.1。
|
||||
|
||||
### Step 13.3 — 旧文件清理 + ChatResponse 删除
|
||||
|
||||
**前置依赖**:Step 13.1(`request.rs` 已删)、Step 13.2(`response.rs` 已删)
|
||||
|
||||
**任务描述**:删除 `old_stream.rs` 和 `ChatResponse`,简化 `stream.rs` 为仅保留 `pub use` 重导出。这是 Phase 13 技术风险最高的 Step。
|
||||
|
||||
**涉及文件**:
|
||||
- `src/llm/types/old_stream.rs` — 整文件删除
|
||||
- `src/llm/types/mod.rs` — 删除 `pub mod old_stream;` + 删除 `ChatResponse` 结构体和两个 `From` impl
|
||||
- `src/llm/stream.rs` — 删除死代码(约 160 行),仅保留 `pub use` 重导出
|
||||
|
||||
**具体操作**:
|
||||
1. **前置验证 A**:执行 `grep -rn 'parse_chunk_stream\|map_legacy_to_ir\|LegacyToIrEventStream\|ChunkToLegacyEventStream' src/` 确认零外部调用方,记录结果到 commit message
|
||||
2. **前置验证 B**:执行 `cargo doc --no-deps 2>&1 | grep -i 'ChatResponse'` 确认零文档引用,记录结果
|
||||
3. 从 `mod.rs` 删除 `pub mod old_stream;`
|
||||
4. 从 `mod.rs` 删除 `ChatResponse` 结构体定义 + `#[allow(deprecated)]` `From<OpenaiChatResponse> for ChatResponse` + `From<ChatResponse> for OpenaiChatChunk`
|
||||
5. 删除 `old_stream.rs` 文件
|
||||
6. 从 `stream.rs` 删除:`use crate::llm::types::old_stream::LegacyStreamEvent`、`parse_chunk_stream`、`parse_chunk_stream_legacy`、`ChunkToLegacyEventStream`、`LegacyToIrEventStream`、`map_legacy_to_ir`、`empty_message_response`
|
||||
7. `stream.rs` 最终只保留 module doc comment + `pub use crate::llm::types::response_v2::StreamEvent;`
|
||||
8. 检查 `cycle.rs:88` 的 `#[allow(deprecated)]` 属性是否仍与 `ChatResponse` 相关——若不相关则无需改动;若因 `ChatResponse` 删除而变脏,清理该属性
|
||||
|
||||
**预估工作量**:S(<1h,cleanup)+ M(需验证过程)
|
||||
|
||||
**风险等级**:中(`ChatResponse` 删除是 Breaking Change,外部可能静默依赖)
|
||||
|
||||
**验收条件**:
|
||||
- `old_stream.rs` 文件不存在
|
||||
- `ChatResponse` 结构体不存在(全局搜索仅保留 `OpenaiChatResponse` 引用)
|
||||
- `stream.rs` 只保留 `pub use` 重导出
|
||||
- `cargo build --all-targets` 编译通过
|
||||
- `cargo test --all-targets` 全绿(预期 285 测试)
|
||||
- `cargo clippy --all-targets -- -D warnings` 0 警告
|
||||
- `cargo doc --no-deps` 无 warning
|
||||
|
||||
**回退方式**:`git revert` 该 commit(单独 revert 即可恢复 `ChatResponse` + `old_stream.rs`)
|
||||
+120
-4
@@ -26,14 +26,127 @@ use crate::llm::convert::{from_openai, to_openai};
|
||||
use crate::llm::error::LlmError;
|
||||
use crate::llm::types::message::{ContentBlock, ContentBlockType, Message};
|
||||
use crate::llm::types::openai_message::{ContentField, OpenaiChatMessage};
|
||||
use crate::llm::types::request::{OpenaiChatRequest, OpenaiTool, StreamOptions};
|
||||
use crate::llm::types::request_v2::MessageRequest;
|
||||
use crate::llm::types::response::{OpenaiChatChunk, OpenaiChatResponse};
|
||||
use crate::llm::types::response_v2::{
|
||||
MessageResponse, PartialMessageResponse, PartialUsage, StopReason, StreamEvent,
|
||||
};
|
||||
use crate::llm::types::shared::FinishReason;
|
||||
use crate::llm::types::tool::OpenaiToolCall;
|
||||
use crate::llm::types::shared::{FinishReason, ResponseFormat, ServiceTier, StopSequence};
|
||||
use crate::llm::types::tool::{OpenaiToolCall, OpenaiToolDefinition, ToolChoice};
|
||||
use serde::Deserialize;
|
||||
|
||||
// =============================================================================
|
||||
// 0. OpenAI wire-format 类型(Phase 13 从 types::request 迁入)
|
||||
// =============================================================================
|
||||
|
||||
/// 流式响应选项。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct StreamOptions {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub include_usage: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub include_obfuscation: Option<bool>,
|
||||
}
|
||||
|
||||
/// OpenAI wire-format 工具定义。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case", tag = "type")]
|
||||
pub(crate) enum OpenaiTool {
|
||||
Function { function: OpenaiToolDefinition },
|
||||
}
|
||||
|
||||
/// 音频输出参数。
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct AudioParam {
|
||||
pub format: String,
|
||||
pub voice: String,
|
||||
}
|
||||
|
||||
/// 预测内容(OpenAI `prediction` 字段)。
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct PredictionContent {
|
||||
#[serde(rename = "type")]
|
||||
pub pred_type: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
/// 用户位置(web search 用)。
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct UserLocation {
|
||||
#[serde(rename = "type")]
|
||||
pub loc_type: String,
|
||||
pub approximate: Approximate,
|
||||
}
|
||||
|
||||
/// 近似位置。
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct Approximate {
|
||||
pub city: String,
|
||||
pub country: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub region: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub timezone: Option<String>,
|
||||
}
|
||||
|
||||
/// Web search 选项。
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct WebSearchOptions {
|
||||
pub search_context_size: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user_location: Option<UserLocation>,
|
||||
}
|
||||
|
||||
/// OpenAI Chat Completions 请求体。
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub(crate) struct OpenaiChatRequest {
|
||||
pub model: String,
|
||||
pub messages: Vec<OpenaiChatMessage>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub frequency_penalty: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub logit_bias: Option<Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_tokens: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub n: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub presence_penalty: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub response_format: Option<ResponseFormat>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub seed: Option<i64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub service_tier: Option<ServiceTier>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stop: Option<StopSequence>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stream: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stream_options: Option<StreamOptions>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub temperature: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub top_p: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tools: Option<Vec<OpenaiTool>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool_choice: Option<ToolChoice>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub parallel_tool_calls: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extra_headers: Option<Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extra_body: Option<Value>,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 1. GenericOpenaiProvider —— OpenAI-compatible 协议共用实现
|
||||
@@ -210,7 +323,10 @@ impl GenericOpenaiProvider {
|
||||
///
|
||||
/// 实现注意:先在函数顶部抽取出所有 needed 字段(clone 或 move),避免后续
|
||||
/// 部分移动 `request` 后无法借用其它字段。
|
||||
pub fn convert_request(&self, request: MessageRequest) -> Result<OpenaiChatRequest, LlmError> {
|
||||
pub(crate) fn convert_request(
|
||||
&self,
|
||||
request: MessageRequest,
|
||||
) -> Result<OpenaiChatRequest, LlmError> {
|
||||
// ponytail: 先抽取 / clone 所有 owned 字段,再访问 request.extra,
|
||||
// 避免部分移动导致后续 `&self` borrow 失败。
|
||||
let model = request.model.clone();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
pub mod message;
|
||||
pub mod old_stream;
|
||||
pub mod openai_message;
|
||||
pub mod request;
|
||||
pub mod request_v2;
|
||||
pub mod response;
|
||||
pub mod response_v2;
|
||||
@@ -12,7 +11,6 @@ pub mod usage;
|
||||
pub use openai_message::{
|
||||
ContentField, FileData, ImageURL, InputAudio, OpenaiChatMessage, OpenaiContentPart,
|
||||
};
|
||||
pub use request::{OpenaiChatRequest, OpenaiTool, StreamOptions};
|
||||
pub use request_v2::{ExtraError, MessageRequest, ThinkingConfig};
|
||||
pub use response::{
|
||||
Annotation, Choice, ChunkChoice, Delta, Logprobs, OpenaiAudio, OpenaiChatChunk,
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
use crate::llm::types::shared::{ResponseFormat, ServiceTier, StopSequence};
|
||||
use crate::llm::types::tool::{OpenaiToolDefinition, ToolChoice};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StreamOptions {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub include_usage: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub include_obfuscation: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case", tag = "type")]
|
||||
pub enum OpenaiTool {
|
||||
Function { function: OpenaiToolDefinition },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AudioParam {
|
||||
pub format: String,
|
||||
pub voice: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PredictionContent {
|
||||
#[serde(rename = "type")]
|
||||
pub pred_type: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserLocation {
|
||||
#[serde(rename = "type")]
|
||||
pub loc_type: String,
|
||||
pub approximate: Approximate,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Approximate {
|
||||
pub city: String,
|
||||
pub country: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub region: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub timezone: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WebSearchOptions {
|
||||
pub search_context_size: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user_location: Option<UserLocation>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct OpenaiChatRequest {
|
||||
pub model: String,
|
||||
pub messages: Vec<crate::llm::types::openai_message::OpenaiChatMessage>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub frequency_penalty: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub logit_bias: Option<Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_tokens: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub n: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub presence_penalty: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub response_format: Option<ResponseFormat>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub seed: Option<i64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub service_tier: Option<ServiceTier>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stop: Option<StopSequence>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stream: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stream_options: Option<StreamOptions>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub temperature: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub top_p: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tools: Option<Vec<OpenaiTool>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool_choice: Option<ToolChoice>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub parallel_tool_calls: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extra_headers: Option<Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extra_body: Option<Value>,
|
||||
}
|
||||
Reference in New Issue
Block a user