use crate::core::types::chat::ChatMessage;
use crate::core::types::message::MessageRole;
use crate::core::types::responses::{
ChatChoice, ChatChunk, ChatDelta, ChatResponse, ChatStreamChoice, FinishReason, LogProbs,
TokenLogProb, TopLogProb, Usage,
};
use crate::core::types::thinking::ThinkingContent;
use super::super::error::OpenAIError;
use super::super::models::*;
pub struct OpenAIResponseTransformer;
impl OpenAIResponseTransformer {
pub fn transform(response: OpenAIChatResponse) -> Result<ChatResponse, OpenAIError> {
let choices = response
.choices
.into_iter()
.map(Self::transform_choice)
.collect::<Result<Vec<_>, _>>()?;
Ok(ChatResponse {
id: response.id,
object: response.object,
created: response.created,
model: response.model,
choices,
usage: response.usage.map(Self::transform_usage),
system_fingerprint: response.system_fingerprint,
})
}
pub fn transform_stream_chunk(chunk: OpenAIStreamChunk) -> Result<ChatChunk, OpenAIError> {
let choices = chunk
.choices
.into_iter()
.map(Self::transform_stream_choice)
.collect::<Result<Vec<_>, _>>()?;
Ok(ChatChunk {
id: chunk.id,
object: chunk.object,
created: chunk.created,
model: chunk.model,
choices,
usage: chunk.usage.map(Self::transform_usage),
system_fingerprint: chunk.system_fingerprint,
})
}
fn transform_stream_choice(
choice: OpenAIStreamChoice,
) -> Result<ChatStreamChoice, OpenAIError> {
Ok(ChatStreamChoice {
index: choice.index,
delta: Self::transform_delta(choice.delta)?,
logprobs: choice.logprobs.and_then(|lp| {
serde_json::from_value::<OpenAILogprobs>(lp)
.ok()
.map(Self::transform_logprobs)
}),
finish_reason: choice.finish_reason.map(Self::transform_finish_reason),
})
}
fn transform_delta(delta: OpenAIDelta) -> Result<ChatDelta, OpenAIError> {
Ok(ChatDelta {
role: delta.role.map(|r| match r.as_str() {
"system" => MessageRole::System,
"user" => MessageRole::User,
"assistant" => MessageRole::Assistant,
"tool" => MessageRole::Tool,
"function" => MessageRole::Function,
_ => MessageRole::Assistant,
}),
content: delta.content,
thinking: None,
tool_calls: None,
function_call: None,
})
}
fn transform_choice(choice: OpenAIChoice) -> Result<ChatChoice, OpenAIError> {
Ok(ChatChoice {
index: choice.index,
message: Self::transform_message_response(choice.message)?,
logprobs: choice.logprobs.and_then(|lp| {
serde_json::from_value::<OpenAILogprobs>(lp)
.ok()
.map(Self::transform_logprobs)
}),
finish_reason: choice.finish_reason.map(Self::transform_finish_reason),
})
}
fn transform_message_response(message: OpenAIMessage) -> Result<ChatMessage, OpenAIError> {
let thinking = message
.reasoning_content
.as_ref()
.filter(|s| !s.is_empty())
.or(message.reasoning.as_ref().filter(|s| !s.is_empty()))
.map(|text| ThinkingContent::Text {
text: text.clone(),
signature: None,
});
let compatible_message =
message
.into_compatible_message()
.map_err(|message| OpenAIError::ResponseParsing {
provider: "openai",
message,
})?;
let mut core_message: ChatMessage = compatible_message.into();
core_message.thinking = thinking;
Ok(core_message)
}
pub(super) fn transform_usage(usage: OpenAIUsage) -> Usage {
Usage {
prompt_tokens: usage.prompt_tokens,
completion_tokens: usage.completion_tokens,
total_tokens: usage.total_tokens,
thinking_usage: None,
prompt_tokens_details: usage.prompt_tokens_details.map(|details| {
crate::core::types::responses::PromptTokensDetails {
cached_tokens: details.cached_tokens,
audio_tokens: details.audio_tokens,
}
}),
completion_tokens_details: usage.completion_tokens_details.map(|details| {
crate::core::types::responses::CompletionTokensDetails {
reasoning_tokens: details.reasoning_tokens,
audio_tokens: details.audio_tokens,
}
}),
}
}
pub(super) fn transform_logprobs(logprobs: OpenAILogprobs) -> LogProbs {
LogProbs {
content: logprobs
.content
.map(|content| {
content
.into_iter()
.map(|token| TokenLogProb {
token: token.token,
logprob: token.logprob,
bytes: token.bytes,
top_logprobs: Some(
token
.top_logprobs
.into_iter()
.map(|top| TopLogProb {
token: top.token,
logprob: top.logprob,
bytes: top.bytes,
})
.collect(),
),
})
.collect()
})
.unwrap_or_default(),
refusal: logprobs.refusal.map(|_| "filtered".to_string()),
}
}
pub(super) fn transform_finish_reason(reason: String) -> FinishReason {
match reason.as_str() {
"stop" => FinishReason::Stop,
"length" => FinishReason::Length,
"function_call" => FinishReason::FunctionCall,
"tool_calls" => FinishReason::ToolCalls,
"content_filter" => FinishReason::ContentFilter,
_ => FinishReason::Stop,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::types::responses::FinishReason;
#[test]
fn test_transform_basic_response() {
let response = OpenAIChatResponse {
id: "chatcmpl-123".to_string(),
object: "chat.completion".to_string(),
created: 1677652288,
model: "gpt-4".to_string(),
choices: vec![OpenAIChoice {
index: 0,
message: OpenAIMessage {
role: "assistant".to_string(),
content: Some(serde_json::json!("Hello!")),
name: None,
tool_calls: None,
tool_call_id: None,
function_call: None,
reasoning: None,
reasoning_details: None,
reasoning_content: None,
},
finish_reason: Some("stop".to_string()),
logprobs: None,
}],
usage: Some(OpenAIUsage {
prompt_tokens: 10,
completion_tokens: 5,
total_tokens: 15,
prompt_tokens_details: None,
completion_tokens_details: None,
}),
system_fingerprint: Some("fp_123".to_string()),
};
let result = OpenAIResponseTransformer::transform(response).unwrap();
assert_eq!(result.id, "chatcmpl-123");
assert_eq!(result.model, "gpt-4");
assert_eq!(result.choices.len(), 1);
assert!(matches!(
result.choices.first().unwrap().finish_reason,
Some(FinishReason::Stop)
));
}
#[test]
fn test_transform_response_with_usage_details() {
let response = OpenAIChatResponse {
id: "chatcmpl-123".to_string(),
object: "chat.completion".to_string(),
created: 1677652288,
model: "gpt-4".to_string(),
choices: vec![],
usage: Some(OpenAIUsage {
prompt_tokens: 100,
completion_tokens: 50,
total_tokens: 150,
prompt_tokens_details: Some(OpenAITokenDetails {
cached_tokens: Some(20),
audio_tokens: Some(5),
reasoning_tokens: None,
}),
completion_tokens_details: Some(OpenAITokenDetails {
cached_tokens: None,
audio_tokens: Some(10),
reasoning_tokens: Some(15),
}),
}),
system_fingerprint: None,
};
let result = OpenAIResponseTransformer::transform(response).unwrap();
let usage = result.usage.unwrap();
assert_eq!(usage.prompt_tokens, 100);
assert_eq!(usage.completion_tokens, 50);
assert_eq!(
usage.prompt_tokens_details.as_ref().unwrap().cached_tokens,
Some(20)
);
assert_eq!(
usage
.completion_tokens_details
.as_ref()
.unwrap()
.reasoning_tokens,
Some(15)
);
}
#[test]
fn test_transform_response_role_mapping() {
let roles = vec!["system", "user", "assistant", "tool", "function", "unknown"];
for role in roles {
let response = OpenAIChatResponse {
id: "test".to_string(),
object: "chat.completion".to_string(),
created: 0,
model: "gpt-4".to_string(),
choices: vec![OpenAIChoice {
index: 0,
message: OpenAIMessage {
role: role.to_string(),
content: Some(serde_json::json!("test")),
name: None,
tool_calls: None,
tool_call_id: None,
function_call: None,
reasoning: None,
reasoning_details: None,
reasoning_content: None,
},
finish_reason: None,
logprobs: None,
}],
usage: None,
system_fingerprint: None,
};
let result = OpenAIResponseTransformer::transform(response);
assert!(result.is_ok());
}
}
#[test]
fn test_transform_finish_reasons() {
let reasons = vec![
("stop", FinishReason::Stop),
("length", FinishReason::Length),
("function_call", FinishReason::FunctionCall),
("tool_calls", FinishReason::ToolCalls),
("content_filter", FinishReason::ContentFilter),
("unknown", FinishReason::Stop), ];
for (reason_str, expected) in reasons {
let response = OpenAIChatResponse {
id: "test".to_string(),
object: "chat.completion".to_string(),
created: 0,
model: "gpt-4".to_string(),
choices: vec![OpenAIChoice {
index: 0,
message: OpenAIMessage {
role: "assistant".to_string(),
content: None,
name: None,
tool_calls: None,
tool_call_id: None,
function_call: None,
reasoning: None,
reasoning_details: None,
reasoning_content: None,
},
finish_reason: Some(reason_str.to_string()),
logprobs: None,
}],
usage: None,
system_fingerprint: None,
};
let result = OpenAIResponseTransformer::transform(response).unwrap();
assert_eq!(
result.choices.first().unwrap().finish_reason,
Some(expected)
);
}
}
#[test]
fn test_transform_response_with_tool_calls() {
let response = OpenAIChatResponse {
id: "test".to_string(),
object: "chat.completion".to_string(),
created: 0,
model: "gpt-4".to_string(),
choices: vec![OpenAIChoice {
index: 0,
message: OpenAIMessage {
role: "assistant".to_string(),
content: None,
name: None,
tool_calls: Some(vec![OpenAIToolCall {
id: "call_abc".to_string(),
tool_type: "function".to_string(),
function: OpenAIFunctionCall {
name: "get_weather".to_string(),
arguments: r#"{"location":"NYC"}"#.to_string(),
},
}]),
tool_call_id: None,
function_call: None,
reasoning: None,
reasoning_details: None,
reasoning_content: None,
},
finish_reason: Some("tool_calls".to_string()),
logprobs: None,
}],
usage: None,
system_fingerprint: None,
};
let result = OpenAIResponseTransformer::transform(response).unwrap();
let tool_calls = result
.choices
.first()
.unwrap()
.message
.tool_calls
.as_ref()
.unwrap();
assert_eq!(tool_calls.len(), 1);
assert_eq!(tool_calls.first().unwrap().id, "call_abc");
assert_eq!(tool_calls.first().unwrap().function.name, "get_weather");
}
#[test]
fn test_transform_response_with_reasoning() {
let response = OpenAIChatResponse {
id: "test".to_string(),
object: "chat.completion".to_string(),
created: 0,
model: "o1-preview".to_string(),
choices: vec![OpenAIChoice {
index: 0,
message: OpenAIMessage {
role: "assistant".to_string(),
content: Some(serde_json::json!("The answer is 42")),
name: None,
tool_calls: None,
tool_call_id: None,
function_call: None,
reasoning: Some("Let me think about this...".to_string()),
reasoning_details: None,
reasoning_content: None,
},
finish_reason: Some("stop".to_string()),
logprobs: None,
}],
usage: None,
system_fingerprint: None,
};
let result = OpenAIResponseTransformer::transform(response).unwrap();
assert!(result.choices.first().unwrap().message.thinking.is_some());
}
#[test]
fn test_transform_response_with_deepseek_reasoning() {
let response = OpenAIChatResponse {
id: "test".to_string(),
object: "chat.completion".to_string(),
created: 0,
model: "deepseek-chat".to_string(),
choices: vec![OpenAIChoice {
index: 0,
message: OpenAIMessage {
role: "assistant".to_string(),
content: Some(serde_json::json!("Result")),
name: None,
tool_calls: None,
tool_call_id: None,
function_call: None,
reasoning: None,
reasoning_details: None,
reasoning_content: Some("DeepSeek thinking process...".to_string()),
},
finish_reason: Some("stop".to_string()),
logprobs: None,
}],
usage: None,
system_fingerprint: None,
};
let result = OpenAIResponseTransformer::transform(response).unwrap();
assert!(result.choices.first().unwrap().message.thinking.is_some());
}
#[test]
fn test_transform_response_null_content() {
let response = OpenAIChatResponse {
id: "test".to_string(),
object: "chat.completion".to_string(),
created: 0,
model: "gpt-4".to_string(),
choices: vec![OpenAIChoice {
index: 0,
message: OpenAIMessage {
role: "assistant".to_string(),
content: Some(serde_json::Value::Null),
name: None,
tool_calls: None,
tool_call_id: None,
function_call: None,
reasoning: None,
reasoning_details: None,
reasoning_content: None,
},
finish_reason: None,
logprobs: None,
}],
usage: None,
system_fingerprint: None,
};
let result = OpenAIResponseTransformer::transform(response).unwrap();
assert!(result.choices.first().unwrap().message.content.is_none());
}
#[test]
fn test_transform_response_empty_content() {
let response = OpenAIChatResponse {
id: "test".to_string(),
object: "chat.completion".to_string(),
created: 0,
model: "gpt-4".to_string(),
choices: vec![OpenAIChoice {
index: 0,
message: OpenAIMessage {
role: "assistant".to_string(),
content: Some(serde_json::json!("")),
name: None,
tool_calls: None,
tool_call_id: None,
function_call: None,
reasoning: None,
reasoning_details: None,
reasoning_content: None,
},
finish_reason: None,
logprobs: None,
}],
usage: None,
system_fingerprint: None,
};
let result = OpenAIResponseTransformer::transform(response).unwrap();
assert!(result.choices.first().unwrap().message.content.is_none());
}
#[test]
fn test_transform_response_with_logprobs() {
let response = OpenAIChatResponse {
id: "test".to_string(),
object: "chat.completion".to_string(),
created: 0,
model: "gpt-4".to_string(),
choices: vec![OpenAIChoice {
index: 0,
message: OpenAIMessage {
role: "assistant".to_string(),
content: Some(serde_json::json!("test")),
name: None,
tool_calls: None,
tool_call_id: None,
function_call: None,
reasoning: None,
reasoning_details: None,
reasoning_content: None,
},
finish_reason: Some("stop".to_string()),
logprobs: Some(serde_json::json!({
"content": [{
"token": "test",
"logprob": -0.5,
"bytes": [116, 101, 115, 116],
"top_logprobs": [{
"token": "test",
"logprob": -0.5,
"bytes": [116, 101, 115, 116]
}]
}]
})),
}],
usage: None,
system_fingerprint: None,
};
let result = OpenAIResponseTransformer::transform(response).unwrap();
assert!(result.choices.first().unwrap().logprobs.is_some());
}
#[test]
fn test_transform_response_content_array() {
let response = OpenAIChatResponse {
id: "test".to_string(),
object: "chat.completion".to_string(),
created: 0,
model: "gpt-4".to_string(),
choices: vec![OpenAIChoice {
index: 0,
message: OpenAIMessage {
role: "assistant".to_string(),
content: Some(serde_json::json!([
{"type": "text", "text": "Hello"}
])),
name: None,
tool_calls: None,
tool_call_id: None,
function_call: None,
reasoning: None,
reasoning_details: None,
reasoning_content: None,
},
finish_reason: None,
logprobs: None,
}],
usage: None,
system_fingerprint: None,
};
let result = OpenAIResponseTransformer::transform(response).unwrap();
assert!(result.choices.first().unwrap().message.content.is_some());
}
#[test]
fn test_transform_response_with_function_call() {
let response = OpenAIChatResponse {
id: "test".to_string(),
object: "chat.completion".to_string(),
created: 0,
model: "gpt-4".to_string(),
choices: vec![OpenAIChoice {
index: 0,
message: OpenAIMessage {
role: "assistant".to_string(),
content: None,
name: None,
tool_calls: None,
tool_call_id: None,
function_call: Some(OpenAIFunctionCall {
name: "get_weather".to_string(),
arguments: r#"{"location":"NYC"}"#.to_string(),
}),
reasoning: None,
reasoning_details: None,
reasoning_content: None,
},
finish_reason: Some("function_call".to_string()),
logprobs: None,
}],
usage: None,
system_fingerprint: None,
};
let result = OpenAIResponseTransformer::transform(response).unwrap();
let func_call = result
.choices
.first()
.unwrap()
.message
.function_call
.as_ref()
.unwrap();
assert_eq!(func_call.name, "get_weather");
}
#[test]
fn test_transform_stream_chunk() {
let chunk = OpenAIStreamChunk {
id: "chatcmpl-123".to_string(),
object: "chat.completion.chunk".to_string(),
created: 1677652288,
model: "gpt-4".to_string(),
choices: vec![OpenAIStreamChoice {
index: 0,
delta: OpenAIDelta {
role: Some("assistant".to_string()),
content: Some("Hello".to_string()),
tool_calls: None,
function_call: None,
},
finish_reason: None,
logprobs: None,
}],
usage: None,
system_fingerprint: None,
};
let result = OpenAIResponseTransformer::transform_stream_chunk(chunk).unwrap();
assert_eq!(result.id, "chatcmpl-123");
assert_eq!(result.choices.len(), 1);
assert_eq!(
result.choices.first().unwrap().delta.content,
Some("Hello".to_string())
);
}
#[test]
fn test_transform_stream_chunk_with_finish() {
let chunk = OpenAIStreamChunk {
id: "chatcmpl-123".to_string(),
object: "chat.completion.chunk".to_string(),
created: 1677652288,
model: "gpt-4".to_string(),
choices: vec![OpenAIStreamChoice {
index: 0,
delta: OpenAIDelta {
role: None,
content: None,
tool_calls: None,
function_call: None,
},
finish_reason: Some("stop".to_string()),
logprobs: None,
}],
usage: Some(OpenAIUsage {
prompt_tokens: 10,
completion_tokens: 20,
total_tokens: 30,
prompt_tokens_details: None,
completion_tokens_details: None,
}),
system_fingerprint: None,
};
let result = OpenAIResponseTransformer::transform_stream_chunk(chunk).unwrap();
assert!(matches!(
result.choices.first().unwrap().finish_reason,
Some(FinishReason::Stop)
));
assert!(result.usage.is_some());
}
#[test]
fn test_transform_delta_roles() {
let roles = vec!["system", "user", "assistant", "tool", "function", "unknown"];
for role in roles {
let delta = OpenAIDelta {
role: Some(role.to_string()),
content: None,
tool_calls: None,
function_call: None,
};
let result = OpenAIResponseTransformer::transform_delta(delta);
assert!(result.is_ok());
}
}
}