diff --git a/models/openai_compatible_bot.py b/models/openai_compatible_bot.py index 1fcad7d7..aba5b327 100644 --- a/models/openai_compatible_bot.py +++ b/models/openai_compatible_bot.py @@ -249,24 +249,49 @@ class OpenAICompatibleBot: def _convert_messages_to_openai_format(self, messages): """ Convert messages from Claude format to OpenAI format - - Claude uses content blocks with types like 'tool_use', 'tool_result' - OpenAI uses 'tool_calls' in assistant messages and 'tool' role for results + + Claude content blocks (tool_use / tool_result / thinking) → OpenAI + tool_calls / tool role / reasoning_content. Some thinking-mode + providers require reasoning_content on assistant messages after a + tool_call appears in history; back-fill with empty string when the + trace was not captured. """ if not messages: return [] - + + # Detect any prior tool-call turn — gates reasoning_content back-fill below. + has_tool_call_history = False + for msg in messages: + if msg.get("role") != "assistant": + continue + if msg.get("tool_calls"): + has_tool_call_history = True + break + inner = msg.get("content") + if isinstance(inner, list) and any( + isinstance(b, dict) and b.get("type") == "tool_use" for b in inner + ): + has_tool_call_history = True + break + openai_messages = [] - + for msg in messages: role = msg.get("role") content = msg.get("content") - + # Handle string content (already in correct format) if isinstance(content, str): - openai_messages.append(msg) + if (role == "assistant" and has_tool_call_history + and isinstance(msg, dict) + and "reasoning_content" not in msg): + patched = dict(msg) + patched["reasoning_content"] = "" + openai_messages.append(patched) + else: + openai_messages.append(msg) continue - + # Handle list content (Claude format with content blocks) if isinstance(content, list): # Check if this is a tool result message (user role with tool_result blocks) @@ -305,14 +330,17 @@ class OpenAICompatibleBot: # Check if this is an assistant message with tool_use blocks elif role == "assistant": - # Separate text content and tool_use blocks text_parts = [] tool_calls = [] + reasoning_parts = [] for block in content: - if block.get("type") == "text": + if not isinstance(block, dict): + continue + btype = block.get("type") + if btype == "text": text_parts.append(block.get("text", "")) - elif block.get("type") == "tool_use": + elif btype == "tool_use": tool_id = block.get("id") or "" if not tool_id: logger.warning(f"[OpenAICompatible] tool_use missing id for '{block.get('name')}'") @@ -324,6 +352,8 @@ class OpenAICompatibleBot: "arguments": json.dumps(block.get("input", {})) } }) + elif btype == "thinking": + reasoning_parts.append(block.get("thinking", "")) # Build OpenAI format assistant message openai_msg = { @@ -334,6 +364,13 @@ class OpenAICompatibleBot: if tool_calls: openai_msg["tool_calls"] = tool_calls + # Round-trip reasoning_content; empty string when missing + # after a tool-call turn keeps strict providers happy. + if reasoning_parts: + openai_msg["reasoning_content"] = "\n".join(reasoning_parts) + elif has_tool_call_history: + openai_msg["reasoning_content"] = "" + if msg.get("_gemini_raw_parts"): openai_msg["_gemini_raw_parts"] = msg["_gemini_raw_parts"]