use serde_json::Value;
use tracing;
use crate::error::ProviderError;
use crate::protocol::{AuthMethod, ProtocolAdapter};
use crate::types::{
CompletionRequest, CompletionResponse, ContentPart, FinishReason, Message, MessageContent,
StreamEvent, TokenUsage, ToolCall, ToolDefinition,
};
#[derive(Debug, Clone)]
pub struct OpenAiResponsesAdapter;
impl OpenAiResponsesAdapter {
pub fn new() -> Self {
Self
}
}
impl Default for OpenAiResponsesAdapter {
fn default() -> Self {
Self::new()
}
}
impl ProtocolAdapter for OpenAiResponsesAdapter {
fn endpoint_path(&self) -> &str {
"/responses"
}
fn build_request_body(
&self,
request: &CompletionRequest,
stream: bool,
) -> Result<Value, ProviderError> {
let model = request.model.as_deref().unwrap_or("gpt-4o");
let mut instructions = String::new();
let mut input_items: Vec<Value> = Vec::new();
for msg in &request.messages {
match msg {
Message::System { content, .. } | Message::Developer { content, .. } => {
if let MessageContent::Text(text) = content {
if !instructions.is_empty() {
instructions.push('\n');
}
instructions.push_str(text);
}
if let MessageContent::MultiPart(parts) = content {
for part in parts {
if let ContentPart::Text { text } = part {
if !instructions.is_empty() {
instructions.push('\n');
}
instructions.push_str(text);
}
}
}
}
Message::User { content } => {
let content_val = message_content_to_value(content);
input_items.push(serde_json::json!({
"role": "user",
"content": content_val,
}));
}
Message::Assistant { content, tool_calls, .. } => {
let content_val = message_content_to_value(content);
let mut item = serde_json::json!({
"role": "assistant",
"content": content_val,
});
if let Some(calls) = tool_calls {
if !calls.is_empty() {
item["tool_calls"] = serde_json::to_value(calls)?;
item["content"] = Value::Null;
}
}
input_items.push(item);
}
Message::Tool { content, tool_call_id, .. } => {
let content_val = message_content_to_value(content);
input_items.push(serde_json::json!({
"role": "tool",
"content": content_val,
"tool_call_id": tool_call_id,
}));
}
}
}
let mut body = serde_json::json!({
"model": model,
"input": input_items,
});
if !instructions.is_empty() {
body["instructions"] = Value::String(instructions);
}
if stream {
body["stream"] = Value::Bool(true);
}
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(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(tools) = &request.tools {
body["tools"] = Value::Array(to_responses_api_tools(tools));
}
if let Some(tool_choice) = &request.tool_choice {
body["tool_choice"] = serde_json::to_value(tool_choice)?;
}
if let Some(response_format) = &request.response_format {
body["response_format"] = serde_json::to_value(response_format)?;
}
if let Some(thinking) = &request.thinking {
body["thinking"] = serde_json::to_value(thinking)?;
}
if let Some(user) = &request.user {
body["user"] = serde_json::to_value(user)?;
}
Ok(body)
}
fn build_auth_headers(&self, 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())]
}
}
}
fn parse_response(&self, body: &Value) -> Result<CompletionResponse, ProviderError> {
let model = body["model"].as_str().unwrap_or("unknown").to_owned();
let id = body["id"].as_str().map(String::from);
let created = body["created"].as_u64();
let system_fingerprint = body["system_fingerprint"].as_str().map(String::from);
let mut content = String::new();
let mut tool_calls: Vec<ToolCall> = Vec::new();
let mut refusal: Option<String> = None;
let mut reasoning: Option<String> = None;
if let Some(outputs) = body["output"].as_array() {
for item in outputs {
match item["type"].as_str() {
Some("message") => {
if let Some(contents) = item["content"].as_array() {
for c in contents {
match c["type"].as_str() {
Some("output_text") => {
if let Some(text) = c["text"].as_str() {
if !content.is_empty() {
content.push('\n');
}
content.push_str(text);
}
}
Some("refusal") => {
if let Some(text) = c["text"].as_str() {
refusal = Some(text.to_owned());
}
}
Some("reasoning") => {
if let Some(text) = c["text"].as_str() {
reasoning = Some(text.to_owned());
}
}
_ => {}
}
}
}
}
Some("function_call") => {
let args_str = item["arguments"].as_str().unwrap_or("{}");
let arguments = serde_json::from_str(args_str).unwrap_or(Value::Null);
tool_calls.push(ToolCall {
id: item["id"].as_str().unwrap_or("").to_owned(),
function_name: item["name"].as_str().unwrap_or("").to_owned(),
arguments,
});
}
_ => {}
}
}
}
let content_opt = if content.is_empty() { None } else { Some(content) };
let usage = parse_responses_usage(body);
let finish_reason = parse_responses_finish_reason(body, refusal.is_some());
Ok(CompletionResponse {
content: content_opt,
thinking: reasoning,
tool_calls,
usage,
model,
finish_reason,
id,
created,
system_fingerprint,
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)?;
match parsed["type"].as_str() {
Some("response.output_text.delta") => {
let delta = parsed.get("delta").and_then(|v| v.as_str()).unwrap_or("");
if delta.is_empty() {
Ok(None)
} else {
Ok(Some(StreamEvent::ContentDelta { delta: delta.to_owned() }))
}
}
Some("response.done") => {
let response = &parsed["response"];
let finish_reason = parse_responses_finish_reason(response, false);
let usage = parse_responses_usage(response);
let usage_opt = if usage.prompt_tokens == 0 && usage.completion_tokens == 0 {
None
} else {
Some(usage)
};
Ok(Some(StreamEvent::Done { finish_reason, usage: usage_opt }))
}
Some("response.function_call_arguments.delta") => {
let index = parsed
.get("item_id")
.and_then(|v| v.as_str())
.and_then(|s| s.rsplit('_').next())
.and_then(|n| n.parse::<usize>().ok())
.unwrap_or(0);
let arguments_delta =
parsed.get("delta").and_then(|v| v.as_str()).unwrap_or("").to_owned();
let id = None;
let function_name = None;
Ok(Some(StreamEvent::ToolCallDelta { index, id, function_name, arguments_delta }))
}
_ => {
tracing::trace!("ignoring unknown Responses SSE event type");
Ok(None)
}
}
}
fn protocol_name(&self) -> &str {
"openai_responses"
}
}
fn message_content_to_value(content: &MessageContent) -> Value {
match content {
MessageContent::Text(text) => Value::String(text.clone()),
MessageContent::MultiPart(parts) => {
let items: Vec<Value> = parts
.iter()
.map(|part| match part {
ContentPart::Text { text } => {
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).unwrap_or(Value::Null);
}
obj
}
ContentPart::ImageBase64 { media_type, data } => {
serde_json::json!({
"type": "image_url",
"image_url": {
"url": format!("data:{};base64,{}", media_type, data)
}
})
}
_ => serde_json::json!({"type": "text", "text": ""}),
})
.collect();
Value::Array(items)
}
MessageContent::None => Value::Null,
}
}
fn parse_responses_usage(body: &Value) -> TokenUsage {
let usage_data = &body["usage"];
if usage_data.is_object() {
let prompt = usage_data["input_tokens"].as_u64().unwrap_or(0) as u32;
let completion = usage_data["output_tokens"].as_u64().unwrap_or(0) as u32;
TokenUsage {
prompt_tokens: prompt,
completion_tokens: completion,
total_tokens: prompt + completion,
cached_tokens: None,
..Default::default()
}
} else {
TokenUsage::new(0, 0)
}
}
fn parse_responses_finish_reason(body: &Value, has_refusal: bool) -> FinishReason {
if has_refusal {
return FinishReason::Refusal;
}
match body["status"].as_str() {
Some("completed") => FinishReason::Stop,
Some("incomplete") => match body["incomplete_details"]["reason"].as_str() {
Some("max_output_tokens") => FinishReason::MaxTokens,
Some("content_filter") => FinishReason::ContentFilter,
Some("refusal") => FinishReason::Refusal,
reason => {
tracing::warn!(
"unknown incomplete_details.reason: {:?}, defaulting to Stop",
reason
);
FinishReason::Stop
}
},
Some("pause_turn") => FinishReason::PauseTurn,
Some(other) => {
tracing::warn!("unknown response status: {}, defaulting to Stop", other);
FinishReason::Stop
}
None => FinishReason::Stop,
}
}
fn to_responses_api_tools(tools: &[ToolDefinition]) -> Vec<Value> {
tools
.iter()
.map(|t| {
serde_json::json!({
"type": "function",
"function": {
"name": t.name,
"description": t.description,
"parameters": t.parameters,
"strict": t.strict,
}
})
})
.collect()
}
#[cfg(test)]
#[path = "openai_responses_tests.rs"]
mod tests;