mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
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:
95
src/services/gemini_files_mapping.py
Normal file
95
src/services/gemini_files_mapping.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
Gemini Files API - 文件与 Key 绑定缓存
|
||||
|
||||
用于在上传文件后记录 file_id -> provider_key_id,
|
||||
并在后续 generateContent 请求中优先使用同一 Key。
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, Optional, Set
|
||||
|
||||
from src.core.cache_service import CacheService
|
||||
|
||||
FILE_MAPPING_TTL_SECONDS = 60 * 60 * 48 # 48小时
|
||||
FILE_MAPPING_CACHE_PREFIX = "gemini_files:key"
|
||||
|
||||
|
||||
def _normalize_file_name(file_name: str) -> str:
|
||||
name = (file_name or "").strip()
|
||||
if not name:
|
||||
return ""
|
||||
return name if name.startswith("files/") else f"files/{name}"
|
||||
|
||||
|
||||
def build_file_mapping_key(file_name: str) -> str:
|
||||
normalized = _normalize_file_name(file_name)
|
||||
return f"{FILE_MAPPING_CACHE_PREFIX}:{normalized}" if normalized else ""
|
||||
|
||||
|
||||
async def store_file_key_mapping(file_name: str, key_id: str) -> None:
|
||||
cache_key = build_file_mapping_key(file_name)
|
||||
if not cache_key or not key_id:
|
||||
return
|
||||
await CacheService.set(cache_key, str(key_id), ttl_seconds=FILE_MAPPING_TTL_SECONDS)
|
||||
|
||||
|
||||
async def get_file_key_mapping(file_name: str) -> Optional[str]:
|
||||
cache_key = build_file_mapping_key(file_name)
|
||||
if not cache_key:
|
||||
return None
|
||||
value = await CacheService.get(cache_key)
|
||||
if value:
|
||||
return str(value)
|
||||
return None
|
||||
|
||||
|
||||
async def delete_file_key_mapping(file_name: str) -> None:
|
||||
cache_key = build_file_mapping_key(file_name)
|
||||
if cache_key:
|
||||
await CacheService.delete(cache_key)
|
||||
|
||||
|
||||
def _extract_file_name_from_uri(file_uri: str) -> Optional[str]:
|
||||
"""
|
||||
从 fileUri 提取 files/xxx 名称。
|
||||
|
||||
支持两种格式:
|
||||
- 完整 URL: https://generativelanguage.googleapis.com/v1beta/files/abc123
|
||||
- 短格式: files/abc123
|
||||
"""
|
||||
if not file_uri:
|
||||
return None
|
||||
# 完整 URL 格式
|
||||
if "/files/" in file_uri:
|
||||
idx = file_uri.rfind("/files/")
|
||||
return file_uri[idx + 1:] # 提取 files/xxx 部分
|
||||
# 短格式
|
||||
if file_uri.startswith("files/"):
|
||||
return file_uri
|
||||
return None
|
||||
|
||||
|
||||
def extract_file_names_from_request(payload: Optional[Dict[str, Any]]) -> Set[str]:
|
||||
"""
|
||||
从 Gemini 请求体中提取 fileUri 使用到的 files/xxx 名称集合。
|
||||
"""
|
||||
results: Set[str] = set()
|
||||
|
||||
def walk(node: Any) -> None:
|
||||
if isinstance(node, dict):
|
||||
file_data = node.get("fileData") or node.get("file_data")
|
||||
if isinstance(file_data, dict):
|
||||
file_uri = file_data.get("fileUri") or file_data.get("file_uri")
|
||||
if isinstance(file_uri, str):
|
||||
file_name = _extract_file_name_from_uri(file_uri)
|
||||
if file_name:
|
||||
results.add(file_name)
|
||||
for value in node.values():
|
||||
walk(value)
|
||||
elif isinstance(node, list):
|
||||
for item in node:
|
||||
walk(item)
|
||||
|
||||
if payload:
|
||||
walk(payload)
|
||||
|
||||
return results
|
||||
@@ -52,6 +52,7 @@ class CandidateResolver:
|
||||
request_id: Optional[str] = None,
|
||||
is_stream: bool = False,
|
||||
capability_requirements: Optional[Dict[str, bool]] = None,
|
||||
preferred_key_ids: Optional[list[str]] = None,
|
||||
) -> Tuple[List[ProviderCandidate], str]:
|
||||
"""
|
||||
获取所有可用候选
|
||||
@@ -64,6 +65,7 @@ class CandidateResolver:
|
||||
request_id: 请求 ID(用于日志)
|
||||
is_stream: 是否是流式请求,如果为 True 则过滤不支持流式的 Provider
|
||||
capability_requirements: 能力需求(用于过滤不满足能力要求的 Key)
|
||||
preferred_key_ids: 优先使用的 Provider Key ID 列表(匹配则置顶)
|
||||
|
||||
Returns:
|
||||
(所有候选组合的列表, global_model_id)
|
||||
@@ -107,6 +109,28 @@ class CandidateResolver:
|
||||
|
||||
logger.debug(f" [{request_id}] 获取到 {len(all_candidates)} 个候选组合")
|
||||
|
||||
if preferred_key_ids:
|
||||
preferred_set = {str(kid) for kid in preferred_key_ids if kid}
|
||||
if preferred_set:
|
||||
preferred_candidates = [
|
||||
c for c in all_candidates if c.key and str(c.key.id) in preferred_set
|
||||
]
|
||||
other_candidates = [
|
||||
c for c in all_candidates if not (c.key and str(c.key.id) in preferred_set)
|
||||
]
|
||||
if preferred_candidates:
|
||||
matched_key_ids = [str(c.key.id) for c in preferred_candidates if c.key]
|
||||
logger.debug(
|
||||
f" [{request_id}] 优先候选命中: {len(preferred_candidates)} 个 "
|
||||
f"(key_ids={matched_key_ids[:3]}{'...' if len(matched_key_ids) > 3 else ''})"
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
f" [{request_id}] 优先候选未命中: 请求的 key_ids={list(preferred_set)[:3]} "
|
||||
"不在可用候选中,将使用普通优先级"
|
||||
)
|
||||
all_candidates = preferred_candidates + other_candidates
|
||||
|
||||
# 如果没有解析到 global_model_id,使用原始 model_name 作为后备
|
||||
return all_candidates, global_model_id or model_name
|
||||
|
||||
|
||||
@@ -176,6 +176,7 @@ class FallbackOrchestrator:
|
||||
request_id: Optional[str] = None,
|
||||
is_stream: bool = False,
|
||||
capability_requirements: Optional[Dict[str, bool]] = None,
|
||||
preferred_key_ids: Optional[list[str]] = None,
|
||||
) -> Tuple[List[ProviderCandidate], str]:
|
||||
"""
|
||||
收集所有可用的 Provider/Endpoint/Key 候选组合
|
||||
@@ -190,6 +191,7 @@ class FallbackOrchestrator:
|
||||
request_id: 请求 ID(用于日志)
|
||||
is_stream: 是否是流式请求,如果为 True 则过滤不支持流式的 Provider
|
||||
capability_requirements: 能力需求(用于过滤不满足能力要求的 Key)
|
||||
preferred_key_ids: 优先使用的 Provider Key ID 列表(匹配则置顶)
|
||||
|
||||
Returns:
|
||||
(所有候选组合的列表, global_model_id)
|
||||
@@ -206,6 +208,7 @@ class FallbackOrchestrator:
|
||||
request_id=request_id,
|
||||
is_stream=is_stream,
|
||||
capability_requirements=capability_requirements,
|
||||
preferred_key_ids=preferred_key_ids,
|
||||
)
|
||||
|
||||
def _create_candidate_records(
|
||||
@@ -988,6 +991,7 @@ class FallbackOrchestrator:
|
||||
request_id: Optional[str] = None,
|
||||
is_stream: bool = False,
|
||||
capability_requirements: Optional[Dict[str, bool]] = None,
|
||||
preferred_key_ids: Optional[list[str]] = None,
|
||||
request_body_ref: Optional[Dict[str, Any]] = None,
|
||||
) -> Tuple[Any, str, Optional[str], Optional[str], Optional[str], Optional[str]]:
|
||||
"""
|
||||
@@ -1001,6 +1005,7 @@ class FallbackOrchestrator:
|
||||
request_id: 请求 ID(用于日志)
|
||||
is_stream: 是否是流式请求,如果为 True 则过滤不支持流式的 Provider
|
||||
capability_requirements: 能力需求(用于过滤不满足能力要求的 Key)
|
||||
preferred_key_ids: 优先使用的 Provider Key ID 列表(匹配则置顶)
|
||||
request_body_ref: 请求体引用容器(用于 Thinking 签名错误重试)
|
||||
|
||||
Returns:
|
||||
@@ -1036,6 +1041,7 @@ class FallbackOrchestrator:
|
||||
request_id=request_id,
|
||||
is_stream=is_stream,
|
||||
capability_requirements=capability_requirements,
|
||||
preferred_key_ids=preferred_key_ids,
|
||||
)
|
||||
|
||||
# 2. 批量创建候选记录
|
||||
|
||||
Reference in New Issue
Block a user