use serde_json::{json, Map, Value};
use crate::canonical::{
CanonicalError, CanonicalRequest, Content, DocumentSource, ErrorKind, ImageSource, Message,
OutputFormat, Role, Tool, ToolChoice,
};
use crate::protocol::json::{finish_body, to_json_string};
use crate::protocol::{ProviderCtx, WireRequest};
pub(super) const REQUEST_PATH: &str = "/responses";
pub(super) fn encode(
req: &CanonicalRequest,
ctx: &ProviderCtx,
) -> Result<WireRequest, CanonicalError> {
let mut body = Map::new();
body.insert("model".into(), json!(ctx.model));
if let Some(text) = instructions(req)? {
body.insert("instructions".into(), json!(text));
}
body.insert("input".into(), input_value(req)?);
if !req.tools.is_empty() {
body.insert("tools".into(), tools_value(&req.tools)?); }
if let Some(tc) = tool_choice_value(&req.tool_choice) {
body.insert("tool_choice".into(), tc); }
if let Some(p) = req.parallel_tool_calls {
body.insert("parallel_tool_calls".into(), json!(p)); }
if let Some(n) = req.max_tokens {
body.insert("max_output_tokens".into(), json!(n)); }
if req.reasoning.is_none() {
if let Some(t) = req.temperature {
body.insert("temperature".into(), json!(t));
}
if let Some(p) = req.top_p {
body.insert("top_p".into(), json!(p));
}
}
if let Some(r) = req.reasoning {
body.insert("reasoning".into(), json!({"effort": r.as_str()})); body.insert("include".into(), json!(["reasoning.encrypted_content"]));
}
body.insert("stream".into(), json!(req.stream.unwrap_or(false))); if let Some(fmt) = text_format(&req.output) {
body.insert("text".into(), json!({ "format": fmt }));
}
for (k, v) in &req.extra {
body.entry(k.clone()).or_insert_with(|| v.clone()); }
Ok(finish_body(body, format!("{}{REQUEST_PATH}", ctx.base_url)))
}
fn slot_err(slot: &str) -> CanonicalError {
CanonicalError {
kind: ErrorKind::ParseInput,
message: format!("{slot} accepts only text content"),
provider_detail: None,
retry_after_seconds: None,
}
}
fn instructions(req: &CanonicalRequest) -> Result<Option<String>, CanonicalError> {
let Some(system) = req.system.as_ref().filter(|s| !s.is_empty()) else {
return Ok(None);
};
let mut text = String::new();
for c in system {
match c {
Content::Text(t) => text.push_str(t),
_ => return Err(slot_err("instructions")),
}
}
Ok(Some(text))
}
fn input_value(req: &CanonicalRequest) -> Result<Value, CanonicalError> {
let mut items = Vec::new();
for m in &req.messages {
message_items(m, &mut items)?;
}
Ok(Value::Array(items))
}
fn message_items(m: &Message, items: &mut Vec<Value>) -> Result<(), CanonicalError> {
let (role, text_type) = match m.role {
Role::User => ("user", "input_text"),
Role::System => ("system", "input_text"),
Role::Assistant => ("assistant", "output_text"),
Role::Tool => {
for c in &m.content {
items.push(function_call_output(c)?);
}
return Ok(());
}
};
let mut content = Vec::new();
let mut reasonings = Vec::new();
let mut calls = Vec::new();
for c in &m.content {
match c {
Content::Text(t) => content.push(json!({ "type": text_type, "text": t })),
Content::Image { source } if role == "user" => content.push(input_image(source)),
Content::Document { source } if role == "user" => content.push(input_file(source)),
Content::ToolUse {
id, name, input, ..
} => calls.push(json!({
"type": "function_call", "call_id": id, "name": name,
"arguments": to_json_string(input),
})),
Content::Thinking {
text,
id,
encrypted_content: Some(enc),
..
} => reasonings.push(reasoning_item(text, id.as_deref(), enc)),
Content::Thinking { .. } | Content::RedactedThinking { .. } => {} _ => return Err(slot_err(role)),
}
}
items.extend(reasonings); if !content.is_empty() {
items.push(json!({ "type": "message", "role": role, "content": content }));
}
items.extend(calls);
Ok(())
}
fn reasoning_item(text: &str, id: Option<&str>, encrypted_content: &str) -> Value {
let summary = if text.is_empty() {
json!([])
} else {
json!([{ "type": "summary_text", "text": text }])
};
let mut item = json!({
"type": "reasoning", "summary": summary, "encrypted_content": encrypted_content,
});
if let Some(id) = id {
item["id"] = json!(id);
}
item
}
fn function_call_output(c: &Content) -> Result<Value, CanonicalError> {
let Content::ToolResult {
tool_use_id,
content,
is_error,
} = c
else {
return Err(slot_err("tool"));
};
let mut text = String::new();
for part in content {
match part {
Content::Text(t) => text.push_str(t),
_ => return Err(slot_err("tool_result")),
}
}
if *is_error {
text = format!("[error] {text}");
}
Ok(json!({ "type": "function_call_output", "call_id": tool_use_id, "output": text }))
}
fn input_image(source: &ImageSource) -> Value {
let url = match source {
ImageSource::Base64 { media_type, data } => format!("data:{media_type};base64,{data}"),
ImageSource::Url { url } => url.clone(),
};
json!({ "type": "input_image", "image_url": url })
}
fn input_file(source: &DocumentSource) -> Value {
match source {
DocumentSource::Base64 { media_type, data } => json!({
"type": "input_file",
"filename": format!("document.{}", media_type.rsplit('/').next().unwrap_or("bin")),
"file_data": format!("data:{media_type};base64,{data}"),
}),
DocumentSource::Url { url } => json!({ "type": "input_file", "file_url": url }),
}
}
fn text_format(output: &Option<OutputFormat>) -> Option<Value> {
Some(match output.as_ref()? {
OutputFormat::Json => json!({ "type": "json_object" }),
OutputFormat::JsonSchema {
name,
schema,
strict,
} => {
let mut f = json!({
"type": "json_schema",
"name": name.as_deref().unwrap_or("response"),
"schema": schema,
});
if let Some(s) = strict {
f["strict"] = json!(s);
}
f
}
})
}
fn tools_value(tools: &[Tool]) -> Result<Value, CanonicalError> {
let mut out = Vec::new();
for t in tools {
let Tool::Custom {
name,
description,
input_schema,
strict,
} = t
else {
return Err(CanonicalError {
kind: ErrorKind::ParseInput,
message: "provider-typed tools are not projected for this dialect".into(),
provider_detail: None,
retry_after_seconds: None,
});
};
let mut f = json!({ "type": "function", "name": name, "parameters": input_schema });
if let Some(d) = description {
f["description"] = json!(d);
}
if let Some(s) = strict {
f["strict"] = json!(s);
}
out.push(f);
}
Ok(Value::Array(out))
}
fn tool_choice_value(tc: &ToolChoice) -> Option<Value> {
Some(match tc {
ToolChoice::Auto => return None,
ToolChoice::Any => json!("required"),
ToolChoice::None => json!("none"),
ToolChoice::Tool { name } => json!({ "type": "function", "name": name }),
})
}