use serde_json::{json, Value};
use crate::canonical::{
CanonicalError, CanonicalRequest, Content, DocumentSource, 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::Document { source } => parts.push(document_part(source)?),
Content::ToolUse {
name,
input,
signature,
..
} => {
let mut part = json!({ "functionCall": { "name": name, "args": input } });
if let Some(sig) = signature {
part["thoughtSignature"] = json!(sig);
}
parts.push(part);
}
Content::ToolResult {
tool_use_id,
content,
is_error,
} => parts.push(function_response(tool_use_id, content, *is_error, req)?),
Content::Thinking { .. }
| Content::RedactedThinking { .. }
| Content::ServerToolUse { .. }
| Content::ServerToolResult { .. } => {} }
}
Ok(Value::Array(parts))
}
fn image_part(source: &ImageSource) -> Result<Value, CanonicalError> {
match source {
ImageSource::Base64 { media_type, data } => {
Ok(json!({ "inlineData": { "mimeType": media_type, "data": data } }))
}
ImageSource::Url { .. } => Err(url_image_err()),
}
}
fn document_part(source: &DocumentSource) -> Result<Value, CanonicalError> {
match source {
DocumentSource::Base64 { media_type, data } => {
Ok(json!({ "inlineData": { "mimeType": media_type, "data": data } }))
}
DocumentSource::Url { .. } => Err(url_document_err()),
}
}
fn url_image_err() -> CanonicalError {
CanonicalError {
kind: ErrorKind::ParseInput,
message: "google: image URLs are not supported — Gemini's fileData.fileUri \
references only files uploaded to the Google Files API, not web URLs, \
and needs a mimeType brazen cannot infer from a URL; download the \
image and re-send it as a base64 image"
.to_string(),
provider_detail: None,
retry_after_seconds: None,
}
}
fn url_document_err() -> CanonicalError {
CanonicalError {
kind: ErrorKind::ParseInput,
message: "google: document URLs are not supported — Gemini's fileData.fileUri \
references only files uploaded to the Google Files API, not web URLs, \
and needs a mimeType brazen cannot infer from a URL; download the \
document and re-send it as a base64 document"
.to_string(),
provider_detail: None,
retry_after_seconds: None,
}
}
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,
retry_after_seconds: None,
}
}