use serde_json::{json, Value};
use crate::canonical::{CanonicalError, CanonicalRequest, Content, ErrorKind, ImageSource, Role};
pub(super) fn system_instruction(req: &CanonicalRequest) -> Result<Option<Value>, CanonicalError> {
let mut text = String::new();
if let Some(s) = &req.system {
text.push_str(&concat_text(s, "system")?);
}
for m in &req.messages {
if m.role == Role::System {
text.push_str(&concat_text(&m.content, "system")?);
}
}
Ok((!text.is_empty()).then(|| json!({ "parts": [{ "text": text }] })))
}
pub(super) fn contents_value(req: &CanonicalRequest) -> Result<Value, CanonicalError> {
let mut out = Vec::new();
for m in &req.messages {
let role = match m.role {
Role::User | Role::Tool => "user",
Role::Assistant => "model",
Role::System => continue, };
out.push(json!({ "role": role, "parts": parts_value(&m.content, req)? }));
}
Ok(Value::Array(out))
}
fn parts_value(content: &[Content], req: &CanonicalRequest) -> Result<Value, CanonicalError> {
let mut parts = Vec::new();
for c in content {
match c {
Content::Text(t) => parts.push(json!({ "text": t })),
Content::Image { source } => parts.push(image_part(source)),
Content::ToolUse { name, input, .. } => {
parts.push(json!({ "functionCall": { "name": name, "args": input } }))
}
Content::ToolResult {
tool_use_id,
content,
is_error,
} => parts.push(function_response(tool_use_id, content, *is_error, req)?),
Content::Thinking { .. } | Content::RedactedThinking { .. } => {} }
}
Ok(Value::Array(parts))
}
fn image_part(source: &ImageSource) -> Value {
match source {
ImageSource::Base64 { media_type, data } => {
json!({ "inlineData": { "mimeType": media_type, "data": data } })
}
ImageSource::Url { url } => json!({ "fileData": { "fileUri": url } }),
}
}
fn function_response(
tool_use_id: &str,
content: &[Content],
is_error: bool,
req: &CanonicalRequest,
) -> Result<Value, CanonicalError> {
let name = req.tool_name(tool_use_id).unwrap_or(tool_use_id);
let mut text = concat_text(content, "tool_result")?;
if is_error {
text = format!("[error] {text}");
}
Ok(json!({ "functionResponse": { "name": name, "response": { "result": text } } }))
}
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 slot_err(slot: &str) -> CanonicalError {
CanonicalError {
kind: ErrorKind::ParseInput,
message: format!("{slot} accepts only text content"),
provider_detail: None,
}
}