use super::{ChatCallForm, GenerationStateAccess, GenerationToolReplay};
use crate::{
Dialect,
capability::StateStore,
transform::{
TransformError, TransformErrorKind,
identity::tool_alias::{self, ToolAlias},
},
wire::{
DeclaredFields,
claude::{content as cc, generate_content as c},
gemini as g,
openai::{chat as h, responses as r},
},
};
use std::collections::{BTreeMap, BTreeSet};
fn name(names: &mut BTreeMap<String, String>, id: &str, value: &str) -> Result<(), TransformError> {
if id.is_empty() || value.is_empty() || names.get(id).is_some_and(|old| old != value) {
return Err(TransformError::shape(
"history.tool_identity",
"empty or contradictory tool ID/name",
));
}
names.insert(id.into(), value.into());
Ok(())
}
pub(super) fn merge_names(
target: &mut BTreeMap<String, String>,
incoming: BTreeMap<String, String>,
) -> Result<(), TransformError> {
for (id, value) in incoming {
name(target, &id, &value)?;
}
Ok(())
}
fn restore(id: &mut String, replay: &GenerationToolReplay) {
if replay.chat_forms.contains_key(id) {
return;
}
if let Some(original) = replay.original_call_ids.get(id) {
*id = original.clone();
}
}
async fn facts<S: StateStore>(
state: &GenerationStateAccess<'_, S>,
ids: BTreeSet<String>,
names: BTreeMap<String, String>,
) -> Result<GenerationToolReplay, TransformError> {
let target = state.target.dialect;
let mut replay = GenerationToolReplay {
names,
..Default::default()
};
if target == Dialect::Gemini
&& ids
.iter()
.any(|id| replay.names.get(id).is_none_or(String::is_empty))
{
return Err(TransformError::new(
TransformErrorKind::MissingState,
"history.tool_name",
"tool result has no call in the declared history",
));
}
for id in ids {
match tool_alias::decode(&id) {
Some(ToolAlias::Upstream { id: original, .. }) => {
if target == Dialect::OpenAiChat {
replay.chat_forms.insert(id.clone(), ChatCallForm::Modern);
}
replay.original_call_ids.insert(id, original);
}
Some(ToolAlias::Missing { legacy: true, .. }) if target == Dialect::OpenAiChat => {
if replay.names.get(&id).is_none_or(String::is_empty) {
return Err(TransformError::new(
TransformErrorKind::MissingState,
"history.native_call",
"legacy Chat function requires its actual complete name",
));
}
replay.chat_forms.insert(id, ChatCallForm::LegacyFunction);
}
_ => {}
}
}
if target != Dialect::OpenAiChat {
for (client, original) in &replay.original_call_ids {
if let Some(value) = replay.names.get(client).cloned() {
name(&mut replay.names, original, &value)?;
}
}
for (client, original) in &replay.original_call_ids {
if let Some(kind) = replay.kinds.get(client).copied() {
replay.kinds.insert(original.clone(), kind);
}
}
}
Ok(replay)
}
fn kind(
replay: &GenerationToolReplay,
id: &str,
expected: super::ToolCallKind,
) -> Result<(), TransformError> {
if replay.kinds.get(id).is_some_and(|kind| *kind != expected) {
return Err(TransformError::shape(
"history.tool_kind",
"declared tool kind differs from actual native call",
));
}
Ok(())
}
pub(super) async fn chat<S: StateStore>(
input: h::GenerateContentRequestBody,
state: &GenerationStateAccess<'_, S>,
) -> Result<(h::GenerateContentRequestBody, GenerationToolReplay), TransformError> {
let mut input = input.into_declared();
let mut names = BTreeMap::new();
let mut ids = BTreeSet::new();
for message in &input.messages {
match message {
h::ChatMessage::Assistant(message) => {
for call in message.tool_calls.iter().flatten() {
match call {
h::MessageToolCall::Function(call) => {
name(&mut names, &call.id, &call.function.name)?;
ids.insert(call.id.clone());
}
h::MessageToolCall::Custom(call) => {
name(&mut names, &call.id, &call.custom.name)?;
ids.insert(call.id.clone());
}
}
}
}
h::ChatMessage::Tool(message) => {
ids.insert(message.tool_call_id.clone());
}
_ => {}
}
}
let mut replay = facts(state, ids, names).await?;
for message in &input.messages {
if let h::ChatMessage::Assistant(message) = message {
for call in message.tool_calls.iter().flatten() {
let (id, kind) = match call {
h::MessageToolCall::Function(call) => (&call.id, super::ToolCallKind::Function),
h::MessageToolCall::Custom(call) => (&call.id, super::ToolCallKind::Custom),
};
if replay
.kinds
.get(id)
.is_some_and(|existing| *existing != kind)
{
return Err(TransformError::shape(
"history.tool_kind",
"declared tool kind differs from saved identity",
));
}
replay.kinds.insert(id.clone(), kind);
}
}
}
for message in &mut input.messages {
match message {
h::ChatMessage::Assistant(message) => {
for call in message.tool_calls.iter_mut().flatten() {
match call {
h::MessageToolCall::Function(call) => restore(&mut call.id, &replay),
h::MessageToolCall::Custom(call) => restore(&mut call.id, &replay),
}
}
}
h::ChatMessage::Tool(message) => restore(&mut message.tool_call_id, &replay),
_ => {}
}
}
Ok((input, replay))
}
pub(super) async fn claude<S: StateStore>(
input: c::GenerateContentRequestBody,
state: &GenerationStateAccess<'_, S>,
) -> Result<(c::GenerateContentRequestBody, GenerationToolReplay), TransformError> {
let mut input = input.into_declared();
let mut names = BTreeMap::new();
let mut ids = BTreeSet::new();
for message in &input.messages {
if let cc::MessageContent::Blocks(blocks) = &message.content {
for b in blocks {
match b {
cc::ContentBlock::ToolUse(b) => {
name(&mut names, &b.id, &b.name)?;
ids.insert(b.id.clone());
}
cc::ContentBlock::ToolResult(b) => {
ids.insert(b.tool_use_id.clone());
}
_ => {}
}
}
}
}
let replay = facts(state, ids, names).await?;
for message in &mut input.messages {
if let cc::MessageContent::Blocks(blocks) = &mut message.content {
for b in blocks {
match b {
cc::ContentBlock::ToolUse(b) => {
kind(&replay, &b.id, super::ToolCallKind::Function)?;
restore(&mut b.id, &replay);
}
cc::ContentBlock::ToolResult(b) => {
kind(&replay, &b.tool_use_id, super::ToolCallKind::Function)?;
restore(&mut b.tool_use_id, &replay);
}
_ => {}
}
}
}
}
Ok((input, replay))
}
pub(super) async fn gemini<S: StateStore>(
input: g::GenerateContentRequestBody,
state: &GenerationStateAccess<'_, S>,
) -> Result<(g::GenerateContentRequestBody, GenerationToolReplay), TransformError> {
let mut input = input.into_declared();
let mut names = BTreeMap::new();
let mut ids = BTreeSet::new();
for p in input.contents.iter().flat_map(|c| c.parts.iter().flatten()) {
if let Some(call) = &p.function_call
&& let Some(id) = &call.id
{
name(&mut names, id, &call.name)?;
ids.insert(id.clone());
}
if let Some(result) = &p.function_response
&& let Some(id) = &result.id
{
name(&mut names, id, &result.name)?;
ids.insert(id.clone());
}
}
let replay = facts(state, ids, names).await?;
for p in input
.contents
.iter_mut()
.flat_map(|c| c.parts.iter_mut().flatten())
{
if let Some(call) = &mut p.function_call
&& let Some(id) = &mut call.id
{
kind(&replay, id, super::ToolCallKind::Function)?;
restore(id, &replay);
}
if let Some(result) = &mut p.function_response
&& let Some(id) = &mut result.id
{
kind(&replay, id, super::ToolCallKind::Function)?;
restore(id, &replay);
}
}
Ok((input, replay))
}
pub(super) async fn responses<S: StateStore>(
input: r::GenerateContentRequestBody,
state: &GenerationStateAccess<'_, S>,
) -> Result<(r::GenerateContentRequestBody, GenerationToolReplay), TransformError> {
let mut input = input.into_declared();
let mut names = BTreeMap::new();
let mut ids = BTreeSet::new();
if let Some(r::Input::Items(items)) = &input.input {
for item in items {
match item {
r::InputItem::FunctionCall(b) => {
name(&mut names, &b.call_id, &b.name)?;
ids.insert(b.call_id.clone());
}
r::InputItem::CustomToolCall(b) => {
name(&mut names, &b.call_id, &b.name)?;
ids.insert(b.call_id.clone());
}
r::InputItem::FunctionCallOutput(b) => {
if let Some(Some(value)) = &b.name {
name(&mut names, &b.call_id, value)?;
}
ids.insert(b.call_id.clone());
}
r::InputItem::CustomToolCallOutput(b) => {
ids.insert(b.call_id.clone());
}
_ => {}
}
}
}
let replay = facts(state, ids, names).await?;
if let Some(r::Input::Items(items)) = &mut input.input {
for item in items {
match item {
r::InputItem::FunctionCall(b) => {
kind(&replay, &b.call_id, super::ToolCallKind::Function)?;
restore(&mut b.call_id, &replay);
}
r::InputItem::CustomToolCall(b) => {
kind(&replay, &b.call_id, super::ToolCallKind::Custom)?;
restore(&mut b.call_id, &replay);
}
r::InputItem::FunctionCallOutput(b) => {
kind(&replay, &b.call_id, super::ToolCallKind::Function)?;
if let Some(value) = replay.names.get(&b.call_id) {
b.name = Some(Some(value.clone()));
}
restore(&mut b.call_id, &replay);
}
r::InputItem::CustomToolCallOutput(b) => {
kind(&replay, &b.call_id, super::ToolCallKind::Custom)?;
restore(&mut b.call_id, &replay);
}
_ => {}
}
}
}
Ok((input, replay))
}