feat(llm): 新增 IR 类型层 + 切换 LlmProvider trait 签名

引入跨 Provider 统一的新类型系统:
- Message 扁平大枚举(System/User/UserImage/Assistant/ToolResult)
- MessageRequest + MessageResponse + StreamEvent(高精度 IR)
- PartialMessageResponse 含 apply_to/finalize 汇聚算法
- ProviderType enum 扩展 + ProviderCapabilities

切换 LlmProvider trait 签名为 chat(MessageRequest) → MessageResponse,
chat_stream 返回 Stream<Item = Result<StreamEvent, LlmError>>,新增 capabilities()。

StreamEvent 命名冲突处理:旧变体迁移至 old_stream::LegacyStreamEvent,
stream.rs 通过 pub use 重新导出新高精度事件。

OpenaiProvider 添加 Phase 0 临时桥接(MessageRequest ↔ OpenaiChatRequest 转换),
标注 ponytail: Phase 0 临时桥接 标记,Phase 1 重写时移除。

测试 147 passed / 0 failed(新增 31 个新类型测试)。
This commit is contained in:
徐涛
2026-07-02 21:58:07 +08:00
parent 925c8f9729
commit 7c299f1cfd
18 changed files with 2605 additions and 441 deletions
+40 -27
View File
@@ -15,7 +15,9 @@ use crate::agent::runtime::RuntimeBundle;
use crate::agent::session_memory::SessionMemory;
use crate::llm::cycle::{CostTracker, CycleConfig, LlmCycle};
use crate::llm::hooks::{HookContext, HookEvent};
use crate::llm::types::ChatResponse;
use crate::llm::types::message::{ContentBlock, Message};
use crate::llm::types::openai_message::OpenaiChatMessage;
use crate::llm::types::response_v2::MessageResponse;
use crate::memory::store::InMemoryStore;
/// Agent 会话实例。
@@ -120,7 +122,7 @@ impl AgentSession {
pub async fn submit_turn(
&mut self,
user_input: impl Into<String>,
) -> Result<ChatResponse, AgentError> {
) -> Result<MessageResponse, AgentError> {
let turn_index = self.turn_index;
let hook_executor = Arc::clone(&self.bundle.hook_executor);
@@ -168,13 +170,15 @@ impl AgentSession {
mod tests {
use super::*;
use crate::agent::builder::AgentBuilder;
use crate::llm::error::LlmError;
use crate::llm::hooks::{Hook, HookContext, HookExecutor, HookResult};
use crate::llm::provider::LlmProvider;
use crate::llm::types::{
ChatRequest, ChatResponse, FinishReason, OpenaiChatMessage,
};
use crate::llm::provider::{LlmProvider, ProviderCapabilities, ProviderFeatures};
use crate::llm::types::request_v2::MessageRequest;
use crate::llm::types::response_v2::{MessageResponse, StopReason, StreamEvent};
use crate::tools::ToolRegistry;
use async_trait::async_trait;
use futures_core::Stream;
use std::pin::Pin;
use std::sync::atomic::{AtomicU32, Ordering};
/// 计数 hook —— 每被调用一次 +1。
@@ -200,11 +204,11 @@ mod tests {
/// MockProvider:按调用顺序返回预设响应。
struct MockProvider {
responses: std::sync::Mutex<Vec<ChatResponse>>,
responses: std::sync::Mutex<Vec<MessageResponse>>,
}
impl MockProvider {
fn new(responses: Vec<ChatResponse>) -> Self {
fn new(responses: Vec<MessageResponse>) -> Self {
Self {
responses: std::sync::Mutex::new(responses),
}
@@ -213,15 +217,27 @@ mod tests {
#[async_trait]
impl LlmProvider for MockProvider {
async fn chat(&self, _request: ChatRequest) -> Result<ChatResponse, crate::llm::error::LlmError> {
async fn chat(&self, _request: MessageRequest) -> Result<MessageResponse, LlmError> {
let mut responses = self.responses.lock().unwrap();
if responses.is_empty() {
return Err(crate::llm::error::LlmError::Other(
"no more mock responses".into(),
));
return Err(LlmError::Other("no more mock responses".into()));
}
Ok(responses.remove(0))
}
async fn chat_stream(
&self,
_request: MessageRequest,
) -> Result<Pin<Box<dyn Stream<Item = Result<StreamEvent, LlmError>> + Send>>, LlmError>
{
unimplemented!()
}
fn capabilities(&self) -> ProviderCapabilities {
ProviderCapabilities {
provider_name: "mock",
supported_models: None,
features: ProviderFeatures::default(),
}
}
}
struct StubAgent {
@@ -238,11 +254,18 @@ mod tests {
}
}
fn assistant_text(text: &str) -> ChatResponse {
ChatResponse {
message: OpenaiChatMessage::assistant_text(text),
fn assistant_text(text: &str) -> MessageResponse {
MessageResponse {
id: String::new(),
model: String::new(),
message: Message::Assistant {
content: vec![ContentBlock::Text {
text: text.into(),
}],
},
usage: crate::llm::types::Usage::from_input_output(10, 5),
stop_reason: Some(FinishReason::Stop),
stop_reason: StopReason::Stop,
extra: std::collections::HashMap::new(),
}
}
@@ -267,17 +290,7 @@ mod tests {
assert_eq!(session.turn_index(), 0);
let response = session.submit_turn("hi").await.unwrap();
let text = match &response.message {
OpenaiChatMessage::Assistant { content, .. } => {
if let crate::llm::types::ContentField::String(s) = content {
s.clone()
} else {
String::new()
}
}
_ => String::new(),
};
assert_eq!(text, "hello back");
assert_eq!(response.text(), "hello back");
assert_eq!(session.turn_index(), 1);
assert_eq!(session.usage().total().prompt_tokens, 10);
assert_eq!(session.usage().total().completion_tokens, 5);