diff --git a/src/api/routes.py b/src/api/routes.py index c7ef803..c33977d 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -139,19 +139,25 @@ def parse_tool_calls(text: str) -> tuple: cleaned_text = re.sub(r'```(?:json)?\s*\n?(.+?)```', r'\1', cleaned_text, flags=re.DOTALL) cleaned_text = cleaned_text.strip() - # Try to find JSON with tool_calls - look for { tool_calls: [...] } pattern + # Try to find JSON with tool_calls - look for { tool_calls: [...] } or { tool_calls: {...} } pattern try: # Look for tool_calls inside braces (handle both quoted and unquoted keys) - pattern = r'\{\s*"?tool_calls"?\s*:\s*(\[.*?\])\s*\}' + # Match either an array \[...\] or a single object {...} + pattern = r'\{\s*"?tool_calls"?\s*:\s*(\[.*?\]|\{.*?\})\s*\}' match = re.search(pattern, cleaned_text, re.DOTALL) if match: - array_str = match.group(1) + value_str = match.group(1) # Try to parse as JSON first try: - tool_calls = json.loads(array_str) + parsed = json.loads(value_str) + # Normalize to list: if it's a dict (single tool), wrap in list + if isinstance(parsed, dict): + tool_calls = [parsed] + else: + tool_calls = parsed except json.JSONDecodeError: # Fix common JSON issues in model output - fixed = array_str + fixed = value_str # Step 1: Handle unquoted keys (JavaScript style) fixed = re.sub(r'([{,])\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:', r'\1"\2":', fixed) @@ -181,7 +187,12 @@ def parse_tool_calls(text: str) -> tuple: fixed = fixed.replace("'", '"') try: - tool_calls = json.loads(fixed) + parsed = json.loads(fixed) + # Normalize to list + if isinstance(parsed, dict): + tool_calls = [parsed] + else: + tool_calls = parsed except json.JSONDecodeError as e2: # If still fails, try one more approach - manual extraction try: @@ -190,7 +201,7 @@ def parse_tool_calls(text: str) -> tuple: # Find all function blocks - need to handle nested braces # Look for "function": {...} where ... can contain nested braces func_pattern = r'"function":\s*(\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\})' - func_matches = list(re.finditer(func_pattern, array_str, re.DOTALL)) + func_matches = list(re.finditer(func_pattern, value_str, re.DOTALL)) for i, func_match in enumerate(func_matches): func_content = func_match.group(1) @@ -367,6 +378,7 @@ async def chat_completions(request: ChatCompletionRequest): # Format messages into prompt (with tools if provided) prompt = format_messages_with_tools(request.messages, request.tools) has_tools = request.tools is not None and len(request.tools) > 0 + print(f"DEBUG: has_tools={has_tools}, tools_count={len(request.tools) if request.tools else 0}") # Generate ID completion_id = f"chatcmpl-{uuid.uuid4().hex[:12]}" @@ -507,6 +519,8 @@ async def chat_completions(request: ChatCompletionRequest): response_text = result.selected_response.text tokens_generated = result.selected_response.tokens_generated + print(f"DEBUG: Generated response (tokens={tokens_generated})") + print(f"DEBUG: Response preview: {response_text[:200]}...") # Parse tool calls if tools were provided content = response_text @@ -514,7 +528,9 @@ async def chat_completions(request: ChatCompletionRequest): finish_reason = "stop" if has_tools: + print(f"DEBUG: Parsing tool calls from response...") content, tool_calls_parsed = parse_tool_calls(response_text) + print(f"DEBUG: parse_tool_calls returned: content_len={len(content)}, parsed={tool_calls_parsed is not None}") if tool_calls_parsed: print(f" 🔧 Model requesting {len(tool_calls_parsed)} tool(s)...") executor = get_tool_executor() @@ -543,6 +559,10 @@ async def chat_completions(request: ChatCompletionRequest): finish_reason = "stop" tool_calls = [] # Clear tool_calls since we executed them print(f" ✅ All tools executed, returning results") + else: + print(f"DEBUG: No tool calls parsed from response") + else: + print(f"DEBUG: No tools requested, returning normal response") # Estimate prompt tokens (rough approximation) prompt_tokens = len(prompt) // 4