fix: Properly parse tool calls in Google streaming responses

- Accumulate all streaming chunks before parsing
- Parse complete response at end of stream
- Detect and convert tool calls from accumulated text content
- Fixes issue where tool calls were returned as text instead of tool_calls structure
parent 7daa1c22
...@@ -308,12 +308,130 @@ class GoogleProviderHandler(BaseProviderHandler): ...@@ -308,12 +308,130 @@ class GoogleProviderHandler(BaseProviderHandler):
logging.info(f"GoogleProviderHandler: Streaming response received (total chunks: {len(chunks)})") logging.info(f"GoogleProviderHandler: Streaming response received (total chunks: {len(chunks)})")
self.record_success() self.record_success()
# Now yield chunks asynchronously # Parse the complete streaming response for tool calls
async def async_generator(): # Accumulate all chunks and parse the complete response
for chunk in chunks: response_text = ""
yield chunk tool_calls = None
finish_reason = "stop"
for chunk in chunks:
if hasattr(chunk, 'candidates') and chunk.candidates:
candidate = chunk.candidates[0]
if hasattr(candidate, 'content') and candidate.content:
if hasattr(candidate.content, 'parts'):
for part in candidate.content.parts:
if hasattr(part, 'text') and part.text:
response_text += part.text
# Check if the accumulated response contains tool calls
if response_text and not tool_calls:
import json
try:
# Try to parse as JSON
parsed_json = json.loads(response_text.strip())
if isinstance(parsed_json, dict):
# Check if it looks like a tool call
if 'action' in parsed_json or 'function' in parsed_json or 'name' in parsed_json:
# This appears to be a tool call in JSON format
# Convert to OpenAI tool_calls format
call_id = 0
openai_tool_calls = []
if 'action' in parsed_json:
# Google-style tool call
openai_tool_call = {
"id": f"call_{call_id}",
"type": "function",
"function": {
"name": parsed_json.get('action', 'unknown'),
"arguments": {k: v for k, v in parsed_json.items() if k != 'action'}
}
}
openai_tool_calls.append(openai_tool_call)
call_id += 1
logging.info(f"Detected tool call in streaming response: {parsed_json}")
# Clear response_text since we're using tool_calls instead
response_text = ""
elif 'function' in parsed_json or 'name' in parsed_json:
# OpenAI-style tool call
openai_tool_call = {
"id": f"call_{call_id}",
"type": "function",
"function": {
"name": parsed_json.get('name', parsed_json.get('function', 'unknown')),
"arguments": parsed_json.get('arguments', parsed_json.get('parameters', {}))
}
}
openai_tool_calls.append(openai_tool_call)
call_id += 1
logging.info(f"Detected tool call in streaming response: {parsed_json}")
# Clear response_text since we're using tool_calls instead
response_text = ""
tool_calls = openai_tool_calls
except (json.JSONDecodeError, Exception) as e:
logging.debug(f"Streaming response text is not valid JSON: {e}")
# Extract usage metadata from the last chunk
prompt_tokens = 0
completion_tokens = 0
total_tokens = 0
if chunks:
last_chunk = chunks[-1]
if hasattr(last_chunk, 'usage_metadata') and last_chunk.usage_metadata:
usage_metadata = last_chunk.usage_metadata
prompt_tokens = getattr(usage_metadata, 'prompt_token_count', 0)
completion_tokens = getattr(usage_metadata, 'candidates_token_count', 0)
total_tokens = getattr(usage_metadata, 'total_token_count', 0)
logging.info(f"GoogleProviderHandler: Usage metadata - prompt: {prompt_tokens}, completion: {completion_tokens}, total: {total_tokens}")
return async_generator() # Build the OpenAI-style response
openai_response = {
"id": f"google-{model}-{int(time.time())}",
"object": "chat.completion",
"created": int(time.time()),
"model": model,
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": response_text if response_text else None
},
"finish_reason": finish_reason
}],
"usage": {
"prompt_tokens": prompt_tokens,
"completion_tokens": completion_tokens,
"total_tokens": total_tokens
}
}
# Add tool_calls to the message if present
if tool_calls:
openai_response["choices"][0]["message"]["tool_calls"] = tool_calls
# If there are tool calls, content should be None (OpenAI convention)
openai_response["choices"][0]["message"]["content"] = None
logging.info(f"Added tool_calls to streaming response message")
# Log the final response structure
logging.info(f"=== FINAL OPENAI STREAMING RESPONSE STRUCTURE ===")
logging.info(f"Response type: {type(openai_response)}")
logging.info(f"Response keys: {openai_response.keys()}")
logging.info(f"Response id: {openai_response['id']}")
logging.info(f"Response object: {openai_response['object']}")
logging.info(f"Response created: {openai_response['created']}")
logging.info(f"Response model: {openai_response['model']}")
logging.info(f"Response choices count: {len(openai_response['choices'])}")
logging.info(f"Response choices[0] index: {openai_response['choices'][0]['index']}")
logging.info(f"Response choices[0] message role: {openai_response['choices'][0]['message']['role']}")
logging.info(f"Response choices[0] message content length: {len(openai_response['choices'][0]['message']['content'])}")
logging.info(f"Response choices[0] message content (first 200 chars): {openai_response['choices'][0]['message']['content'][:200]}")
logging.info(f"Response choices[0] finish_reason: {openai_response['choices'][0]['finish_reason']}")
logging.info(f"Response usage: {openai_response['usage']}")
logging.info(f"=== END FINAL OPENAI STREAMING RESPONSE STRUCTURE ===")
# Return the response dict directly
logging.info(f"GoogleProviderHandler: Returning streaming response dict")
return openai_response
else: else:
# Non-streaming request # Non-streaming request
# Generate content using the google-genai client # Generate content using the google-genai client
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment