use serde_json::Value;
use super::{DialectEvidence, ToolDialectId};
use crate::client::{Message, ToolCall};
use crate::normalize::NormalizedTurn;
use crate::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct DetectScore(pub(crate) u8);
#[derive(Debug)]
pub(crate) struct DialectRequest<'a> {
body: &'a mut Value,
}
impl<'a> DialectRequest<'a> {
pub(crate) fn new(body: &'a mut Value) -> DialectRequest<'a> {
DialectRequest { body }
}
fn object_mut(&mut self) -> Result<&mut serde_json::Map<String, Value>> {
self.body
.as_object_mut()
.ok_or_else(|| Error::MalformedResponse("request body was not a JSON object".into()))
}
pub(crate) fn validate_shape(&mut self) -> Result<()> {
let obj = self.object_mut()?;
if let Some(messages) = obj.get("messages")
&& !messages.is_array()
{
return Err(Error::MalformedResponse(
"request `messages` was present but not an array".into(),
));
}
Ok(())
}
pub(crate) fn get(&self, key: &str) -> Option<&Value> {
self.body.get(key)
}
pub(crate) fn remove(&mut self, key: &str) -> Result<Option<Value>> {
Ok(self.object_mut()?.remove(key))
}
pub(crate) fn prepend_message(&mut self, message: Value) -> Result<()> {
let obj = self.object_mut()?;
let messages = obj
.entry("messages")
.or_insert_with(|| Value::Array(Vec::new()));
let Some(list) = messages.as_array_mut() else {
return Err(Error::MalformedResponse(
"request `messages` was present but not an array".into(),
));
};
list.insert(0, message);
Ok(())
}
}
pub(crate) trait ToolDialect: Send + Sync {
fn id(&self) -> ToolDialectId;
fn detect(&self, evidence: &DialectEvidence) -> Option<DetectScore>;
fn prepare_request(&self, request: &mut DialectRequest<'_>) -> Result<()>;
fn parse_turn(&self, body: &Value) -> Result<NormalizedTurn>;
fn echo_tool_results(
&self,
conversation: &mut Vec<Message>,
calls: &[ToolCall],
results: &[FramedToolResult],
) -> Result<()>;
}
#[derive(Debug, Clone)]
pub(crate) struct FramedToolResult {
id: String,
content: String,
}
impl FramedToolResult {
pub(crate) fn new(id: String, content: String) -> FramedToolResult {
FramedToolResult { id, content }
}
pub(crate) fn id(&self) -> &str {
&self.id
}
pub(crate) fn content(&self) -> &str {
&self.content
}
}
pub(crate) fn correlate_tool_results(
calls: &[ToolCall],
results: &[FramedToolResult],
) -> Result<()> {
if calls.len() != results.len() {
return Err(Error::Internal(
"tool-result echo: result count does not match call count",
));
}
let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
for (call, result) in calls.iter().zip(results.iter()) {
if !seen.insert(call.id.as_str()) {
return Err(Error::Internal(
"tool-result echo: duplicate tool call id within one turn",
));
}
if call.id != result.id() {
return Err(Error::Internal(
"tool-result echo: result id does not correlate with its call id in order",
));
}
}
Ok(())
}