mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-09-04 23:19:17 +08:00
feat: 添加对OpenAI工具调用功能的支持
改进消息转换器以处理OpenAI的tool_calls格式 添加JSON解析以正确转换函数调用参数 优化消息处理逻辑,增加更多空值检查 在流式响应中添加工具调用检测和处理 根据工具调用状态设置适当的finish_reason
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
# app/services/chat/message_converter.py
|
# app/services/chat/message_converter.py
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
import json
|
||||||
import re
|
import re
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
import requests
|
import requests
|
||||||
@@ -114,6 +115,36 @@ class OpenAIMessageConverter(MessageConverter):
|
|||||||
|
|
||||||
for idx, msg in enumerate(messages):
|
for idx, msg in enumerate(messages):
|
||||||
role = msg.get("role", "")
|
role = msg.get("role", "")
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
# 特别处理最后一个assistant的消息,按\n\n分割
|
||||||
|
if "content" in msg and isinstance(msg["content"], str) and msg["content"] and role == "assistant" and idx == len(messages) - 2:
|
||||||
|
# 按\n\n分割消息
|
||||||
|
content_parts = msg["content"].split("\n\n")
|
||||||
|
for part in content_parts:
|
||||||
|
if not part.strip(): # 跳过空内容
|
||||||
|
continue
|
||||||
|
# 处理可能包含图片的文本
|
||||||
|
parts.extend(_process_text_with_image(part))
|
||||||
|
elif "content" in msg and isinstance(msg["content"], str) and msg["content"]:
|
||||||
|
# 请求 gemini 接口时如果包含 content 字段但内容为空时会返回 400 错误,所以需要判断是否为空并移除
|
||||||
|
parts.extend(_process_text_with_image(msg["content"]))
|
||||||
|
elif "content" in msg and isinstance(msg["content"], list):
|
||||||
|
for content in msg["content"]:
|
||||||
|
if isinstance(content, str) and content:
|
||||||
|
parts.append({"text": content})
|
||||||
|
elif isinstance(content, dict):
|
||||||
|
if content["type"] == "text" and content["text"]:
|
||||||
|
parts.append({"text": content["text"]})
|
||||||
|
elif content["type"] == "image_url":
|
||||||
|
parts.append(_convert_image(content["image_url"]["url"]))
|
||||||
|
elif "tool_calls" in msg and isinstance(msg["tool_calls"], list):
|
||||||
|
for tool_call in msg["tool_calls"]:
|
||||||
|
function_call = tool_call.get("function",{})
|
||||||
|
function_call["args"] = json.loads(function_call.get("arguments","{}"))
|
||||||
|
del function_call["arguments"]
|
||||||
|
parts.append({"functionCall": function_call})
|
||||||
|
|
||||||
if role not in SUPPORTED_ROLES:
|
if role not in SUPPORTED_ROLES:
|
||||||
if role == "tool":
|
if role == "tool":
|
||||||
role = "user"
|
role = "user"
|
||||||
@@ -123,30 +154,6 @@ class OpenAIMessageConverter(MessageConverter):
|
|||||||
role = "user"
|
role = "user"
|
||||||
else:
|
else:
|
||||||
role = "model"
|
role = "model"
|
||||||
|
|
||||||
parts = []
|
|
||||||
# 特别处理最后一个assistant的消息,按\n\n分割
|
|
||||||
if role == "assistant" and idx == len(messages) - 2 and isinstance(msg["content"], str) and msg["content"]:
|
|
||||||
# 按\n\n分割消息
|
|
||||||
content_parts = msg["content"].split("\n\n")
|
|
||||||
for part in content_parts:
|
|
||||||
if not part.strip(): # 跳过空内容
|
|
||||||
continue
|
|
||||||
# 处理可能包含图片的文本
|
|
||||||
parts.extend(_process_text_with_image(part))
|
|
||||||
elif isinstance(msg["content"], str) and msg["content"]:
|
|
||||||
# 请求 gemini 接口时如果包含 content 字段但内容为空时会返回 400 错误,所以需要判断是否为空并移除
|
|
||||||
parts.extend(_process_text_with_image(msg["content"]))
|
|
||||||
elif isinstance(msg["content"], list):
|
|
||||||
for content in msg["content"]:
|
|
||||||
if isinstance(content, str) and content:
|
|
||||||
parts.append({"text": content})
|
|
||||||
elif isinstance(content, dict):
|
|
||||||
if content["type"] == "text" and content["text"]:
|
|
||||||
parts.append({"text": content["text"]})
|
|
||||||
elif content["type"] == "image_url":
|
|
||||||
parts.append(_convert_image(content["image_url"]["url"]))
|
|
||||||
|
|
||||||
if parts:
|
if parts:
|
||||||
if role == "system":
|
if role == "system":
|
||||||
system_instruction_parts.extend(parts)
|
system_instruction_parts.extend(parts)
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ def _build_tools(
|
|||||||
tool.pop("googleSearch", None)
|
tool.pop("googleSearch", None)
|
||||||
tool.pop("codeExecution", None)
|
tool.pop("codeExecution", None)
|
||||||
|
|
||||||
return [tool]
|
return [tool] if tool else []
|
||||||
|
|
||||||
|
|
||||||
def _get_safety_settings(model: str) -> List[Dict[str, str]]:
|
def _get_safety_settings(model: str) -> List[Dict[str, str]]:
|
||||||
@@ -201,10 +201,11 @@ class OpenAIChatService:
|
|||||||
max_retries = 3
|
max_retries = 3
|
||||||
while retries < max_retries:
|
while retries < max_retries:
|
||||||
try:
|
try:
|
||||||
|
tool_call_flag = False
|
||||||
async for line in self.api_client.stream_generate_content(
|
async for line in self.api_client.stream_generate_content(
|
||||||
payload, model, api_key
|
payload, model, api_key
|
||||||
):
|
):
|
||||||
# print(line)
|
print(line)
|
||||||
if line.startswith("data:"):
|
if line.startswith("data:"):
|
||||||
chunk = json.loads(line[6:])
|
chunk = json.loads(line[6:])
|
||||||
openai_chunk = self.response_handler.handle_response(
|
openai_chunk = self.response_handler.handle_response(
|
||||||
@@ -227,8 +228,13 @@ class OpenAIChatService:
|
|||||||
yield optimized_chunk
|
yield optimized_chunk
|
||||||
else:
|
else:
|
||||||
# 如果没有文本内容(如工具调用等),整块输出
|
# 如果没有文本内容(如工具调用等),整块输出
|
||||||
|
if "tool_calls" in json.dumps(openai_chunk):
|
||||||
|
tool_call_flag = True
|
||||||
yield f"data: {json.dumps(openai_chunk)}\n\n"
|
yield f"data: {json.dumps(openai_chunk)}\n\n"
|
||||||
yield f"data: {json.dumps(self.response_handler.handle_response({}, model, stream=True, finish_reason='stop'))}\n\n"
|
if tool_call_flag:
|
||||||
|
yield f"data: {json.dumps(self.response_handler.handle_response({}, model, stream=True, finish_reason='tool_calls'))}\n\n"
|
||||||
|
else:
|
||||||
|
yield f"data: {json.dumps(self.response_handler.handle_response({}, model, stream=True, finish_reason='stop'))}\n\n"
|
||||||
yield "data: [DONE]\n\n"
|
yield "data: [DONE]\n\n"
|
||||||
logger.info("Streaming completed successfully")
|
logger.info("Streaming completed successfully")
|
||||||
break # 成功后退出循环
|
break # 成功后退出循环
|
||||||
|
|||||||
Reference in New Issue
Block a user