use async_openai::types::chat::{CreateChatCompletionResponse, CreateChatCompletionStreamResponse};
use langfuse_core::types::UsageDetails;
use std::collections::BTreeMap;
#[must_use]
pub fn extract_model(response: &CreateChatCompletionResponse) -> String {
response.model.clone()
}
#[must_use]
pub fn extract_usage(response: &CreateChatCompletionResponse) -> Option<UsageDetails> {
response.usage.as_ref().map(|u| UsageDetails {
input: Some(u64::from(u.prompt_tokens)),
output: Some(u64::from(u.completion_tokens)),
total: Some(u64::from(u.total_tokens)),
})
}
#[must_use]
pub fn extract_output(response: &CreateChatCompletionResponse) -> serde_json::Value {
let Some(choice) = response.choices.first() else {
return serde_json::Value::Null;
};
let message = &choice.message;
if message.tool_calls.as_ref().is_some_and(|tc| !tc.is_empty()) {
return serde_json::to_value(message).unwrap_or(serde_json::Value::Null);
}
match &message.content {
Some(text) => serde_json::Value::String(text.clone()),
None => serde_json::Value::Null,
}
}
#[must_use]
pub fn extract_stream_chunk_content(chunk: &CreateChatCompletionStreamResponse) -> Option<String> {
chunk.choices.first().and_then(|c| c.delta.content.clone())
}
#[must_use]
pub fn extract_stream_usage(chunk: &CreateChatCompletionStreamResponse) -> Option<UsageDetails> {
chunk.usage.as_ref().map(|u| UsageDetails {
input: Some(u64::from(u.prompt_tokens)),
output: Some(u64::from(u.completion_tokens)),
total: Some(u64::from(u.total_tokens)),
})
}
#[must_use]
pub fn extract_tool_calls(response: &CreateChatCompletionResponse) -> Option<serde_json::Value> {
let choice = response.choices.first()?;
let tool_calls = choice.message.tool_calls.as_ref()?;
if tool_calls.is_empty() {
return None;
}
serde_json::to_value(tool_calls).ok()
}
#[derive(Debug, Default, Clone)]
pub struct ToolCallAccumulator {
calls: BTreeMap<usize, AccumulatedToolCall>,
}
#[derive(Debug, Default, Clone)]
struct AccumulatedToolCall {
id: String,
r#type: String,
function_name: String,
function_arguments: String,
}
impl AccumulatedToolCall {
fn is_empty(&self) -> bool {
self.id.is_empty()
&& self.r#type.is_empty()
&& self.function_name.is_empty()
&& self.function_arguments.is_empty()
}
}
impl ToolCallAccumulator {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn accumulate(&mut self, chunk: &CreateChatCompletionStreamResponse) {
let Some(choice) = chunk.choices.first() else {
return;
};
let Some(tool_calls) = &choice.delta.tool_calls else {
return;
};
for tc in tool_calls {
let idx = tc.index as usize;
let entry = self.calls.entry(idx).or_default();
if let Some(id) = &tc.id {
entry.id.push_str(id);
}
if let Some(t) = &tc.r#type {
entry.r#type = serde_json::to_value(t)
.ok()
.and_then(|v| v.as_str().map(String::from))
.unwrap_or_default();
}
if let Some(func) = &tc.function {
if let Some(name) = &func.name {
entry.function_name.push_str(name);
}
if let Some(args) = &func.arguments {
entry.function_arguments.push_str(args);
}
}
}
}
#[must_use]
pub fn has_calls(&self) -> bool {
self.calls.values().any(|call| !call.is_empty())
}
#[must_use]
pub fn finalize(&self) -> serde_json::Value {
let arr: Vec<serde_json::Value> = self
.calls
.values()
.filter(|call| !call.is_empty())
.map(|c| {
serde_json::json!({
"id": c.id,
"type": c.r#type,
"function": {
"name": c.function_name,
"arguments": c.function_arguments
}
})
})
.collect();
serde_json::Value::Array(arr)
}
}