refactor(types): response.rs 类型移入 provider/openai.rs
- 删除 types/response.rs(177 行) - 所有 OpenAI wire-format 响应类型迁入 provider/openai.rs,可见性 pub(crate): TokenLogprob / TopLogprob / Logprobs / URLCitation / Annotation / OpenaiAudio / Choice / OpenaiChatResponse / Delta / ChunkChoice / OpenaiChatChunk - From<OpenaiChatMessage> for Delta 与 From<OpenaiChatResponse> for OpenaiChatChunk 同步迁入 openai.rs - types/mod.rs 删除 pub mod response; 与对应 re-export - convert_response 同步降级为 pub(crate) 以匹配 OpenaiChatResponse 可见性 - stream.rs: OpenaiChatChunk import 路径改为 crate::llm::provider::openai - stream.rs 同步简化为 module doc + pub use 重导出(合并 Step 13.3 的清理动作, 避免遗留 dead_code 警告来回) - mod.rs: ChatResponse 的两个 From impl 同步删除(impl 内引用的 OpenaiChatResponse / OpenaiChatChunk / Delta / ChunkChoice 已不在 types 模块),结构体保留到 Step 13.3 - 公共 re-export 路径 agcore::llm::types::OpenaiChatResponse/Chunk 等已删除 (Breaking Change,见 CHANGELOG)
This commit is contained in:
+195
-4
@@ -25,9 +25,8 @@ use super::{LlmProvider, ProviderCapabilities, ProviderFeatures};
|
||||
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::openai_message::{ContentField, OpenaiChatMessage, OpenaiContentPart};
|
||||
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,
|
||||
};
|
||||
@@ -148,6 +147,198 @@ pub(crate) struct OpenaiChatRequest {
|
||||
pub extra_body: Option<Value>,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 0b. OpenAI wire-format 响应类型(Phase 13 从 types::response 迁入)
|
||||
// =============================================================================
|
||||
|
||||
/// 单 token logprob。
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct TokenLogprob {
|
||||
pub token: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bytes: Option<Vec<u32>>,
|
||||
pub logprob: f64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub top_logprobs: Option<Vec<TopLogprob>>,
|
||||
}
|
||||
|
||||
/// Top-K logprob。
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct TopLogprob {
|
||||
pub token: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bytes: Option<Vec<u32>>,
|
||||
pub logprob: f64,
|
||||
}
|
||||
|
||||
/// Logprobs 容器。
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct Logprobs {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub content: Option<Vec<TokenLogprob>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub refusal: Option<Vec<TokenLogprob>>,
|
||||
}
|
||||
|
||||
/// URL 引用(annotation 用)。
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct URLCitation {
|
||||
pub end_index: u32,
|
||||
pub start_index: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
/// 注释(response 中可包含)。
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct Annotation {
|
||||
#[serde(rename = "type")]
|
||||
pub ann_type: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub url_citation: Option<URLCitation>,
|
||||
}
|
||||
|
||||
/// OpenAI 音频输出。
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct OpenaiAudio {
|
||||
pub id: String,
|
||||
pub data: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub expires_at: Option<i64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub transcript: Option<String>,
|
||||
}
|
||||
|
||||
/// 非流式 choice。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct Choice {
|
||||
pub index: u32,
|
||||
pub message: OpenaiChatMessage,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub finish_reason: Option<FinishReason>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub logprobs: Option<Logprobs>,
|
||||
}
|
||||
|
||||
/// OpenAI Chat Completions 响应。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct OpenaiChatResponse {
|
||||
pub id: String,
|
||||
pub object: String,
|
||||
pub created: u64,
|
||||
pub model: String,
|
||||
pub choices: Vec<Choice>,
|
||||
pub usage: crate::llm::types::usage::Usage,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub system_fingerprint: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub service_tier: Option<ServiceTier>,
|
||||
}
|
||||
|
||||
/// 流式响应 delta。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct Delta {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub role: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub content: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub refusal: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool_calls: Option<Vec<OpenaiToolCall>>,
|
||||
}
|
||||
|
||||
/// 流式 chunk 的 choice。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct ChunkChoice {
|
||||
pub index: u32,
|
||||
pub delta: Delta,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub logprobs: Option<Logprobs>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub finish_reason: Option<FinishReason>,
|
||||
}
|
||||
|
||||
/// OpenAI Chat Completions 流式 chunk。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct OpenaiChatChunk {
|
||||
pub id: String,
|
||||
pub object: String,
|
||||
pub created: u64,
|
||||
pub model: String,
|
||||
pub choices: Vec<ChunkChoice>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub usage: Option<crate::llm::types::usage::Usage>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub system_fingerprint: Option<String>,
|
||||
}
|
||||
|
||||
impl From<OpenaiChatMessage> for Delta {
|
||||
fn from(msg: OpenaiChatMessage) -> Self {
|
||||
match msg {
|
||||
OpenaiChatMessage::Assistant {
|
||||
content,
|
||||
tool_calls,
|
||||
..
|
||||
} => Delta {
|
||||
role: Some("assistant".to_string()),
|
||||
content: match content {
|
||||
ContentField::String(s) => Some(s),
|
||||
ContentField::Array(parts) => {
|
||||
let mut text = String::new();
|
||||
for part in parts {
|
||||
if let OpenaiContentPart::Text { text: t } = part {
|
||||
text.push_str(&t);
|
||||
}
|
||||
}
|
||||
if text.is_empty() { None } else { Some(text) }
|
||||
}
|
||||
},
|
||||
refusal: None,
|
||||
tool_calls,
|
||||
},
|
||||
_ => Delta {
|
||||
role: None,
|
||||
content: None,
|
||||
refusal: None,
|
||||
tool_calls: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OpenaiChatResponse> for OpenaiChatChunk {
|
||||
fn from(response: OpenaiChatResponse) -> Self {
|
||||
let choices = response
|
||||
.choices
|
||||
.into_iter()
|
||||
.map(|c| ChunkChoice {
|
||||
index: c.index,
|
||||
delta: Delta::from(c.message),
|
||||
logprobs: c.logprobs,
|
||||
finish_reason: c.finish_reason,
|
||||
})
|
||||
.collect();
|
||||
|
||||
OpenaiChatChunk {
|
||||
id: response.id,
|
||||
object: "chat.completion.chunk".to_string(),
|
||||
created: response.created,
|
||||
model: response.model,
|
||||
choices,
|
||||
usage: Some(response.usage),
|
||||
system_fingerprint: response.system_fingerprint,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 1. GenericOpenaiProvider —— OpenAI-compatible 协议共用实现
|
||||
// =============================================================================
|
||||
@@ -389,7 +580,7 @@ impl GenericOpenaiProvider {
|
||||
/// `OpenaiChatResponse` → `MessageResponse`。
|
||||
///
|
||||
/// 返回 `Err(LlmError::Other)` 当 `choices` 为空。
|
||||
pub fn convert_response(
|
||||
pub(crate) fn convert_response(
|
||||
&self,
|
||||
response: OpenaiChatResponse,
|
||||
) -> Result<MessageResponse, LlmError> {
|
||||
@@ -1102,7 +1293,7 @@ data: [DONE]\n\n";
|
||||
object: "chat.completion".into(),
|
||||
created: 0,
|
||||
model: "gpt-4o".into(),
|
||||
choices: vec![crate::llm::types::response::Choice {
|
||||
choices: vec![Choice {
|
||||
index: 0,
|
||||
message: OpenaiChatMessage::Assistant {
|
||||
content: ContentField::String(String::new()),
|
||||
|
||||
+5
-199
@@ -1,203 +1,9 @@
|
||||
//! 流式事件系统 —— 将 LLM 流式响应解析为语义化事件。
|
||||
//! 流式事件系统 —— 重导出 `StreamEvent` 供向后兼容。
|
||||
//!
|
||||
//! Phase 0 修订(参见 `docs/10a-phase0-types-and-trait.md` §"StreamEvent 命名冲突处理"):
|
||||
//! 历史说明(Phase 0 → Phase 13):
|
||||
//! - 对外暴露的 `StreamEvent` 是高精度 IR 版本(来自 `response_v2::StreamEvent`)。
|
||||
//! - 旧变体(`AssistantTextDelta` / `ToolExecutionStarted` 等)重命名为 `LegacyStreamEvent`
|
||||
//! 放在 `crate::llm::types::old_stream` 模块,本文件内部消费。
|
||||
//! - Phase 1 重写 Provider 时可直接消费新事件流后整体删除 `LegacyStreamEvent` 相关代码。
|
||||
//!
|
||||
//! 当前实现:旧的 `parse_chunk_stream` 内部消费 `OpenaiChatChunk`,映射为
|
||||
//! `LegacyStreamEvent`,再在 `LegacyToIrEventStream` 中映射为新 IR `StreamEvent`
|
||||
//! 后输出。Phase 1 会重写此层(OpenAI Provider 直接产出新事件流)。
|
||||
//! - 旧版 chunk 解析 + LegacyStreamEvent 适配层在 Phase 13 完成后已整体删除。
|
||||
//! - 当前文件仅保留 `pub use` 重导出,保持与既有
|
||||
//! `use crate::llm::stream::StreamEvent` 的代码兼容。
|
||||
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use futures_core::stream::Stream;
|
||||
use futures_util::FutureExt;
|
||||
use futures_util::future::poll_fn;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::llm::error::LlmError;
|
||||
use crate::llm::types::old_stream::LegacyStreamEvent;
|
||||
use crate::llm::types::response_v2::MessageResponse;
|
||||
use crate::llm::types::response_v2::StopReason;
|
||||
use crate::llm::types::usage::Usage;
|
||||
use crate::llm::types::{OpenaiChatChunk, OpenaiToolCall};
|
||||
|
||||
// 唯一的对外 `StreamEvent` 定义(高精度 IR 事件,来自 `response_v2`)。
|
||||
//
|
||||
// 此 `pub use` 同时起到两个作用:
|
||||
// 1. 让 `crate::llm::stream::StreamEvent` 路径仍指向新高精度 IR 事件,
|
||||
// 保持与既有 `use crate::llm::stream::StreamEvent` 的代码兼容;
|
||||
// 2. 把模块内部的 `StreamEvent` 名字指向 `response_v2::StreamEvent`。
|
||||
pub use crate::llm::types::response_v2::StreamEvent;
|
||||
|
||||
/// 将原始 OpenaiChatChunk 流解析为新高精度 IR StreamEvent 流。
|
||||
///
|
||||
/// ponytail: 每个产出事件都用 `Result<_, LlmError>` 包装,让上层 `chat_stream`
|
||||
/// trait 方法直接消费并保持错误传播链。当前 `LegacyToIrEventStream` 内部
|
||||
/// 不会产生错误,所有结果都是 `Ok`;后续 Phase 1 重写 Provider 时,
|
||||
/// 真实 IR 流转换可在此层注入 error 事件。
|
||||
pub fn parse_chunk_stream(
|
||||
chunks: Pin<Box<dyn futures_core::Stream<Item = Result<OpenaiChatChunk, LlmError>> + Send>>,
|
||||
) -> Pin<Box<dyn futures_core::Stream<Item = Result<StreamEvent, LlmError>> + Send>> {
|
||||
let legacy = parse_chunk_stream_legacy(chunks);
|
||||
Box::pin(LegacyToIrEventStream { inner: legacy })
|
||||
}
|
||||
|
||||
// --- 内部:chunk → LegacyStreamEvent ---
|
||||
|
||||
fn parse_chunk_stream_legacy(
|
||||
chunks: Pin<Box<dyn futures_core::Stream<Item = Result<OpenaiChatChunk, LlmError>> + Send>>,
|
||||
) -> Pin<Box<dyn futures_core::Stream<Item = LegacyStreamEvent> + Send>> {
|
||||
Box::pin(ChunkToLegacyEventStream { chunks })
|
||||
}
|
||||
|
||||
struct ChunkToLegacyEventStream {
|
||||
chunks: Pin<Box<dyn futures_core::Stream<Item = Result<OpenaiChatChunk, LlmError>> + Send>>,
|
||||
}
|
||||
|
||||
impl Stream for ChunkToLegacyEventStream {
|
||||
type Item = LegacyStreamEvent;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let this = &mut *self;
|
||||
poll_fn(|cx| match Pin::new(&mut this.chunks).poll_next(cx) {
|
||||
Poll::Ready(Some(Ok(chunk))) => {
|
||||
for choice in &chunk.choices {
|
||||
let delta = &choice.delta;
|
||||
|
||||
if let Some(content) = &delta.content {
|
||||
return Poll::Ready(Some(LegacyStreamEvent::AssistantTextDelta {
|
||||
text: content.clone(),
|
||||
}));
|
||||
}
|
||||
|
||||
if let Some(tool_calls) = &delta.tool_calls
|
||||
&& let Some(tc) = tool_calls.first()
|
||||
{
|
||||
let OpenaiToolCall::Function { id, function } = tc;
|
||||
let args: Value =
|
||||
serde_json::from_str(&function.arguments).unwrap_or(Value::Null);
|
||||
return Poll::Ready(Some(LegacyStreamEvent::ToolExecutionStarted {
|
||||
tool_name: function.name.clone(),
|
||||
input: args,
|
||||
tool_call_id: id.clone(),
|
||||
}));
|
||||
}
|
||||
|
||||
if let Some(finish_reason) = &choice.finish_reason {
|
||||
return Poll::Ready(Some(LegacyStreamEvent::TurnComplete {
|
||||
reason: *finish_reason,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(usage) = &chunk.usage {
|
||||
return Poll::Ready(Some(LegacyStreamEvent::CostUpdate { usage: *usage }));
|
||||
}
|
||||
|
||||
Poll::Ready(None)
|
||||
}
|
||||
Poll::Ready(Some(Err(e))) => Poll::Ready(Some(LegacyStreamEvent::error(e.to_string()))),
|
||||
Poll::Ready(None) => Poll::Ready(None),
|
||||
Poll::Pending => Poll::Pending,
|
||||
})
|
||||
.poll_unpin(cx)
|
||||
}
|
||||
}
|
||||
|
||||
// --- 内部:LegacyStreamEvent → 新 StreamEvent ---
|
||||
|
||||
struct LegacyToIrEventStream {
|
||||
inner: Pin<Box<dyn futures_core::Stream<Item = LegacyStreamEvent> + Send>>,
|
||||
}
|
||||
|
||||
impl Stream for LegacyToIrEventStream {
|
||||
type Item = Result<StreamEvent, LlmError>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let this = &mut *self;
|
||||
match Pin::new(&mut this.inner).poll_next(cx) {
|
||||
Poll::Ready(Some(legacy)) => Poll::Ready(Some(Ok(map_legacy_to_ir(legacy)))),
|
||||
Poll::Ready(None) => {
|
||||
// 旧流结束 → 主动补一个 MessageComplete(full_response 为兜底空快照)。
|
||||
// ponytail: Phase 0 中 OpenaiProvider 桥接层负责产出真实 MessageResponse,
|
||||
// 此处仅防止消费方无限等待。若 Provider 层已正确发出 MessageComplete,
|
||||
// LlmCycle 不会走到这里 —— 因为桥接层 inline 处理。
|
||||
Poll::Ready(Some(Ok(StreamEvent::MessageComplete {
|
||||
full_response: empty_message_response(),
|
||||
})))
|
||||
}
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn empty_message_response() -> MessageResponse {
|
||||
use crate::llm::types::message::Message;
|
||||
use std::collections::HashMap;
|
||||
MessageResponse {
|
||||
id: String::new(),
|
||||
model: String::new(),
|
||||
message: Message::Assistant { content: vec![] },
|
||||
usage: Usage::default(),
|
||||
stop_reason: StopReason::Stop,
|
||||
extra: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 把旧 LegacyStreamEvent 映射到新高精度 IR StreamEvent。
|
||||
///
|
||||
/// Phase 1 重写 Provider 后可直接删除此映射函数。当前映射语义:
|
||||
/// - `AssistantTextDelta` → `TextDelta`
|
||||
/// - `ToolExecutionStarted` → `ToolCallArgumentsDelta`(OpenAI 单 chunk 模式下整段 arguments 一次性下发)
|
||||
/// - `CostUpdate` → `CostUpdate`(Usage → PartialUsage 全字段)
|
||||
/// - `TurnComplete` → `MessageComplete`(Phase 1 重写 Provider 后正确产出)
|
||||
/// - `Error` → `Error`
|
||||
///
|
||||
/// ponytail: 这是一个"目前能跑通未来会被删除"的适配层。当前实现为单事件映射,
|
||||
/// 旧 `ToolExecutionStarted` 携带的 (id, name) 暂未填入 IR 事件(消费方
|
||||
/// Phase 2 中通过 MessageComplete.full_response.tool_use 提取)。Phase 1 重写时
|
||||
/// 由 OpenAI Provider 直接产出 IR 流,整体删除此映射。
|
||||
fn map_legacy_to_ir(legacy: LegacyStreamEvent) -> StreamEvent {
|
||||
use crate::llm::types::response_v2::PartialUsage;
|
||||
|
||||
match legacy {
|
||||
LegacyStreamEvent::AssistantTextDelta { text } => StreamEvent::TextDelta { text },
|
||||
LegacyStreamEvent::ToolExecutionStarted { input, .. } => {
|
||||
let arguments = serde_json::to_string(&input).unwrap_or_default();
|
||||
StreamEvent::ToolCallArgumentsDelta {
|
||||
index: 0,
|
||||
arguments,
|
||||
}
|
||||
}
|
||||
LegacyStreamEvent::ToolExecutionCompleted { .. } => {
|
||||
// 旧 ToolExecutionCompleted 不在 IR 流协议中——工具执行是消费方职责。
|
||||
// Phase 1 重写时此处整体删除。当前给一个无副作用的占位事件。
|
||||
StreamEvent::CostUpdate {
|
||||
usage: PartialUsage::default(),
|
||||
}
|
||||
}
|
||||
LegacyStreamEvent::CostUpdate { usage } => StreamEvent::CostUpdate {
|
||||
usage: PartialUsage {
|
||||
prompt_tokens: Some(usage.prompt_tokens),
|
||||
completion_tokens: Some(usage.completion_tokens),
|
||||
total_tokens: Some(usage.total_tokens),
|
||||
completion_tokens_details: usage.completion_tokens_details,
|
||||
prompt_tokens_details: usage.prompt_tokens_details,
|
||||
},
|
||||
},
|
||||
LegacyStreamEvent::TurnComplete { reason } => {
|
||||
// 旧 TurnComplete 不直接对应 IR;映射为带 StopReason 的 MessageComplete。
|
||||
// ponytail: Phase 1 重写 Provider 后此适配整体删除,
|
||||
// OpenAI Provider 直接产出带正确 stop_reason 的 MessageComplete。
|
||||
let _ = reason;
|
||||
StreamEvent::MessageComplete {
|
||||
full_response: empty_message_response(),
|
||||
}
|
||||
}
|
||||
LegacyStreamEvent::Error { message } => StreamEvent::Error { message },
|
||||
}
|
||||
}
|
||||
|
||||
+1
-55
@@ -2,7 +2,6 @@ pub mod message;
|
||||
pub mod old_stream;
|
||||
pub mod openai_message;
|
||||
pub mod request_v2;
|
||||
pub mod response;
|
||||
pub mod response_v2;
|
||||
pub mod shared;
|
||||
pub mod tool;
|
||||
@@ -12,10 +11,6 @@ pub use openai_message::{
|
||||
ContentField, FileData, ImageURL, InputAudio, OpenaiChatMessage, OpenaiContentPart,
|
||||
};
|
||||
pub use request_v2::{ExtraError, MessageRequest, ThinkingConfig};
|
||||
pub use response::{
|
||||
Annotation, Choice, ChunkChoice, Delta, Logprobs, OpenaiAudio, OpenaiChatChunk,
|
||||
OpenaiChatResponse, TokenLogprob, TopLogprob, URLCitation,
|
||||
};
|
||||
pub use response_v2::{
|
||||
ContentBlockBuilder, MessageResponse, PartialMessageResponse, PartialUsage, StopReason,
|
||||
StreamEvent,
|
||||
@@ -36,7 +31,7 @@ pub use usage::{CompletionTokensDetails, CostTracker, PromptTokensDetails, Usage
|
||||
// 避免新类型阴影。Phase 2 完成后再统一收敛。
|
||||
//
|
||||
// Phase 1 起移除 `ChatRequest` 别名 —— 新代码统一使用 `MessageRequest`(v2 IR)。
|
||||
// `ChatResponse` 结构体仍存在,作为 OpenAI `chat_inner()` 内部 wire-format 转换目标。
|
||||
// `ChatResponse` 结构体仍存在(无 From impl,将在 Step 13.3 整体删除)。
|
||||
/// 旧 wire-format 响应结构(保留用于 OpenAI 内部转换层)。
|
||||
#[deprecated(since = "0.1.0", note = "请改用 MessageResponse")]
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -45,52 +40,3 @@ pub struct ChatResponse {
|
||||
pub usage: Usage,
|
||||
pub stop_reason: Option<FinishReason>,
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl From<OpenaiChatResponse> for ChatResponse {
|
||||
fn from(response: OpenaiChatResponse) -> Self {
|
||||
let message = response
|
||||
.choices
|
||||
.first()
|
||||
.map(|c| c.message.clone())
|
||||
.unwrap_or_else(|| OpenaiChatMessage::assistant_text(""));
|
||||
let stop_reason = response.choices.first().and_then(|c| c.finish_reason);
|
||||
ChatResponse {
|
||||
message,
|
||||
usage: response.usage,
|
||||
stop_reason,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl From<ChatResponse> for OpenaiChatChunk {
|
||||
fn from(response: ChatResponse) -> Self {
|
||||
let delta = Delta::from(response.message.clone());
|
||||
let chunk_choice = ChunkChoice {
|
||||
index: 0,
|
||||
delta,
|
||||
logprobs: None,
|
||||
finish_reason: response.stop_reason,
|
||||
};
|
||||
|
||||
OpenaiChatChunk {
|
||||
id: format!(
|
||||
"chunk-{}",
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos())
|
||||
.unwrap_or(0)
|
||||
),
|
||||
object: "chat.completion.chunk".to_string(),
|
||||
created: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0),
|
||||
model: String::new(),
|
||||
choices: vec![chunk_choice],
|
||||
usage: Some(response.usage),
|
||||
system_fingerprint: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ pub enum LegacyStreamEvent {
|
||||
}
|
||||
|
||||
impl LegacyStreamEvent {
|
||||
#[allow(dead_code)] // ponytail: Step 13.3 将整个文件删除
|
||||
pub(crate) fn error(message: impl Into<String>) -> Self {
|
||||
Self::Error {
|
||||
message: message.into(),
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
use crate::llm::types::openai_message::OpenaiChatMessage;
|
||||
use crate::llm::types::shared::{FinishReason, ServiceTier};
|
||||
use crate::llm::types::tool::OpenaiToolCall;
|
||||
use crate::llm::types::usage::Usage;
|
||||
use crate::llm::types::{ContentField, OpenaiContentPart};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TokenLogprob {
|
||||
pub token: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bytes: Option<Vec<u32>>,
|
||||
pub logprob: f64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub top_logprobs: Option<Vec<TopLogprob>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TopLogprob {
|
||||
pub token: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bytes: Option<Vec<u32>>,
|
||||
pub logprob: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Logprobs {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub content: Option<Vec<TokenLogprob>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub refusal: Option<Vec<TokenLogprob>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct URLCitation {
|
||||
pub end_index: u32,
|
||||
pub start_index: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub title: Option<String>,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Annotation {
|
||||
#[serde(rename = "type")]
|
||||
pub ann_type: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub url_citation: Option<URLCitation>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OpenaiAudio {
|
||||
pub id: String,
|
||||
pub data: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub expires_at: Option<i64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub transcript: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Choice {
|
||||
pub index: u32,
|
||||
pub message: OpenaiChatMessage,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub finish_reason: Option<FinishReason>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub logprobs: Option<Logprobs>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OpenaiChatResponse {
|
||||
pub id: String,
|
||||
pub object: String,
|
||||
pub created: u64,
|
||||
pub model: String,
|
||||
pub choices: Vec<Choice>,
|
||||
pub usage: Usage,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub system_fingerprint: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub service_tier: Option<ServiceTier>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Delta {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub role: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub content: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub refusal: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool_calls: Option<Vec<OpenaiToolCall>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChunkChoice {
|
||||
pub index: u32,
|
||||
pub delta: Delta,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub logprobs: Option<Logprobs>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub finish_reason: Option<FinishReason>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OpenaiChatChunk {
|
||||
pub id: String,
|
||||
pub object: String,
|
||||
pub created: u64,
|
||||
pub model: String,
|
||||
pub choices: Vec<ChunkChoice>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub usage: Option<Usage>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub system_fingerprint: Option<String>,
|
||||
}
|
||||
|
||||
impl From<OpenaiChatMessage> for Delta {
|
||||
fn from(msg: OpenaiChatMessage) -> Self {
|
||||
match msg {
|
||||
OpenaiChatMessage::Assistant {
|
||||
content,
|
||||
tool_calls,
|
||||
..
|
||||
} => Delta {
|
||||
role: Some("assistant".to_string()),
|
||||
content: match content {
|
||||
ContentField::String(s) => Some(s),
|
||||
ContentField::Array(parts) => {
|
||||
let mut text = String::new();
|
||||
for part in parts {
|
||||
if let OpenaiContentPart::Text { text: t } = part {
|
||||
text.push_str(&t);
|
||||
}
|
||||
}
|
||||
if text.is_empty() { None } else { Some(text) }
|
||||
}
|
||||
},
|
||||
refusal: None,
|
||||
tool_calls,
|
||||
},
|
||||
_ => Delta {
|
||||
role: None,
|
||||
content: None,
|
||||
refusal: None,
|
||||
tool_calls: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OpenaiChatResponse> for OpenaiChatChunk {
|
||||
fn from(response: OpenaiChatResponse) -> Self {
|
||||
let choices = response
|
||||
.choices
|
||||
.into_iter()
|
||||
.map(|c| ChunkChoice {
|
||||
index: c.index,
|
||||
delta: Delta::from(c.message),
|
||||
logprobs: c.logprobs,
|
||||
finish_reason: c.finish_reason,
|
||||
})
|
||||
.collect();
|
||||
|
||||
OpenaiChatChunk {
|
||||
id: response.id,
|
||||
object: "chat.completion.chunk".to_string(),
|
||||
created: response.created,
|
||||
model: response.model,
|
||||
choices,
|
||||
usage: Some(response.usage),
|
||||
system_fingerprint: response.system_fingerprint,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user