fix: model narration included in history and between tool calls to prevent repetition
CI / All (ubuntu-latest) (push) Failing after 25s
CI / All (macos-latest) (push) Has been cancelled
CI / All (windows-latest) (push) Has been cancelled

This commit is contained in:
2026-07-17 16:57:51 -06:00
parent b908fc20ba
commit 078e6e3744
6 changed files with 95 additions and 40 deletions
+5
View File
@@ -397,6 +397,11 @@ fn build_chat_completions_body(data: ChatCompletionsData, model: &Model) -> Resu
})) }))
} }
for tool_result in tool_results { for tool_result in tool_results {
if let Some(round_text) = &tool_result.text {
assistant_parts.push(json!({
"text": round_text,
}))
}
assistant_parts.push(json!({ assistant_parts.push(json!({
"toolUse": { "toolUse": {
"toolUseId": tool_result.call.id, "toolUseId": tool_result.call.id,
+6
View File
@@ -320,6 +320,12 @@ pub fn claude_build_chat_completions_body(
})) }))
} }
for tool_result in tool_results { for tool_result in tool_results {
if let Some(round_text) = &tool_result.text {
assistant_parts.push(json!({
"type": "text",
"text": round_text,
}))
}
assistant_parts.push(json!({ assistant_parts.push(json!({
"type": "tool_use", "type": "tool_use",
"id": tool_result.call.id, "id": tool_result.call.id,
+9 -2
View File
@@ -118,6 +118,9 @@ impl MessageContent {
lines.push(text.clone()) lines.push(text.clone())
} }
for tool_result in tool_results { for tool_result in tool_results {
if let Some(round_text) = &tool_result.text {
lines.push(round_text.clone())
}
let mut parts = vec!["Call".to_string()]; let mut parts = vec!["Call".to_string()];
if let Some((agent_name, functions)) = agent_info if let Some((agent_name, functions)) = agent_info
&& functions.contains(&tool_result.call.name) && functions.contains(&tool_result.call.name)
@@ -201,9 +204,13 @@ impl MessageContentToolCalls {
} }
} }
pub fn merge(&mut self, tool_results: Vec<ToolResult>, _text: String) { pub fn merge(&mut self, mut tool_results: Vec<ToolResult>, text: String) {
if !text.is_empty()
&& let Some(first) = tool_results.first_mut()
{
first.text = Some(text);
}
self.tool_results.extend(tool_results); self.tool_results.extend(tool_results);
self.text.clear();
self.sequence = true; self.sequence = true;
} }
} }
+57 -33
View File
@@ -370,7 +370,7 @@ pub fn openai_build_chat_completions_body(data: ChatCompletionsData, model: &Mod
match content { match content {
MessageContent::ToolCalls(MessageContentToolCalls { MessageContent::ToolCalls(MessageContentToolCalls {
tool_results, tool_results,
text: _, text,
sequence, sequence,
}) => { }) => {
if !sequence { if !sequence {
@@ -387,9 +387,12 @@ pub fn openai_build_chat_completions_body(data: ChatCompletionsData, model: &Mod
}) })
}) })
.collect(); .collect();
let mut messages = vec![ let mut assistant_message =
json!({ "role": MessageRole::Assistant, "tool_calls": tool_calls }), json!({ "role": MessageRole::Assistant, "tool_calls": tool_calls });
]; if !text.is_empty() {
assistant_message["content"] = strip_think_tag(&text).into();
}
let mut messages = vec![assistant_message];
for tool_result in tool_results { for tool_result in tool_results {
messages.push(json!({ messages.push(json!({
"role": "tool", "role": "tool",
@@ -399,21 +402,30 @@ pub fn openai_build_chat_completions_body(data: ChatCompletionsData, model: &Mod
} }
messages messages
} else { } else {
tool_results.into_iter().flat_map(|tool_result| { tool_results.into_iter().enumerate().flat_map(|(index, tool_result)| {
let round_text = if index == 0 && !text.is_empty() {
Some(text.clone())
} else {
tool_result.text.clone()
};
let mut assistant_message = json!({
"role": MessageRole::Assistant,
"tool_calls": [
{
"id": tool_result.call.id,
"type": "function",
"function": {
"name": tool_result.call.name,
"arguments": tool_result.call.arguments.to_string(),
},
}
]
});
if let Some(round_text) = round_text {
assistant_message["content"] = strip_think_tag(&round_text).into();
}
vec![ vec![
json!({ assistant_message,
"role": MessageRole::Assistant,
"tool_calls": [
{
"id": tool_result.call.id,
"type": "function",
"function": {
"name": tool_result.call.name,
"arguments": tool_result.call.arguments.to_string(),
},
}
]
}),
json!({ json!({
"role": "tool", "role": "tool",
"content": tool_result.output.to_string(), "content": tool_result.output.to_string(),
@@ -552,24 +564,36 @@ pub fn openai_build_responses_body(data: ChatCompletionsData, model: &Model) ->
match content { match content {
MessageContent::ToolCalls(MessageContentToolCalls { MessageContent::ToolCalls(MessageContentToolCalls {
tool_results, tool_results,
text: _, text,
sequence: _, sequence: _,
}) => tool_results }) => tool_results
.into_iter() .into_iter()
.flat_map(|tool_result| { .enumerate()
vec![ .flat_map(|(index, tool_result)| {
json!({ let round_text = if index == 0 && !text.is_empty() {
"type": "function_call", Some(text.clone())
"call_id": tool_result.call.id, } else {
"name": tool_result.call.name, tool_result.text.clone()
"arguments": tool_result.call.arguments.to_string(), };
}), let mut items = vec![];
json!({ if let Some(round_text) = round_text {
"type": "function_call_output", items.push(json!({
"call_id": tool_result.call.id, "role": MessageRole::Assistant,
"output": tool_result.output.to_string(), "content": strip_think_tag(&round_text),
}), }));
] }
items.push(json!({
"type": "function_call",
"call_id": tool_result.call.id,
"name": tool_result.call.name,
"arguments": tool_result.call.arguments.to_string(),
}));
items.push(json!({
"type": "function_call_output",
"call_id": tool_result.call.id,
"output": tool_result.output.to_string(),
}));
items
}) })
.collect(), .collect(),
MessageContent::Text(text) if role.is_assistant() && i != messages_len - 1 => { MessageContent::Text(text) if role.is_assistant() && i != messages_len - 1 => {
+11 -4
View File
@@ -372,8 +372,15 @@ pub fn gemini_build_chat_completions_body(
.collect(); .collect();
vec![json!({ "role": role, "parts": parts })] vec![json!({ "role": role, "parts": parts })]
}, },
MessageContent::ToolCalls(MessageContentToolCalls { tool_results, .. }) => { MessageContent::ToolCalls(MessageContentToolCalls { tool_results, text, .. }) => {
let model_parts: Vec<Value> = tool_results.iter().map(|tool_result| { let mut model_parts: Vec<Value> = vec![];
if !text.is_empty() {
model_parts.push(json!({ "text": text }));
}
for tool_result in tool_results.iter() {
if let Some(round_text) = &tool_result.text {
model_parts.push(json!({ "text": round_text }));
}
let mut part = json!({ let mut part = json!({
"functionCall": { "functionCall": {
"name": tool_result.call.name, "name": tool_result.call.name,
@@ -383,8 +390,8 @@ pub fn gemini_build_chat_completions_body(
if let Some(sig) = &tool_result.call.thought_signature { if let Some(sig) = &tool_result.call.thought_signature {
part["thoughtSignature"] = json!(sig); part["thoughtSignature"] = json!(sig);
} }
part model_parts.push(part);
}).collect(); }
let function_parts: Vec<Value> = tool_results.into_iter().map(|tool_result| { let function_parts: Vec<Value> = tool_results.into_iter().map(|tool_result| {
json!({ json!({
"functionResponse": { "functionResponse": {
+7 -1
View File
@@ -203,11 +203,17 @@ fn normalize_tool_result(result: Value) -> Value {
pub struct ToolResult { pub struct ToolResult {
pub call: ToolCall, pub call: ToolCall,
pub output: Value, pub output: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
} }
impl ToolResult { impl ToolResult {
pub fn new(call: ToolCall, output: Value) -> Self { pub fn new(call: ToolCall, output: Value) -> Self {
Self { call, output } Self {
call,
output,
text: None,
}
} }
} }