From 58df53c94d05da33a981796fdb46c99b028b501d Mon Sep 17 00:00:00 2001 From: luijait Date: Mon, 29 Sep 2025 17:30:40 +0200 Subject: [PATCH] Agent as a tool fix --- .../agents/models/openai_chatcompletions.py | 27 ++++++----- src/cai/util.py | 48 ++++++++++++++++++- 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/src/cai/sdk/agents/models/openai_chatcompletions.py b/src/cai/sdk/agents/models/openai_chatcompletions.py index b7e424f9..80706946 100644 --- a/src/cai/sdk/agents/models/openai_chatcompletions.py +++ b/src/cai/sdk/agents/models/openai_chatcompletions.py @@ -2534,21 +2534,22 @@ class OpenAIChatCompletionsModel(Model): # start by re-fetching self.is_ollama self.is_ollama = os.getenv("OLLAMA") is not None and os.getenv("OLLAMA").lower() == "true" - # IMPORTANT: Include existing message history for context + # IMPORTANT: Build the prompt from history and avoid double-adding the current input. + # History already contains the latest user/tool messages for this turn. converted_messages = [] - - # First, add all existing messages from history + + # Add existing history first (copy to avoid in-place mutations) if self.message_history: for msg in self.message_history: - msg_copy = msg.copy() # Use copy to avoid modifying original - # Remove any existing cache_control to avoid exceeding the 4-block limit - if "cache_control" in msg_copy: - del msg_copy["cache_control"] + msg_copy = msg.copy() + # Remove any existing cache_control to avoid exceeding provider limits + msg_copy.pop("cache_control", None) converted_messages.append(msg_copy) - - # Then convert and add the new input - new_messages = self._converter.items_to_messages(input, model_instance=self) - converted_messages.extend(new_messages) + + # Only add converted input if there is no history yet (e.g., very first turn) + if not self.message_history: + new_messages = self._converter.items_to_messages(input, model_instance=self) + converted_messages.extend(new_messages) if system_instructions: # Check if we already have a system message @@ -3233,7 +3234,7 @@ class OpenAIChatCompletionsModel(Model): try: if stream: # Standard LiteLLM handling for streaming - ret = await litellm.acompletion(**kwargs) + # Create a single streaming generator; avoid duplicate calls which leak connections stream_obj = await litellm.acompletion(**kwargs) response = Response( @@ -3287,7 +3288,7 @@ class OpenAIChatCompletionsModel(Model): kwargs["messages"] = messages # Retry once, silently if stream: - ret = await litellm.acompletion(**kwargs) + # Retry with a single streaming generator stream_obj = await litellm.acompletion(**kwargs) response = Response( id=FAKE_RESPONSES_ID, diff --git a/src/cai/util.py b/src/cai/util.py index 8385d9a5..fa4c4405 100644 --- a/src/cai/util.py +++ b/src/cai/util.py @@ -1377,7 +1377,53 @@ def fix_message_list(messages): # pylint: disable=R0914,R0915,R0912 i += 2 else: i += 1 - return processed_messages + + # Final de-duplication pass to remove accidental duplicates in chat history + # This addresses cases where the latest user message or tool pair was added twice upstream. + deduped = [] + for msg in processed_messages: + if not deduped: + deduped.append(msg) + continue + + prev = deduped[-1] + + # Normalize messages for comparison (ignore ephemeral fields ordering) + def _norm_tool_calls(m): + tcs = m.get("tool_calls") or [] + norm = [] + for tc in tcs: + if isinstance(tc, dict): + fn = tc.get("function") or {} + norm.append((tc.get("type"), fn.get("name"), fn.get("arguments"))) + return tuple(norm) + + same_role = prev.get("role") == msg.get("role") + # Compare content for text messages + same_content = str(prev.get("content")) == str(msg.get("content")) + # Compare tool ids for tool messages + same_tool_id = ( + prev.get("role") == "tool" + and msg.get("role") == "tool" + and str(prev.get("tool_call_id")) == str(msg.get("tool_call_id")) + and str(prev.get("content")) == str(msg.get("content")) + ) + # Compare assistant tool calls by name/args (IDs may differ by truncation) + same_assistant_tool_calls = ( + prev.get("role") == "assistant" + and msg.get("role") == "assistant" + and prev.get("tool_calls") + and msg.get("tool_calls") + and _norm_tool_calls(prev) == _norm_tool_calls(msg) + ) + + if (same_role and same_content) or same_tool_id or same_assistant_tool_calls: + # Skip exact duplicate + continue + + deduped.append(msg) + + return deduped def cli_print_tool_call(tool_name="", args="", output="", prefix=" "):