mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
refactor: 移除 Python 后端源码,全面迁移至 Rust gateway 架构
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/ - 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层 - 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构 - 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations) - 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image - 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
This commit is contained in:
438
_deprecated_py_src/api/handlers/gemini/adapter.py
Normal file
438
_deprecated_py_src/api/handlers/gemini/adapter.py
Normal file
@@ -0,0 +1,438 @@
|
||||
"""
|
||||
Gemini Chat Adapter
|
||||
|
||||
处理 Gemini API 格式的请求适配
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
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.api_format import ApiFamily, get_auth_handler, resolve_header_name_case
|
||||
from src.core.api_format.enums import AuthMethod
|
||||
from src.core.logger import logger
|
||||
from src.models.gemini import GeminiRequest
|
||||
from src.services.gemini_files_mapping import extract_file_names_from_request
|
||||
|
||||
|
||||
class GeminiCapabilityDetector:
|
||||
"""Gemini API 能力检测器"""
|
||||
|
||||
@staticmethod
|
||||
def detect_from_request(
|
||||
headers: dict[str, str], # noqa: ARG004 - 预留
|
||||
request_body: dict[str, Any] | None = None,
|
||||
) -> dict[str, bool]:
|
||||
"""
|
||||
从请求体检测 Gemini 能力需求
|
||||
|
||||
检测规则:
|
||||
- fileData.fileUri -> gemini_files: True
|
||||
"""
|
||||
requirements: dict[str, bool] = {}
|
||||
if request_body and extract_file_names_from_request(request_body):
|
||||
requirements["gemini_files"] = True
|
||||
return requirements
|
||||
|
||||
|
||||
@register_adapter
|
||||
class GeminiChatAdapter(ChatAdapterBase):
|
||||
"""
|
||||
Gemini Chat API 适配器
|
||||
|
||||
处理 Gemini Chat 格式的请求
|
||||
端点: /v1beta/models/{model}:generateContent
|
||||
"""
|
||||
|
||||
FORMAT_ID = "gemini:chat"
|
||||
API_FAMILY = ApiFamily.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: list[str] | None = None):
|
||||
super().__init__(allowed_api_formats)
|
||||
logger.info(
|
||||
f"[{self.name}] 初始化 Gemini Chat 适配器 | API格式: {self.allowed_api_formats}"
|
||||
)
|
||||
|
||||
def extract_api_key(self, request: Request) -> str | None:
|
||||
"""
|
||||
从请求中提取 API 密钥 - Gemini 支持 header 和 query 两种方式
|
||||
|
||||
优先级(与 Google SDK 行为一致):
|
||||
1. URL 参数 ?key=
|
||||
2. x-goog-api-key 请求头
|
||||
"""
|
||||
handler = get_auth_handler(AuthMethod.GOOG_API_KEY)
|
||||
return handler.extract_credentials(request)
|
||||
|
||||
def detect_capability_requirements(
|
||||
self,
|
||||
headers: dict[str, str],
|
||||
request_body: dict[str, Any] | None = None,
|
||||
) -> dict[str, bool]:
|
||||
"""从请求体检测 Gemini 能力需求(fileData.fileUri -> gemini_files)"""
|
||||
return GeminiCapabilityDetector.detect_from_request(headers, request_body)
|
||||
|
||||
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 = None
|
||||
) -> 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: Any) -> 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: Any) -> dict[str, Any]:
|
||||
"""构建 Gemini Chat 特定的审计元数据"""
|
||||
role_counts: dict[str, int] = {}
|
||||
|
||||
contents = getattr(request_obj, "contents", []) or []
|
||||
for content in contents:
|
||||
if isinstance(content, dict):
|
||||
role = content.get("role", "unknown")
|
||||
else:
|
||||
role = getattr(content, "role", None) or "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(),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def build_endpoint_url(
|
||||
cls,
|
||||
base_url: str,
|
||||
request_data: dict[str, Any] | None = None,
|
||||
model_name: str | None = None,
|
||||
*,
|
||||
provider_type: str | None = None,
|
||||
) -> str:
|
||||
"""构建Gemini API端点URL"""
|
||||
base_url = base_url.rstrip("/")
|
||||
if base_url.endswith("/v1beta"):
|
||||
return base_url # 子类需要处理model参数
|
||||
else:
|
||||
return f"{base_url}/v1beta"
|
||||
|
||||
# build_request_body 使用基类实现,通过 format_conversion_registry 自动转换 OPENAI -> GEMINI
|
||||
|
||||
@classmethod
|
||||
async def check_endpoint(
|
||||
cls,
|
||||
client: httpx.AsyncClient,
|
||||
base_url: str,
|
||||
api_key: str,
|
||||
request_data: dict[str, Any],
|
||||
extra_headers: dict[str, str] | None = None,
|
||||
# 端点规则参数
|
||||
body_rules: list[dict[str, Any]] | None = None,
|
||||
header_rules: list[dict[str, Any]] | None = None,
|
||||
# 用量计算参数
|
||||
db: Any | None = None,
|
||||
user: Any | None = None,
|
||||
provider_name: str | None = None,
|
||||
provider_id: str | None = None,
|
||||
api_key_id: str | None = None,
|
||||
model_name: str | None = None,
|
||||
# Provider 上下文
|
||||
auth_type: str | None = None,
|
||||
provider_type: str | None = None,
|
||||
decrypted_auth_config: dict[str, Any] | None = None,
|
||||
provider_endpoint: Any | None = None,
|
||||
provider_api_key: Any | None = None,
|
||||
# 代理配置
|
||||
proxy_config: dict[str, Any] | None = None,
|
||||
timeout_seconds: float | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""测试 Gemini API 模型连接性(非流式)"""
|
||||
from src.api.handlers.base.endpoint_checker import run_endpoint_check
|
||||
from src.api.handlers.base.request_builder import (
|
||||
apply_body_rules,
|
||||
evaluate_condition,
|
||||
)
|
||||
from src.core.api_format.headers import HeaderBuilder
|
||||
from src.services.provider.adapters.vertex_ai.transport import is_vertex_ai_context
|
||||
|
||||
# Gemini需要从request_data或model_name参数获取model名称
|
||||
effective_model_name = model_name or request_data.get("model", "")
|
||||
if not effective_model_name:
|
||||
return {
|
||||
"error": "Model name is required for Gemini API",
|
||||
"status_code": 400,
|
||||
}
|
||||
|
||||
is_antigravity = provider_type and provider_type.lower() == "antigravity"
|
||||
is_gemini_cli = provider_type and provider_type.lower() == "gemini_cli"
|
||||
is_vertex = is_vertex_ai_context(
|
||||
base_url=base_url,
|
||||
provider_type=provider_type,
|
||||
endpoint=provider_endpoint,
|
||||
key=provider_api_key,
|
||||
)
|
||||
is_oauth = auth_type == "oauth"
|
||||
vertex_auth_info: Any | None = None
|
||||
|
||||
# Antigravity provider 使用 v1internal 路径,而非标准 Gemini API 路径
|
||||
if is_antigravity:
|
||||
from src.services.provider.adapters.antigravity.constants import (
|
||||
V1INTERNAL_PATH_TEMPLATE,
|
||||
get_v1internal_extra_headers,
|
||||
)
|
||||
from src.services.provider.adapters.antigravity.url_availability import url_availability
|
||||
|
||||
ordered_urls = url_availability.get_ordered_urls(prefer_daily=True)
|
||||
ag_base = ordered_urls[0] if ordered_urls else base_url
|
||||
path = V1INTERNAL_PATH_TEMPLATE.format(action="generateContent")
|
||||
url = f"{str(ag_base).rstrip('/')}{path}"
|
||||
elif is_gemini_cli:
|
||||
from src.services.provider.adapters.gemini_cli.constants import V1INTERNAL_PATH_TEMPLATE
|
||||
|
||||
path = V1INTERNAL_PATH_TEMPLATE.format(action="generateContent")
|
||||
url = f"{str(base_url).rstrip('/')}{path}"
|
||||
elif is_vertex and provider_endpoint is not None and provider_api_key is not None:
|
||||
# Vertex AI: test-model 必须走统一 provider transport/auth,
|
||||
# 否则会错误命中普通 Gemini URL(导致 404)。
|
||||
from src.services.provider.auth import get_provider_auth
|
||||
from src.services.provider.transport import build_provider_url
|
||||
|
||||
vertex_auth_info = await get_provider_auth(provider_endpoint, provider_api_key)
|
||||
effective_auth_config = (
|
||||
vertex_auth_info.decrypted_auth_config
|
||||
if vertex_auth_info
|
||||
else decrypted_auth_config
|
||||
)
|
||||
if effective_auth_config:
|
||||
decrypted_auth_config = effective_auth_config
|
||||
url = build_provider_url(
|
||||
provider_endpoint,
|
||||
path_params={"model": effective_model_name},
|
||||
is_stream=bool(request_data.get("stream", False)),
|
||||
key=provider_api_key,
|
||||
decrypted_auth_config=effective_auth_config,
|
||||
)
|
||||
else:
|
||||
# 使用基类配置方法,但重写URL构建逻辑
|
||||
base_url_resolved = cls.build_endpoint_url(base_url)
|
||||
url = f"{base_url_resolved}/models/{effective_model_name}:generateContent"
|
||||
|
||||
# 构建请求组件
|
||||
# Antigravity 需要特定的 User-Agent
|
||||
merged_extra = dict(extra_headers) if extra_headers else {}
|
||||
if is_antigravity:
|
||||
merged_extra.update(get_v1internal_extra_headers())
|
||||
elif is_gemini_cli:
|
||||
from src.services.provider.adapters.gemini_cli.constants import (
|
||||
get_v1internal_extra_headers,
|
||||
)
|
||||
|
||||
merged_extra.update(get_v1internal_extra_headers())
|
||||
if is_vertex and provider_endpoint is not None and provider_api_key is not None:
|
||||
headers = dict(merged_extra)
|
||||
if (
|
||||
vertex_auth_info
|
||||
and getattr(vertex_auth_info, "auth_header", None)
|
||||
and getattr(vertex_auth_info, "auth_value", None)
|
||||
):
|
||||
headers[str(vertex_auth_info.auth_header)] = str(vertex_auth_info.auth_value)
|
||||
else:
|
||||
headers = cls.build_headers_with_extra(api_key, merged_extra if merged_extra else None)
|
||||
|
||||
# OAuth 统一处理:替换端点默认认证头(x-goog-api-key)为 Authorization: Bearer
|
||||
if is_oauth:
|
||||
from src.core.api_format import get_auth_config_for_endpoint
|
||||
|
||||
default_auth_header, _ = get_auth_config_for_endpoint(cls.FORMAT_ID)
|
||||
if default_auth_header.lower() != "authorization":
|
||||
headers.pop(default_auth_header, None)
|
||||
auth_header_name = resolve_header_name_case(extra_headers, "Authorization")
|
||||
headers[auth_header_name] = f"Bearer {api_key}"
|
||||
|
||||
body = cls.build_request_body(request_data)
|
||||
|
||||
# 应用请求体规则(在格式转换后应用,确保规则效果不被覆盖)
|
||||
if body_rules:
|
||||
body = apply_body_rules(
|
||||
body,
|
||||
body_rules,
|
||||
original_body=body,
|
||||
)
|
||||
|
||||
# Antigravity 需要将请求体包装为 v1internal 信封格式
|
||||
if is_antigravity:
|
||||
from src.services.provider.adapters.antigravity.envelope import wrap_v1internal_request
|
||||
|
||||
project_id = (decrypted_auth_config or {}).get("project_id", "")
|
||||
body = wrap_v1internal_request(
|
||||
body,
|
||||
project_id=project_id,
|
||||
model=effective_model_name,
|
||||
request_type="endpoint_test",
|
||||
)
|
||||
elif is_gemini_cli:
|
||||
from src.services.provider.adapters.gemini_cli.envelope import wrap_v1internal_request
|
||||
|
||||
project_id = (decrypted_auth_config or {}).get("project_id", "")
|
||||
body = wrap_v1internal_request(
|
||||
body,
|
||||
project_id=project_id,
|
||||
model=effective_model_name,
|
||||
)
|
||||
|
||||
# 应用请求头规则(在请求头构建后应用)
|
||||
if header_rules:
|
||||
# 获取认证头名称,防止被规则覆盖
|
||||
from src.core.api_format import get_auth_config_for_endpoint
|
||||
|
||||
auth_header, _ = get_auth_config_for_endpoint(cls.FORMAT_ID)
|
||||
protected_keys = {auth_header.lower(), "content-type"}
|
||||
if vertex_auth_info and getattr(vertex_auth_info, "auth_header", None):
|
||||
protected_keys.add(str(vertex_auth_info.auth_header).lower())
|
||||
|
||||
header_builder = HeaderBuilder()
|
||||
header_builder.add_many(headers)
|
||||
header_builder.apply_rules(
|
||||
header_rules,
|
||||
protected_keys,
|
||||
body=body,
|
||||
original_body=body,
|
||||
condition_evaluator=evaluate_condition,
|
||||
)
|
||||
headers = header_builder.build()
|
||||
|
||||
return await run_endpoint_check(
|
||||
client=client,
|
||||
url=url,
|
||||
headers=headers,
|
||||
json_body=body,
|
||||
api_format=cls.FORMAT_ID,
|
||||
is_stream=bool(request_data.get("stream", False)),
|
||||
# 用量计算参数(现在强制记录)
|
||||
db=db,
|
||||
user=user,
|
||||
provider_name=provider_name,
|
||||
provider_id=provider_id,
|
||||
api_key_id=api_key_id,
|
||||
model_name=effective_model_name,
|
||||
proxy_config=proxy_config,
|
||||
timeout=timeout_seconds,
|
||||
)
|
||||
|
||||
|
||||
def build_gemini_adapter(x_app_header: str = "") -> GeminiChatAdapter: # noqa: ARG001
|
||||
"""
|
||||
根据请求头构建适当的 Gemini 适配器
|
||||
|
||||
Args:
|
||||
x_app_header: X-App 请求头值
|
||||
|
||||
Returns:
|
||||
GeminiChatAdapter 实例
|
||||
"""
|
||||
# 目前只有一种 Gemini 适配器
|
||||
# 未来可以根据 x_app_header 返回不同的适配器(如 CLI 模式)
|
||||
return GeminiChatAdapter()
|
||||
|
||||
|
||||
__all__ = ["GeminiChatAdapter", "build_gemini_adapter"]
|
||||
Reference in New Issue
Block a user