use serde_json::Value;
use crate::error::ProviderError;
use crate::protocol::openai_wire::{
apply_chat_completion_options, map_openai_finish_reason, openai_style_auth_headers,
parse_chat_usage, to_openai_messages,
};
use crate::protocol::{AuthMethod, ProtocolAdapter};
use crate::types::{CompletionRequest, CompletionResponse, StreamEvent, ToolCall};
#[derive(Debug)]
pub struct OpenAiChatAdapter;
impl OpenAiChatAdapter {
pub fn new() -> Self {
Self
}
}
impl Default for OpenAiChatAdapter {
fn default() -> Self {
Self::new()
}
}
impl ProtocolAdapter for OpenAiChatAdapter {
fn endpoint_path(&self) -> &str {
"/chat/completions"
}
fn build_request_body(
&self,
request: &CompletionRequest,
stream: bool,
) -> Result<Value, ProviderError> {
let model = request.model.as_deref().unwrap_or("gpt-4o");
let messages = to_openai_messages(&request.messages)?;
let mut body = serde_json::json!({
"model": model,
"messages": messages,
"stream": stream,
});
apply_chat_completion_options(&mut body, request)?;
Ok(body)
}
fn build_auth_headers(&self, auth: &AuthMethod) -> Vec<(String, String)> {
openai_style_auth_headers(auth)
}
fn parse_response(&self, body: &Value) -> Result<CompletionResponse, ProviderError> {
let model = body["model"].as_str().unwrap_or("unknown").to_owned();
let choice = &body["choices"][0];
if choice.is_null() {
return Err(ProviderError::Format("missing choices[0] in response".to_owned()));
}
let message_obj = choice["message"]
.as_object()
.ok_or_else(|| ProviderError::Format("missing message in response".to_owned()))?;
let content = message_obj["content"].as_str().map(|s| s.to_owned());
let tool_calls: Vec<ToolCall> = message_obj
.get("tool_calls")
.and_then(|tc| tc.as_array())
.map(|calls| {
calls
.iter()
.filter_map(|call| {
Some(ToolCall {
id: call["id"].as_str()?.to_owned(),
function_name: call["function"]["name"].as_str()?.to_owned(),
arguments: serde_json::from_str(
call["function"]["arguments"].as_str()?,
)
.ok()?,
})
})
.collect()
})
.unwrap_or_default();
let finish_reason = map_openai_finish_reason(choice["finish_reason"].as_str());
let usage = parse_chat_usage(&body["usage"]);
let thinking =
message_obj.get("reasoning_content").and_then(|v| v.as_str()).map(|s| s.to_string());
let refusal = message_obj.get("refusal").and_then(|v| v.as_str()).map(|s| s.to_string());
Ok(CompletionResponse {
content,
thinking,
tool_calls,
usage,
model,
finish_reason,
id: body["id"].as_str().map(|s| s.to_string()),
created: body["created"].as_u64(),
system_fingerprint: body["system_fingerprint"].as_str().map(|s| s.to_string()),
refusal,
..Default::default()
})
}
fn parse_sse_event(&self, data: &str) -> Result<Option<StreamEvent>, ProviderError> {
if data == "[DONE]" {
return Ok(None);
}
let parsed: Value = serde_json::from_str(data)?;
let has_choices =
parsed.get("choices").and_then(|c| c.as_array()).is_some_and(|arr| !arr.is_empty());
if has_choices {
let choice = &parsed["choices"][0];
if let Some(tool_calls) = choice["delta"]["tool_calls"].as_array() {
let mut chosen: Option<&Value> = None;
let mut chosen_index = usize::MAX;
for tc in tool_calls {
if let Some(index) = tc["index"].as_u64() {
let idx = index as usize;
if idx < chosen_index {
chosen_index = idx;
chosen = Some(tc);
}
}
}
if let Some(tc) = chosen {
return Ok(Some(StreamEvent::ToolCallDelta {
index: chosen_index,
id: tc["id"].as_str().map(String::from),
function_name: tc["function"]["name"].as_str().map(String::from),
arguments_delta: tc["function"]["arguments"]
.as_str()
.unwrap_or("")
.to_owned(),
}));
}
}
if let Some(reasoning) = choice["delta"]["reasoning_content"].as_str() {
if !reasoning.is_empty() {
return Ok(Some(StreamEvent::ThinkingDelta {
delta: reasoning.to_owned(),
}));
}
}
if let Some(content) = choice["delta"]["content"].as_str() {
if !content.is_empty() {
return Ok(Some(StreamEvent::ContentDelta { delta: content.to_owned() }));
}
}
if let Some(fr) = choice["finish_reason"].as_str() {
if !fr.is_empty() {
let finish_reason = map_openai_finish_reason(Some(fr));
let usage = parsed.get("usage").filter(|u| u.is_object()).map(parse_chat_usage);
return Ok(Some(StreamEvent::Done { finish_reason, usage }));
}
}
}
if let Some(usage_val) = parsed.get("usage").filter(|u| u.is_object()) {
return Ok(Some(StreamEvent::Usage { usage: parse_chat_usage(usage_val) }));
}
Ok(None)
}
fn protocol_name(&self) -> &str {
"openai-chat"
}
}
#[cfg(test)]
#[path = "openai_chat_tests.rs"]
mod tests;