feat: add Gemini Files API proxy with key capability support

- Add gemini_files_api capability for key-level Files API support
- Implement Gemini Files API proxy endpoints (upload/list/get/delete)
- Add file-to-key mapping cache for correct key routing
- Support preferred_key_ids to prioritize keys that uploaded files
- Enhance logging for file mapping diagnostics
This commit is contained in:
fawney19
2026-01-30 11:37:34 +08:00
parent 32b293ef3e
commit c33cd07ea9
12 changed files with 856 additions and 0 deletions

View File

@@ -439,6 +439,14 @@ class BaseMessageHandler:
adapter_detector=self.adapter_detector,
)
async def _resolve_preferred_key_ids(
self,
model_name: str,
request_body: Optional[Dict[str, Any]] = None,
) -> Optional[list[str]]:
"""可选的 Key 优先级解析钩子(默认不启用)。"""
return None
def get_api_format(self, provider_type: Optional[str] = None) -> APIFormat:
"""根据 provider_type 解析 API 格式,未知类型默认 OPENAI"""
if provider_type:

View File

@@ -511,6 +511,11 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
capability_requirements = self._resolve_capability_requirements(
model_name=model,
request_headers=original_headers,
request_body=original_request_body,
)
preferred_key_ids = await self._resolve_preferred_key_ids(
model_name=model,
request_body=original_request_body,
)
# 执行请求(通过 FallbackOrchestrator
@@ -529,6 +534,7 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
request_id=self.request_id,
is_stream=True,
capability_requirements=capability_requirements or None,
preferred_key_ids=preferred_key_ids or None,
request_body_ref=request_body_ref, # 传递容器引用
)
@@ -1126,6 +1132,11 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
capability_requirements = self._resolve_capability_requirements(
model_name=model,
request_headers=original_headers,
request_body=original_request_body,
)
preferred_key_ids = await self._resolve_preferred_key_ids(
model_name=model,
request_body=original_request_body,
)
(
@@ -1142,6 +1153,7 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
request_func=sync_request_func,
request_id=self.request_id,
capability_requirements=capability_requirements or None,
preferred_key_ids=preferred_key_ids or None,
request_body_ref=request_body_ref, # 传递容器引用
)

View File

@@ -568,6 +568,11 @@ class CliMessageHandlerBase(BaseMessageHandler):
capability_requirements = self._resolve_capability_requirements(
model_name=ctx.model,
request_headers=original_headers,
request_body=original_request_body,
)
preferred_key_ids = await self._resolve_preferred_key_ids(
model_name=ctx.model,
request_body=original_request_body,
)
# 执行请求(通过 FallbackOrchestrator
@@ -586,6 +591,7 @@ class CliMessageHandlerBase(BaseMessageHandler):
request_id=self.request_id,
is_stream=True,
capability_requirements=capability_requirements or None,
preferred_key_ids=preferred_key_ids or None,
request_body_ref=request_body_ref, # 传递容器引用
)
@@ -2324,6 +2330,11 @@ class CliMessageHandlerBase(BaseMessageHandler):
capability_requirements = self._resolve_capability_requirements(
model_name=model,
request_headers=original_headers,
request_body=original_request_body,
)
preferred_key_ids = await self._resolve_preferred_key_ids(
model_name=model,
request_body=original_request_body,
)
(
@@ -2340,6 +2351,7 @@ class CliMessageHandlerBase(BaseMessageHandler):
request_func=sync_request_func,
request_id=self.request_id,
capability_requirements=capability_requirements or None,
preferred_key_ids=preferred_key_ids or None,
request_body_ref=request_body_ref, # 传递容器引用
)

View File

@@ -15,6 +15,7 @@ from src.api.handlers.base.chat_handler_base import ChatHandlerBase
from src.core.api_format import extract_client_api_key_with_query
from src.core.logger import logger
from src.models.gemini import GeminiRequest
from src.services.gemini_files_mapping import extract_file_names_from_request
from src.services.provider.transport import redact_url_for_log
@@ -56,6 +57,16 @@ class GeminiChatAdapter(ChatAdapterBase):
self._get_api_format(),
)
def detect_capability_requirements(
self,
headers: Dict[str, str], # noqa: ARG002 - 预留
request_body: Optional[Dict[str, Any]] = None,
) -> Dict[str, bool]:
"""检测是否需要 Gemini Files API 能力"""
if request_body and extract_file_names_from_request(request_body):
return {"gemini_files_api": True}
return {}
def _merge_path_params(
self, original_request_body: Dict[str, Any], path_params: Dict[str, Any] # noqa: ARG002
) -> Dict[str, Any]:

View File

@@ -22,6 +22,58 @@ class GeminiChatHandler(ChatHandlerBase):
FORMAT_ID = "GEMINI"
async def _resolve_preferred_key_ids(
self,
model_name: str, # noqa: ARG002 - 仅做文件绑定
request_body: Optional[Dict[str, Any]] = None,
) -> Optional[list[str]]:
"""
从 files/xxx 绑定关系中解析优先 Key ID 列表。
Gemini 文件与上传它的 API Key 绑定,必须使用同一 Key 访问。
此方法从缓存中查找文件→Key 映射,优先使用正确的 Key。
注意事项:
- 如果映射缺失(缓存过期/重启),会记录警告,请求可能失败
- 如果多个文件属于不同 Key只能使用其中一个其他文件可能无法访问
"""
from src.core.logger import logger
from src.services.gemini_files_mapping import (
extract_file_names_from_request,
get_file_key_mapping,
)
file_names = extract_file_names_from_request(request_body or {})
if not file_names:
return None
preferred_key_ids: list[str] = []
unmapped_files: list[str] = [] # 记录找不到映射的文件
for file_name in file_names:
key_id = await get_file_key_mapping(file_name)
if key_id:
if key_id not in preferred_key_ids:
preferred_key_ids.append(key_id)
else:
unmapped_files.append(file_name)
# 警告:映射缺失
if unmapped_files:
logger.warning(
f"[{self.request_id}] Gemini 文件→Key 映射缺失: {unmapped_files}"
"请求可能失败(文件属于其他 Key 或映射已过期)"
)
# 警告:多个文件属于不同 Key
if len(preferred_key_ids) > 1:
logger.warning(
f"[{self.request_id}] 请求使用了多个文件,但它们属于不同的 Key: "
f"{preferred_key_ids},只能使用第一个 Key其他文件可能无法访问"
)
return preferred_key_ids or None
def extract_model_from_request(
self,
request_body: Dict[str, Any],