Files
Aether/src/api/handlers/gemini/converter.py

549 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Gemini 格式转换器
提供 Gemini 与其他 API 格式Claude、OpenAI之间的转换
"""
from typing import Any, Dict, List, Optional
class ClaudeToGeminiConverter:
"""
Claude -> Gemini 请求转换器
将 Claude Messages API 格式转换为 Gemini generateContent 格式
"""
def convert_request(self, claude_request: Dict[str, Any]) -> Dict[str, Any]:
"""
将 Claude 请求转换为 Gemini 请求
Args:
claude_request: Claude 格式的请求字典
Returns:
Gemini 格式的请求字典
"""
gemini_request: Dict[str, Any] = {
"contents": self._convert_messages(claude_request.get("messages", [])),
}
# 转换 system prompt
system = claude_request.get("system")
if system:
gemini_request["system_instruction"] = self._convert_system(system)
# 转换生成配置
generation_config = self._build_generation_config(claude_request)
if generation_config:
gemini_request["generation_config"] = generation_config
# 转换工具
tools = claude_request.get("tools")
if tools:
gemini_request["tools"] = self._convert_tools(tools)
return gemini_request
def _convert_messages(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""转换消息列表"""
contents = []
for msg in messages:
role = msg.get("role", "user")
# Gemini 使用 "model" 而不是 "assistant"
gemini_role = "model" if role == "assistant" else "user"
content = msg.get("content", "")
parts = self._convert_content_to_parts(content)
contents.append(
{
"role": gemini_role,
"parts": parts,
}
)
return contents
def _convert_content_to_parts(self, content: Any) -> List[Dict[str, Any]]:
"""将 Claude 内容转换为 Gemini parts"""
if isinstance(content, str):
return [{"text": content}]
if isinstance(content, list):
parts: List[Dict[str, Any]] = []
for block in content:
if isinstance(block, str):
parts.append({"text": block})
elif isinstance(block, dict):
block_type = block.get("type")
if block_type == "text":
parts.append({"text": block.get("text", "")})
elif block_type == "image":
# 转换图片
source = block.get("source", {})
if source.get("type") == "base64":
parts.append(
{
"inline_data": {
"mime_type": source.get("media_type", "image/png"),
"data": source.get("data", ""),
}
}
)
elif block_type == "tool_use":
# 转换工具调用
parts.append(
{
"function_call": {
"name": block.get("name", ""),
"args": block.get("input", {}),
}
}
)
elif block_type == "tool_result":
# 转换工具结果
parts.append(
{
"function_response": {
"name": block.get("tool_use_id", ""),
"response": {"result": block.get("content", "")},
}
}
)
return parts
return [{"text": str(content)}]
def _convert_system(self, system: Any) -> Dict[str, Any]:
"""转换 system prompt"""
if isinstance(system, str):
return {"parts": [{"text": system}]}
if isinstance(system, list):
parts = []
for item in system:
if isinstance(item, str):
parts.append({"text": item})
elif isinstance(item, dict) and item.get("type") == "text":
parts.append({"text": item.get("text", "")})
return {"parts": parts}
return {"parts": [{"text": str(system)}]}
def _build_generation_config(self, claude_request: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""构建生成配置"""
config: Dict[str, Any] = {}
if "max_tokens" in claude_request:
config["max_output_tokens"] = claude_request["max_tokens"]
if "temperature" in claude_request:
config["temperature"] = claude_request["temperature"]
if "top_p" in claude_request:
config["top_p"] = claude_request["top_p"]
if "top_k" in claude_request:
config["top_k"] = claude_request["top_k"]
if "stop_sequences" in claude_request:
config["stop_sequences"] = claude_request["stop_sequences"]
return config if config else None
def _convert_tools(self, tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""转换工具定义"""
function_declarations = []
for tool in tools:
func_decl = {
"name": tool.get("name", ""),
}
if "description" in tool:
func_decl["description"] = tool["description"]
if "input_schema" in tool:
func_decl["parameters"] = tool["input_schema"]
function_declarations.append(func_decl)
return [{"function_declarations": function_declarations}]
class GeminiToClaudeConverter:
"""
Gemini -> Claude 响应转换器
将 Gemini generateContent 响应转换为 Claude Messages API 格式
"""
def convert_response(self, gemini_response: Dict[str, Any]) -> Dict[str, Any]:
"""
将 Gemini 响应转换为 Claude 响应
Args:
gemini_response: Gemini 格式的响应字典
Returns:
Claude 格式的响应字典
"""
candidates = gemini_response.get("candidates", [])
if not candidates:
return self._create_empty_response()
candidate = candidates[0]
content = candidate.get("content", {})
parts = content.get("parts", [])
# 转换内容块
claude_content = self._convert_parts_to_content(parts)
# 转换使用量
usage = self._convert_usage(gemini_response.get("usageMetadata", {}))
# 转换停止原因
stop_reason = self._convert_finish_reason(candidate.get("finishReason"))
return {
"id": f"msg_{gemini_response.get('modelVersion', 'gemini')}",
"type": "message",
"role": "assistant",
"content": claude_content,
"model": gemini_response.get("modelVersion", "gemini"),
"stop_reason": stop_reason,
"stop_sequence": None,
"usage": usage,
}
def _convert_parts_to_content(self, parts: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""将 Gemini parts 转换为 Claude content blocks"""
content = []
for part in parts:
if "text" in part:
content.append(
{
"type": "text",
"text": part["text"],
}
)
elif "functionCall" in part:
func_call = part["functionCall"]
content.append(
{
"type": "tool_use",
"id": f"toolu_{func_call.get('name', '')}",
"name": func_call.get("name", ""),
"input": func_call.get("args", {}),
}
)
return content
def _convert_usage(self, usage_metadata: Dict[str, Any]) -> Dict[str, int]:
"""转换使用量信息"""
return {
"input_tokens": usage_metadata.get("promptTokenCount", 0),
"output_tokens": usage_metadata.get("candidatesTokenCount", 0),
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": usage_metadata.get("cachedContentTokenCount", 0),
}
def _convert_finish_reason(self, finish_reason: Optional[str]) -> Optional[str]:
"""转换停止原因"""
mapping = {
"STOP": "end_turn",
"MAX_TOKENS": "max_tokens",
"SAFETY": "content_filtered",
"RECITATION": "content_filtered",
"OTHER": "stop_sequence",
}
if finish_reason is None:
return "end_turn"
return mapping.get(finish_reason, "end_turn")
def _create_empty_response(self) -> Dict[str, Any]:
"""创建空响应"""
return {
"id": "msg_empty",
"type": "message",
"role": "assistant",
"content": [],
"model": "gemini",
"stop_reason": "end_turn",
"stop_sequence": None,
"usage": {
"input_tokens": 0,
"output_tokens": 0,
},
}
class OpenAIToGeminiConverter:
"""
OpenAI -> Gemini 请求转换器
将 OpenAI Chat Completions API 格式转换为 Gemini generateContent 格式
"""
def convert_request(self, openai_request: Dict[str, Any]) -> Dict[str, Any]:
"""
将 OpenAI 请求转换为 Gemini 请求
Args:
openai_request: OpenAI 格式的请求字典
Returns:
Gemini 格式的请求字典
"""
messages = openai_request.get("messages", [])
# 分离 system 消息和其他消息
system_messages = []
other_messages = []
for msg in messages:
if msg.get("role") == "system":
system_messages.append(msg)
else:
other_messages.append(msg)
gemini_request: Dict[str, Any] = {
"contents": self._convert_messages(other_messages),
}
# 转换 system messages
if system_messages:
system_text = "\n".join(msg.get("content", "") for msg in system_messages)
gemini_request["system_instruction"] = {"parts": [{"text": system_text}]}
# 转换生成配置
generation_config = self._build_generation_config(openai_request)
if generation_config:
gemini_request["generation_config"] = generation_config
# 转换工具
tools = openai_request.get("tools")
if tools:
gemini_request["tools"] = self._convert_tools(tools)
return gemini_request
def _convert_messages(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""转换消息列表"""
contents = []
for msg in messages:
role = msg.get("role", "user")
gemini_role = "model" if role == "assistant" else "user"
content = msg.get("content", "")
parts = self._convert_content_to_parts(content)
# 处理工具调用
tool_calls = msg.get("tool_calls", [])
for tc in tool_calls:
if tc.get("type") == "function":
func = tc.get("function", {})
import json
try:
args = json.loads(func.get("arguments", "{}"))
except json.JSONDecodeError:
args = {}
parts.append(
{
"function_call": {
"name": func.get("name", ""),
"args": args,
}
}
)
if parts:
contents.append(
{
"role": gemini_role,
"parts": parts,
}
)
return contents
def _convert_content_to_parts(self, content: Any) -> List[Dict[str, Any]]:
"""将 OpenAI 内容转换为 Gemini parts"""
if content is None:
return []
if isinstance(content, str):
return [{"text": content}]
if isinstance(content, list):
parts: List[Dict[str, Any]] = []
for item in content:
if isinstance(item, str):
parts.append({"text": item})
elif isinstance(item, dict):
item_type = item.get("type")
if item_type == "text":
parts.append({"text": item.get("text", "")})
elif item_type == "image_url":
# OpenAI 图片 URL 格式
image_url = item.get("image_url", {})
url = image_url.get("url", "")
if url.startswith("data:"):
# base64 数据 URL
# 格式: data:image/png;base64,xxxxx
try:
header, data = url.split(",", 1)
mime_type = header.split(":")[1].split(";")[0]
parts.append(
{
"inline_data": {
"mime_type": mime_type,
"data": data,
}
}
)
except (ValueError, IndexError):
pass
return parts
return [{"text": str(content)}]
def _build_generation_config(self, openai_request: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""构建生成配置"""
config: Dict[str, Any] = {}
if "max_tokens" in openai_request:
config["max_output_tokens"] = openai_request["max_tokens"]
if "temperature" in openai_request:
config["temperature"] = openai_request["temperature"]
if "top_p" in openai_request:
config["top_p"] = openai_request["top_p"]
if "stop" in openai_request:
stop = openai_request["stop"]
if isinstance(stop, str):
config["stop_sequences"] = [stop]
elif isinstance(stop, list):
config["stop_sequences"] = stop
if "n" in openai_request:
config["candidate_count"] = openai_request["n"]
return config if config else None
def _convert_tools(self, tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""转换工具定义"""
function_declarations = []
for tool in tools:
if tool.get("type") == "function":
func = tool.get("function", {})
func_decl = {
"name": func.get("name", ""),
}
if "description" in func:
func_decl["description"] = func["description"]
if "parameters" in func:
func_decl["parameters"] = func["parameters"]
function_declarations.append(func_decl)
return [{"function_declarations": function_declarations}]
class GeminiToOpenAIConverter:
"""
Gemini -> OpenAI 响应转换器
将 Gemini generateContent 响应转换为 OpenAI Chat Completions API 格式
"""
def convert_response(self, gemini_response: Dict[str, Any]) -> Dict[str, Any]:
"""
将 Gemini 响应转换为 OpenAI 响应
Args:
gemini_response: Gemini 格式的响应字典
Returns:
OpenAI 格式的响应字典
"""
import time
candidates = gemini_response.get("candidates", [])
choices = []
for i, candidate in enumerate(candidates):
content = candidate.get("content", {})
parts = content.get("parts", [])
# 提取文本内容
text_parts = []
tool_calls = []
for part in parts:
if "text" in part:
text_parts.append(part["text"])
elif "functionCall" in part:
func_call = part["functionCall"]
import json
tool_calls.append(
{
"id": f"call_{func_call.get('name', '')}_{i}",
"type": "function",
"function": {
"name": func_call.get("name", ""),
"arguments": json.dumps(func_call.get("args", {})),
},
}
)
message: Dict[str, Any] = {
"role": "assistant",
"content": "".join(text_parts) if text_parts else None,
}
if tool_calls:
message["tool_calls"] = tool_calls
finish_reason = self._convert_finish_reason(candidate.get("finishReason"))
choices.append(
{
"index": i,
"message": message,
"finish_reason": finish_reason,
}
)
# 转换使用量
usage = self._convert_usage(gemini_response.get("usageMetadata", {}))
return {
"id": f"chatcmpl-{gemini_response.get('modelVersion', 'gemini')}",
"object": "chat.completion",
"created": int(time.time()),
"model": gemini_response.get("modelVersion", "gemini"),
"choices": choices,
"usage": usage,
}
def _convert_usage(self, usage_metadata: Dict[str, Any]) -> Dict[str, int]:
"""转换使用量信息"""
prompt_tokens = usage_metadata.get("promptTokenCount", 0)
completion_tokens = usage_metadata.get("candidatesTokenCount", 0)
return {
"prompt_tokens": prompt_tokens,
"completion_tokens": completion_tokens,
"total_tokens": prompt_tokens + completion_tokens,
}
def _convert_finish_reason(self, finish_reason: Optional[str]) -> str:
"""转换停止原因"""
mapping = {
"STOP": "stop",
"MAX_TOKENS": "length",
"SAFETY": "content_filter",
"RECITATION": "content_filter",
"OTHER": "stop",
}
if finish_reason is None:
return "stop"
return mapping.get(finish_reason, "stop")
__all__ = [
"ClaudeToGeminiConverter",
"GeminiToClaudeConverter",
"OpenAIToGeminiConverter",
"GeminiToOpenAIConverter",
]