2025-06-21 16:33:21 +08:00
|
|
|
"""
|
|
|
|
|
Gemini API 适配器
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
|
|
|
|
|
|
from zhenxun.services.log import logger
|
|
|
|
|
|
2025-12-07 18:57:55 +08:00
|
|
|
from ..config.generation import ResponseFormat
|
|
|
|
|
from ..types import LLMContentPart
|
2025-06-21 16:33:21 +08:00
|
|
|
from ..types.exceptions import LLMErrorCode, LLMException
|
2025-12-07 18:57:55 +08:00
|
|
|
from ..types.models import BasePlatformTool, ToolChoice
|
2025-06-21 16:33:21 +08:00
|
|
|
from .base import BaseAdapter, RequestData, ResponseData
|
2025-12-07 18:57:55 +08:00
|
|
|
from .components.gemini_components import (
|
|
|
|
|
GeminiConfigMapper,
|
|
|
|
|
GeminiMessageConverter,
|
|
|
|
|
GeminiResponseParser,
|
|
|
|
|
GeminiToolSerializer,
|
|
|
|
|
)
|
2025-06-21 16:33:21 +08:00
|
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
2025-12-07 18:57:55 +08:00
|
|
|
from ..config.generation import LLMEmbeddingConfig, LLMGenerationConfig
|
2025-06-21 16:33:21 +08:00
|
|
|
from ..service import LLMModel
|
2025-12-07 18:57:55 +08:00
|
|
|
from ..types import LLMMessage
|
2025-06-21 16:33:21 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class GeminiAdapter(BaseAdapter):
|
|
|
|
|
"""Gemini API 适配器"""
|
|
|
|
|
|
2025-12-07 18:57:55 +08:00
|
|
|
@property
|
|
|
|
|
def log_sanitization_context(self) -> str:
|
|
|
|
|
return "gemini_request"
|
|
|
|
|
|
2025-06-21 16:33:21 +08:00
|
|
|
@property
|
|
|
|
|
def api_type(self) -> str:
|
|
|
|
|
return "gemini"
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def supported_api_types(self) -> list[str]:
|
|
|
|
|
return ["gemini"]
|
|
|
|
|
|
|
|
|
|
def get_base_headers(self, api_key: str) -> dict[str, str]:
|
|
|
|
|
"""获取基础请求头"""
|
|
|
|
|
from zhenxun.utils.user_agent import get_user_agent
|
|
|
|
|
|
|
|
|
|
headers = get_user_agent()
|
|
|
|
|
headers.update({"Content-Type": "application/json"})
|
|
|
|
|
headers["x-goog-api-key"] = api_key
|
|
|
|
|
|
|
|
|
|
return headers
|
|
|
|
|
|
2025-07-08 11:15:15 +08:00
|
|
|
async def prepare_advanced_request(
|
2025-06-21 16:33:21 +08:00
|
|
|
self,
|
|
|
|
|
model: "LLMModel",
|
|
|
|
|
api_key: str,
|
|
|
|
|
messages: list["LLMMessage"],
|
|
|
|
|
config: "LLMGenerationConfig | None" = None,
|
2025-12-07 18:57:55 +08:00
|
|
|
tools: list[Any] | None = None,
|
|
|
|
|
tool_choice: str | dict[str, Any] | ToolChoice | None = None,
|
2025-06-21 16:33:21 +08:00
|
|
|
) -> RequestData:
|
|
|
|
|
"""准备高级请求"""
|
|
|
|
|
effective_config = config if config is not None else model._generation_config
|
|
|
|
|
|
2025-12-07 18:57:55 +08:00
|
|
|
if tools:
|
|
|
|
|
from ..types.models import GeminiUrlContext
|
|
|
|
|
|
|
|
|
|
context_urls: list[str] = []
|
|
|
|
|
for tool in tools:
|
|
|
|
|
if isinstance(tool, GeminiUrlContext):
|
|
|
|
|
context_urls.extend(tool.urls)
|
|
|
|
|
|
|
|
|
|
if context_urls and messages:
|
|
|
|
|
last_msg = messages[-1]
|
|
|
|
|
if last_msg.role == "user":
|
|
|
|
|
url_text = "\n\n[Context URLs]:\n" + "\n".join(context_urls)
|
|
|
|
|
if isinstance(last_msg.content, str):
|
|
|
|
|
last_msg.content += url_text
|
|
|
|
|
elif isinstance(last_msg.content, list):
|
|
|
|
|
last_msg.content.append(LLMContentPart.text_part(url_text))
|
|
|
|
|
|
|
|
|
|
has_function_tools = False
|
|
|
|
|
if tools:
|
|
|
|
|
has_function_tools = any(hasattr(tool, "get_definition") for tool in tools)
|
|
|
|
|
|
|
|
|
|
is_structured = False
|
|
|
|
|
if effective_config and effective_config.output:
|
|
|
|
|
if (
|
|
|
|
|
effective_config.output.response_schema
|
|
|
|
|
or effective_config.output.response_format == ResponseFormat.JSON
|
|
|
|
|
or effective_config.output.response_mime_type == "application/json"
|
|
|
|
|
):
|
|
|
|
|
is_structured = True
|
|
|
|
|
|
|
|
|
|
if (has_function_tools or is_structured) and effective_config:
|
|
|
|
|
if effective_config.reasoning is None:
|
|
|
|
|
from ..config.generation import ReasoningConfig
|
|
|
|
|
|
|
|
|
|
effective_config.reasoning = ReasoningConfig()
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
effective_config.reasoning.budget_tokens is None
|
|
|
|
|
and effective_config.reasoning.effort is None
|
|
|
|
|
):
|
|
|
|
|
reason_desc = "工具调用" if has_function_tools else "结构化输出"
|
|
|
|
|
logger.debug(
|
|
|
|
|
f"检测到{reason_desc},自动为模型 {model.model_name} 开启思维链增强"
|
|
|
|
|
)
|
|
|
|
|
effective_config.reasoning.budget_tokens = -1
|
|
|
|
|
|
2025-06-21 16:33:21 +08:00
|
|
|
endpoint = self._get_gemini_endpoint(model, effective_config)
|
|
|
|
|
url = self.get_api_url(model, endpoint)
|
|
|
|
|
headers = self.get_base_headers(api_key)
|
|
|
|
|
|
2025-12-07 18:57:55 +08:00
|
|
|
converter = GeminiMessageConverter()
|
2025-06-21 16:33:21 +08:00
|
|
|
system_instruction_parts: list[dict[str, Any]] | None = None
|
|
|
|
|
for msg in messages:
|
|
|
|
|
if msg.role == "system":
|
|
|
|
|
if isinstance(msg.content, str):
|
|
|
|
|
system_instruction_parts = [{"text": msg.content}]
|
|
|
|
|
elif isinstance(msg.content, list):
|
|
|
|
|
system_instruction_parts = [
|
2025-12-08 22:58:12 +08:00
|
|
|
await converter.convert_part(part) for part in msg.content
|
2025-06-21 16:33:21 +08:00
|
|
|
]
|
|
|
|
|
continue
|
|
|
|
|
|
2025-12-07 18:57:55 +08:00
|
|
|
gemini_contents = await converter.convert_messages_async(messages)
|
2025-06-21 16:33:21 +08:00
|
|
|
|
|
|
|
|
body: dict[str, Any] = {"contents": gemini_contents}
|
|
|
|
|
|
|
|
|
|
if system_instruction_parts:
|
|
|
|
|
body["systemInstruction"] = {"parts": system_instruction_parts}
|
|
|
|
|
|
|
|
|
|
all_tools_for_request = []
|
2025-12-07 18:57:55 +08:00
|
|
|
has_user_functions = False
|
2025-06-21 16:33:21 +08:00
|
|
|
if tools:
|
2025-12-07 18:57:55 +08:00
|
|
|
from ..types.protocols import ToolExecutable
|
2025-08-04 23:36:12 +08:00
|
|
|
|
2025-12-07 18:57:55 +08:00
|
|
|
function_tools: list[ToolExecutable] = []
|
|
|
|
|
gemini_tools_dict: dict[str, Any] = {}
|
2025-08-04 23:36:12 +08:00
|
|
|
|
2025-12-07 18:57:55 +08:00
|
|
|
for tool in tools:
|
|
|
|
|
if isinstance(tool, BasePlatformTool):
|
|
|
|
|
declaration = tool.get_tool_declaration()
|
|
|
|
|
if declaration:
|
|
|
|
|
gemini_tools_dict.update(declaration)
|
|
|
|
|
elif hasattr(tool, "get_definition"):
|
|
|
|
|
function_tools.append(tool)
|
2025-08-04 23:36:12 +08:00
|
|
|
|
2025-12-07 18:57:55 +08:00
|
|
|
if function_tools:
|
|
|
|
|
import asyncio
|
2025-08-04 23:36:12 +08:00
|
|
|
|
2025-12-07 18:57:55 +08:00
|
|
|
definition_tasks = [
|
|
|
|
|
executable.get_definition() for executable in function_tools
|
|
|
|
|
]
|
|
|
|
|
tool_definitions = await asyncio.gather(*definition_tasks)
|
2025-06-21 16:33:21 +08:00
|
|
|
|
2025-12-07 18:57:55 +08:00
|
|
|
serializer = GeminiToolSerializer()
|
|
|
|
|
function_declarations = serializer.serialize_tools(tool_definitions)
|
2025-06-21 16:33:21 +08:00
|
|
|
|
2025-12-07 18:57:55 +08:00
|
|
|
if function_declarations:
|
|
|
|
|
gemini_tools_dict["functionDeclarations"] = function_declarations
|
|
|
|
|
has_user_functions = True
|
|
|
|
|
|
|
|
|
|
if gemini_tools_dict:
|
|
|
|
|
all_tools_for_request.append(gemini_tools_dict)
|
2025-06-21 16:33:21 +08:00
|
|
|
|
|
|
|
|
if all_tools_for_request:
|
2025-07-08 11:15:15 +08:00
|
|
|
body["tools"] = all_tools_for_request
|
2025-06-21 16:33:21 +08:00
|
|
|
|
2025-12-07 18:57:55 +08:00
|
|
|
tool_config_updates: dict[str, Any] = {}
|
|
|
|
|
if (
|
|
|
|
|
effective_config
|
|
|
|
|
and effective_config.custom_params
|
|
|
|
|
and "user_location" in effective_config.custom_params
|
|
|
|
|
):
|
|
|
|
|
tool_config_updates["retrievalConfig"] = {
|
|
|
|
|
"latLng": effective_config.custom_params["user_location"]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if tool_config_updates:
|
|
|
|
|
body.setdefault("toolConfig", {}).update(tool_config_updates)
|
|
|
|
|
|
|
|
|
|
converted_params: dict[str, Any] = {}
|
|
|
|
|
if effective_config:
|
|
|
|
|
converted_params = self.convert_generation_config(effective_config, model)
|
|
|
|
|
|
|
|
|
|
if converted_params:
|
|
|
|
|
if "toolConfig" in converted_params:
|
|
|
|
|
tool_config_payload = converted_params.pop("toolConfig")
|
|
|
|
|
fc_config = tool_config_payload.get("functionCallingConfig")
|
|
|
|
|
should_apply_fc = has_user_functions or (
|
|
|
|
|
fc_config and fc_config.get("mode") == "NONE"
|
2025-06-21 16:33:21 +08:00
|
|
|
)
|
2025-12-07 18:57:55 +08:00
|
|
|
if should_apply_fc:
|
|
|
|
|
body.setdefault("toolConfig", {}).update(tool_config_payload)
|
|
|
|
|
elif fc_config and fc_config.get("mode") != "AUTO":
|
|
|
|
|
logger.debug(
|
|
|
|
|
"Gemini: 忽略针对纯内置工具的 functionCallingConfig (API限制)"
|
|
|
|
|
)
|
2025-06-21 16:33:21 +08:00
|
|
|
|
2025-12-07 18:57:55 +08:00
|
|
|
if "safetySettings" in converted_params:
|
|
|
|
|
body["safetySettings"] = converted_params.pop("safetySettings")
|
2025-06-21 16:33:21 +08:00
|
|
|
|
2025-12-07 18:57:55 +08:00
|
|
|
if converted_params:
|
|
|
|
|
body["generationConfig"] = converted_params
|
2025-06-21 16:33:21 +08:00
|
|
|
|
|
|
|
|
return RequestData(url=url, headers=headers, body=body)
|
|
|
|
|
|
|
|
|
|
def apply_config_override(
|
|
|
|
|
self,
|
|
|
|
|
model: "LLMModel",
|
|
|
|
|
body: dict[str, Any],
|
|
|
|
|
config: "LLMGenerationConfig | None" = None,
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
"""应用配置覆盖 - Gemini 不需要额外的配置覆盖"""
|
|
|
|
|
return body
|
|
|
|
|
|
|
|
|
|
def _get_gemini_endpoint(
|
|
|
|
|
self, model: "LLMModel", config: "LLMGenerationConfig | None" = None
|
|
|
|
|
) -> str:
|
2025-12-07 18:57:55 +08:00
|
|
|
"""返回Gemini generateContent 端点"""
|
2025-06-21 16:33:21 +08:00
|
|
|
return f"/v1beta/models/{model.model_name}:generateContent"
|
|
|
|
|
|
|
|
|
|
def parse_response(
|
|
|
|
|
self,
|
|
|
|
|
model: "LLMModel",
|
|
|
|
|
response_json: dict[str, Any],
|
|
|
|
|
is_advanced: bool = False,
|
|
|
|
|
) -> ResponseData:
|
|
|
|
|
"""解析 Gemini API 响应"""
|
2025-12-07 18:57:55 +08:00
|
|
|
_ = model, is_advanced
|
|
|
|
|
parser = GeminiResponseParser()
|
|
|
|
|
return parser.parse(response_json)
|
2025-06-21 16:33:21 +08:00
|
|
|
|
|
|
|
|
def prepare_embedding_request(
|
|
|
|
|
self,
|
|
|
|
|
model: "LLMModel",
|
|
|
|
|
api_key: str,
|
|
|
|
|
texts: list[str],
|
2025-12-07 18:57:55 +08:00
|
|
|
config: "LLMEmbeddingConfig",
|
2025-06-21 16:33:21 +08:00
|
|
|
) -> RequestData:
|
|
|
|
|
"""准备文本嵌入请求"""
|
|
|
|
|
api_model_name = model.model_name
|
|
|
|
|
if not api_model_name.startswith("models/"):
|
|
|
|
|
api_model_name = f"models/{api_model_name}"
|
|
|
|
|
|
2025-12-07 18:57:55 +08:00
|
|
|
if not model.api_base:
|
|
|
|
|
raise LLMException(
|
|
|
|
|
f"模型 {model.model_name} 的 api_base 未设置",
|
|
|
|
|
code=LLMErrorCode.CONFIGURATION_ERROR,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
base_url = model.api_base.rstrip("/")
|
|
|
|
|
url = f"{base_url}/v1beta/{api_model_name}:batchEmbedContents"
|
2025-06-21 16:33:21 +08:00
|
|
|
headers = self.get_base_headers(api_key)
|
|
|
|
|
|
|
|
|
|
requests_payload = []
|
|
|
|
|
for text_content in texts:
|
2025-12-07 18:57:55 +08:00
|
|
|
safe_text = text_content if text_content else " "
|
2025-06-21 16:33:21 +08:00
|
|
|
request_item: dict[str, Any] = {
|
2025-12-07 18:57:55 +08:00
|
|
|
"model": api_model_name,
|
|
|
|
|
"content": {"parts": [{"text": safe_text}]},
|
2025-06-21 16:33:21 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-07 18:57:55 +08:00
|
|
|
if config.task_type:
|
|
|
|
|
request_item["task_type"] = str(config.task_type).upper()
|
|
|
|
|
if config.title:
|
|
|
|
|
request_item["title"] = config.title
|
|
|
|
|
if config.output_dimensionality:
|
|
|
|
|
request_item["output_dimensionality"] = config.output_dimensionality
|
2025-06-21 16:33:21 +08:00
|
|
|
|
|
|
|
|
requests_payload.append(request_item)
|
|
|
|
|
|
|
|
|
|
body = {"requests": requests_payload}
|
|
|
|
|
return RequestData(url=url, headers=headers, body=body)
|
|
|
|
|
|
|
|
|
|
def parse_embedding_response(
|
|
|
|
|
self, response_json: dict[str, Any]
|
|
|
|
|
) -> list[list[float]]:
|
|
|
|
|
"""解析文本嵌入响应"""
|
|
|
|
|
try:
|
|
|
|
|
embeddings_data = response_json["embeddings"]
|
|
|
|
|
return [item["values"] for item in embeddings_data]
|
|
|
|
|
except KeyError as e:
|
|
|
|
|
logger.error(f"解析Gemini嵌入响应时缺少键: {e}. 响应: {response_json}")
|
|
|
|
|
raise LLMException(
|
|
|
|
|
"Gemini嵌入响应格式错误",
|
|
|
|
|
code=LLMErrorCode.RESPONSE_PARSE_ERROR,
|
|
|
|
|
details={"error": str(e)},
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(
|
|
|
|
|
f"解析Gemini嵌入响应时发生未知错误: {e}. 响应: {response_json}"
|
|
|
|
|
)
|
|
|
|
|
raise LLMException(
|
|
|
|
|
f"解析Gemini嵌入响应失败: {e}",
|
|
|
|
|
code=LLMErrorCode.RESPONSE_PARSE_ERROR,
|
|
|
|
|
cause=e,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def validate_embedding_response(self, response_json: dict[str, Any]) -> None:
|
|
|
|
|
"""验证嵌入响应"""
|
|
|
|
|
super().validate_embedding_response(response_json)
|
|
|
|
|
if "embeddings" not in response_json or not isinstance(
|
|
|
|
|
response_json["embeddings"], list
|
|
|
|
|
):
|
|
|
|
|
raise LLMException(
|
|
|
|
|
"Gemini嵌入响应缺少'embeddings'字段或格式不正确",
|
|
|
|
|
code=LLMErrorCode.RESPONSE_PARSE_ERROR,
|
|
|
|
|
details=response_json,
|
|
|
|
|
)
|
|
|
|
|
for item in response_json["embeddings"]:
|
|
|
|
|
if "values" not in item:
|
|
|
|
|
raise LLMException(
|
|
|
|
|
"Gemini嵌入响应的条目中缺少'values'字段",
|
|
|
|
|
code=LLMErrorCode.RESPONSE_PARSE_ERROR,
|
|
|
|
|
details=response_json,
|
|
|
|
|
)
|
2025-12-07 18:57:55 +08:00
|
|
|
|
|
|
|
|
def convert_generation_config(
|
|
|
|
|
self, config: "LLMGenerationConfig", model: "LLMModel"
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
mapper = GeminiConfigMapper()
|
|
|
|
|
return mapper.map_config(config, model.model_detail, model.capabilities)
|