use serde_json::{Map, Value};
use super::super::super::canonical::{
CanonicalContent, CanonicalMessage, CanonicalRequest, CanonicalTool, CanonicalToolChoice,
ImageSource, Role, ThinkingConfig,
};
use super::super::InboundParseError;
const DEFAULT_MAX_OUTPUT_TOKENS: u32 = 4096;
pub(super) fn parse(value: &Value) -> Result<CanonicalRequest, InboundParseError> {
let model = value
.get("model")
.and_then(Value::as_str)
.ok_or(InboundParseError::MissingField("model"))?
.to_string();
let max_tokens = value
.get("max_output_tokens")
.and_then(Value::as_u64)
.map_or(DEFAULT_MAX_OUTPUT_TOKENS, |v| v as u32);
let system = value
.get("instructions")
.and_then(Value::as_str)
.filter(|s| !s.is_empty())
.map(ToString::to_string);
let messages = value
.get("input")
.map(parse_input)
.transpose()?
.unwrap_or_default();
let temperature = value
.get("temperature")
.and_then(Value::as_f64)
.map(|v| v as f32);
let top_p = value.get("top_p").and_then(Value::as_f64).map(|v| v as f32);
let stop_sequences = value
.get("stop")
.and_then(|v| match v {
Value::String(s) => Some(vec![s.clone()]),
Value::Array(arr) => Some(
arr.iter()
.filter_map(|x| x.as_str().map(ToString::to_string))
.collect(),
),
_ => None,
})
.unwrap_or_default();
let tools = value
.get("tools")
.and_then(Value::as_array)
.map(|arr| arr.iter().filter_map(parse_tool).collect::<Vec<_>>())
.unwrap_or_default();
let tool_choice = value.get("tool_choice").and_then(parse_tool_choice);
let stream = value
.get("stream")
.and_then(Value::as_bool)
.unwrap_or(false);
let thinking = value.get("reasoning").map(parse_reasoning);
let metadata = value.get("metadata").cloned();
Ok(CanonicalRequest {
model,
system,
messages,
max_tokens,
temperature,
top_p,
top_k: None,
stop_sequences,
tools,
tool_choice,
stream,
thinking,
metadata,
})
}
fn parse_input(value: &Value) -> Result<Vec<CanonicalMessage>, InboundParseError> {
let arr = match value {
Value::String(s) => {
return Ok(vec![CanonicalMessage {
role: Role::User,
content: vec![CanonicalContent::Text(s.clone())],
}]);
},
Value::Array(a) => a,
other => {
return Err(InboundParseError::Unsupported {
field: "input",
detail: format!("expected string or array, got {other}"),
});
},
};
let mut messages: Vec<CanonicalMessage> = Vec::new();
for item in arr {
let kind = item
.get("type")
.and_then(Value::as_str)
.unwrap_or("message");
match kind {
"message" => messages.push(parse_message_item(item)?),
"function_call" => messages.push(parse_function_call(item)),
"function_call_output" => messages.push(parse_function_call_output(item)),
"reasoning" => {
if let Some(msg) = parse_reasoning_item(item) {
messages.push(msg);
}
},
other => {
return Err(InboundParseError::Unsupported {
field: "input[].type",
detail: other.to_string(),
});
},
}
}
Ok(messages)
}
fn parse_function_call(item: &Value) -> CanonicalMessage {
let id = item
.get("call_id")
.and_then(Value::as_str)
.or_else(|| item.get("id").and_then(Value::as_str))
.unwrap_or("")
.to_string();
let name = item
.get("name")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
let arguments = item
.get("arguments")
.and_then(Value::as_str)
.unwrap_or("{}");
let input: Value = serde_json::from_str(arguments).unwrap_or(Value::Null);
CanonicalMessage {
role: Role::Assistant,
content: vec![CanonicalContent::ToolUse { id, name, input }],
}
}
fn parse_function_call_output(item: &Value) -> CanonicalMessage {
let tool_use_id = item
.get("call_id")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
let output_text = item
.get("output")
.and_then(Value::as_str)
.unwrap_or("")
.to_string();
CanonicalMessage {
role: Role::Tool,
content: vec![CanonicalContent::ToolResult {
tool_use_id,
content: vec![CanonicalContent::Text(output_text)],
is_error: false,
}],
}
}
fn parse_reasoning_item(item: &Value) -> Option<CanonicalMessage> {
let text = item
.get("summary")
.and_then(Value::as_array)
.map(|arr| {
arr.iter()
.filter_map(|v| v.get("text").and_then(Value::as_str))
.collect::<Vec<_>>()
.join("\n")
})
.unwrap_or_default();
if text.is_empty() {
return None;
}
Some(CanonicalMessage {
role: Role::Assistant,
content: vec![CanonicalContent::Thinking {
text,
signature: None,
}],
})
}
fn parse_message_item(value: &Value) -> Result<CanonicalMessage, InboundParseError> {
let role_str = value.get("role").and_then(Value::as_str).unwrap_or("user");
let role = match role_str {
"user" => Role::User,
"assistant" => Role::Assistant,
"system" | "developer" => Role::System,
other => {
return Err(InboundParseError::Unsupported {
field: "input[].role",
detail: other.to_string(),
});
},
};
let content_value = value.get("content").unwrap_or(&Value::Null);
let content = match content_value {
Value::String(s) => vec![CanonicalContent::Text(s.clone())],
Value::Array(parts) => parts.iter().filter_map(parse_content_part).collect(),
Value::Null => Vec::new(),
other => {
return Err(InboundParseError::Unsupported {
field: "input[].content",
detail: format!("unexpected: {other}"),
});
},
};
Ok(CanonicalMessage { role, content })
}
fn parse_content_part(value: &Value) -> Option<CanonicalContent> {
let kind = value.get("type").and_then(Value::as_str).unwrap_or("");
match kind {
"input_text" | "output_text" | "text" => Some(CanonicalContent::Text(
value
.get("text")
.and_then(Value::as_str)
.unwrap_or("")
.to_string(),
)),
"input_image" => {
let url = value.get("image_url").and_then(Value::as_str)?;
Some(CanonicalContent::Image(ImageSource::Url(url.to_string())))
},
_ => None,
}
}
fn parse_tool(value: &Value) -> Option<CanonicalTool> {
let kind = value
.get("type")
.and_then(Value::as_str)
.unwrap_or("function");
if kind != "function" {
return None;
}
let name = value.get("name").and_then(Value::as_str)?;
let description = value
.get("description")
.and_then(Value::as_str)
.map(ToString::to_string);
let parameters = value
.get("parameters")
.cloned()
.unwrap_or(Value::Object(Map::new()));
Some(CanonicalTool {
name: name.to_string(),
description,
input_schema: parameters,
})
}
fn parse_tool_choice(value: &Value) -> Option<CanonicalToolChoice> {
if let Some(s) = value.as_str() {
return match s {
"auto" => Some(CanonicalToolChoice::Auto),
"none" => Some(CanonicalToolChoice::None),
"required" => Some(CanonicalToolChoice::Required),
_ => None,
};
}
let kind = value.get("type").and_then(Value::as_str)?;
if kind == "function" {
return value
.get("name")
.and_then(Value::as_str)
.map(|n| CanonicalToolChoice::Tool(n.to_string()));
}
None
}
fn parse_reasoning(value: &Value) -> ThinkingConfig {
let effort = value.get("effort").and_then(Value::as_str).unwrap_or("");
let enabled = !effort.is_empty();
let budget_tokens = match effort {
"low" => Some(1024),
"medium" => Some(4096),
"high" => Some(16384),
_ => None,
};
ThinkingConfig {
enabled,
budget_tokens,
}
}