use std::collections::HashSet;
use locode_protocol::{ContentBlock, ReasoningFormat, Usage};
use serde_json::Value;
use super::wire;
use crate::completion::{Completion, StopReason};
use crate::provider::ProviderError;
#[allow(clippy::implicit_hasher)] pub fn response_to_completion(
resp: wire::ResponsesResponse,
freeform_names: &HashSet<String>,
) -> Result<Completion, ProviderError> {
if resp.status.as_deref() == Some("failed") || resp.error.is_some() {
let error = resp.error.unwrap_or(wire::ResponseError {
code: None,
message: "response status: failed (no error body)".to_string(),
});
return Err(match error.code.as_deref() {
Some("rate_limit_exceeded") => ProviderError::RateLimited { retry_after: None },
Some("server_error") => ProviderError::Api {
status: 500,
message: error.message,
},
_ => ProviderError::Api {
status: 400,
message: error.message,
},
});
}
let mut content: Vec<ContentBlock> = Vec::with_capacity(resp.output.len());
let mut saw_refusal = false;
for item in resp.output {
match item.get("type").and_then(Value::as_str) {
Some("reasoning") => {
let text = summary_text(&item);
content.push(ContentBlock::Reasoning {
format: ReasoningFormat::OpenAiResponses,
text,
signature: None,
payload: Some(item),
});
}
Some("message") => {
for part in item
.get("content")
.and_then(Value::as_array)
.into_iter()
.flatten()
{
match part.get("type").and_then(Value::as_str) {
Some("output_text") => {
if let Some(text) = part.get("text").and_then(Value::as_str) {
content.push(ContentBlock::Text {
text: text.to_string(),
});
}
}
Some("refusal") => {
saw_refusal = true;
if let Some(text) = part.get("refusal").and_then(Value::as_str) {
content.push(ContentBlock::Text {
text: text.to_string(),
});
}
}
_ => {}
}
}
}
Some("function_call") => {
let call_id = str_field(&item, "call_id");
let name = str_field(&item, "name");
let raw = str_field(&item, "arguments");
let input = decode_arguments(&name, &raw, freeform_names);
content.push(ContentBlock::ToolUse {
id: call_id,
name,
input,
});
}
Some("custom_tool_call") => {
content.push(ContentBlock::ToolUse {
id: str_field(&item, "call_id"),
name: str_field(&item, "name"),
input: Value::String(str_field(&item, "input")),
});
}
_ => {}
}
}
let has_tool_calls = content
.iter()
.any(|b| matches!(b, ContentBlock::ToolUse { .. }));
let stop = map_stop(
resp.status.as_deref(),
resp.incomplete_details.as_ref(),
has_tool_calls,
saw_refusal,
);
Ok(Completion {
content,
usage: map_usage(resp.usage.as_ref()),
stop,
})
}
fn summary_text(item: &Value) -> String {
let mut out = String::new();
for entry in item
.get("summary")
.and_then(Value::as_array)
.into_iter()
.flatten()
{
if let Some(text) = entry.get("text").and_then(Value::as_str) {
if !out.is_empty() {
out.push_str("\n\n");
}
out.push_str(text);
}
}
out
}
fn str_field(item: &Value, key: &str) -> String {
item.get(key)
.and_then(Value::as_str)
.unwrap_or_default()
.to_string()
}
fn decode_arguments(name: &str, raw: &str, freeform_names: &HashSet<String>) -> Value {
match serde_json::from_str::<Value>(raw) {
Ok(parsed) => {
if freeform_names.contains(name)
&& let Some(input) = parsed.get("input").and_then(Value::as_str)
{
return Value::String(input.to_string());
}
parsed
}
Err(_) => Value::String(raw.to_string()),
}
}
fn map_stop(
status: Option<&str>,
incomplete: Option<&wire::IncompleteDetails>,
has_tool_calls: bool,
saw_refusal: bool,
) -> StopReason {
if saw_refusal {
return StopReason::Refusal;
}
match status {
Some("completed") => {
if has_tool_calls {
StopReason::ToolUse
} else {
StopReason::EndTurn
}
}
Some("incomplete") => match incomplete.and_then(|d| d.reason.as_deref()) {
Some("max_output_tokens") => StopReason::MaxTokens,
Some(reason) => StopReason::Unknown(reason.to_string()),
None => StopReason::Unknown("incomplete".to_string()),
},
Some(other) => StopReason::Unknown(other.to_string()),
None => StopReason::Unknown("(missing status)".to_string()),
}
}
fn map_usage(usage: Option<&wire::ResponsesUsage>) -> Usage {
let Some(usage) = usage else {
return Usage::default();
};
Usage {
input_tokens: usage.input_tokens.unwrap_or_default(),
output_tokens: usage.output_tokens.unwrap_or_default(),
cache_read_tokens: usage
.input_tokens_details
.as_ref()
.and_then(|d| d.cached_tokens),
cache_creation_tokens: usage
.input_tokens_details
.as_ref()
.and_then(|d| d.cache_write_tokens),
reasoning_tokens: usage
.output_tokens_details
.as_ref()
.and_then(|d| d.reasoning_tokens),
}
}