1use serde_json::Value;
20use tracing;
21
22use crate::error::ProviderError;
23use crate::protocol::{AuthMethod, ProtocolAdapter};
24use crate::types::{
25 CompletionRequest, CompletionResponse, ContentPart, FinishReason, Message, MessageContent,
26 StreamEvent, TokenUsage, ToolCall, ToolDefinition,
27};
28
29#[derive(Debug, Clone)]
56pub struct OpenAiResponsesAdapter;
57
58impl OpenAiResponsesAdapter {
59 pub fn new() -> Self {
61 Self
62 }
63}
64
65impl Default for OpenAiResponsesAdapter {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71impl ProtocolAdapter for OpenAiResponsesAdapter {
72 fn endpoint_path(&self) -> &str {
73 "/responses"
74 }
75
76 fn build_request_body(
77 &self,
78 request: &CompletionRequest,
79 stream: bool,
80 ) -> Result<Value, ProviderError> {
81 let model = request.model.as_deref().unwrap_or("gpt-4o");
82
83 let mut instructions = String::new();
85 let mut input_items: Vec<Value> = Vec::new();
86
87 for msg in &request.messages {
88 match msg {
89 Message::System { content, .. } | Message::Developer { content, .. } => {
90 if let MessageContent::Text(text) = content {
91 if !instructions.is_empty() {
92 instructions.push('\n');
93 }
94 instructions.push_str(text);
95 }
96 if let MessageContent::MultiPart(parts) = content {
98 for part in parts {
99 if let ContentPart::Text { text } = part {
100 if !instructions.is_empty() {
101 instructions.push('\n');
102 }
103 instructions.push_str(text);
104 }
105 }
106 }
107 }
108 Message::User { content } => {
109 let content_val = message_content_to_value(content);
110 input_items.push(serde_json::json!({
111 "role": "user",
112 "content": content_val,
113 }));
114 }
115 Message::Assistant { content, tool_calls, .. } => {
116 let content_val = message_content_to_value(content);
117 let mut item = serde_json::json!({
118 "role": "assistant",
119 "content": content_val,
120 });
121 if let Some(calls) = tool_calls {
122 if !calls.is_empty() {
123 item["tool_calls"] = serde_json::to_value(calls)?;
124 item["content"] = Value::Null;
126 }
127 }
128 input_items.push(item);
129 }
130 Message::Tool { content, tool_call_id, .. } => {
131 let content_val = message_content_to_value(content);
132 input_items.push(serde_json::json!({
133 "role": "tool",
134 "content": content_val,
135 "tool_call_id": tool_call_id,
136 }));
137 }
138 }
139 }
140
141 let mut body = serde_json::json!({
142 "model": model,
143 "input": input_items,
144 });
145
146 if !instructions.is_empty() {
147 body["instructions"] = Value::String(instructions);
148 }
149
150 if stream {
151 body["stream"] = Value::Bool(true);
152 }
153
154 if let Some(temp) = request.temperature {
156 body["temperature"] = serde_json::to_value(temp)?;
157 }
158 if let Some(max_tokens) = request.max_tokens {
159 body["max_tokens"] = serde_json::to_value(max_tokens)?;
160 }
161 if let Some(max_ct) = request.max_completion_tokens {
162 body["max_completion_tokens"] = serde_json::to_value(max_ct)?;
163 }
164 if let Some(stop) = &request.stop {
165 body["stop"] = serde_json::to_value(stop)?;
166 }
167 if let Some(top_p) = request.top_p {
168 body["top_p"] = serde_json::to_value(top_p)?;
169 }
170 if let Some(seed) = request.seed {
171 body["seed"] = serde_json::to_value(seed)?;
172 }
173 if let Some(ref re) = request.reasoning_effort {
174 body["reasoning_effort"] = serde_json::to_value(re)?;
175 }
176 if let Some(tools) = &request.tools {
177 body["tools"] = Value::Array(to_responses_api_tools(tools));
178 }
179 if let Some(tool_choice) = &request.tool_choice {
180 body["tool_choice"] = serde_json::to_value(tool_choice)?;
181 }
182 if let Some(response_format) = &request.response_format {
183 body["response_format"] = serde_json::to_value(response_format)?;
184 }
185
186 if let Some(thinking) = &request.thinking {
188 body["thinking"] = serde_json::to_value(thinking)?;
189 }
190 if let Some(user) = &request.user {
191 body["user"] = serde_json::to_value(user)?;
192 }
193
194 Ok(body)
195 }
196
197 fn build_auth_headers(&self, auth: &AuthMethod) -> Vec<(String, String)> {
198 match auth {
199 AuthMethod::None => vec![],
200 AuthMethod::Bearer { token } => {
201 vec![("Authorization".to_owned(), format!("Bearer {}", token))]
202 }
203 AuthMethod::ApiKey { header_name, key } => {
204 vec![(header_name.clone(), key.clone())]
205 }
206 }
207 }
208
209 fn parse_response(&self, body: &Value) -> Result<CompletionResponse, ProviderError> {
210 let model = body["model"].as_str().unwrap_or("unknown").to_owned();
211
212 let id = body["id"].as_str().map(String::from);
214 let created = body["created"].as_u64();
215 let system_fingerprint = body["system_fingerprint"].as_str().map(String::from);
216
217 let mut content = String::new();
219 let mut tool_calls: Vec<ToolCall> = Vec::new();
220 let mut refusal: Option<String> = None;
221 let mut reasoning: Option<String> = None;
222
223 if let Some(outputs) = body["output"].as_array() {
224 for item in outputs {
225 match item["type"].as_str() {
226 Some("message") => {
227 if let Some(contents) = item["content"].as_array() {
228 for c in contents {
229 match c["type"].as_str() {
230 Some("output_text") => {
231 if let Some(text) = c["text"].as_str() {
232 if !content.is_empty() {
233 content.push('\n');
234 }
235 content.push_str(text);
236 }
237 }
238 Some("refusal") => {
239 if let Some(text) = c["text"].as_str() {
240 refusal = Some(text.to_owned());
241 }
242 }
243 Some("reasoning") => {
244 if let Some(text) = c["text"].as_str() {
245 reasoning = Some(text.to_owned());
246 }
247 }
248 _ => {}
249 }
250 }
251 }
252 }
253 Some("function_call") => {
254 let args_str = item["arguments"].as_str().unwrap_or("{}");
255 let arguments = serde_json::from_str(args_str).unwrap_or(Value::Null);
256 tool_calls.push(ToolCall {
257 id: item["id"].as_str().unwrap_or("").to_owned(),
258 function_name: item["name"].as_str().unwrap_or("").to_owned(),
259 arguments,
260 });
261 }
262 _ => {}
263 }
264 }
265 }
266
267 let content_opt = if content.is_empty() { None } else { Some(content) };
268
269 let usage = parse_responses_usage(body);
271
272 let finish_reason = parse_responses_finish_reason(body, refusal.is_some());
274
275 Ok(CompletionResponse {
276 content: content_opt,
277 thinking: reasoning,
278 tool_calls,
279 usage,
280 model,
281 finish_reason,
282 id,
283 created,
284 system_fingerprint,
285 refusal,
286 ..Default::default()
287 })
288 }
289
290 fn parse_sse_event(&self, data: &str) -> Result<Option<StreamEvent>, ProviderError> {
291 if data == "[DONE]" {
293 return Ok(None);
294 }
295
296 let parsed: Value = serde_json::from_str(data)?;
297
298 match parsed["type"].as_str() {
299 Some("response.output_text.delta") => {
300 let delta = parsed.get("delta").and_then(|v| v.as_str()).unwrap_or("");
301 if delta.is_empty() {
302 Ok(None)
303 } else {
304 Ok(Some(StreamEvent::ContentDelta { delta: delta.to_owned() }))
305 }
306 }
307 Some("response.done") => {
308 let response = &parsed["response"];
309 let finish_reason = parse_responses_finish_reason(response, false);
311
312 let usage = parse_responses_usage(response);
313 let usage_opt = if usage.prompt_tokens == 0 && usage.completion_tokens == 0 {
314 None
315 } else {
316 Some(usage)
317 };
318
319 Ok(Some(StreamEvent::Done { finish_reason, usage: usage_opt }))
320 }
321 Some("response.function_call_arguments.delta") => {
322 let index = parsed
324 .get("item_id")
325 .and_then(|v| v.as_str())
326 .and_then(|s| s.rsplit('_').next())
327 .and_then(|n| n.parse::<usize>().ok())
328 .unwrap_or(0);
329 let arguments_delta =
330 parsed.get("delta").and_then(|v| v.as_str()).unwrap_or("").to_owned();
331 let id = None;
332 let function_name = None;
333
334 Ok(Some(StreamEvent::ToolCallDelta { index, id, function_name, arguments_delta }))
335 }
336 _ => {
337 tracing::trace!("ignoring unknown Responses SSE event type");
339 Ok(None)
340 }
341 }
342 }
343
344 fn protocol_name(&self) -> &str {
345 "openai_responses"
346 }
347}
348
349fn message_content_to_value(content: &MessageContent) -> Value {
354 match content {
355 MessageContent::Text(text) => Value::String(text.clone()),
356 MessageContent::MultiPart(parts) => {
357 let items: Vec<Value> = parts
358 .iter()
359 .map(|part| match part {
360 ContentPart::Text { text } => {
361 serde_json::json!({"type": "text", "text": text})
362 }
363 ContentPart::ImageUrl { url, detail } => {
364 let mut obj = serde_json::json!({
365 "type": "image_url",
366 "image_url": { "url": url }
367 });
368 if let Some(d) = detail {
369 obj["image_url"]["detail"] =
370 serde_json::to_value(d).unwrap_or(Value::Null);
371 }
372 obj
373 }
374 ContentPart::ImageBase64 { media_type, data } => {
375 serde_json::json!({
376 "type": "image_url",
377 "image_url": {
378 "url": format!("data:{};base64,{}", media_type, data)
379 }
380 })
381 }
382 _ => serde_json::json!({"type": "text", "text": ""}),
383 })
384 .collect();
385 Value::Array(items)
386 }
387 MessageContent::None => Value::Null,
388 }
389}
390
391fn parse_responses_usage(body: &Value) -> TokenUsage {
396 let usage_data = &body["usage"];
397 if usage_data.is_object() {
398 let prompt = usage_data["input_tokens"].as_u64().unwrap_or(0) as u32;
399 let completion = usage_data["output_tokens"].as_u64().unwrap_or(0) as u32;
400 TokenUsage {
401 prompt_tokens: prompt,
402 completion_tokens: completion,
403 total_tokens: prompt + completion,
404 cached_tokens: None,
405 ..Default::default()
406 }
407 } else {
408 TokenUsage::new(0, 0)
409 }
410}
411
412fn parse_responses_finish_reason(body: &Value, has_refusal: bool) -> FinishReason {
426 if has_refusal {
427 return FinishReason::Refusal;
428 }
429
430 match body["status"].as_str() {
431 Some("completed") => FinishReason::Stop,
432 Some("incomplete") => match body["incomplete_details"]["reason"].as_str() {
433 Some("max_output_tokens") => FinishReason::MaxTokens,
434 Some("content_filter") => FinishReason::ContentFilter,
435 Some("refusal") => FinishReason::Refusal,
436 reason => {
437 tracing::warn!(
438 "unknown incomplete_details.reason: {:?}, defaulting to Stop",
439 reason
440 );
441 FinishReason::Stop
442 }
443 },
444 Some("pause_turn") => FinishReason::PauseTurn,
445 Some(other) => {
446 tracing::warn!("unknown response status: {}, defaulting to Stop", other);
447 FinishReason::Stop
448 }
449 None => FinishReason::Stop,
450 }
451}
452
453fn to_responses_api_tools(tools: &[ToolDefinition]) -> Vec<Value> {
458 tools
459 .iter()
460 .map(|t| {
461 serde_json::json!({
462 "type": "function",
463 "function": {
464 "name": t.name,
465 "description": t.description,
466 "parameters": t.parameters,
467 "strict": t.strict,
468 }
469 })
470 })
471 .collect()
472}
473
474#[cfg(test)]
475#[path = "openai_responses_tests.rs"]
476mod tests;