use serde_json::Value;
use crate::error::ProviderError;
use crate::protocol::AuthMethod;
use crate::types::{
ContentPart, FinishReason, Message, MessageContent, TokenUsage, ToolDefinition,
};
pub(crate) fn to_openai_function_tools(tools: &[ToolDefinition]) -> Vec<Value> {
tools
.iter()
.map(|t| {
serde_json::json!({
"type": "function",
"function": t,
})
})
.collect()
}
pub(crate) fn map_openai_finish_reason(raw: Option<&str>) -> FinishReason {
match raw {
Some("stop") | None => FinishReason::Stop,
Some("tool_calls") => FinishReason::ToolCall,
Some("length") => FinishReason::MaxTokens,
Some("content_filter") => FinishReason::ContentFilter,
Some("pause_turn") => FinishReason::PauseTurn,
Some("refusal") => FinishReason::Refusal,
Some(other) => {
tracing::warn!(
finish_reason = %other,
"Unknown OpenAI finish_reason, degrading to Stop"
);
FinishReason::Stop
}
}
}
pub(crate) fn parse_chat_usage(usage: &Value) -> TokenUsage {
if !usage.is_object() {
return TokenUsage::new(0, 0);
}
TokenUsage {
prompt_tokens: usage.get("prompt_tokens").and_then(|v| v.as_u64()).unwrap_or(0) as u32,
completion_tokens: usage.get("completion_tokens").and_then(|v| v.as_u64()).unwrap_or(0)
as u32,
total_tokens: usage.get("total_tokens").and_then(|v| v.as_u64()).unwrap_or(0) as u32,
cached_tokens: usage
.get("prompt_tokens_details")
.and_then(|d| d.get("cached_tokens"))
.and_then(|v| v.as_u64())
.map(|v| v as u32),
reasoning_tokens: usage
.get("completion_tokens_details")
.and_then(|d| d.get("reasoning_tokens"))
.and_then(|v| v.as_u64())
.map(|v| v as u32),
prompt_cache_hit_tokens: usage
.get("prompt_cache_hit_tokens")
.and_then(|v| v.as_u64())
.or_else(|| {
usage
.get("prompt_tokens_details")
.and_then(|d| d.get("cached_tokens"))
.and_then(|v| v.as_u64())
})
.map(|v| v as u32),
prompt_cache_miss_tokens: usage
.get("prompt_cache_miss_tokens")
.and_then(|v| v.as_u64())
.map(|v| v as u32),
..Default::default()
}
}
pub(crate) fn openai_style_auth_headers(auth: &AuthMethod) -> Vec<(String, String)> {
match auth {
AuthMethod::None => vec![],
AuthMethod::Bearer { token } => {
vec![("Authorization".to_owned(), format!("Bearer {token}"))]
}
AuthMethod::ApiKey { header_name, key } => {
vec![(header_name.clone(), key.clone())]
}
}
}
pub(crate) fn to_openai_messages(messages: &[Message]) -> Result<Vec<Value>, ProviderError> {
messages
.iter()
.map(|msg| {
let content = match &msg {
Message::System { content, .. }
| Message::Developer { content, .. }
| Message::User { content, .. }
| Message::Assistant { content, .. }
| Message::Tool { content, .. } => content,
};
let content_value = match content {
MessageContent::Text(text) => Value::String(text.clone()),
MessageContent::MultiPart(parts) => {
let result: Vec<Value> = parts
.iter()
.map(|part| -> Result<Value, ProviderError> {
match part {
ContentPart::Text { text } => {
Ok(serde_json::json!({"type": "text", "text": text}))
}
ContentPart::ImageUrl { url, detail } => {
let mut obj = serde_json::json!({
"type": "image_url",
"image_url": { "url": url }
});
if let Some(d) = detail {
obj["image_url"]["detail"] = serde_json::to_value(d)?;
}
Ok(obj)
}
ContentPart::ImageBase64 { media_type, data } => {
Ok(serde_json::json!({
"type": "image_url",
"image_url": {
"url": format!("data:{media_type};base64,{data}")
}
}))
}
ContentPart::AudioBase64 { .. }
| ContentPart::File { .. }
| ContentPart::Document { .. }
| ContentPart::RedactedThinking { .. }
| ContentPart::ToolReference { .. } => {
Ok(serde_json::json!({"type": "text", "text": ""}))
}
}
})
.collect::<Result<Vec<_>, _>>()?;
Value::Array(result)
}
MessageContent::None => Value::Null,
};
let mut m = serde_json::json!({
"role": msg.role_str(),
"content": content_value,
});
match msg {
Message::Tool { tool_call_id, .. } => {
m["tool_call_id"] = Value::String(tool_call_id.clone());
}
Message::System { cache_control: Some(cc), .. }
| Message::Developer { cache_control: Some(cc), .. }
| Message::Assistant { cache_control: Some(cc), .. } => {
m["cache_control"] = serde_json::to_value(cc)?;
}
_ => {}
}
if let Message::Assistant { tool_calls: Some(calls), .. } = msg {
if !calls.is_empty() {
m["tool_calls"] = serde_json::to_value(calls)?;
m["content"] = Value::Null;
}
}
match msg {
Message::Assistant {
tool_calls: Some(calls),
reasoning_content,
..
} if !calls.is_empty() => {
m["reasoning_content"] = Value::String(
reasoning_content.clone().unwrap_or_default(),
);
}
Message::Assistant {
reasoning_content: Some(rc),
..
} if !rc.is_empty() => {
m["reasoning_content"] = Value::String(rc.clone());
}
_ => {}
}
Ok(m)
})
.collect()
}
pub(crate) fn apply_chat_completion_options(
body: &mut Value,
request: &crate::types::CompletionRequest,
) -> Result<(), ProviderError> {
if let Some(temp) = request.temperature {
body["temperature"] = serde_json::to_value(temp)?;
}
if let Some(max_tokens) = request.max_tokens {
body["max_tokens"] = serde_json::to_value(max_tokens)?;
}
if let Some(max_ct) = request.max_completion_tokens {
body["max_completion_tokens"] = serde_json::to_value(max_ct)?;
}
if let Some(stop) = &request.stop {
body["stop"] = serde_json::to_value(stop)?;
}
if let Some(top_p) = request.top_p {
body["top_p"] = serde_json::to_value(top_p)?;
}
if let Some(top_k) = request.top_k {
body["top_k"] = serde_json::to_value(top_k)?;
}
if let Some(freq_penalty) = request.frequency_penalty {
body["frequency_penalty"] = serde_json::to_value(freq_penalty)?;
}
if let Some(pres_penalty) = request.presence_penalty {
body["presence_penalty"] = serde_json::to_value(pres_penalty)?;
}
if let Some(seed) = request.seed {
body["seed"] = serde_json::to_value(seed)?;
}
if let Some(ref re) = request.reasoning_effort {
body["reasoning_effort"] = serde_json::to_value(re)?;
}
if let Some(ref logprobs) = request.logprobs {
body["logprobs"] = serde_json::to_value(logprobs)?;
}
if let Some(ref logit_bias) = request.logit_bias {
body["logit_bias"] = serde_json::to_value(logit_bias)?;
}
if let Some(tools) = &request.tools {
body["tools"] = Value::Array(to_openai_function_tools(tools));
}
if let Some(tool_choice) = &request.tool_choice {
body["tool_choice"] = serde_json::to_value(tool_choice)?;
}
if let Some(ref response_format) = request.response_format {
body["response_format"] = serde_json::to_value(response_format)?;
}
if let Some(include_usage) = request.stream_include_usage {
if body.get("stream").and_then(|v| v.as_bool()) == Some(true) {
body["stream_options"] = serde_json::json!({ "include_usage": include_usage });
}
}
if let Some(parallel_tool_calls) = request.parallel_tool_calls {
body["parallel_tool_calls"] = serde_json::to_value(parallel_tool_calls)?;
}
if let Some(ref user) = request.user {
body["user"] = serde_json::to_value(user)?;
}
if let Some(ref store) = request.store {
body["store"] = serde_json::to_value(store)?;
}
if let Some(ref metadata) = request.metadata {
body["metadata"] = serde_json::to_value(metadata)?;
}
if let Some(ref service_tier) = request.service_tier {
body["service_tier"] = serde_json::to_value(service_tier)?;
}
if let Some(ref thinking) = request.thinking {
body["thinking"] = serde_json::to_value(thinking)?;
}
Ok(())
}