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:
徐涛
2026-07-08 22:58:18 +08:00
parent 760de46623
commit 1c0e1e0ed1
5 changed files with 202 additions and 435 deletions
+195 -4
View File
@@ -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
View File
@@ -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) => {
// 旧流结束 → 主动补一个 MessageCompletefull_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
View File
@@ -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,
}
}
}
+1
View File
@@ -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(),
-177
View File
@@ -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,
}
}
}