mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-09-09 01:46:38 +08:00
refactor: 重构Gemini和OpenAI聊天服务以支持工具和安全设置
- 将 `_build_payload`、`_build_tools`、`_get_safety_settings` 和 `_has_image_parts` 函数从 `OpenAIChatService` 和 `GeminiChatService` 类中提取为独立的函数。 - 将 `_handle_stream_response` 和 `_handle_normal_response` 函数从 `GeminiResponseHandler` 和 `OpenAIResponseHandler` 类中提取为独立的函数。 - 将 `_extract_text` 函数从 `OpenAIResponseHandler` 类中提取为独立的函数, 并在 `GeminiResponseHandler` 中复用。 - 将 `_convert_image` 函数从 `OpenAIMessageConverter` 类中提取为独立的函数。 - 优化 `OpenAIChatService` 和 `GeminiChatService` 中的代码结构, 使其更清晰。 - 优化 `app/api/openai_routes.py` 和 `app/api/gemini_routes.py` 中的路由函数, 移除不必要的参数。
This commit is contained in:
+13
-10
@@ -1,5 +1,4 @@
|
|||||||
from http.client import HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from fastapi import APIRouter, Depends
|
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
@@ -10,6 +9,7 @@ from app.services.gemini_chat_service import GeminiChatService
|
|||||||
from app.services.key_manager import KeyManager
|
from app.services.key_manager import KeyManager
|
||||||
from app.services.model_service import ModelService
|
from app.services.model_service import ModelService
|
||||||
from app.services.chat.retry_handler import RetryHandler
|
from app.services.chat.retry_handler import RetryHandler
|
||||||
|
|
||||||
router = APIRouter(prefix="/gemini/v1beta")
|
router = APIRouter(prefix="/gemini/v1beta")
|
||||||
router_v1beta = APIRouter(prefix="/v1beta")
|
router_v1beta = APIRouter(prefix="/v1beta")
|
||||||
logger = get_gemini_logger()
|
logger = get_gemini_logger()
|
||||||
@@ -22,26 +22,29 @@ model_service = ModelService(settings.MODEL_SEARCH)
|
|||||||
|
|
||||||
@router.get("/models")
|
@router.get("/models")
|
||||||
@router_v1beta.get("/models")
|
@router_v1beta.get("/models")
|
||||||
async def list_models(
|
async def list_models(_=Depends(security_service.verify_key)):
|
||||||
key: str = None,
|
|
||||||
token: str = Depends(security_service.verify_key),
|
|
||||||
):
|
|
||||||
"""获取可用的Gemini模型列表"""
|
"""获取可用的Gemini模型列表"""
|
||||||
logger.info("-" * 50 + "list_gemini_models" + "-" * 50)
|
logger.info("-" * 50 + "list_gemini_models" + "-" * 50)
|
||||||
logger.info("Handling Gemini models list request")
|
logger.info("Handling Gemini models list request")
|
||||||
api_key = await key_manager.get_next_working_key()
|
api_key = await key_manager.get_next_working_key()
|
||||||
logger.info(f"Using API key: {api_key}")
|
logger.info(f"Using API key: {api_key}")
|
||||||
models_json = model_service.get_gemini_models(api_key)
|
models_json = model_service.get_gemini_models(api_key)
|
||||||
models_json["models"].append({"name": "models/gemini-2.0-flash-exp-search", "version": "2.0", "displayName": "Gemini 2.0 Flash Search Experimental", "description": "Gemini 2.0 Flash Search Experimental", "inputTokenLimit": 32767, "outputTokenLimit": 8192, "supportedGenerationMethods": ["generateContent", "countTokens"], "temperature": 1, "topP": 0.95, "topK": 64, "maxTemperature": 2})
|
models_json["models"].append({"name": "models/gemini-2.0-flash-exp-search", "version": "2.0",
|
||||||
|
"displayName": "Gemini 2.0 Flash Search Experimental",
|
||||||
|
"description": "Gemini 2.0 Flash Search Experimental", "inputTokenLimit": 32767,
|
||||||
|
"outputTokenLimit": 8192,
|
||||||
|
"supportedGenerationMethods": ["generateContent", "countTokens"], "temperature": 1,
|
||||||
|
"topP": 0.95, "topK": 64, "maxTemperature": 2})
|
||||||
return models_json
|
return models_json
|
||||||
|
|
||||||
|
|
||||||
@router.post("/models/{model_name}:generateContent")
|
@router.post("/models/{model_name}:generateContent")
|
||||||
@router_v1beta.post("/models/{model_name}:generateContent")
|
@router_v1beta.post("/models/{model_name}:generateContent")
|
||||||
@RetryHandler(max_retries=3, key_manager=key_manager, key_arg="api_key")
|
@RetryHandler(max_retries=3, key_manager=key_manager, key_arg="api_key")
|
||||||
async def generate_content(
|
async def generate_content(
|
||||||
model_name: str,
|
model_name: str,
|
||||||
request: GeminiRequest,
|
request: GeminiRequest,
|
||||||
x_goog_api_key: str = Depends(security_service.verify_goog_api_key),
|
_=Depends(security_service.verify_goog_api_key),
|
||||||
api_key: str = Depends(key_manager.get_next_working_key),
|
api_key: str = Depends(key_manager.get_next_working_key),
|
||||||
):
|
):
|
||||||
chat_service = GeminiChatService(settings.BASE_URL, key_manager)
|
chat_service = GeminiChatService(settings.BASE_URL, key_manager)
|
||||||
@@ -70,7 +73,7 @@ async def generate_content(
|
|||||||
async def stream_generate_content(
|
async def stream_generate_content(
|
||||||
model_name: str,
|
model_name: str,
|
||||||
request: GeminiRequest,
|
request: GeminiRequest,
|
||||||
x_goog_api_key: str = Depends(security_service.verify_goog_api_key),
|
_=Depends(security_service.verify_goog_api_key),
|
||||||
api_key: str = Depends(key_manager.get_next_working_key),
|
api_key: str = Depends(key_manager.get_next_working_key),
|
||||||
):
|
):
|
||||||
chat_service = GeminiChatService(settings.BASE_URL, key_manager)
|
chat_service = GeminiChatService(settings.BASE_URL, key_manager)
|
||||||
@@ -81,7 +84,7 @@ async def stream_generate_content(
|
|||||||
logger.info(f"Using API key: {api_key}")
|
logger.info(f"Using API key: {api_key}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response_stream =chat_service.stream_generate_content(
|
response_stream = chat_service.stream_generate_content(
|
||||||
model=model_name,
|
model=model_name,
|
||||||
request=request,
|
request=request,
|
||||||
api_key=api_key
|
api_key=api_key
|
||||||
|
|||||||
+12
-20
@@ -1,16 +1,15 @@
|
|||||||
from http.client import HTTPException
|
from fastapi import HTTPException, APIRouter, Depends
|
||||||
from fastapi import APIRouter, Depends, Header
|
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.core.logger import get_openai_logger
|
||||||
from app.core.security import SecurityService
|
from app.core.security import SecurityService
|
||||||
|
from app.schemas.openai_models import ChatRequest, EmbeddingRequest
|
||||||
from app.services.chat.retry_handler import RetryHandler
|
from app.services.chat.retry_handler import RetryHandler
|
||||||
|
from app.services.embedding_service import EmbeddingService
|
||||||
from app.services.key_manager import KeyManager
|
from app.services.key_manager import KeyManager
|
||||||
from app.services.model_service import ModelService
|
from app.services.model_service import ModelService
|
||||||
from app.services.openai_chat_service import OpenAIChatService
|
from app.services.openai_chat_service import OpenAIChatService
|
||||||
from app.services.embedding_service import EmbeddingService
|
|
||||||
from app.schemas.openai_models import ChatRequest, EmbeddingRequest
|
|
||||||
from app.core.config import settings
|
|
||||||
from app.core.logger import get_openai_logger
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
logger = get_openai_logger()
|
logger = get_openai_logger()
|
||||||
@@ -24,10 +23,7 @@ embedding_service = EmbeddingService(settings.BASE_URL)
|
|||||||
|
|
||||||
@router.get("/v1/models")
|
@router.get("/v1/models")
|
||||||
@router.get("/hf/v1/models")
|
@router.get("/hf/v1/models")
|
||||||
async def list_models(
|
async def list_models(_=Depends(security_service.verify_authorization)):
|
||||||
authorization: str = Header(None),
|
|
||||||
token: str = Depends(security_service.verify_authorization),
|
|
||||||
):
|
|
||||||
logger.info("-" * 50 + "list_models" + "-" * 50)
|
logger.info("-" * 50 + "list_models" + "-" * 50)
|
||||||
logger.info("Handling models list request")
|
logger.info("Handling models list request")
|
||||||
api_key = await key_manager.get_next_working_key()
|
api_key = await key_manager.get_next_working_key()
|
||||||
@@ -43,10 +39,9 @@ async def list_models(
|
|||||||
@router.post("/hf/v1/chat/completions")
|
@router.post("/hf/v1/chat/completions")
|
||||||
@RetryHandler(max_retries=3, key_manager=key_manager, key_arg="api_key")
|
@RetryHandler(max_retries=3, key_manager=key_manager, key_arg="api_key")
|
||||||
async def chat_completion(
|
async def chat_completion(
|
||||||
request: ChatRequest,
|
request: ChatRequest,
|
||||||
authorization: str = Header(None),
|
_=Depends(security_service.verify_authorization),
|
||||||
token: str = Depends(security_service.verify_authorization),
|
api_key: str = Depends(key_manager.get_next_working_key),
|
||||||
api_key: str = Depends(key_manager.get_next_working_key),
|
|
||||||
):
|
):
|
||||||
chat_service = OpenAIChatService(settings.BASE_URL, key_manager)
|
chat_service = OpenAIChatService(settings.BASE_URL, key_manager)
|
||||||
logger.info("-" * 50 + "chat_completion" + "-" * 50)
|
logger.info("-" * 50 + "chat_completion" + "-" * 50)
|
||||||
@@ -67,15 +62,13 @@ async def chat_completion(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Chat completion failed after retries: {str(e)}")
|
logger.error(f"Chat completion failed after retries: {str(e)}")
|
||||||
raise HTTPException(status_code=500, detail="Chat completion failed") from e
|
raise HTTPException(status_code=500, detail="Chat completion failed") from e
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/v1/embeddings")
|
@router.post("/v1/embeddings")
|
||||||
@router.post("/hf/v1/embeddings")
|
@router.post("/hf/v1/embeddings")
|
||||||
async def embedding(
|
async def embedding(
|
||||||
request: EmbeddingRequest,
|
request: EmbeddingRequest,
|
||||||
authorization: str = Header(None),
|
_=Depends(security_service.verify_authorization),
|
||||||
token: str = Depends(security_service.verify_authorization),
|
|
||||||
):
|
):
|
||||||
logger.info("-" * 50 + "embedding" + "-" * 50)
|
logger.info("-" * 50 + "embedding" + "-" * 50)
|
||||||
logger.info(f"Handling embedding request for model: {request.model}")
|
logger.info(f"Handling embedding request for model: {request.model}")
|
||||||
@@ -95,8 +88,7 @@ async def embedding(
|
|||||||
@router.get("/v1/keys/list")
|
@router.get("/v1/keys/list")
|
||||||
@router.get("/hf/v1/keys/list")
|
@router.get("/hf/v1/keys/list")
|
||||||
async def get_keys_list(
|
async def get_keys_list(
|
||||||
authorization: str = Header(None),
|
_=Depends(security_service.verify_auth_token),
|
||||||
token: str = Depends(security_service.verify_auth_token),
|
|
||||||
):
|
):
|
||||||
"""获取有效和无效的API key列表"""
|
"""获取有效和无效的API key列表"""
|
||||||
logger.info("-" * 50 + "get_keys_list" + "-" * 50)
|
logger.info("-" * 50 + "get_keys_list" + "-" * 50)
|
||||||
|
|||||||
+1
-1
@@ -128,4 +128,4 @@ def get_request_logger():
|
|||||||
|
|
||||||
|
|
||||||
def get_retry_logger():
|
def get_retry_logger():
|
||||||
return Logger.setup_logger("retry")
|
return Logger.setup_logger("retry")
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class SecurityService:
|
|||||||
return key
|
return key
|
||||||
|
|
||||||
async def verify_authorization(
|
async def verify_authorization(
|
||||||
self, authorization: Optional[str] = Header(None)
|
self, authorization: Optional[str] = Header(None)
|
||||||
) -> str:
|
) -> str:
|
||||||
if not authorization:
|
if not authorization:
|
||||||
logger.error("Missing Authorization header")
|
logger.error("Missing Authorization header")
|
||||||
@@ -45,7 +45,7 @@ class SecurityService:
|
|||||||
if x_goog_api_key not in self.allowed_tokens and x_goog_api_key != self.auth_token:
|
if x_goog_api_key not in self.allowed_tokens and x_goog_api_key != self.auth_token:
|
||||||
logger.error("Invalid x-goog-api-key")
|
logger.error("Invalid x-goog-api-key")
|
||||||
raise HTTPException(status_code=401, detail="Invalid x-goog-api-key")
|
raise HTTPException(status_code=401, detail="Invalid x-goog-api-key")
|
||||||
|
|
||||||
return x_goog_api_key
|
return x_goog_api_key
|
||||||
|
|
||||||
async def verify_auth_token(self, authorization: Optional[str] = Header(None)) -> str:
|
async def verify_auth_token(self, authorization: Optional[str] = Header(None)) -> str:
|
||||||
@@ -56,5 +56,5 @@ class SecurityService:
|
|||||||
if token != self.auth_token:
|
if token != self.auth_token:
|
||||||
logger.error("Invalid auth_token")
|
logger.error("Invalid auth_token")
|
||||||
raise HTTPException(status_code=401, detail="Invalid auth_token")
|
raise HTTPException(status_code=401, detail="Invalid auth_token")
|
||||||
|
|
||||||
return token
|
return token
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ from pydantic import BaseModel
|
|||||||
|
|
||||||
|
|
||||||
class SafetySetting(BaseModel):
|
class SafetySetting(BaseModel):
|
||||||
category: Optional[Literal[
|
category: Optional[Literal["HARM_CATEGORY_HATE_SPEECH", "HARM_CATEGORY_DANGEROUS_CONTENT", "HARM_CATEGORY_HARASSMENT", "HARM_CATEGORY_SEXUALLY_EXPLICIT", "HARM_CATEGORY_CIVIC_INTEGRITY"]] = None
|
||||||
"HARM_CATEGORY_HATE_SPEECH", "HARM_CATEGORY_DANGEROUS_CONTENT", "HARM_CATEGORY_HARASSMENT", "HARM_CATEGORY_SEXUALLY_EXPLICIT", "HARM_CATEGORY_CIVIC_INTEGRITY"]] = None
|
threshold: Optional[Literal["HARM_BLOCK_THRESHOLD_UNSPECIFIED", "BLOCK_LOW_AND_ABOVE", "BLOCK_MEDIUM_AND_ABOVE", "BLOCK_ONLY_HIGH", "BLOCK_NONE", "OFF"]] = None
|
||||||
threshold: Optional[Literal[
|
|
||||||
"HARM_BLOCK_THRESHOLD_UNSPECIFIED", "BLOCK_LOW_AND_ABOVE", "BLOCK_MEDIUM_AND_ABOVE", "BLOCK_ONLY_HIGH", "BLOCK_NONE", "OFF"]] = None
|
|
||||||
|
|
||||||
|
|
||||||
class GenerationConfig(BaseModel):
|
class GenerationConfig(BaseModel):
|
||||||
|
|||||||
@@ -4,24 +4,26 @@ from typing import Dict, Any, AsyncGenerator
|
|||||||
import httpx
|
import httpx
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
class ApiClient(ABC):
|
class ApiClient(ABC):
|
||||||
"""API客户端基类"""
|
"""API客户端基类"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def generate_content(self, payload: Dict[str, Any], model: str, api_key: str) -> Dict[str, Any]:
|
async def generate_content(self, payload: Dict[str, Any], model: str, api_key: str) -> Dict[str, Any]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def stream_generate_content(self, payload: Dict[str, Any], model: str, api_key: str) -> AsyncGenerator[str, None]:
|
async def stream_generate_content(self, payload: Dict[str, Any], model: str, api_key: str) -> AsyncGenerator[str, None]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class GeminiApiClient(ApiClient):
|
class GeminiApiClient(ApiClient):
|
||||||
"""Gemini API客户端"""
|
"""Gemini API客户端"""
|
||||||
|
|
||||||
def __init__(self, base_url: str, timeout: int = 300):
|
def __init__(self, base_url: str, timeout: int = 300):
|
||||||
self.base_url = base_url
|
self.base_url = base_url
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
|
|
||||||
def generate_content(self, payload: Dict[str, Any], model: str, api_key: str) -> Dict[str, Any]:
|
def generate_content(self, payload: Dict[str, Any], model: str, api_key: str) -> Dict[str, Any]:
|
||||||
timeout = httpx.Timeout(self.timeout, read=self.timeout)
|
timeout = httpx.Timeout(self.timeout, read=self.timeout)
|
||||||
if model.endswith("-search"):
|
if model.endswith("-search"):
|
||||||
@@ -33,14 +35,14 @@ class GeminiApiClient(ApiClient):
|
|||||||
error_content = response.text
|
error_content = response.text
|
||||||
raise Exception(f"API call failed with status code {response.status_code}, {error_content}")
|
raise Exception(f"API call failed with status code {response.status_code}, {error_content}")
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
async def stream_generate_content(self, payload: Dict[str, Any], model: str, api_key: str) -> AsyncGenerator[str, None]:
|
async def stream_generate_content(self, payload: Dict[str, Any], model: str, api_key: str) -> AsyncGenerator[str, None]:
|
||||||
timeout = httpx.Timeout(self.timeout, read=self.timeout)
|
timeout = httpx.Timeout(self.timeout, read=self.timeout)
|
||||||
if model.endswith("-search"):
|
if model.endswith("-search"):
|
||||||
model = model[:-7]
|
model = model[:-7]
|
||||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||||
url = f"{self.base_url}/models/{model}:streamGenerateContent?alt=sse&key={api_key}"
|
url = f"{self.base_url}/models/{model}:streamGenerateContent?alt=sse&key={api_key}"
|
||||||
async with client.stream("POST", url, json=payload) as response:
|
async with client.stream(method="POST", url=url, json=payload) as response:
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
error_content = await response.aread()
|
error_content = await response.aread()
|
||||||
error_msg = error_content.decode("utf-8")
|
error_msg = error_content.decode("utf-8")
|
||||||
|
|||||||
@@ -3,22 +3,39 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import List, Dict, Any
|
from typing import List, Dict, Any
|
||||||
|
|
||||||
|
|
||||||
class MessageConverter(ABC):
|
class MessageConverter(ABC):
|
||||||
"""消息转换器基类"""
|
"""消息转换器基类"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def convert(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
def convert(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_image(image_url: str) -> Dict[str, Any]:
|
||||||
|
if image_url.startswith("data:image"):
|
||||||
|
return {
|
||||||
|
"inline_data": {
|
||||||
|
"mime_type": "image/jpeg",
|
||||||
|
"data": image_url.split(",")[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"image_url": {
|
||||||
|
"url": image_url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class OpenAIMessageConverter(MessageConverter):
|
class OpenAIMessageConverter(MessageConverter):
|
||||||
"""OpenAI消息格式转换器"""
|
"""OpenAI消息格式转换器"""
|
||||||
|
|
||||||
def convert(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
def convert(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
converted_messages = []
|
converted_messages = []
|
||||||
for msg in messages:
|
for msg in messages:
|
||||||
role = "user" if msg["role"] == "user" else "model"
|
role = "user" if msg["role"] == "user" else "model"
|
||||||
parts = []
|
parts = []
|
||||||
|
|
||||||
if isinstance(msg["content"], str):
|
if isinstance(msg["content"], str):
|
||||||
parts.append({"text": msg["content"]})
|
parts.append({"text": msg["content"]})
|
||||||
elif isinstance(msg["content"], list):
|
elif isinstance(msg["content"], list):
|
||||||
@@ -29,22 +46,8 @@ class OpenAIMessageConverter(MessageConverter):
|
|||||||
if content["type"] == "text":
|
if content["type"] == "text":
|
||||||
parts.append({"text": content["text"]})
|
parts.append({"text": content["text"]})
|
||||||
elif content["type"] == "image_url":
|
elif content["type"] == "image_url":
|
||||||
parts.append(self._convert_image(content["image_url"]["url"]))
|
parts.append(_convert_image(content["image_url"]["url"]))
|
||||||
|
|
||||||
converted_messages.append({"role": role, "parts": parts})
|
converted_messages.append({"role": role, "parts": parts})
|
||||||
|
|
||||||
return converted_messages
|
return converted_messages
|
||||||
|
|
||||||
def _convert_image(self, image_url: str) -> Dict[str, Any]:
|
|
||||||
if image_url.startswith("data:image"):
|
|
||||||
return {
|
|
||||||
"inline_data": {
|
|
||||||
"mime_type": "image/jpeg",
|
|
||||||
"data": image_url.split(",")[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
"image_url": {
|
|
||||||
"url": image_url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,331 +6,225 @@ import time
|
|||||||
import uuid
|
import uuid
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
|
|
||||||
|
|
||||||
class ResponseHandler(ABC):
|
class ResponseHandler(ABC):
|
||||||
"""响应处理器基类"""
|
"""响应处理器基类"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def handle_response(self, response: Dict[str, Any], model: str, stream: bool = False) -> Dict[str, Any]:
|
def handle_response(self, response: Dict[str, Any], model: str, stream: bool = False) -> Dict[str, Any]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class GeminiResponseHandler(ResponseHandler):
|
class GeminiResponseHandler(ResponseHandler):
|
||||||
"""Gemini响应处理器"""
|
"""Gemini响应处理器"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.thinking_first = True
|
self.thinking_first = True
|
||||||
self.thinking_status = False
|
self.thinking_status = False
|
||||||
|
|
||||||
def handle_response(self, response: Dict[str, Any], model: str, stream: bool = False) -> Dict[str, Any]:
|
def handle_response(self, response: Dict[str, Any], model: str, stream: bool = False) -> Dict[str, Any]:
|
||||||
if stream:
|
if stream:
|
||||||
return self._handle_stream_response(response, model, stream)
|
return _handle_gemini_stream_response(response, model, stream)
|
||||||
return self._handle_normal_response(response, model, stream)
|
return _handle_gemini_normal_response(response, model, stream)
|
||||||
|
|
||||||
def _handle_stream_response(self, response: Dict[str, Any], model: str, stream: bool) -> Dict[str, Any]:
|
|
||||||
text = self._extract_text(response, model, stream=stream)
|
|
||||||
content = {"parts": [{"text": text}],"role": "model"}
|
|
||||||
response["candidates"][0]["content"] = content
|
|
||||||
return response
|
|
||||||
|
|
||||||
def _handle_normal_response(self, response: Dict[str, Any], model: str, stream: bool) -> Dict[str, Any]:
|
|
||||||
text = self._extract_text(response, model, stream=stream)
|
|
||||||
content = {"parts": [{"text": text}],"role": "model"}
|
|
||||||
response["candidates"][0]["content"] = content
|
|
||||||
return response
|
|
||||||
|
|
||||||
def _extract_text(self, response: Dict[str, Any], model: str, stream: bool = False) -> str:
|
def _handle_openai_stream_response(response: Dict[str, Any], model: str, finish_reason: str) -> Dict[str, Any]:
|
||||||
text = ""
|
text = _extract_text(response, model, stream=True)
|
||||||
if stream:
|
return {
|
||||||
if response.get("candidates"):
|
"id": f"chatcmpl-{uuid.uuid4()}",
|
||||||
candidate = response["candidates"][0]
|
"object": "chat.completion.chunk",
|
||||||
content = candidate.get("content", {})
|
"created": int(time.time()),
|
||||||
parts = content.get("parts", [])
|
"model": model,
|
||||||
# if "thinking" in model:
|
"choices": [{
|
||||||
# if settings.SHOW_THINKING_PROCESS:
|
"index": 0,
|
||||||
# if len(parts) == 1:
|
"delta": {"content": text} if text else {},
|
||||||
# if self.thinking_first:
|
"finish_reason": finish_reason
|
||||||
# self.thinking_first = False
|
}]
|
||||||
# self.thinking_status = True
|
}
|
||||||
# text = "> thinking\n\n" + parts[0].get("text")
|
|
||||||
# else:
|
|
||||||
# text = parts[0].get("text")
|
|
||||||
|
|
||||||
# if len(parts) == 2:
|
|
||||||
# self.thinking_status = False
|
|
||||||
# if self.thinking_first:
|
|
||||||
# self.thinking_first = False
|
|
||||||
# text = (
|
|
||||||
# "> thinking\n\n"
|
|
||||||
# + parts[0].get("text")
|
|
||||||
# + "\n\n---\n> output\n\n"
|
|
||||||
# + parts[1].get("text")
|
|
||||||
# )
|
|
||||||
# else:
|
|
||||||
# text = (
|
|
||||||
# parts[0].get("text")
|
|
||||||
# + "\n\n---\n> output\n\n"
|
|
||||||
# + parts[1].get("text")
|
|
||||||
# )
|
|
||||||
# else:
|
|
||||||
# if len(parts) == 1:
|
|
||||||
# if self.thinking_first:
|
|
||||||
# self.thinking_first = False
|
|
||||||
# self.thinking_status = True
|
|
||||||
# text = ""
|
|
||||||
# elif self.thinking_status:
|
|
||||||
# text = ""
|
|
||||||
# else:
|
|
||||||
# text = parts[0].get("text")
|
|
||||||
|
|
||||||
# if len(parts) == 2:
|
|
||||||
# self.thinking_status = False
|
|
||||||
# if self.thinking_first:
|
|
||||||
# self.thinking_first = False
|
|
||||||
# text = parts[1].get("text")
|
|
||||||
# else:
|
|
||||||
# text = parts[1].get("text")
|
|
||||||
# else:
|
|
||||||
# if "text" in parts[0]:
|
|
||||||
# text = parts[0].get("text")
|
|
||||||
# elif "executableCode" in parts[0]:
|
|
||||||
# text = _format_code_block(parts[0]["executableCode"])
|
|
||||||
# elif "codeExecution" in parts[0]:
|
|
||||||
# text = _format_code_block(parts[0]["codeExecution"])
|
|
||||||
# elif "executableCodeResult" in parts[0]:
|
|
||||||
# text = _format_execution_result(
|
|
||||||
# parts[0]["executableCodeResult"]
|
|
||||||
# )
|
|
||||||
# elif "codeExecutionResult" in parts[0]:
|
|
||||||
# text = _format_execution_result(
|
|
||||||
# parts[0]["codeExecutionResult"]
|
|
||||||
# )
|
|
||||||
# else:
|
|
||||||
# text = ""
|
|
||||||
if "text" in parts[0]:
|
|
||||||
text = parts[0].get("text")
|
|
||||||
elif "executableCode" in parts[0]:
|
|
||||||
text = _format_code_block(parts[0]["executableCode"])
|
|
||||||
elif "codeExecution" in parts[0]:
|
|
||||||
text = _format_code_block(parts[0]["codeExecution"])
|
|
||||||
elif "executableCodeResult" in parts[0]:
|
|
||||||
text = _format_execution_result(
|
|
||||||
parts[0]["executableCodeResult"]
|
|
||||||
)
|
|
||||||
elif "codeExecutionResult" in parts[0]:
|
|
||||||
text = _format_execution_result(
|
|
||||||
parts[0]["codeExecutionResult"]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
text = ""
|
|
||||||
text = _add_search_link_text(model, candidate, text)
|
|
||||||
else:
|
|
||||||
if response.get("candidates"):
|
|
||||||
candidate = response["candidates"][0]
|
|
||||||
if "thinking" in model:
|
|
||||||
if settings.SHOW_THINKING_PROCESS:
|
|
||||||
if len(candidate["content"]["parts"]) == 2:
|
|
||||||
text = (
|
|
||||||
"> thinking\n\n"
|
|
||||||
+ candidate["content"]["parts"][0]["text"]
|
|
||||||
+ "\n\n---\n> output\n\n"
|
|
||||||
+ candidate["content"]["parts"][1]["text"]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
text = candidate["content"]["parts"][0]["text"]
|
|
||||||
else:
|
|
||||||
if len(candidate["content"]["parts"]) == 2:
|
|
||||||
text = candidate["content"]["parts"][1]["text"]
|
|
||||||
else:
|
|
||||||
text = candidate["content"]["parts"][0]["text"]
|
|
||||||
else:
|
|
||||||
text = candidate["content"]["parts"][0]["text"]
|
|
||||||
text = _add_search_link_text(model, candidate, text)
|
|
||||||
else:
|
|
||||||
text = "暂无返回"
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
def _handle_openai_normal_response(response: Dict[str, Any], model: str, finish_reason: str) -> Dict[str, Any]:
|
||||||
|
text = _extract_text(response, model, stream=False)
|
||||||
|
return {
|
||||||
|
"id": f"chatcmpl-{uuid.uuid4()}",
|
||||||
|
"object": "chat.completion",
|
||||||
|
"created": int(time.time()),
|
||||||
|
"model": model,
|
||||||
|
"choices": [{
|
||||||
|
"index": 0,
|
||||||
|
"message": {
|
||||||
|
"role": "assistant",
|
||||||
|
"content": text
|
||||||
|
},
|
||||||
|
"finish_reason": finish_reason
|
||||||
|
}],
|
||||||
|
"usage": {
|
||||||
|
"prompt_tokens": 0,
|
||||||
|
"completion_tokens": 0,
|
||||||
|
"total_tokens": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class OpenAIResponseHandler(ResponseHandler):
|
class OpenAIResponseHandler(ResponseHandler):
|
||||||
"""OpenAI响应处理器"""
|
"""OpenAI响应处理器"""
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.thinking_first = True
|
self.thinking_first = True
|
||||||
self.thinking_status = False
|
self.thinking_status = False
|
||||||
|
|
||||||
def handle_response(
|
def handle_response(
|
||||||
self,
|
self,
|
||||||
response: Dict[str, Any],
|
response: Dict[str, Any],
|
||||||
model: str,
|
model: str,
|
||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
finish_reason: str = None
|
finish_reason: str = None
|
||||||
) -> Optional[Dict[str, Any]]:
|
) -> Optional[Dict[str, Any]]:
|
||||||
if stream:
|
if stream:
|
||||||
return self._handle_stream_response(response, model, finish_reason)
|
return _handle_openai_stream_response(response, model, finish_reason)
|
||||||
return self._handle_normal_response(response, model, finish_reason)
|
return _handle_openai_normal_response(response, model, finish_reason)
|
||||||
|
|
||||||
def _handle_stream_response(self, response: Dict[str, Any], model: str, finish_reason: str) -> Dict[str, Any]:
|
|
||||||
text = self._extract_text(response, model, stream=True)
|
|
||||||
return {
|
|
||||||
"id": f"chatcmpl-{uuid.uuid4()}",
|
|
||||||
"object": "chat.completion.chunk",
|
|
||||||
"created": int(time.time()),
|
|
||||||
"model": model,
|
|
||||||
"choices": [{
|
|
||||||
"index": 0,
|
|
||||||
"delta": {"content": text} if text else {},
|
|
||||||
"finish_reason": finish_reason
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
def _handle_normal_response(self, response: Dict[str, Any], model: str, finish_reason: str) -> Dict[str, Any]:
|
|
||||||
text = self._extract_text(response, model, stream=False)
|
|
||||||
return {
|
|
||||||
"id": f"chatcmpl-{uuid.uuid4()}",
|
|
||||||
"object": "chat.completion",
|
|
||||||
"created": int(time.time()),
|
|
||||||
"model": model,
|
|
||||||
"choices": [{
|
|
||||||
"index": 0,
|
|
||||||
"message": {
|
|
||||||
"role": "assistant",
|
|
||||||
"content": text
|
|
||||||
},
|
|
||||||
"finish_reason": finish_reason
|
|
||||||
}],
|
|
||||||
"usage": {
|
|
||||||
"prompt_tokens": 0,
|
|
||||||
"completion_tokens": 0,
|
|
||||||
"total_tokens": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def _extract_text(self, response: Dict[str, Any], model: str, stream: bool = False) -> str:
|
|
||||||
text = ""
|
|
||||||
if stream:
|
|
||||||
if response.get("candidates"):
|
|
||||||
candidate = response["candidates"][0]
|
|
||||||
content = candidate.get("content", {})
|
|
||||||
parts = content.get("parts", [])
|
|
||||||
# if "thinking" in model:
|
|
||||||
# if settings.SHOW_THINKING_PROCESS:
|
|
||||||
# if len(parts) == 1:
|
|
||||||
# if self.thinking_first:
|
|
||||||
# self.thinking_first = False
|
|
||||||
# self.thinking_status = True
|
|
||||||
# text = "> thinking\n\n" + parts[0].get("text")
|
|
||||||
# else:
|
|
||||||
# text = parts[0].get("text")
|
|
||||||
|
|
||||||
# if len(parts) == 2:
|
|
||||||
# self.thinking_status = False
|
|
||||||
# if self.thinking_first:
|
|
||||||
# self.thinking_first = False
|
|
||||||
# text = (
|
|
||||||
# "> thinking\n\n"
|
|
||||||
# + parts[0].get("text")
|
|
||||||
# + "\n\n---\n> output\n\n"
|
|
||||||
# + parts[1].get("text")
|
|
||||||
# )
|
|
||||||
# else:
|
|
||||||
# text = (
|
|
||||||
# parts[0].get("text")
|
|
||||||
# + "\n\n---\n> output\n\n"
|
|
||||||
# + parts[1].get("text")
|
|
||||||
# )
|
|
||||||
# else:
|
|
||||||
# if len(parts) == 1:
|
|
||||||
# if self.thinking_first:
|
|
||||||
# self.thinking_first = False
|
|
||||||
# self.thinking_status = True
|
|
||||||
# text = ""
|
|
||||||
# elif self.thinking_status:
|
|
||||||
# text = ""
|
|
||||||
# else:
|
|
||||||
# text = parts[0].get("text")
|
|
||||||
|
|
||||||
# if len(parts) == 2:
|
def _extract_text(response: Dict[str, Any], model: str, stream: bool = False) -> str:
|
||||||
# self.thinking_status = False
|
text = ""
|
||||||
# if self.thinking_first:
|
if stream:
|
||||||
# self.thinking_first = False
|
if response.get("candidates"):
|
||||||
# text = parts[1].get("text")
|
candidate = response["candidates"][0]
|
||||||
# else:
|
content = candidate.get("content", {})
|
||||||
# text = parts[1].get("text")
|
parts = content.get("parts", [])
|
||||||
# else:
|
# if "thinking" in model:
|
||||||
# if "text" in parts[0]:
|
# if settings.SHOW_THINKING_PROCESS:
|
||||||
# text = parts[0].get("text")
|
# if len(parts) == 1:
|
||||||
# elif "executableCode" in parts[0]:
|
# if self.thinking_first:
|
||||||
# text = _format_code_block(parts[0]["executableCode"])
|
# self.thinking_first = False
|
||||||
# elif "codeExecution" in parts[0]:
|
# self.thinking_status = True
|
||||||
# text = _format_code_block(parts[0]["codeExecution"])
|
# text = "> thinking\n\n" + parts[0].get("text")
|
||||||
# elif "executableCodeResult" in parts[0]:
|
# else:
|
||||||
# text = _format_execution_result(
|
# text = parts[0].get("text")
|
||||||
# parts[0]["executableCodeResult"]
|
|
||||||
# )
|
# if len(parts) == 2:
|
||||||
# elif "codeExecutionResult" in parts[0]:
|
# self.thinking_status = False
|
||||||
# text = _format_execution_result(
|
# if self.thinking_first:
|
||||||
# parts[0]["codeExecutionResult"]
|
# self.thinking_first = False
|
||||||
# )
|
# text = (
|
||||||
# else:
|
# "> thinking\n\n"
|
||||||
# text = ""
|
# + parts[0].get("text")
|
||||||
# text = _add_search_link_text(model, candidate, text)
|
# + "\n\n---\n> output\n\n"
|
||||||
if "text" in parts[0]:
|
# + parts[1].get("text")
|
||||||
text = parts[0].get("text")
|
# )
|
||||||
elif "executableCode" in parts[0]:
|
# else:
|
||||||
text = _format_code_block(parts[0]["executableCode"])
|
# text = (
|
||||||
elif "codeExecution" in parts[0]:
|
# parts[0].get("text")
|
||||||
text = _format_code_block(parts[0]["codeExecution"])
|
# + "\n\n---\n> output\n\n"
|
||||||
elif "executableCodeResult" in parts[0]:
|
# + parts[1].get("text")
|
||||||
text = _format_execution_result(
|
# )
|
||||||
parts[0]["executableCodeResult"]
|
# else:
|
||||||
)
|
# if len(parts) == 1:
|
||||||
elif "codeExecutionResult" in parts[0]:
|
# if self.thinking_first:
|
||||||
text = _format_execution_result(
|
# self.thinking_first = False
|
||||||
parts[0]["codeExecutionResult"]
|
# self.thinking_status = True
|
||||||
)
|
# text = ""
|
||||||
else:
|
# elif self.thinking_status:
|
||||||
text = ""
|
# text = ""
|
||||||
text = _add_search_link_text(model, candidate, text)
|
# else:
|
||||||
else:
|
# text = parts[0].get("text")
|
||||||
if response.get("candidates"):
|
|
||||||
candidate = response["candidates"][0]
|
# if len(parts) == 2:
|
||||||
if "thinking" in model:
|
# self.thinking_status = False
|
||||||
if settings.SHOW_THINKING_PROCESS:
|
# if self.thinking_first:
|
||||||
if len(candidate["content"]["parts"]) == 2:
|
# self.thinking_first = False
|
||||||
text = (
|
# text = parts[1].get("text")
|
||||||
|
# else:
|
||||||
|
# text = parts[1].get("text")
|
||||||
|
# else:
|
||||||
|
# if "text" in parts[0]:
|
||||||
|
# text = parts[0].get("text")
|
||||||
|
# elif "executableCode" in parts[0]:
|
||||||
|
# text = _format_code_block(parts[0]["executableCode"])
|
||||||
|
# elif "codeExecution" in parts[0]:
|
||||||
|
# text = _format_code_block(parts[0]["codeExecution"])
|
||||||
|
# elif "executableCodeResult" in parts[0]:
|
||||||
|
# text = _format_execution_result(
|
||||||
|
# parts[0]["executableCodeResult"]
|
||||||
|
# )
|
||||||
|
# elif "codeExecutionResult" in parts[0]:
|
||||||
|
# text = _format_execution_result(
|
||||||
|
# parts[0]["codeExecutionResult"]
|
||||||
|
# )
|
||||||
|
# else:
|
||||||
|
# text = ""
|
||||||
|
if "text" in parts[0]:
|
||||||
|
text = parts[0].get("text")
|
||||||
|
elif "executableCode" in parts[0]:
|
||||||
|
text = _format_code_block(parts[0]["executableCode"])
|
||||||
|
elif "codeExecution" in parts[0]:
|
||||||
|
text = _format_code_block(parts[0]["codeExecution"])
|
||||||
|
elif "executableCodeResult" in parts[0]:
|
||||||
|
text = _format_execution_result(
|
||||||
|
parts[0]["executableCodeResult"]
|
||||||
|
)
|
||||||
|
elif "codeExecutionResult" in parts[0]:
|
||||||
|
text = _format_execution_result(
|
||||||
|
parts[0]["codeExecutionResult"]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
text = ""
|
||||||
|
text = _add_search_link_text(model, candidate, text)
|
||||||
|
else:
|
||||||
|
if response.get("candidates"):
|
||||||
|
candidate = response["candidates"][0]
|
||||||
|
if "thinking" in model:
|
||||||
|
if settings.SHOW_THINKING_PROCESS:
|
||||||
|
if len(candidate["content"]["parts"]) == 2:
|
||||||
|
text = (
|
||||||
"> thinking\n\n"
|
"> thinking\n\n"
|
||||||
+ candidate["content"]["parts"][0]["text"]
|
+ candidate["content"]["parts"][0]["text"]
|
||||||
+ "\n\n---\n> output\n\n"
|
+ "\n\n---\n> output\n\n"
|
||||||
+ candidate["content"]["parts"][1]["text"]
|
+ candidate["content"]["parts"][1]["text"]
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
text = candidate["content"]["parts"][0]["text"]
|
|
||||||
else:
|
else:
|
||||||
if len(candidate["content"]["parts"]) == 2:
|
text = candidate["content"]["parts"][0]["text"]
|
||||||
text = candidate["content"]["parts"][1]["text"]
|
|
||||||
else:
|
|
||||||
text = candidate["content"]["parts"][0]["text"]
|
|
||||||
else:
|
else:
|
||||||
text = candidate["content"]["parts"][0]["text"]
|
if len(candidate["content"]["parts"]) == 2:
|
||||||
text = _add_search_link_text(model, candidate, text)
|
text = candidate["content"]["parts"][1]["text"]
|
||||||
|
else:
|
||||||
|
text = candidate["content"]["parts"][0]["text"]
|
||||||
else:
|
else:
|
||||||
text = "暂无返回"
|
text = candidate["content"]["parts"][0]["text"]
|
||||||
return text
|
text = _add_search_link_text(model, candidate, text)
|
||||||
|
else:
|
||||||
|
text = "暂无返回"
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_gemini_stream_response(response: Dict[str, Any], model: str, stream: bool) -> Dict[str, Any]:
|
||||||
|
text = _extract_text(response, model, stream=stream)
|
||||||
|
content = {"parts": [{"text": text}], "role": "model"}
|
||||||
|
response["candidates"][0]["content"] = content
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_gemini_normal_response(response: Dict[str, Any], model: str, stream: bool) -> Dict[str, Any]:
|
||||||
|
text = _extract_text(response, model, stream=stream)
|
||||||
|
content = {"parts": [{"text": text}], "role": "model"}
|
||||||
|
response["candidates"][0]["content"] = content
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _format_code_block(code_data: dict) -> str:
|
def _format_code_block(code_data: dict) -> str:
|
||||||
"""格式化代码块输出"""
|
"""格式化代码块输出"""
|
||||||
language = code_data.get("language", "").lower()
|
language = code_data.get("language", "").lower()
|
||||||
code = code_data.get("code", "").strip()
|
code = code_data.get("code", "").strip()
|
||||||
return f"""\n\n---\n\n【代码执行】\n```{language}\n{code}\n```\n"""
|
return f"""\n\n---\n\n【代码执行】\n```{language}\n{code}\n```\n"""
|
||||||
|
|
||||||
|
|
||||||
def _add_search_link_text(model:str, candidate:dict, text:str) -> str:
|
def _add_search_link_text(model: str, candidate: dict, text: str) -> str:
|
||||||
if (
|
if (
|
||||||
settings.SHOW_SEARCH_LINK
|
settings.SHOW_SEARCH_LINK
|
||||||
and model.endswith("-search")
|
and model.endswith("-search")
|
||||||
and "groundingMetadata" in candidate
|
and "groundingMetadata" in candidate
|
||||||
and "groundingChunks" in candidate["groundingMetadata"]
|
and "groundingChunks" in candidate["groundingMetadata"]
|
||||||
):
|
):
|
||||||
grounding_chunks = candidate["groundingMetadata"]["groundingChunks"]
|
grounding_chunks = candidate["groundingMetadata"]["groundingChunks"]
|
||||||
text += "\n\n---\n\n"
|
text += "\n\n---\n\n"
|
||||||
@@ -351,4 +245,4 @@ def _format_execution_result(result_data: dict) -> str:
|
|||||||
"""格式化执行结果输出"""
|
"""格式化执行结果输出"""
|
||||||
outcome = result_data.get("outcome", "")
|
outcome = result_data.get("outcome", "")
|
||||||
output = result_data.get("output", "").strip()
|
output = result_data.get("output", "").strip()
|
||||||
return f"""\n【执行结果】\n> outcome: {outcome}\n\n【输出结果】\n```plaintext\n{output}\n```\n\n---\n\n"""
|
return f"""\n【执行结果】\n> outcome: {outcome}\n\n【输出结果】\n```plaintext\n{output}\n```\n\n---\n\n"""
|
||||||
|
|||||||
@@ -8,26 +8,27 @@ from app.services.key_manager import KeyManager
|
|||||||
T = TypeVar('T')
|
T = TypeVar('T')
|
||||||
logger = get_retry_logger()
|
logger = get_retry_logger()
|
||||||
|
|
||||||
|
|
||||||
class RetryHandler:
|
class RetryHandler:
|
||||||
"""重试处理装饰器"""
|
"""重试处理装饰器"""
|
||||||
|
|
||||||
def __init__(self, max_retries: int = 3, key_manager: KeyManager = None, key_arg: str = "api_key"):
|
def __init__(self, max_retries: int = 3, key_manager: KeyManager = None, key_arg: str = "api_key"):
|
||||||
self.max_retries = max_retries
|
self.max_retries = max_retries
|
||||||
self.key_manager = key_manager
|
self.key_manager = key_manager
|
||||||
self.key_arg = key_arg
|
self.key_arg = key_arg
|
||||||
|
|
||||||
def __call__(self, func: Callable[..., T]) -> Callable[..., T]:
|
def __call__(self, func: Callable[..., T]) -> Callable[..., T]:
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
async def wrapper(*args, **kwargs) -> T:
|
async def wrapper(*args, **kwargs) -> T:
|
||||||
last_exception = None
|
last_exception = None
|
||||||
|
|
||||||
for attempt in range(self.max_retries):
|
for attempt in range(self.max_retries):
|
||||||
try:
|
try:
|
||||||
return await func(*args, **kwargs)
|
return await func(*args, **kwargs)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
last_exception = e
|
last_exception = e
|
||||||
logger.warning(f"API call failed with error: {str(e)}. Attempt {attempt + 1} of {self.max_retries}")
|
logger.warning(f"API call failed with error: {str(e)}. Attempt {attempt + 1} of {self.max_retries}")
|
||||||
|
|
||||||
if self.key_manager:
|
if self.key_manager:
|
||||||
old_key = kwargs.get(self.key_arg)
|
old_key = kwargs.get(self.key_arg)
|
||||||
new_key = await self.key_manager.handle_api_failure(old_key)
|
new_key = await self.key_manager.handle_api_failure(old_key)
|
||||||
@@ -36,5 +37,5 @@ class RetryHandler:
|
|||||||
|
|
||||||
logger.error(f"All retry attempts failed, raising final exception: {str(last_exception)}")
|
logger.error(f"All retry attempts failed, raising final exception: {str(last_exception)}")
|
||||||
raise last_exception
|
raise last_exception
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|||||||
@@ -10,6 +10,61 @@ from app.services.chat.response_handler import GeminiResponseHandler
|
|||||||
from app.services.key_manager import KeyManager
|
from app.services.key_manager import KeyManager
|
||||||
|
|
||||||
logger = get_gemini_logger()
|
logger = get_gemini_logger()
|
||||||
|
|
||||||
|
|
||||||
|
def _has_image_parts(contents: List[Dict[str, Any]]) -> bool:
|
||||||
|
"""判断消息是否包含图片部分"""
|
||||||
|
for content in contents:
|
||||||
|
if "parts" in content:
|
||||||
|
for part in content["parts"]:
|
||||||
|
if "image_url" in part or "inline_data" in part:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _build_tools(model: str, payload: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
|
"""构建工具"""
|
||||||
|
tools = []
|
||||||
|
if settings.TOOLS_CODE_EXECUTION_ENABLED and not (
|
||||||
|
model.endswith("-search") or "-thinking" in model
|
||||||
|
) and not _has_image_parts(payload.get("contents", [])):
|
||||||
|
tools.append({"code_execution": {}})
|
||||||
|
if model.endswith("-search"):
|
||||||
|
tools.append({"googleSearch": {}})
|
||||||
|
return tools
|
||||||
|
|
||||||
|
|
||||||
|
def _get_safety_settings(model: str) -> List[Dict[str, str]]:
|
||||||
|
"""获取安全设置"""
|
||||||
|
if model == "gemini-2.0-flash-exp":
|
||||||
|
return [
|
||||||
|
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "OFF"},
|
||||||
|
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "OFF"},
|
||||||
|
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "OFF"},
|
||||||
|
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "OFF"},
|
||||||
|
{"category": "HARM_CATEGORY_CIVIC_INTEGRITY", "threshold": "OFF"}
|
||||||
|
]
|
||||||
|
return [
|
||||||
|
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
|
||||||
|
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
|
||||||
|
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
|
||||||
|
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
|
||||||
|
{"category": "HARM_CATEGORY_CIVIC_INTEGRITY", "threshold": "BLOCK_NONE"}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _build_payload(model: str, request: GeminiRequest) -> Dict[str, Any]:
|
||||||
|
"""构建请求payload"""
|
||||||
|
payload = request.model_dump()
|
||||||
|
return {
|
||||||
|
"contents": payload.get("contents", []),
|
||||||
|
"tools": _build_tools(model, payload),
|
||||||
|
"safetySettings": _get_safety_settings(model),
|
||||||
|
"generationConfig": payload.get("generationConfig", {}),
|
||||||
|
"systemInstruction": payload.get("systemInstruction", [])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class GeminiChatService:
|
class GeminiChatService:
|
||||||
"""聊天服务"""
|
"""聊天服务"""
|
||||||
|
|
||||||
@@ -17,18 +72,18 @@ class GeminiChatService:
|
|||||||
self.api_client = GeminiApiClient(base_url)
|
self.api_client = GeminiApiClient(base_url)
|
||||||
self.key_manager = key_manager
|
self.key_manager = key_manager
|
||||||
self.response_handler = GeminiResponseHandler()
|
self.response_handler = GeminiResponseHandler()
|
||||||
|
|
||||||
def generate_content(self, model: str, request: GeminiRequest, api_key: str) -> Dict[str, Any]:
|
def generate_content(self, model: str, request: GeminiRequest, api_key: str) -> Dict[str, Any]:
|
||||||
"""生成内容"""
|
"""生成内容"""
|
||||||
payload = self._build_payload(model, request)
|
payload = _build_payload(model, request)
|
||||||
response = self.api_client.generate_content(payload, model, api_key)
|
response = self.api_client.generate_content(payload, model, api_key)
|
||||||
return self.response_handler.handle_response(response, model, stream=False)
|
return self.response_handler.handle_response(response, model, stream=False)
|
||||||
|
|
||||||
async def stream_generate_content(self, model: str, request: GeminiRequest, api_key: str) -> AsyncGenerator[str, None]:
|
async def stream_generate_content(self, model: str, request: GeminiRequest, api_key: str) -> AsyncGenerator[str, None]:
|
||||||
"""流式生成内容"""
|
"""流式生成内容"""
|
||||||
retries = 0
|
retries = 0
|
||||||
max_retries = 3
|
max_retries = 3
|
||||||
payload = self._build_payload(model, request)
|
payload = _build_payload(model, request)
|
||||||
while retries < max_retries:
|
while retries < max_retries:
|
||||||
try:
|
try:
|
||||||
async for line in self.api_client.stream_generate_content(payload, model, api_key):
|
async for line in self.api_client.stream_generate_content(payload, model, api_key):
|
||||||
@@ -47,52 +102,3 @@ class GeminiChatService:
|
|||||||
if retries >= max_retries:
|
if retries >= max_retries:
|
||||||
logger.error(f"Max retries ({max_retries}) reached for streaming. Raising error")
|
logger.error(f"Max retries ({max_retries}) reached for streaming. Raising error")
|
||||||
break
|
break
|
||||||
|
|
||||||
def _build_payload(self,model: str, request: GeminiRequest) -> Dict[str, Any]:
|
|
||||||
"""构建请求payload"""
|
|
||||||
payload = request.model_dump()
|
|
||||||
return {
|
|
||||||
"contents": payload.get("contents", []),
|
|
||||||
"tools": self._build_tools(model, payload),
|
|
||||||
"safetySettings": self._get_safety_settings(model),
|
|
||||||
"generationConfig": payload.get("generationConfig", {}),
|
|
||||||
"systemInstruction": payload.get("systemInstruction", [])
|
|
||||||
}
|
|
||||||
|
|
||||||
def _build_tools(self, model: str, payload: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
||||||
"""构建工具"""
|
|
||||||
tools = []
|
|
||||||
if settings.TOOLS_CODE_EXECUTION_ENABLED and not (
|
|
||||||
model.endswith("-search") or "-thinking" in model
|
|
||||||
) and not self._has_image_parts(payload.get("contents", [])):
|
|
||||||
tools.append({"code_execution": {}})
|
|
||||||
if model.endswith("-search"):
|
|
||||||
tools.append({"googleSearch": {}})
|
|
||||||
return tools
|
|
||||||
|
|
||||||
def _has_image_parts(self, contents: List[Dict[str, Any]]) -> bool:
|
|
||||||
"""判断消息是否包含图片部分"""
|
|
||||||
for content in contents:
|
|
||||||
if "parts" in content:
|
|
||||||
for part in content["parts"]:
|
|
||||||
if "image_url" in part or "inline_data" in part:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _get_safety_settings(self, model: str) -> List[Dict[str, str]]:
|
|
||||||
"""获取安全设置"""
|
|
||||||
if model == "gemini-2.0-flash-exp":
|
|
||||||
return [
|
|
||||||
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "OFF"},
|
|
||||||
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "OFF"},
|
|
||||||
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "OFF"},
|
|
||||||
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "OFF"},
|
|
||||||
{"category": "HARM_CATEGORY_CIVIC_INTEGRITY", "threshold": "OFF"}
|
|
||||||
]
|
|
||||||
return [
|
|
||||||
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
|
|
||||||
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
|
|
||||||
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
|
|
||||||
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
|
|
||||||
{"category": "HARM_CATEGORY_CIVIC_INTEGRITY", "threshold": "BLOCK_NONE"}
|
|
||||||
]
|
|
||||||
@@ -36,9 +36,9 @@ class ModelService:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def convert_to_openai_models_format(
|
def convert_to_openai_models_format(
|
||||||
self, gemini_models: Dict[str, Any]
|
self, gemini_models: Dict[str, Any]
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
openai_format = {"object": "list", "data": [],"success": True}
|
openai_format = {"object": "list", "data": [], "success": True}
|
||||||
|
|
||||||
for model in gemini_models.get("models", []):
|
for model in gemini_models.get("models", []):
|
||||||
model_id = model["name"].split("/")[-1]
|
model_id = model["name"].split("/")[-1]
|
||||||
|
|||||||
@@ -13,6 +13,76 @@ from app.services.key_manager import KeyManager
|
|||||||
logger = get_openai_logger()
|
logger = get_openai_logger()
|
||||||
|
|
||||||
|
|
||||||
|
def _has_image_parts(contents: List[Dict[str, Any]]) -> bool:
|
||||||
|
"""判断消息是否包含图片部分"""
|
||||||
|
for content in contents:
|
||||||
|
if "parts" in content:
|
||||||
|
for part in content["parts"]:
|
||||||
|
if "image_url" in part or "inline_data" in part:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _build_tools(
|
||||||
|
request: ChatRequest, messages: List[Dict[str, Any]]
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""构建工具"""
|
||||||
|
tools = []
|
||||||
|
model = request.model
|
||||||
|
|
||||||
|
if (
|
||||||
|
settings.TOOLS_CODE_EXECUTION_ENABLED
|
||||||
|
and not (model.endswith("-search") or "-thinking" in model)
|
||||||
|
and not _has_image_parts(messages)
|
||||||
|
):
|
||||||
|
tools.append({"code_execution": {}})
|
||||||
|
if model.endswith("-search"):
|
||||||
|
tools.append({"googleSearch": {}})
|
||||||
|
return tools
|
||||||
|
|
||||||
|
|
||||||
|
def _get_safety_settings(model: str) -> List[Dict[str, str]]:
|
||||||
|
"""获取安全设置"""
|
||||||
|
# if (
|
||||||
|
# "2.0" in model
|
||||||
|
# and "gemini-2.0-flash-thinking-exp" not in model
|
||||||
|
# and "gemini-2.0-pro-exp" not in model
|
||||||
|
# ):
|
||||||
|
if model == "gemini-2.0-flash-exp":
|
||||||
|
return [
|
||||||
|
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "OFF"},
|
||||||
|
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "OFF"},
|
||||||
|
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "OFF"},
|
||||||
|
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "OFF"},
|
||||||
|
{"category": "HARM_CATEGORY_CIVIC_INTEGRITY", "threshold": "OFF"},
|
||||||
|
]
|
||||||
|
return [
|
||||||
|
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
|
||||||
|
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
|
||||||
|
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
|
||||||
|
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
|
||||||
|
{"category": "HARM_CATEGORY_CIVIC_INTEGRITY", "threshold": "BLOCK_NONE"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _build_payload(
|
||||||
|
request: ChatRequest, messages: List[Dict[str, Any]]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""构建请求payload"""
|
||||||
|
return {
|
||||||
|
"contents": messages,
|
||||||
|
"generationConfig": {
|
||||||
|
"temperature": request.temperature,
|
||||||
|
"maxOutputTokens": request.max_tokens,
|
||||||
|
"stopSequences": request.stop,
|
||||||
|
"topP": request.top_p,
|
||||||
|
"topK": request.top_k,
|
||||||
|
},
|
||||||
|
"tools": _build_tools(request, messages),
|
||||||
|
"safetySettings": _get_safety_settings(request.model),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class OpenAIChatService:
|
class OpenAIChatService:
|
||||||
"""聊天服务"""
|
"""聊天服务"""
|
||||||
|
|
||||||
@@ -32,7 +102,7 @@ class OpenAIChatService:
|
|||||||
messages = self.message_converter.convert(request.messages)
|
messages = self.message_converter.convert(request.messages)
|
||||||
|
|
||||||
# 构建请求payload
|
# 构建请求payload
|
||||||
payload = self._build_payload(request, messages)
|
payload = _build_payload(request, messages)
|
||||||
|
|
||||||
if request.stream:
|
if request.stream:
|
||||||
return self._handle_stream_completion(request.model, payload, api_key)
|
return self._handle_stream_completion(request.model, payload, api_key)
|
||||||
@@ -84,69 +154,3 @@ class OpenAIChatService:
|
|||||||
yield f"data: {json.dumps({'error': 'Streaming failed after retries'})}\n\n"
|
yield f"data: {json.dumps({'error': 'Streaming failed after retries'})}\n\n"
|
||||||
yield "data: [DONE]\n\n"
|
yield "data: [DONE]\n\n"
|
||||||
break
|
break
|
||||||
|
|
||||||
def _build_payload(
|
|
||||||
self, request: ChatRequest, messages: List[Dict[str, Any]]
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
"""构建请求payload"""
|
|
||||||
return {
|
|
||||||
"contents": messages,
|
|
||||||
"generationConfig": {
|
|
||||||
"temperature": request.temperature,
|
|
||||||
"maxOutputTokens": request.max_tokens,
|
|
||||||
"stopSequences": request.stop,
|
|
||||||
"topP": request.top_p,
|
|
||||||
"topK": request.top_k,
|
|
||||||
},
|
|
||||||
"tools": self._build_tools(request, messages),
|
|
||||||
"safetySettings": self._get_safety_settings(request.model),
|
|
||||||
}
|
|
||||||
|
|
||||||
def _build_tools(
|
|
||||||
self, request: ChatRequest, messages: List[Dict[str, Any]]
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
"""构建工具"""
|
|
||||||
tools = []
|
|
||||||
model = request.model
|
|
||||||
|
|
||||||
if (
|
|
||||||
settings.TOOLS_CODE_EXECUTION_ENABLED
|
|
||||||
and not (model.endswith("-search") or "-thinking" in model)
|
|
||||||
and not self._has_image_parts(messages)
|
|
||||||
):
|
|
||||||
tools.append({"code_execution": {}})
|
|
||||||
if model.endswith("-search"):
|
|
||||||
tools.append({"googleSearch": {}})
|
|
||||||
return tools
|
|
||||||
|
|
||||||
def _has_image_parts(self, contents: List[Dict[str, Any]]) -> bool:
|
|
||||||
"""判断消息是否包含图片部分"""
|
|
||||||
for content in contents:
|
|
||||||
if "parts" in content:
|
|
||||||
for part in content["parts"]:
|
|
||||||
if "image_url" in part or "inline_data" in part:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _get_safety_settings(self, model: str) -> List[Dict[str, str]]:
|
|
||||||
"""获取安全设置"""
|
|
||||||
# if (
|
|
||||||
# "2.0" in model
|
|
||||||
# and "gemini-2.0-flash-thinking-exp" not in model
|
|
||||||
# and "gemini-2.0-pro-exp" not in model
|
|
||||||
# ):
|
|
||||||
if model == "gemini-2.0-flash-exp":
|
|
||||||
return [
|
|
||||||
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "OFF"},
|
|
||||||
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "OFF"},
|
|
||||||
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "OFF"},
|
|
||||||
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "OFF"},
|
|
||||||
{"category": "HARM_CATEGORY_CIVIC_INTEGRITY", "threshold": "OFF"},
|
|
||||||
]
|
|
||||||
return [
|
|
||||||
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
|
|
||||||
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
|
|
||||||
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
|
|
||||||
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
|
|
||||||
{"category": "HARM_CATEGORY_CIVIC_INTEGRITY", "threshold": "BLOCK_NONE"},
|
|
||||||
]
|
|
||||||
|
|||||||
Reference in New Issue
Block a user