mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 09:50:21 +08:00
Initial commit
This commit is contained in:
26
src/api/handlers/gemini/__init__.py
Normal file
26
src/api/handlers/gemini/__init__.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
Gemini API Handler 模块
|
||||
|
||||
提供 Gemini API 格式的请求处理
|
||||
"""
|
||||
|
||||
from src.api.handlers.gemini.adapter import GeminiChatAdapter, build_gemini_adapter
|
||||
from src.api.handlers.gemini.converter import (
|
||||
ClaudeToGeminiConverter,
|
||||
GeminiToClaudeConverter,
|
||||
GeminiToOpenAIConverter,
|
||||
OpenAIToGeminiConverter,
|
||||
)
|
||||
from src.api.handlers.gemini.handler import GeminiChatHandler
|
||||
from src.api.handlers.gemini.stream_parser import GeminiStreamParser
|
||||
|
||||
__all__ = [
|
||||
"GeminiChatAdapter",
|
||||
"GeminiChatHandler",
|
||||
"GeminiStreamParser",
|
||||
"ClaudeToGeminiConverter",
|
||||
"GeminiToClaudeConverter",
|
||||
"OpenAIToGeminiConverter",
|
||||
"GeminiToOpenAIConverter",
|
||||
"build_gemini_adapter",
|
||||
]
|
||||
170
src/api/handlers/gemini/adapter.py
Normal file
170
src/api/handlers/gemini/adapter.py
Normal file
@@ -0,0 +1,170 @@
|
||||
"""
|
||||
Gemini Chat Adapter
|
||||
|
||||
处理 Gemini API 格式的请求适配
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, Optional, Type
|
||||
|
||||
from fastapi import HTTPException, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from src.api.handlers.base.chat_adapter_base import ChatAdapterBase, register_adapter
|
||||
from src.api.handlers.base.chat_handler_base import ChatHandlerBase
|
||||
from src.core.logger import logger
|
||||
from src.models.gemini import GeminiRequest
|
||||
|
||||
|
||||
@register_adapter
|
||||
class GeminiChatAdapter(ChatAdapterBase):
|
||||
"""
|
||||
Gemini Chat API 适配器
|
||||
|
||||
处理 Gemini Chat 格式的请求
|
||||
端点: /v1beta/models/{model}:generateContent
|
||||
"""
|
||||
|
||||
FORMAT_ID = "GEMINI"
|
||||
name = "gemini.chat"
|
||||
|
||||
@property
|
||||
def HANDLER_CLASS(self) -> Type[ChatHandlerBase]:
|
||||
"""延迟导入 Handler 类避免循环依赖"""
|
||||
from src.api.handlers.gemini.handler import GeminiChatHandler
|
||||
|
||||
return GeminiChatHandler
|
||||
|
||||
def __init__(self, allowed_api_formats: Optional[list[str]] = None):
|
||||
super().__init__(allowed_api_formats or ["GEMINI"])
|
||||
logger.info(f"[{self.name}] 初始化 Gemini Chat 适配器 | API格式: {self.allowed_api_formats}")
|
||||
|
||||
def extract_api_key(self, request: Request) -> Optional[str]:
|
||||
"""从请求中提取 API 密钥 (x-goog-api-key)"""
|
||||
return request.headers.get("x-goog-api-key")
|
||||
|
||||
def _merge_path_params(
|
||||
self, original_request_body: Dict[str, Any], path_params: Dict[str, Any] # noqa: ARG002
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
合并 URL 路径参数到请求体 - Gemini 特化版本
|
||||
|
||||
Gemini API 特点:
|
||||
- model 不合并到请求体(通过 extract_model_from_request 从 path_params 获取)
|
||||
- stream 不合并到请求体(Gemini API 通过 URL 端点区分流式/非流式)
|
||||
|
||||
Handler 层的 extract_model_from_request 会从 path_params 获取 model,
|
||||
prepare_provider_request_body 会确保发送给 Gemini API 的请求体不含 model。
|
||||
|
||||
Args:
|
||||
original_request_body: 原始请求体字典
|
||||
path_params: URL 路径参数字典(不使用)
|
||||
|
||||
Returns:
|
||||
原始请求体(不合并任何 path_params)
|
||||
"""
|
||||
return original_request_body.copy()
|
||||
|
||||
def _validate_request_body(self, original_request_body: dict, path_params: dict = None):
|
||||
"""验证请求体"""
|
||||
path_params = path_params or {}
|
||||
is_stream = path_params.get("stream", False)
|
||||
model = path_params.get("model", "unknown")
|
||||
|
||||
try:
|
||||
if not isinstance(original_request_body, dict):
|
||||
raise ValueError("Request body must be a JSON object")
|
||||
|
||||
# Gemini 必需字段: contents
|
||||
if "contents" not in original_request_body:
|
||||
raise ValueError("Missing required field: contents")
|
||||
|
||||
request = GeminiRequest.model_validate(
|
||||
original_request_body,
|
||||
strict=False,
|
||||
)
|
||||
except ValueError as e:
|
||||
logger.error(f"请求体基本验证失败: {str(e)}")
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.warning(f"Pydantic验证警告(将继续处理): {str(e)}")
|
||||
request = GeminiRequest.model_construct(
|
||||
contents=original_request_body.get("contents", []),
|
||||
)
|
||||
|
||||
# 设置 model(从 path_params 获取,用于日志和审计)
|
||||
request.model = model
|
||||
# 设置 stream 属性(用于 ChatAdapterBase 判断流式模式)
|
||||
request.stream = is_stream
|
||||
return request
|
||||
|
||||
def _extract_message_count(self, payload: Dict[str, Any], request_obj) -> int:
|
||||
"""提取消息数量"""
|
||||
contents = payload.get("contents", [])
|
||||
if hasattr(request_obj, "contents"):
|
||||
contents = request_obj.contents
|
||||
return len(contents) if isinstance(contents, list) else 0
|
||||
|
||||
def _build_audit_metadata(self, payload: Dict[str, Any], request_obj) -> Dict[str, Any]:
|
||||
"""构建 Gemini Chat 特定的审计元数据"""
|
||||
role_counts: dict[str, int] = {}
|
||||
|
||||
contents = getattr(request_obj, "contents", []) or []
|
||||
for content in contents:
|
||||
role = getattr(content, "role", None) or content.get("role", "unknown")
|
||||
role_counts[role] = role_counts.get(role, 0) + 1
|
||||
|
||||
generation_config = getattr(request_obj, "generation_config", None) or {}
|
||||
if hasattr(generation_config, "dict"):
|
||||
generation_config = generation_config.dict()
|
||||
elif not isinstance(generation_config, dict):
|
||||
generation_config = {}
|
||||
|
||||
# 判断流式模式
|
||||
stream = getattr(request_obj, "stream", False)
|
||||
|
||||
return {
|
||||
"action": "gemini_generate_content",
|
||||
"model": getattr(request_obj, "model", payload.get("model", "unknown")),
|
||||
"stream": bool(stream),
|
||||
"max_output_tokens": generation_config.get("max_output_tokens"),
|
||||
"temperature": generation_config.get("temperature"),
|
||||
"top_p": generation_config.get("top_p"),
|
||||
"top_k": generation_config.get("top_k"),
|
||||
"contents_count": len(contents),
|
||||
"content_roles": role_counts,
|
||||
"tools_count": len(getattr(request_obj, "tools", None) or []),
|
||||
"system_instruction_present": bool(getattr(request_obj, "system_instruction", None)),
|
||||
"safety_settings_count": len(getattr(request_obj, "safety_settings", None) or []),
|
||||
}
|
||||
|
||||
def _error_response(self, status_code: int, error_type: str, message: str) -> JSONResponse:
|
||||
"""生成 Gemini 格式的错误响应"""
|
||||
# Gemini 错误响应格式
|
||||
return JSONResponse(
|
||||
status_code=status_code,
|
||||
content={
|
||||
"error": {
|
||||
"code": status_code,
|
||||
"message": message,
|
||||
"status": error_type.upper(),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def build_gemini_adapter(x_app_header: str = "") -> GeminiChatAdapter:
|
||||
"""
|
||||
根据请求头构建适当的 Gemini 适配器
|
||||
|
||||
Args:
|
||||
x_app_header: X-App 请求头值
|
||||
|
||||
Returns:
|
||||
GeminiChatAdapter 实例
|
||||
"""
|
||||
# 目前只有一种 Gemini 适配器
|
||||
# 未来可以根据 x_app_header 返回不同的适配器(如 CLI 模式)
|
||||
return GeminiChatAdapter()
|
||||
|
||||
|
||||
__all__ = ["GeminiChatAdapter", "build_gemini_adapter"]
|
||||
544
src/api/handlers/gemini/converter.py
Normal file
544
src/api/handlers/gemini/converter.py
Normal file
@@ -0,0 +1,544 @@
|
||||
"""
|
||||
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 = []
|
||||
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",
|
||||
}
|
||||
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 = []
|
||||
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]) -> Optional[str]:
|
||||
"""转换停止原因"""
|
||||
mapping = {
|
||||
"STOP": "stop",
|
||||
"MAX_TOKENS": "length",
|
||||
"SAFETY": "content_filter",
|
||||
"RECITATION": "content_filter",
|
||||
"OTHER": "stop",
|
||||
}
|
||||
return mapping.get(finish_reason, "stop")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ClaudeToGeminiConverter",
|
||||
"GeminiToClaudeConverter",
|
||||
"OpenAIToGeminiConverter",
|
||||
"GeminiToOpenAIConverter",
|
||||
]
|
||||
164
src/api/handlers/gemini/handler.py
Normal file
164
src/api/handlers/gemini/handler.py
Normal file
@@ -0,0 +1,164 @@
|
||||
"""
|
||||
Gemini Chat Handler
|
||||
|
||||
处理 Gemini API 格式的请求
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from src.api.handlers.base.chat_handler_base import ChatHandlerBase
|
||||
|
||||
|
||||
class GeminiChatHandler(ChatHandlerBase):
|
||||
"""
|
||||
Gemini Chat Handler - 处理 Google Gemini API 格式的请求
|
||||
|
||||
格式特点:
|
||||
- 使用 promptTokenCount / candidatesTokenCount
|
||||
- 支持 cachedContentTokenCount
|
||||
- 请求格式: GeminiRequest
|
||||
- 响应格式: JSON 数组流(非 SSE)
|
||||
"""
|
||||
|
||||
FORMAT_ID = "GEMINI"
|
||||
|
||||
def extract_model_from_request(
|
||||
self,
|
||||
request_body: Dict[str, Any],
|
||||
path_params: Optional[Dict[str, Any]] = None,
|
||||
) -> str:
|
||||
"""
|
||||
从请求中提取模型名 - Gemini Chat 格式实现
|
||||
|
||||
Gemini Chat 模式下,model 在请求体中(经过转换后的 GeminiRequest)。
|
||||
与 Gemini CLI 不同,CLI 模式的 model 在 URL 路径中。
|
||||
|
||||
Args:
|
||||
request_body: 请求体
|
||||
path_params: URL 路径参数(Chat 模式通常不使用)
|
||||
|
||||
Returns:
|
||||
模型名
|
||||
"""
|
||||
# 优先从请求体获取,其次从 path_params
|
||||
model = request_body.get("model")
|
||||
if model:
|
||||
return str(model)
|
||||
if path_params and "model" in path_params:
|
||||
return str(path_params["model"])
|
||||
return "unknown"
|
||||
|
||||
async def _convert_request(self, request):
|
||||
"""
|
||||
将请求转换为 Gemini 格式
|
||||
|
||||
支持自动转换:
|
||||
- Claude 格式 → Gemini 格式
|
||||
- OpenAI 格式 → Gemini 格式
|
||||
|
||||
Args:
|
||||
request: 原始请求对象(可能是 Gemini/Claude/OpenAI 格式)
|
||||
|
||||
Returns:
|
||||
GeminiRequest 对象
|
||||
"""
|
||||
from src.api.handlers.gemini.converter import (
|
||||
ClaudeToGeminiConverter,
|
||||
OpenAIToGeminiConverter,
|
||||
)
|
||||
from src.models.claude import ClaudeMessagesRequest
|
||||
from src.models.gemini import GeminiRequest
|
||||
from src.models.openai import OpenAIRequest
|
||||
|
||||
# 如果已经是 Gemini 格式,直接返回
|
||||
if isinstance(request, GeminiRequest):
|
||||
return request
|
||||
|
||||
# 如果是 Claude 格式,转换为 Gemini 格式
|
||||
if isinstance(request, ClaudeMessagesRequest):
|
||||
converter = ClaudeToGeminiConverter()
|
||||
gemini_dict = converter.convert_request(request.model_dump())
|
||||
return GeminiRequest(**gemini_dict)
|
||||
|
||||
# 如果是 OpenAI 格式,转换为 Gemini 格式
|
||||
if isinstance(request, OpenAIRequest):
|
||||
converter = OpenAIToGeminiConverter()
|
||||
gemini_dict = converter.convert_request(request.model_dump())
|
||||
return GeminiRequest(**gemini_dict)
|
||||
|
||||
# 如果是字典,根据内容判断格式并转换
|
||||
if isinstance(request, dict):
|
||||
# 检测 Gemini 格式特征: contents 字段
|
||||
if "contents" in request:
|
||||
return GeminiRequest(**request)
|
||||
|
||||
# 检测 Claude 格式特征: messages + 没有 choices
|
||||
if "messages" in request and "choices" not in request:
|
||||
# 进一步区分 Claude 和 OpenAI
|
||||
# Claude 使用 max_tokens,OpenAI 也可能有
|
||||
# Claude 的 messages[].content 可以是数组,OpenAI 通常是字符串
|
||||
messages = request.get("messages", [])
|
||||
if messages and isinstance(messages[0].get("content"), list):
|
||||
# 可能是 Claude 格式
|
||||
converter = ClaudeToGeminiConverter()
|
||||
gemini_dict = converter.convert_request(request)
|
||||
return GeminiRequest(**gemini_dict)
|
||||
else:
|
||||
# 可能是 OpenAI 格式
|
||||
converter = OpenAIToGeminiConverter()
|
||||
gemini_dict = converter.convert_request(request)
|
||||
return GeminiRequest(**gemini_dict)
|
||||
|
||||
# 默认尝试作为 Gemini 格式
|
||||
return GeminiRequest(**request)
|
||||
|
||||
return request
|
||||
|
||||
def _extract_usage(self, response: Dict) -> Dict[str, int]:
|
||||
"""
|
||||
从 Gemini 响应中提取 token 使用情况
|
||||
|
||||
调用 GeminiStreamParser.extract_usage 作为单一实现源
|
||||
"""
|
||||
from src.api.handlers.gemini.stream_parser import GeminiStreamParser
|
||||
|
||||
usage = GeminiStreamParser().extract_usage(response)
|
||||
|
||||
if not usage:
|
||||
return {
|
||||
"input_tokens": 0,
|
||||
"output_tokens": 0,
|
||||
"cache_creation_input_tokens": 0,
|
||||
"cache_read_input_tokens": 0,
|
||||
}
|
||||
|
||||
return {
|
||||
"input_tokens": usage.get("input_tokens", 0),
|
||||
"output_tokens": usage.get("output_tokens", 0),
|
||||
"cache_creation_input_tokens": 0, # Gemini 不区分缓存创建
|
||||
"cache_read_input_tokens": usage.get("cached_tokens", 0),
|
||||
}
|
||||
|
||||
def _normalize_response(self, response: Dict) -> Dict:
|
||||
"""
|
||||
规范化 Gemini 响应
|
||||
|
||||
Args:
|
||||
response: 原始响应
|
||||
|
||||
Returns:
|
||||
规范化后的响应
|
||||
|
||||
TODO: 如果需要,实现响应规范化逻辑
|
||||
"""
|
||||
# 可选:使用 response_normalizer 进行规范化
|
||||
# if (
|
||||
# self.response_normalizer
|
||||
# and self.response_normalizer.should_normalize(response)
|
||||
# ):
|
||||
# return self.response_normalizer.normalize_gemini_response(
|
||||
# response_data=response,
|
||||
# request_id=self.request_id,
|
||||
# strict=False,
|
||||
# )
|
||||
return response
|
||||
307
src/api/handlers/gemini/stream_parser.py
Normal file
307
src/api/handlers/gemini/stream_parser.py
Normal file
@@ -0,0 +1,307 @@
|
||||
"""
|
||||
Gemini SSE/JSON 流解析器
|
||||
|
||||
Gemini API 的流式响应格式与 Claude/OpenAI 不同:
|
||||
- 使用 JSON 数组格式 (不是 SSE)
|
||||
- 每个块是一个完整的 JSON 对象
|
||||
- 响应以 [ 开始,以 ] 结束,块之间用 , 分隔
|
||||
|
||||
参考: https://ai.google.dev/api/generate-content#method:-models.streamgeneratecontent
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
class GeminiStreamParser:
|
||||
"""
|
||||
Gemini 流解析器
|
||||
|
||||
解析 Gemini streamGenerateContent API 的响应流。
|
||||
|
||||
Gemini 流式响应特点:
|
||||
- 返回 JSON 数组格式: [{chunk1}, {chunk2}, ...]
|
||||
- 每个 chunk 包含 candidates、usageMetadata 等字段
|
||||
- finish_reason 可能值: STOP, MAX_TOKENS, SAFETY, RECITATION, OTHER
|
||||
"""
|
||||
|
||||
# 停止原因
|
||||
FINISH_REASON_STOP = "STOP"
|
||||
FINISH_REASON_MAX_TOKENS = "MAX_TOKENS"
|
||||
FINISH_REASON_SAFETY = "SAFETY"
|
||||
FINISH_REASON_RECITATION = "RECITATION"
|
||||
FINISH_REASON_OTHER = "OTHER"
|
||||
|
||||
def __init__(self):
|
||||
self._buffer = ""
|
||||
self._in_array = False
|
||||
self._brace_depth = 0
|
||||
|
||||
def reset(self):
|
||||
"""重置解析器状态"""
|
||||
self._buffer = ""
|
||||
self._in_array = False
|
||||
self._brace_depth = 0
|
||||
|
||||
def parse_chunk(self, chunk: bytes | str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
解析流式数据块
|
||||
|
||||
Args:
|
||||
chunk: 原始数据(bytes 或 str)
|
||||
|
||||
Returns:
|
||||
解析后的事件列表
|
||||
"""
|
||||
if isinstance(chunk, bytes):
|
||||
text = chunk.decode("utf-8")
|
||||
else:
|
||||
text = chunk
|
||||
|
||||
events: List[Dict[str, Any]] = []
|
||||
|
||||
for char in text:
|
||||
if char == "[" and not self._in_array:
|
||||
self._in_array = True
|
||||
continue
|
||||
|
||||
if char == "]" and self._in_array and self._brace_depth == 0:
|
||||
# 数组结束
|
||||
self._in_array = False
|
||||
if self._buffer.strip():
|
||||
try:
|
||||
obj = json.loads(self._buffer.strip().rstrip(","))
|
||||
events.append(obj)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
self._buffer = ""
|
||||
continue
|
||||
|
||||
if self._in_array:
|
||||
if char == "{":
|
||||
self._brace_depth += 1
|
||||
elif char == "}":
|
||||
self._brace_depth -= 1
|
||||
|
||||
self._buffer += char
|
||||
|
||||
# 当 brace_depth 回到 0 时,说明一个完整的 JSON 对象结束
|
||||
if self._brace_depth == 0 and self._buffer.strip():
|
||||
try:
|
||||
obj = json.loads(self._buffer.strip().rstrip(","))
|
||||
events.append(obj)
|
||||
self._buffer = ""
|
||||
except json.JSONDecodeError:
|
||||
# 可能还不完整,继续累积
|
||||
pass
|
||||
|
||||
return events
|
||||
|
||||
def parse_line(self, line: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
解析单行 JSON 数据
|
||||
|
||||
Args:
|
||||
line: JSON 数据行
|
||||
|
||||
Returns:
|
||||
解析后的事件字典,如果无法解析返回 None
|
||||
"""
|
||||
if not line or line.strip() in ["[", "]", ","]:
|
||||
return None
|
||||
|
||||
try:
|
||||
return json.loads(line.strip().rstrip(","))
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
def is_done_event(self, event: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
判断是否为结束事件
|
||||
|
||||
Args:
|
||||
event: 事件字典
|
||||
|
||||
Returns:
|
||||
True 如果是结束事件
|
||||
"""
|
||||
candidates = event.get("candidates", [])
|
||||
if candidates:
|
||||
for candidate in candidates:
|
||||
finish_reason = candidate.get("finishReason")
|
||||
if finish_reason in (
|
||||
self.FINISH_REASON_STOP,
|
||||
self.FINISH_REASON_MAX_TOKENS,
|
||||
self.FINISH_REASON_SAFETY,
|
||||
self.FINISH_REASON_RECITATION,
|
||||
self.FINISH_REASON_OTHER,
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_error_event(self, event: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
判断是否为错误事件
|
||||
|
||||
检测多种 Gemini 错误格式:
|
||||
1. 顶层 error: {"error": {...}}
|
||||
2. chunks 内嵌套 error: {"chunks": [{"error": {...}}]}
|
||||
3. candidates 内的错误状态
|
||||
|
||||
Args:
|
||||
event: 事件字典
|
||||
|
||||
Returns:
|
||||
True 如果是错误事件
|
||||
"""
|
||||
# 顶层 error
|
||||
if "error" in event:
|
||||
return True
|
||||
|
||||
# chunks 内嵌套 error (某些 Gemini 响应格式)
|
||||
chunks = event.get("chunks", [])
|
||||
if chunks and isinstance(chunks, list):
|
||||
for chunk in chunks:
|
||||
if isinstance(chunk, dict) and "error" in chunk:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def extract_error_info(self, event: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
从事件中提取错误信息
|
||||
|
||||
Args:
|
||||
event: 事件字典
|
||||
|
||||
Returns:
|
||||
错误信息字典 {"code": int, "message": str, "status": str},无错误返回 None
|
||||
"""
|
||||
# 顶层 error
|
||||
if "error" in event:
|
||||
error = event["error"]
|
||||
if isinstance(error, dict):
|
||||
return {
|
||||
"code": error.get("code"),
|
||||
"message": error.get("message", str(error)),
|
||||
"status": error.get("status"),
|
||||
}
|
||||
return {"code": None, "message": str(error), "status": None}
|
||||
|
||||
# chunks 内嵌套 error
|
||||
chunks = event.get("chunks", [])
|
||||
if chunks and isinstance(chunks, list):
|
||||
for chunk in chunks:
|
||||
if isinstance(chunk, dict) and "error" in chunk:
|
||||
error = chunk["error"]
|
||||
if isinstance(error, dict):
|
||||
return {
|
||||
"code": error.get("code"),
|
||||
"message": error.get("message", str(error)),
|
||||
"status": error.get("status"),
|
||||
}
|
||||
return {"code": None, "message": str(error), "status": None}
|
||||
|
||||
return None
|
||||
|
||||
def get_finish_reason(self, event: Dict[str, Any]) -> Optional[str]:
|
||||
"""
|
||||
获取结束原因
|
||||
|
||||
Args:
|
||||
event: 事件字典
|
||||
|
||||
Returns:
|
||||
结束原因字符串
|
||||
"""
|
||||
candidates = event.get("candidates", [])
|
||||
if candidates:
|
||||
return candidates[0].get("finishReason")
|
||||
return None
|
||||
|
||||
def extract_text_delta(self, event: Dict[str, Any]) -> Optional[str]:
|
||||
"""
|
||||
从响应中提取文本内容
|
||||
|
||||
Args:
|
||||
event: 事件字典
|
||||
|
||||
Returns:
|
||||
文本内容,如果没有文本返回 None
|
||||
"""
|
||||
candidates = event.get("candidates", [])
|
||||
if not candidates:
|
||||
return None
|
||||
|
||||
content = candidates[0].get("content", {})
|
||||
parts = content.get("parts", [])
|
||||
|
||||
text_parts = []
|
||||
for part in parts:
|
||||
if "text" in part:
|
||||
text_parts.append(part["text"])
|
||||
|
||||
return "".join(text_parts) if text_parts else None
|
||||
|
||||
def extract_usage(self, event: Dict[str, Any]) -> Optional[Dict[str, int]]:
|
||||
"""
|
||||
从事件中提取 token 使用量
|
||||
|
||||
这是 Gemini token 提取的单一实现源,其他地方都应该调用此方法。
|
||||
|
||||
Args:
|
||||
event: 事件字典(包含 usageMetadata)
|
||||
|
||||
Returns:
|
||||
使用量字典,如果没有完整的使用量信息返回 None
|
||||
|
||||
注意:
|
||||
- 只有当 totalTokenCount 存在时才提取(确保是完整的 usage 数据)
|
||||
- 输出 token = thoughtsTokenCount + candidatesTokenCount
|
||||
"""
|
||||
usage_metadata = event.get("usageMetadata", {})
|
||||
if not usage_metadata or "totalTokenCount" not in usage_metadata:
|
||||
return None
|
||||
|
||||
# 输出 token = thoughtsTokenCount + candidatesTokenCount
|
||||
thoughts_tokens = usage_metadata.get("thoughtsTokenCount", 0)
|
||||
candidates_tokens = usage_metadata.get("candidatesTokenCount", 0)
|
||||
output_tokens = thoughts_tokens + candidates_tokens
|
||||
|
||||
return {
|
||||
"input_tokens": usage_metadata.get("promptTokenCount", 0),
|
||||
"output_tokens": output_tokens,
|
||||
"total_tokens": usage_metadata.get("totalTokenCount", 0),
|
||||
"cached_tokens": usage_metadata.get("cachedContentTokenCount", 0),
|
||||
}
|
||||
|
||||
def extract_model_version(self, event: Dict[str, Any]) -> Optional[str]:
|
||||
"""
|
||||
从响应中提取模型版本
|
||||
|
||||
Args:
|
||||
event: 事件字典
|
||||
|
||||
Returns:
|
||||
模型版本,如果没有返回 None
|
||||
"""
|
||||
return event.get("modelVersion")
|
||||
|
||||
def extract_safety_ratings(self, event: Dict[str, Any]) -> Optional[List[Dict[str, Any]]]:
|
||||
"""
|
||||
从响应中提取安全评级
|
||||
|
||||
Args:
|
||||
event: 事件字典
|
||||
|
||||
Returns:
|
||||
安全评级列表,如果没有返回 None
|
||||
"""
|
||||
candidates = event.get("candidates", [])
|
||||
if not candidates:
|
||||
return None
|
||||
|
||||
return candidates[0].get("safetyRatings")
|
||||
|
||||
|
||||
__all__ = ["GeminiStreamParser"]
|
||||
Reference in New Issue
Block a user