use serde_json::{Value, json};
use std::collections::HashSet;
use crate::driver_registry::LlmCallConfig;
use crate::error::{AgentLoopError, LlmErrorKind};
use super::*;
pub(crate) fn configuration_update_item(
effort: crate::model::ReasoningEffort,
) -> ResponsesInputItem {
ResponsesInputItem::ConfigurationUpdate {
r#type: "configuration_update".into(),
reasoning: crate::compact::ConfigurationReasoning { effort },
}
}
pub(crate) fn coalesce_configuration_updates(
items: Vec<ResponsesInputItem>,
) -> Vec<ResponsesInputItem> {
let mut output = Vec::with_capacity(items.len());
for item in items {
if matches!(item, ResponsesInputItem::ConfigurationUpdate { .. })
&& matches!(
output.last(),
Some(ResponsesInputItem::ConfigurationUpdate { .. })
)
{
output.pop();
}
output.push(item);
}
output
}
pub(crate) fn compute_delta_input_items(items: Vec<ResponsesInputItem>) -> Vec<ResponsesInputItem> {
let last_assistant_turn_idx = items
.iter()
.enumerate()
.rev()
.find_map(|(i, item)| match item {
ResponsesInputItem::Message { role, .. } if role == "assistant" => Some(i),
ResponsesInputItem::Reasoning { .. } => Some(i),
ResponsesInputItem::FunctionCall { .. } => Some(i),
_ => None,
});
match last_assistant_turn_idx {
Some(idx) => items.into_iter().skip(idx + 1).collect(),
None => items,
}
}
pub(crate) fn finalize_input_for_request(
input_items: Vec<ResponsesInputItem>,
previous_response_id: &Option<String>,
) -> Vec<ResponsesInputItem> {
coalesce_configuration_updates(if previous_response_id.is_some() {
compute_delta_input_items(input_items)
} else {
repair_unpaired_function_call_items(input_items)
})
}
pub(crate) fn unpaired_function_call_ids(items: &[ResponsesInputItem]) -> Vec<String> {
let call_ids: HashSet<&str> = items
.iter()
.filter_map(|item| match item {
ResponsesInputItem::FunctionCall { call_id, .. } => Some(call_id.as_str()),
_ => None,
})
.collect();
let output_ids: HashSet<&str> = items
.iter()
.filter_map(|item| match item {
ResponsesInputItem::FunctionCallOutput { call_id, .. } => Some(call_id.as_str()),
_ => None,
})
.collect();
items
.iter()
.filter_map(|item| match item {
ResponsesInputItem::FunctionCall { call_id, .. }
if !output_ids.contains(call_id.as_str()) =>
{
Some(call_id.clone())
}
ResponsesInputItem::FunctionCallOutput { call_id, .. }
if !call_ids.contains(call_id.as_str()) =>
{
Some(call_id.clone())
}
_ => None,
})
.collect()
}
pub(crate) fn repair_unpaired_function_call_items(
input_items: Vec<ResponsesInputItem>,
) -> Vec<ResponsesInputItem> {
let unpaired: HashSet<String> = unpaired_function_call_ids(&input_items)
.into_iter()
.collect();
if unpaired.is_empty() {
return input_items;
}
tracing::warn!(
unpaired_call_ids = ?unpaired,
"dropping unpaired function_call / function_call_output items before \
stateless Responses replay; one side of the pair was likely evicted by \
compaction or model-view masking (EVE-597/EVE-519)"
);
input_items
.into_iter()
.filter(|item| match item {
ResponsesInputItem::FunctionCall { call_id, .. }
| ResponsesInputItem::FunctionCallOutput { call_id, .. } => {
!unpaired.contains(call_id.as_str())
}
_ => true,
})
.collect()
}
pub(crate) fn apply_cache_options(body: &mut Value, config: &LlmCallConfig, native_openai: bool) {
if !native_openai || !crate::openai_compat::supports_cache_options(&config.model) {
return;
}
let Some(cache) = config.prompt_cache.as_ref().filter(|c| c.enabled) else {
return;
};
let explicit = cache.strategy == crate::driver_registry::PromptCacheStrategy::Explicit;
body["prompt_cache_options"] =
json!({"ttl": "30m", "mode": if explicit { "explicit" } else { "implicit" }});
if explicit
&& let Some(instructions) = body
.get("instructions")
.and_then(Value::as_str)
.map(str::to_owned)
{
body.as_object_mut().unwrap().remove("instructions");
body["input"].as_array_mut().unwrap().insert(
0,
json!({
"type": "message", "role": "developer", "content": [{
"type": "input_text", "text": instructions,
"prompt_cache_breakpoint": {"mode": "explicit"}
}]
}),
);
}
}
pub(crate) fn is_missing_tool_output_continuation_error(error: &AgentLoopError) -> bool {
if !matches!(error.llm_error_kind(), Some(LlmErrorKind::InvalidRequest)) {
return false;
}
let message = error.to_string().to_ascii_lowercase();
message.contains("no tool output found for function call")
|| message.contains("no tool call found for function call output")
|| message.contains("previous_response_not_found")
|| (message.contains("previous response") && message.contains("not found"))
}