use serde_json::Value;
use super::{arr_of, bad, err, f32_of, messages, obj_of, opt_str, str_of, u32_of};
use crate::canonical::{CanonicalRequest, OutputFormat, Tool, ToolChoice};
use crate::ingress::IngressError;
pub(crate) fn decode_request(bytes: &[u8]) -> Result<CanonicalRequest, IngressError> {
let top: Value =
serde_json::from_slice(bytes).map_err(|e| err(format!("request body is not JSON: {e}")))?;
let Value::Object(body) = top else {
return Err(bad("request body", "a JSON object"));
};
let mut req = CanonicalRequest::default();
for (k, v) in body {
match k.as_str() {
"model" => req.model = str_of(Some(&v), "model")?.to_owned(),
"messages" => messages::fold(&v, &mut req)?,
"system" => req.system = Some(messages::system(&v)?),
"tools" => req.tools = tools(&v)?,
"tool_choice" => tool_choice(&v, &mut req)?,
"max_tokens" => req.max_tokens = Some(u32_of(&v, &k)?),
"temperature" => req.temperature = Some(f32_of(&v, &k)?),
"top_p" => req.top_p = Some(f32_of(&v, &k)?),
"stop_sequences" => req.stop = stop(&v)?,
"stream" => req.stream = Some(v.as_bool().ok_or_else(|| bad(&k, "a boolean"))?),
"output_config" => req.output = output(&v)?,
_ => {
req.extra.insert(k, v);
}
}
}
Ok(req)
}
fn stop(v: &Value) -> Result<Vec<String>, IngressError> {
match v {
Value::String(s) => Ok(vec![s.clone()]),
Value::Array(a) => a
.iter()
.enumerate()
.map(|(i, s)| Ok(str_of(Some(s), &format!("stop_sequences[{i}]"))?.to_owned()))
.collect(),
_ => Err(bad("stop_sequences", "a string or an array of strings")),
}
}
fn tools(v: &Value) -> Result<Vec<Tool>, IngressError> {
let mut out = Vec::new();
for (i, t) in arr_of(Some(v), "tools")?.iter().enumerate() {
let path = format!("tools[{i}]");
let obj = obj_of(Some(t), &path)?;
out.push(match obj.get("type") {
Some(ty) => {
let kind = str_of(Some(ty), &format!("{path}.type"))?.to_owned();
let mut config = obj.clone();
config.remove("type");
config.remove("name");
Tool::Provider {
name: str_of(obj.get("name"), &format!("{path}.name"))?.to_owned(),
kind,
config,
}
}
None => Tool::Custom {
name: str_of(obj.get("name"), &format!("{path}.name"))?.to_owned(),
description: opt_str(obj.get("description"), &format!("{path}.description"))?,
input_schema: obj.get("input_schema").cloned().unwrap_or(Value::Null),
strict: opt_bool(obj.get("strict"), &format!("{path}.strict"))?,
},
});
}
Ok(out)
}
fn tool_choice(v: &Value, req: &mut CanonicalRequest) -> Result<(), IngressError> {
let o = obj_of(Some(v), "tool_choice")?;
req.tool_choice = match str_of(o.get("type"), "tool_choice.type")? {
"auto" => ToolChoice::Auto,
"any" => ToolChoice::Any,
"none" => ToolChoice::None,
"tool" => ToolChoice::Tool {
name: str_of(o.get("name"), "tool_choice.name")?.to_owned(),
},
other => {
return Err(err(format!(
"`tool_choice.type` \"{other}\" has no canonical projection"
)))
}
};
if o.get("disable_parallel_tool_use") == Some(&Value::Bool(true)) {
req.parallel_tool_calls = Some(false);
}
Ok(())
}
fn output(v: &Value) -> Result<Option<OutputFormat>, IngressError> {
let f = obj_of(
obj_of(Some(v), "output_config")?.get("format"),
"output_config.format",
)?;
match str_of(f.get("type"), "output_config.format.type")? {
"json_schema" => Ok(Some(OutputFormat::JsonSchema {
name: None,
schema: f.get("schema").cloned().unwrap_or(Value::Null),
strict: None,
})),
other => Err(err(format!(
"`output_config.format.type` \"{other}\" has no canonical projection"
))),
}
}
fn opt_bool(v: Option<&Value>, path: &str) -> Result<Option<bool>, IngressError> {
match v {
None | Some(Value::Null) => Ok(None),
Some(Value::Bool(b)) => Ok(Some(*b)),
Some(_) => Err(bad(path, "a boolean")),
}
}