use serde_json::{json, Map, Value};
use crate::canonical::{
CanonicalError, CanonicalRequest, Content, ErrorKind, ImageSource, Role, Tool,
};
use crate::protocol::json::finish_body;
use crate::protocol::{ProviderCtx, WireRequest};
pub(super) const REQUEST_PATH: &str = "/api/chat";
pub(super) fn encode(
req: &CanonicalRequest,
ctx: &ProviderCtx,
) -> Result<WireRequest, CanonicalError> {
let mut body = Map::new();
body.insert("model".into(), json!(ctx.model));
body.insert("messages".into(), messages_value(req)?);
if !req.tools.is_empty() {
body.insert("tools".into(), tools_value(&req.tools)); }
let options = options_value(req);
if !options.is_empty() {
body.insert("options".into(), Value::Object(options)); }
body.insert("stream".into(), json!(req.stream.unwrap_or(false)));
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 options_value(req: &CanonicalRequest) -> Map<String, Value> {
let mut options = Map::new();
if let Some(n) = req.max_tokens {
options.insert("num_predict".into(), json!(n)); }
if let Some(t) = req.temperature {
options.insert("temperature".into(), json!(t));
}
if let Some(p) = req.top_p {
options.insert("top_p".into(), json!(p));
}
if !req.stop.is_empty() {
options.insert("stop".into(), json!(req.stop));
}
options
}
fn slot_err(slot: &str) -> CanonicalError {
CanonicalError {
kind: ErrorKind::ParseInput,
message: format!("{slot} accepts only text content"),
provider_detail: None,
}
}
fn messages_value(req: &CanonicalRequest) -> Result<Value, CanonicalError> {
let mut out = Vec::new();
if let Some(system) = req.system.as_ref().filter(|s| !s.is_empty()) {
out.push(json!({"role": "system", "content": concat_text(system, "system")?}));
}
for m in &req.messages {
match m.role {
Role::System => {
out.push(json!({"role": "system", "content": concat_text(&m.content, "system")?}))
}
Role::User => out.push(user_message(&m.content)?),
Role::Assistant => out.push(assistant_message(&m.content)?),
Role::Tool => tool_messages(&m.content, req, &mut out)?,
}
}
Ok(Value::Array(out))
}
fn user_message(content: &[Content]) -> Result<Value, CanonicalError> {
let mut text = String::new();
let mut images = Vec::new();
for c in content {
match c {
Content::Text(t) => text.push_str(t),
Content::Image { source } => images.push(json!(image_b64(source)?)),
_ => return Err(slot_err("user")),
}
}
let mut obj = Map::new();
obj.insert("role".into(), json!("user"));
obj.insert("content".into(), json!(text));
if !images.is_empty() {
obj.insert("images".into(), Value::Array(images));
}
Ok(Value::Object(obj))
}
fn assistant_message(content: &[Content]) -> Result<Value, CanonicalError> {
let mut text = String::new();
let mut thinking = String::new();
let mut calls = Vec::new();
for c in content {
match c {
Content::Text(t) => text.push_str(t),
Content::ToolUse { name, input, .. } => {
calls.push(json!({"function": {"name": name, "arguments": input}}))
}
Content::Thinking { text: t, .. } => thinking.push_str(t), Content::RedactedThinking { .. } => {} _ => return Err(slot_err("assistant")),
}
}
let mut obj = Map::new();
obj.insert("role".into(), json!("assistant"));
obj.insert("content".into(), json!(text));
if !thinking.is_empty() {
obj.insert("thinking".into(), json!(thinking)); }
if !calls.is_empty() {
obj.insert("tool_calls".into(), Value::Array(calls));
}
Ok(Value::Object(obj))
}
fn tool_messages(
content: &[Content],
req: &CanonicalRequest,
out: &mut Vec<Value>,
) -> Result<(), CanonicalError> {
for c in content {
let Content::ToolResult {
tool_use_id,
content,
is_error,
} = c
else {
return Err(slot_err("tool"));
};
let mut text = concat_text(content, "tool_result")?;
if *is_error {
text = format!("[error] {text}");
}
let mut obj = Map::new();
obj.insert("role".into(), json!("tool"));
obj.insert("content".into(), json!(text));
if let Some(name) = req.tool_name(tool_use_id) {
obj.insert("tool_name".into(), json!(name));
}
out.push(Value::Object(obj));
}
Ok(())
}
fn concat_text(content: &[Content], slot: &str) -> Result<String, CanonicalError> {
let mut text = String::new();
for c in content {
match c {
Content::Text(t) => text.push_str(t),
_ => return Err(slot_err(slot)),
}
}
Ok(text)
}
fn image_b64(source: &ImageSource) -> Result<String, CanonicalError> {
match source {
ImageSource::Base64 { data, .. } => Ok(data.clone()),
ImageSource::Url { .. } => Err(slot_err("image")),
}
}
fn tools_value(tools: &[Tool]) -> Value {
Value::Array(
tools
.iter()
.map(|t| {
let mut f = json!({"name": t.name, "parameters": t.input_schema});
if let Some(d) = &t.description {
f["description"] = json!(d);
}
json!({"type": "function", "function": f})
})
.collect(),
)
}