use serde::Deserialize;
use serde_json::Value;
use uuid::Uuid;
use crate::wire::canonical::{
CanonicalContent, CanonicalResponse, CanonicalStopReason, CanonicalUsage,
};
use crate::wire::defect::{BodyDefect, buffered_body_defect};
use crate::wire::error::WireParseError;
#[derive(Debug, Default, Deserialize)]
struct ChatCompletion {
#[serde(default)]
id: Option<String>,
#[serde(default)]
model: Option<String>,
#[serde(default)]
usage: Option<ChatUsage>,
#[serde(default)]
choices: Vec<ChatChoice>,
}
fn null_as_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: serde::Deserializer<'de>,
T: Deserialize<'de> + Default,
{
Ok(Option::<T>::deserialize(deserializer)?.unwrap_or_default())
}
#[derive(Debug, Default, Deserialize)]
struct ChatUsage {
#[serde(default)]
prompt_tokens: u32,
#[serde(default)]
completion_tokens: u32,
#[serde(default)]
total_tokens: u32,
#[serde(default, deserialize_with = "null_as_default")]
prompt_tokens_details: ChatPromptTokensDetails,
#[serde(default, deserialize_with = "null_as_default")]
completion_tokens_details: ChatCompletionTokensDetails,
}
#[derive(Debug, Default, Deserialize)]
struct ChatPromptTokensDetails {
#[serde(default)]
cached_tokens: u32,
}
#[derive(Debug, Default, Deserialize)]
struct ChatCompletionTokensDetails {
#[serde(default)]
reasoning_tokens: u32,
}
impl ChatUsage {
const fn into_canonical(self) -> CanonicalUsage {
let cached = self.prompt_tokens_details.cached_tokens;
CanonicalUsage {
input_tokens: self.prompt_tokens.saturating_sub(cached),
output_tokens: self.completion_tokens,
cache_read_tokens: cached,
cache_creation_tokens: 0,
reasoning_tokens: self.completion_tokens_details.reasoning_tokens,
total_tokens: self.total_tokens,
}
}
}
#[derive(Debug, Default, Deserialize)]
struct ChatChoice {
#[serde(default)]
finish_reason: Option<String>,
#[serde(default)]
message: Option<ChatMessage>,
}
#[derive(Debug, Default, Deserialize)]
struct ChatMessage {
#[serde(default)]
content: Option<String>,
#[serde(default, alias = "reasoning")]
reasoning_content: Option<String>,
#[serde(default, deserialize_with = "null_as_default")]
tool_calls: Vec<ChatToolCall>,
}
#[derive(Debug, Default, Deserialize)]
struct ChatToolCall {
#[serde(default)]
id: String,
#[serde(default)]
function: ChatFunction,
}
#[derive(Debug, Default, Deserialize)]
struct ChatFunction {
#[serde(default)]
name: String,
#[serde(default)]
arguments: String,
}
pub fn parse_response(
value: &Value,
fallback_model: &str,
) -> Result<CanonicalResponse, WireParseError> {
let resp = ChatCompletion::deserialize(value).map_err(WireParseError::OpenAiChat)?;
let id = resp
.id
.unwrap_or_else(|| format!("msg_{}", Uuid::new_v4().simple()));
let model = resp.model.unwrap_or_else(|| fallback_model.to_owned());
let usage = resp
.usage
.map(ChatUsage::into_canonical)
.unwrap_or_default();
let mut content: Vec<CanonicalContent> = Vec::new();
let mut stop_reason = None;
let mut raw_finish_reason = None;
if let Some(choice) = resp.choices.into_iter().next() {
raw_finish_reason.clone_from(&choice.finish_reason);
stop_reason = choice
.finish_reason
.as_deref()
.map(CanonicalStopReason::from_openai);
if let Some(msg) = choice.message {
collect_message_content(msg, &mut content);
}
let has_tool_use = content
.iter()
.any(|c| matches!(c, CanonicalContent::ToolUse { .. }));
stop_reason = stop_reason.map(|r| r.with_tool_use(has_tool_use));
}
Ok(CanonicalResponse {
id,
model,
content,
stop_reason,
usage,
grounding: None,
code_execution: None,
raw_finish_reason,
..Default::default()
})
}
fn collect_message_content(msg: ChatMessage, content: &mut Vec<CanonicalContent>) {
if let Some(reasoning) = msg.reasoning_content
&& !reasoning.is_empty()
{
content.push(CanonicalContent::Thinking {
text: reasoning,
signature: None,
id: None,
encrypted_content: None,
});
}
if let Some(text) = msg.content
&& !text.is_empty()
{
content.push(CanonicalContent::Text(text));
}
for tc in msg.tool_calls {
let args = if tc.function.arguments.is_empty() {
"{}"
} else {
&tc.function.arguments
};
let input: Value =
serde_json::from_str(args).unwrap_or_else(|_| Value::Object(serde_json::Map::new()));
content.push(CanonicalContent::ToolUse {
id: tc.id,
name: tc.function.name,
input,
signature: None,
});
}
}
#[must_use]
pub fn buffered_defect(value: &Value) -> Option<BodyDefect> {
buffered_body_defect(value, "choices", "usage")
}