mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-09-05 15:36:45 +08:00
feat: support function call
This commit is contained in:
@@ -37,13 +37,15 @@ class OpenAIMessageConverter(MessageConverter):
|
|||||||
parts = []
|
parts = []
|
||||||
|
|
||||||
if isinstance(msg["content"], str):
|
if isinstance(msg["content"], str):
|
||||||
parts.append({"text": msg["content"]})
|
# 请求 gemini 接口时如果包含 content 字段但内容为空时会返回 400 错误,所以需要判断是否为空
|
||||||
|
if msg["content"]:
|
||||||
|
parts.append({"text": msg["content"]})
|
||||||
elif isinstance(msg["content"], list):
|
elif isinstance(msg["content"], list):
|
||||||
for content in msg["content"]:
|
for content in msg["content"]:
|
||||||
if isinstance(content, str):
|
if isinstance(content, str) and content:
|
||||||
parts.append({"text": content})
|
parts.append({"text": content})
|
||||||
elif isinstance(content, dict):
|
elif isinstance(content, dict):
|
||||||
if content["type"] == "text":
|
if content["type"] == "text" and content["text"]:
|
||||||
parts.append({"text": content["text"]})
|
parts.append({"text": content["text"]})
|
||||||
elif content["type"] == "image_url":
|
elif content["type"] == "image_url":
|
||||||
parts.append(_convert_image(content["image_url"]["url"]))
|
parts.append(_convert_image(content["image_url"]["url"]))
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
# app/services/chat/response_handler.py
|
# app/services/chat/response_handler.py
|
||||||
|
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
import string
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, List, Optional
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
@@ -29,40 +32,38 @@ class GeminiResponseHandler(ResponseHandler):
|
|||||||
|
|
||||||
|
|
||||||
def _handle_openai_stream_response(response: Dict[str, Any], model: str, finish_reason: str) -> Dict[str, Any]:
|
def _handle_openai_stream_response(response: Dict[str, Any], model: str, finish_reason: str) -> Dict[str, Any]:
|
||||||
text = _extract_text(response, model, stream=True)
|
text, tool_calls = _extract_result(response, model, stream=True, gemini_format=False)
|
||||||
|
if not text and not tool_calls:
|
||||||
|
delta = {}
|
||||||
|
else:
|
||||||
|
delta = {"content": text, "role": "assistant"}
|
||||||
|
if tool_calls:
|
||||||
|
delta["tool_calls"] = tool_calls
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"id": f"chatcmpl-{uuid.uuid4()}",
|
"id": f"chatcmpl-{uuid.uuid4()}",
|
||||||
"object": "chat.completion.chunk",
|
"object": "chat.completion.chunk",
|
||||||
"created": int(time.time()),
|
"created": int(time.time()),
|
||||||
"model": model,
|
"model": model,
|
||||||
"choices": [{
|
"choices": [{"index": 0, "delta": delta, "finish_reason": finish_reason}],
|
||||||
"index": 0,
|
|
||||||
"delta": {"content": text} if text else {},
|
|
||||||
"finish_reason": finish_reason
|
|
||||||
}]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _handle_openai_normal_response(response: Dict[str, Any], model: str, finish_reason: str) -> Dict[str, Any]:
|
def _handle_openai_normal_response(response: Dict[str, Any], model: str, finish_reason: str) -> Dict[str, Any]:
|
||||||
text = _extract_text(response, model, stream=False)
|
text, tool_calls = _extract_result(response, model, stream=False, gemini_format=False)
|
||||||
return {
|
return {
|
||||||
"id": f"chatcmpl-{uuid.uuid4()}",
|
"id": f"chatcmpl-{uuid.uuid4()}",
|
||||||
"object": "chat.completion",
|
"object": "chat.completion",
|
||||||
"created": int(time.time()),
|
"created": int(time.time()),
|
||||||
"model": model,
|
"model": model,
|
||||||
"choices": [{
|
"choices": [
|
||||||
"index": 0,
|
{
|
||||||
"message": {
|
"index": 0,
|
||||||
"role": "assistant",
|
"message": {"role": "assistant", "content": text, "tool_calls": tool_calls},
|
||||||
"content": text
|
"finish_reason": finish_reason,
|
||||||
},
|
}
|
||||||
"finish_reason": finish_reason
|
],
|
||||||
}],
|
"usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0},
|
||||||
"usage": {
|
|
||||||
"prompt_tokens": 0,
|
|
||||||
"completion_tokens": 0,
|
|
||||||
"total_tokens": 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -127,8 +128,8 @@ def _handle_openai_normal_image_response(image_str: str,model: str,finish_reason
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _extract_text(response: Dict[str, Any], model: str, stream: bool = False) -> str:
|
def _extract_result(response: Dict[str, Any], model: str, stream: bool = False, gemini_format: bool = False) -> tuple[str, List[Dict[str, Any]]]:
|
||||||
text = ""
|
text, tool_calls = "", []
|
||||||
if stream:
|
if stream:
|
||||||
if response.get("candidates"):
|
if response.get("candidates"):
|
||||||
candidate = response["candidates"][0]
|
candidate = response["candidates"][0]
|
||||||
@@ -212,6 +213,7 @@ def _extract_text(response: Dict[str, Any], model: str, stream: bool = False) ->
|
|||||||
else:
|
else:
|
||||||
text = ""
|
text = ""
|
||||||
text = _add_search_link_text(model, candidate, text)
|
text = _add_search_link_text(model, candidate, text)
|
||||||
|
tool_calls = _extract_tool_calls(parts, gemini_format)
|
||||||
else:
|
else:
|
||||||
if response.get("candidates"):
|
if response.get("candidates"):
|
||||||
candidate = response["candidates"][0]
|
candidate = response["candidates"][0]
|
||||||
@@ -234,23 +236,65 @@ def _extract_text(response: Dict[str, Any], model: str, stream: bool = False) ->
|
|||||||
else:
|
else:
|
||||||
text = ""
|
text = ""
|
||||||
for part in candidate["content"]["parts"]:
|
for part in candidate["content"]["parts"]:
|
||||||
text += part["text"]
|
text += part.get("text", "")
|
||||||
text = _add_search_link_text(model, candidate, text)
|
text = _add_search_link_text(model, candidate, text)
|
||||||
|
tool_calls = _extract_tool_calls(candidate["content"]["parts"], gemini_format)
|
||||||
else:
|
else:
|
||||||
text = "暂无返回"
|
text = "暂无返回"
|
||||||
return text
|
return text, tool_calls
|
||||||
|
|
||||||
|
def _extract_tool_calls(parts: List[Dict[str, Any]], gemini_format: bool) -> List[Dict[str, Any]]:
|
||||||
|
"""提取工具调用信息"""
|
||||||
|
if not parts or not isinstance(parts, list):
|
||||||
|
return []
|
||||||
|
|
||||||
|
letters = string.ascii_lowercase + string.digits
|
||||||
|
|
||||||
|
tool_calls = list()
|
||||||
|
for i in range(len(parts)):
|
||||||
|
part = parts[i]
|
||||||
|
if not part or not isinstance(part, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
item = part.get("functionCall", {})
|
||||||
|
if not item or not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if gemini_format:
|
||||||
|
tool_calls.append(part)
|
||||||
|
else:
|
||||||
|
id = f"call_{''.join(random.sample(letters, 32))}"
|
||||||
|
name = item.get("name", "")
|
||||||
|
arguments = json.dumps(item.get("args", None) or {})
|
||||||
|
|
||||||
|
tool_calls.append(
|
||||||
|
{
|
||||||
|
"index": i,
|
||||||
|
"id": id,
|
||||||
|
"type": "function",
|
||||||
|
"function": {"name": name, "arguments": arguments},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return tool_calls
|
||||||
|
|
||||||
|
|
||||||
def _handle_gemini_stream_response(response: Dict[str, Any], model: str, stream: bool) -> Dict[str, Any]:
|
def _handle_gemini_stream_response(response: Dict[str, Any], model: str, stream: bool) -> Dict[str, Any]:
|
||||||
text = _extract_text(response, model, stream=stream)
|
text, tool_calls = _extract_result(response, model, stream=stream, gemini_format=True)
|
||||||
content = {"parts": [{"text": text}], "role": "model"}
|
if tool_calls:
|
||||||
|
content = {"parts": tool_calls, "role": "model"}
|
||||||
|
else:
|
||||||
|
content = {"parts": [{"text": text}], "role": "model"}
|
||||||
response["candidates"][0]["content"] = content
|
response["candidates"][0]["content"] = content
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def _handle_gemini_normal_response(response: Dict[str, Any], model: str, stream: bool) -> Dict[str, Any]:
|
def _handle_gemini_normal_response(response: Dict[str, Any], model: str, stream: bool) -> Dict[str, Any]:
|
||||||
text = _extract_text(response, model, stream=stream)
|
text, tool_calls = _extract_result(response, model, stream=stream, gemini_format=True)
|
||||||
content = {"parts": [{"text": text}], "role": "model"}
|
if tool_calls:
|
||||||
|
content = {"parts": tool_calls, "role": "model"}
|
||||||
|
else:
|
||||||
|
content = {"parts": [{"text": text}], "role": "model"}
|
||||||
response["candidates"][0]["content"] = content
|
response["candidates"][0]["content"] = content
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# app/services/chat_service.py
|
# app/services/chat_service.py
|
||||||
|
|
||||||
|
from copy import deepcopy
|
||||||
import json
|
import json
|
||||||
from typing import Dict, Any, AsyncGenerator, List, Union
|
from typing import Dict, Any, AsyncGenerator, List, Union
|
||||||
from app.core.logger import get_openai_logger
|
from app.core.logger import get_openai_logger
|
||||||
@@ -39,6 +40,25 @@ def _build_tools(
|
|||||||
tools.append({"code_execution": {}})
|
tools.append({"code_execution": {}})
|
||||||
if model.endswith("-search"):
|
if model.endswith("-search"):
|
||||||
tools.append({"googleSearch": {}})
|
tools.append({"googleSearch": {}})
|
||||||
|
|
||||||
|
# 将 request 中的 tools 合并到 tools 中
|
||||||
|
if request.tools:
|
||||||
|
function_declarations = []
|
||||||
|
for tool in request.tools:
|
||||||
|
if not tool or not isinstance(tool, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if tool.get("type", "") == "function" and tool.get("function"):
|
||||||
|
function = deepcopy(tool.get("function"))
|
||||||
|
parameters = function.get("parameters", {})
|
||||||
|
if parameters.get("type") == "object" and not parameters.get("properties", {}):
|
||||||
|
function.pop("parameters", None)
|
||||||
|
|
||||||
|
function_declarations.append(function)
|
||||||
|
|
||||||
|
if function_declarations:
|
||||||
|
tools.append({"functionDeclarations": function_declarations})
|
||||||
|
|
||||||
return tools
|
return tools
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user