mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-09-06 16:16:37 +08:00
fix: 修复 OpenAI 和 Gemini API 调用重试逻辑及日志记录
This commit is contained in:
@@ -36,6 +36,7 @@ share/python-wheels/
|
|||||||
.installed.cfg
|
.installed.cfg
|
||||||
*.egg
|
*.egg
|
||||||
MANIFEST
|
MANIFEST
|
||||||
|
.idea/
|
||||||
|
|
||||||
# PyInstaller
|
# PyInstaller
|
||||||
# Usually these files are written by a python script from a template
|
# Usually these files are written by a python script from a template
|
||||||
|
|||||||
+14
-12
@@ -1,14 +1,13 @@
|
|||||||
from email.header import Header
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.core.logger import get_gemini_logger
|
||||||
from app.core.security import SecurityService
|
from app.core.security import SecurityService
|
||||||
|
from app.schemas.gemini_models import GeminiRequest
|
||||||
from app.services.chat_service import ChatService
|
from app.services.chat_service import ChatService
|
||||||
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.schemas.gemini_models import GeminiRequest
|
|
||||||
from app.core.config import settings
|
|
||||||
from app.core.logger import get_gemini_logger
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/gemini/v1beta")
|
router = APIRouter(prefix="/gemini/v1beta")
|
||||||
logger = get_gemini_logger()
|
logger = get_gemini_logger()
|
||||||
@@ -19,10 +18,11 @@ key_manager = KeyManager(settings.API_KEYS)
|
|||||||
model_service = ModelService(settings.MODEL_SEARCH)
|
model_service = ModelService(settings.MODEL_SEARCH)
|
||||||
chat_service = ChatService(base_url=settings.BASE_URL, key_manager=key_manager)
|
chat_service = ChatService(base_url=settings.BASE_URL, key_manager=key_manager)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/models")
|
@router.get("/models")
|
||||||
async def list_models(
|
async def list_models(
|
||||||
key: str = None,
|
key: str = None,
|
||||||
token: str = Depends(security_service.verify_key),
|
token: str = Depends(security_service.verify_key),
|
||||||
):
|
):
|
||||||
"""获取可用的Gemini模型列表"""
|
"""获取可用的Gemini模型列表"""
|
||||||
logger.info("-" * 50 + "list_gemini_models" + "-" * 50)
|
logger.info("-" * 50 + "list_gemini_models" + "-" * 50)
|
||||||
@@ -31,11 +31,12 @@ async def list_models(
|
|||||||
logger.info(f"Using API key: {api_key}")
|
logger.info(f"Using API key: {api_key}")
|
||||||
return model_service.get_gemini_models(api_key)
|
return model_service.get_gemini_models(api_key)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/models/{model_name}:generateContent")
|
@router.post("/models/{model_name}:generateContent")
|
||||||
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),
|
x_goog_api_key: str = Depends(security_service.verify_goog_api_key),
|
||||||
):
|
):
|
||||||
"""非流式生成内容"""
|
"""非流式生成内容"""
|
||||||
logger.info("-" * 50 + "gemini_generate_content" + "-" * 50)
|
logger.info("-" * 50 + "gemini_generate_content" + "-" * 50)
|
||||||
@@ -66,11 +67,12 @@ async def generate_content(
|
|||||||
if retries >= MAX_RETRIES:
|
if retries >= MAX_RETRIES:
|
||||||
logger.error(f"Max retries ({MAX_RETRIES}) reached. Raising error")
|
logger.error(f"Max retries ({MAX_RETRIES}) reached. Raising error")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/models/{model_name}:streamGenerateContent")
|
@router.post("/models/{model_name}:streamGenerateContent")
|
||||||
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),
|
x_goog_api_key: str = Depends(security_service.verify_goog_api_key),
|
||||||
):
|
):
|
||||||
"""流式生成内容"""
|
"""流式生成内容"""
|
||||||
logger.info("-" * 50 + "gemini_stream_generate_content" + "-" * 50)
|
logger.info("-" * 50 + "gemini_stream_generate_content" + "-" * 50)
|
||||||
|
|||||||
@@ -48,9 +48,9 @@ async def chat_completion(
|
|||||||
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}")
|
||||||
retries = 0
|
retries = 0
|
||||||
MAX_RETRIES = 3
|
max_retries = 3
|
||||||
|
|
||||||
while retries < MAX_RETRIES:
|
while retries < max_retries:
|
||||||
try:
|
try:
|
||||||
response = await chat_service.create_chat_completion(
|
response = await chat_service.create_chat_completion(
|
||||||
request=request,
|
request=request,
|
||||||
@@ -64,13 +64,13 @@ async def chat_completion(
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"API call failed with error: {str(e)}. Attempt {retries + 1} of {MAX_RETRIES}"
|
f"API call failed with error: {str(e)}. Attempt {retries + 1} of {max_retries}"
|
||||||
)
|
)
|
||||||
api_key = await key_manager.handle_api_failure(api_key)
|
api_key = await key_manager.handle_api_failure(api_key)
|
||||||
logger.info(f"Switched to new API key: {api_key}")
|
logger.info(f"Switched to new API key: {api_key}")
|
||||||
retries += 1
|
retries += 1
|
||||||
if retries >= MAX_RETRIES:
|
if retries >= max_retries:
|
||||||
logger.error(f"Max retries ({MAX_RETRIES}) reached. Raising error")
|
logger.error(f"Max retries ({max_retries}) reached. Raising error")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class Settings(BaseSettings):
|
|||||||
MODEL_SEARCH: List[str] = ["gemini-2.0-flash-exp"]
|
MODEL_SEARCH: List[str] = ["gemini-2.0-flash-exp"]
|
||||||
TOOLS_CODE_EXECUTION_ENABLED: bool = False
|
TOOLS_CODE_EXECUTION_ENABLED: bool = False
|
||||||
SHOW_SEARCH_LINK: bool = True
|
SHOW_SEARCH_LINK: bool = True
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
env_file = ".env"
|
env_file = ".env"
|
||||||
|
|
||||||
|
|||||||
+23
-7
@@ -5,16 +5,17 @@ import platform
|
|||||||
|
|
||||||
# ANSI转义序列颜色代码
|
# ANSI转义序列颜色代码
|
||||||
COLORS = {
|
COLORS = {
|
||||||
'DEBUG': '\033[34m', # 蓝色
|
'DEBUG': '\033[34m', # 蓝色
|
||||||
'INFO': '\033[32m', # 绿色
|
'INFO': '\033[32m', # 绿色
|
||||||
'WARNING': '\033[33m', # 黄色
|
'WARNING': '\033[33m', # 黄色
|
||||||
'ERROR': '\033[31m', # 红色
|
'ERROR': '\033[31m', # 红色
|
||||||
'CRITICAL': '\033[1;31m' # 红色加粗
|
'CRITICAL': '\033[1;31m' # 红色加粗
|
||||||
}
|
}
|
||||||
|
|
||||||
# Windows系统启用ANSI支持
|
# Windows系统启用ANSI支持
|
||||||
if platform.system() == 'Windows':
|
if platform.system() == 'Windows':
|
||||||
import ctypes
|
import ctypes
|
||||||
|
|
||||||
kernel32 = ctypes.windll.kernel32
|
kernel32 = ctypes.windll.kernel32
|
||||||
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ class ColoredFormatter(logging.Formatter):
|
|||||||
"""
|
"""
|
||||||
自定义的日志格式化器,添加颜色支持
|
自定义的日志格式化器,添加颜色支持
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def format(self, record):
|
def format(self, record):
|
||||||
# 获取对应级别的颜色代码
|
# 获取对应级别的颜色代码
|
||||||
color = COLORS.get(record.levelname, '')
|
color = COLORS.get(record.levelname, '')
|
||||||
@@ -30,6 +32,7 @@ class ColoredFormatter(logging.Formatter):
|
|||||||
record.levelname = f"{color}{record.levelname}\033[0m"
|
record.levelname = f"{color}{record.levelname}\033[0m"
|
||||||
return super().format(record)
|
return super().format(record)
|
||||||
|
|
||||||
|
|
||||||
# 日志格式
|
# 日志格式
|
||||||
FORMATTER = ColoredFormatter(
|
FORMATTER = ColoredFormatter(
|
||||||
"%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s"
|
"%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s"
|
||||||
@@ -44,13 +47,17 @@ LOG_LEVELS = {
|
|||||||
"critical": logging.CRITICAL,
|
"critical": logging.CRITICAL,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class Logger:
|
class Logger:
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
_loggers: Dict[str, logging.Logger] = {}
|
_loggers: Dict[str, logging.Logger] = {}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def setup_logger(
|
def setup_logger(
|
||||||
name: str,
|
name: str,
|
||||||
level: str = "debug",
|
level: str = "debug",
|
||||||
) -> logging.Logger:
|
) -> logging.Logger:
|
||||||
"""
|
"""
|
||||||
设置并获取logger
|
设置并获取logger
|
||||||
@@ -82,30 +89,39 @@ class Logger:
|
|||||||
"""
|
"""
|
||||||
return Logger._loggers.get(name)
|
return Logger._loggers.get(name)
|
||||||
|
|
||||||
|
|
||||||
# 预定义的loggers
|
# 预定义的loggers
|
||||||
def get_openai_logger():
|
def get_openai_logger():
|
||||||
return Logger.setup_logger("openai")
|
return Logger.setup_logger("openai")
|
||||||
|
|
||||||
|
|
||||||
def get_gemini_logger():
|
def get_gemini_logger():
|
||||||
return Logger.setup_logger("gemini")
|
return Logger.setup_logger("gemini")
|
||||||
|
|
||||||
|
|
||||||
def get_chat_logger():
|
def get_chat_logger():
|
||||||
return Logger.setup_logger("chat")
|
return Logger.setup_logger("chat")
|
||||||
|
|
||||||
|
|
||||||
def get_model_logger():
|
def get_model_logger():
|
||||||
return Logger.setup_logger("model")
|
return Logger.setup_logger("model")
|
||||||
|
|
||||||
|
|
||||||
def get_security_logger():
|
def get_security_logger():
|
||||||
return Logger.setup_logger("security")
|
return Logger.setup_logger("security")
|
||||||
|
|
||||||
|
|
||||||
def get_key_manager_logger():
|
def get_key_manager_logger():
|
||||||
return Logger.setup_logger("key_manager")
|
return Logger.setup_logger("key_manager")
|
||||||
|
|
||||||
|
|
||||||
def get_main_logger():
|
def get_main_logger():
|
||||||
return Logger.setup_logger("main")
|
return Logger.setup_logger("main")
|
||||||
|
|
||||||
|
|
||||||
def get_embeddings_logger():
|
def get_embeddings_logger():
|
||||||
return Logger.setup_logger("embeddings")
|
return Logger.setup_logger("embeddings")
|
||||||
|
|
||||||
|
|
||||||
def get_request_logger():
|
def get_request_logger():
|
||||||
return Logger.setup_logger("request")
|
return Logger.setup_logger("request")
|
||||||
@@ -3,9 +3,9 @@ from starlette.middleware.base import BaseHTTPMiddleware
|
|||||||
import json
|
import json
|
||||||
from app.core.logger import get_request_logger
|
from app.core.logger import get_request_logger
|
||||||
|
|
||||||
|
|
||||||
logger = get_request_logger()
|
logger = get_request_logger()
|
||||||
|
|
||||||
|
|
||||||
# 添加中间件类
|
# 添加中间件类
|
||||||
class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
||||||
async def dispatch(self, request: Request, call_next):
|
async def dispatch(self, request: Request, call_next):
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
from typing import List, Optional, Dict, Any, Literal
|
from typing import List, Optional, Dict, Any, Literal
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class SafetySetting(BaseModel):
|
class SafetySetting(BaseModel):
|
||||||
category: Optional[Literal["HARM_CATEGORY_HATE_SPEECH", "HARM_CATEGORY_DANGEROUS_CONTENT", "HARM_CATEGORY_HARASSMENT", "HARM_CATEGORY_SEXUALLY_EXPLICIT"]] = None
|
category: Optional[Literal[
|
||||||
threshold: Optional[Literal["HARM_BLOCK_THRESHOLD_UNSPECIFIED", "BLOCK_LOW_AND_ABOVE", "BLOCK_MEDIUM_AND_ABOVE","BLOCK_ONLY_HIGH","BLOCK_NONE","OFF"]] = None
|
"HARM_CATEGORY_HATE_SPEECH", "HARM_CATEGORY_DANGEROUS_CONTENT", "HARM_CATEGORY_HARASSMENT", "HARM_CATEGORY_SEXUALLY_EXPLICIT"]] = 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):
|
||||||
|
|||||||
+125
-127
@@ -11,61 +11,73 @@ from app.schemas.openai_models import ChatRequest
|
|||||||
logger = get_chat_logger()
|
logger = get_chat_logger()
|
||||||
|
|
||||||
|
|
||||||
|
def convert_messages_to_gemini_format(messages: list) -> list:
|
||||||
|
"""Convert OpenAI message format to Gemini format"""
|
||||||
|
converted_messages = []
|
||||||
|
for msg in messages:
|
||||||
|
role = "user" if msg["role"] == "user" else "model"
|
||||||
|
parts = []
|
||||||
|
|
||||||
|
# 处理文本内容
|
||||||
|
if isinstance(msg["content"], str):
|
||||||
|
parts.append({"text": msg["content"]})
|
||||||
|
# 处理包含图片的消息
|
||||||
|
elif isinstance(msg["content"], list):
|
||||||
|
for content in msg["content"]:
|
||||||
|
if isinstance(content, str):
|
||||||
|
parts.append({"text": content})
|
||||||
|
elif isinstance(content, dict) and content["type"] == "text":
|
||||||
|
parts.append({"text": content["text"]})
|
||||||
|
elif isinstance(content, dict) and content["type"] == "image_url":
|
||||||
|
# 处理图片URL
|
||||||
|
image_url = content["image_url"]["url"]
|
||||||
|
if image_url.startswith("data:image"):
|
||||||
|
# 处理base64图片
|
||||||
|
parts.append(
|
||||||
|
{
|
||||||
|
"inline_data": {
|
||||||
|
"mime_type": "image/jpeg",
|
||||||
|
"data": image_url.split(",")[1],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 处理普通URL图片
|
||||||
|
parts.append(
|
||||||
|
{
|
||||||
|
"image_url": {
|
||||||
|
"url": image_url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
converted_messages.append({"role": role, "parts": parts})
|
||||||
|
|
||||||
|
return converted_messages
|
||||||
|
|
||||||
|
|
||||||
|
def format_execution_result(result_data: dict) -> str:
|
||||||
|
"""格式化执行结果输出"""
|
||||||
|
outcome = result_data.get("outcome", "")
|
||||||
|
output = result_data.get("output", "").strip()
|
||||||
|
return f"""\n【执行结果】\n> outcome: {outcome}\n\n【输出结果】\n```plaintext\n{output}\n```\n"""
|
||||||
|
|
||||||
|
|
||||||
|
def create_search_link(web):
|
||||||
|
return f'\n- [{web["title"]}]({web["uri"]})'
|
||||||
|
|
||||||
|
|
||||||
class ChatService:
|
class ChatService:
|
||||||
def __init__(self, base_url: str, key_manager=None):
|
def __init__(self, base_url: str, key_manager=None):
|
||||||
self.base_url = base_url
|
self.base_url = base_url
|
||||||
self.key_manager = key_manager
|
self.key_manager = key_manager
|
||||||
|
|
||||||
def convert_messages_to_gemini_format(self, messages: list) -> list:
|
|
||||||
"""Convert OpenAI message format to Gemini format"""
|
|
||||||
converted_messages = []
|
|
||||||
for msg in messages:
|
|
||||||
role = "user" if msg["role"] == "user" else "model"
|
|
||||||
parts = []
|
|
||||||
|
|
||||||
# 处理文本内容
|
|
||||||
if isinstance(msg["content"], str):
|
|
||||||
parts.append({"text": msg["content"]})
|
|
||||||
# 处理包含图片的消息
|
|
||||||
elif isinstance(msg["content"], list):
|
|
||||||
for content in msg["content"]:
|
|
||||||
if isinstance(content, str):
|
|
||||||
parts.append({"text": content})
|
|
||||||
elif isinstance(content, dict) and content["type"] == "text":
|
|
||||||
parts.append({"text": content["text"]})
|
|
||||||
elif isinstance(content, dict) and content["type"] == "image_url":
|
|
||||||
# 处理图片URL
|
|
||||||
image_url = content["image_url"]["url"]
|
|
||||||
if image_url.startswith("data:image"):
|
|
||||||
# 处理base64图片
|
|
||||||
parts.append(
|
|
||||||
{
|
|
||||||
"inline_data": {
|
|
||||||
"mime_type": "image/jpeg",
|
|
||||||
"data": image_url.split(",")[1],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# 处理普通URL图片
|
|
||||||
parts.append(
|
|
||||||
{
|
|
||||||
"image_url": {
|
|
||||||
"url": image_url,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
converted_messages.append({"role": role, "parts": parts})
|
|
||||||
|
|
||||||
return converted_messages
|
|
||||||
|
|
||||||
def convert_gemini_response_to_openai(
|
def convert_gemini_response_to_openai(
|
||||||
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]]:
|
||||||
"""Convert Gemini response to OpenAI format"""
|
"""Convert Gemini response to OpenAI format"""
|
||||||
if stream:
|
if stream:
|
||||||
@@ -82,28 +94,17 @@ class ChatService:
|
|||||||
elif "codeExecution" in parts[0]:
|
elif "codeExecution" in parts[0]:
|
||||||
text = self.format_code_block(parts[0]["codeExecution"])
|
text = self.format_code_block(parts[0]["codeExecution"])
|
||||||
elif "executableCodeResult" in parts[0]:
|
elif "executableCodeResult" in parts[0]:
|
||||||
text = self.format_execution_result(
|
text = format_execution_result(
|
||||||
parts[0]["executableCodeResult"]
|
parts[0]["executableCodeResult"]
|
||||||
)
|
)
|
||||||
elif "codeExecutionResult" in parts[0]:
|
elif "codeExecutionResult" in parts[0]:
|
||||||
text = self.format_execution_result(
|
text = format_execution_result(
|
||||||
parts[0]["codeExecutionResult"]
|
parts[0]["codeExecutionResult"]
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
text = ""
|
text = ""
|
||||||
|
|
||||||
if (
|
text = self.add_search_link_text(model, candidate, text)
|
||||||
settings.SHOW_SEARCH_LINK
|
|
||||||
and model.endswith("-search")
|
|
||||||
and "groundingMetadata" in candidate
|
|
||||||
and "groundingChunks" in candidate["groundingMetadata"]
|
|
||||||
):
|
|
||||||
groundingChunks = candidate["groundingMetadata"]["groundingChunks"]
|
|
||||||
text += "\n\n---\n\n"
|
|
||||||
text += f"**【引用来源】**\n\n"
|
|
||||||
for _, groundingChunk in enumerate(groundingChunks, 1):
|
|
||||||
if "web" in groundingChunk:
|
|
||||||
text += self.create_search_link(groundingChunk["web"])
|
|
||||||
else:
|
else:
|
||||||
text = ""
|
text = ""
|
||||||
|
|
||||||
@@ -131,37 +132,26 @@ class ChatService:
|
|||||||
"created": int(time.time()),
|
"created": int(time.time()),
|
||||||
"model": model,
|
"model": model,
|
||||||
"choices": [
|
"choices": [
|
||||||
{
|
{
|
||||||
"index": 0,
|
"index": 0,
|
||||||
"message": {
|
"message": {
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"content": response["candidates"][0]["content"]["parts"][0]["text"],
|
"content": response["candidates"][0]["content"]["parts"][0]["text"],
|
||||||
},
|
},
|
||||||
"finish_reason": finish_reason,
|
"finish_reason": finish_reason,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"usage": {
|
"usage": {
|
||||||
"prompt_tokens": 0,
|
"prompt_tokens": 0,
|
||||||
"completion_tokens": 0,
|
"completion_tokens": 0,
|
||||||
"total_tokens": 0,
|
"total_tokens": 0,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
if response.get("candidates"):
|
if response.get("candidates"):
|
||||||
text = response["candidates"][0]["content"]["parts"][0]["text"]
|
text = response["candidates"][0]["content"]["parts"][0]["text"]
|
||||||
candidate = response["candidates"][0]
|
candidate = response["candidates"][0]
|
||||||
if (
|
text = self.add_search_link_text(model, candidate, text)
|
||||||
settings.SHOW_SEARCH_LINK
|
|
||||||
and model.endswith("-search")
|
|
||||||
and "groundingMetadata" in candidate
|
|
||||||
and "groundingChunks" in candidate["groundingMetadata"]
|
|
||||||
):
|
|
||||||
groundingChunks = candidate["groundingMetadata"]["groundingChunks"]
|
|
||||||
text += "\n\n---\n\n"
|
|
||||||
text += f"**【引用来源】**\n\n"
|
|
||||||
for _, groundingChunk in enumerate(groundingChunks, 1):
|
|
||||||
if "web" in groundingChunk:
|
|
||||||
text += self.create_search_link(groundingChunk["web"])
|
|
||||||
res["choices"][0]["message"]["content"] = text
|
res["choices"][0]["message"]["content"] = text
|
||||||
return res
|
return res
|
||||||
else:
|
else:
|
||||||
@@ -173,10 +163,27 @@ class ChatService:
|
|||||||
res["choices"][0]["message"]["content"] = f"Error converting Gemini response: {str(e)}"
|
res["choices"][0]["message"]["content"] = f"Error converting Gemini response: {str(e)}"
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def add_search_link_text(self, model, candidate, text):
|
||||||
|
if (
|
||||||
|
settings.SHOW_SEARCH_LINK
|
||||||
|
and model.endswith("-search")
|
||||||
|
and "groundingMetadata" in candidate
|
||||||
|
and "groundingChunks" in candidate["groundingMetadata"]
|
||||||
|
):
|
||||||
|
grounding_chunks = candidate["groundingMetadata"]["groundingChunks"]
|
||||||
|
text += "\n\n---\n\n"
|
||||||
|
text += f"**【引用来源】**\n\n"
|
||||||
|
for _, grounding_chunk in enumerate(grounding_chunks, 1):
|
||||||
|
if "web" in grounding_chunk:
|
||||||
|
text += create_search_link(grounding_chunk["web"])
|
||||||
|
return text
|
||||||
|
else:
|
||||||
|
return text
|
||||||
|
|
||||||
async def create_chat_completion(
|
async def create_chat_completion(
|
||||||
self,
|
self,
|
||||||
request: ChatRequest,
|
request: ChatRequest,
|
||||||
api_key: str,
|
api_key: str,
|
||||||
) -> Union[Dict[str, Any], AsyncGenerator[str, None]]:
|
) -> Union[Dict[str, Any], AsyncGenerator[str, None]]:
|
||||||
"""Create chat completion using either Gemini or OpenAI API"""
|
"""Create chat completion using either Gemini or OpenAI API"""
|
||||||
model = request.model
|
model = request.model
|
||||||
@@ -184,7 +191,7 @@ class ChatService:
|
|||||||
if tools is None:
|
if tools is None:
|
||||||
tools = []
|
tools = []
|
||||||
if settings.TOOLS_CODE_EXECUTION_ENABLED and not (
|
if settings.TOOLS_CODE_EXECUTION_ENABLED and not (
|
||||||
model.endswith("-search") or "-thinking" in model
|
model.endswith("-search") or "-thinking" in model
|
||||||
):
|
):
|
||||||
tools.append({"code_execution": {}})
|
tools.append({"code_execution": {}})
|
||||||
if model.endswith("-search"):
|
if model.endswith("-search"):
|
||||||
@@ -192,10 +199,10 @@ class ChatService:
|
|||||||
return await self._gemini_chat_completion(request, api_key, tools)
|
return await self._gemini_chat_completion(request, api_key, tools)
|
||||||
|
|
||||||
async def _gemini_chat_completion(
|
async def _gemini_chat_completion(
|
||||||
self,
|
self,
|
||||||
request: ChatRequest,
|
request: ChatRequest,
|
||||||
api_key: str,
|
api_key: str,
|
||||||
tools: Optional[list] = None,
|
tools: Optional[list] = None,
|
||||||
) -> Union[Dict[str, Any], AsyncGenerator[str, None]]:
|
) -> Union[Dict[str, Any], AsyncGenerator[str, None]]:
|
||||||
"""Handle Gemini API chat completion"""
|
"""Handle Gemini API chat completion"""
|
||||||
model = request.model
|
model = request.model
|
||||||
@@ -210,7 +217,7 @@ class ChatService:
|
|||||||
gemini_model = model[:-7] # Remove -search suffix
|
gemini_model = model[:-7] # Remove -search suffix
|
||||||
else:
|
else:
|
||||||
gemini_model = model
|
gemini_model = model
|
||||||
gemini_messages = self.convert_messages_to_gemini_format(messages)
|
gemini_messages = convert_messages_to_gemini_format(messages)
|
||||||
|
|
||||||
if not stream:
|
if not stream:
|
||||||
# 非流式模式下,移除代码执行工具
|
# 非流式模式下,移除代码执行工具
|
||||||
@@ -247,26 +254,26 @@ class ChatService:
|
|||||||
if stream:
|
if stream:
|
||||||
async def generate():
|
async def generate():
|
||||||
retries = 0
|
retries = 0
|
||||||
MAX_RETRIES = 3
|
max_retries = 3
|
||||||
current_api_key = api_key
|
current_api_key = api_key
|
||||||
|
|
||||||
while retries < MAX_RETRIES:
|
while retries < max_retries:
|
||||||
try:
|
try:
|
||||||
timeout = httpx.Timeout(
|
timeout = httpx.Timeout(
|
||||||
60.0, read=60.0
|
60.0, read=60.0
|
||||||
) # 连接超时60秒,读取超时60秒
|
) # 连接超时60秒,读取超时60秒
|
||||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
async with httpx.AsyncClient(timeout=timeout) as async_client:
|
||||||
stream_url = f"https://generativelanguage.googleapis.com/v1beta/models/{gemini_model}:streamGenerateContent?alt=sse&key={current_api_key}"
|
stream_url = f"https://generativelanguage.googleapis.com/v1beta/models/{gemini_model}:streamGenerateContent?alt=sse&key={current_api_key}"
|
||||||
async with client.stream(
|
async with async_client.stream(
|
||||||
"POST", stream_url, json=payload
|
"POST", stream_url, json=payload
|
||||||
) as response:
|
) as async_response:
|
||||||
if response.status_code != 200:
|
if async_response.status_code != 200:
|
||||||
error_content = await response.read()
|
error_content = await async_response.read()
|
||||||
error_msg = error_content.decode("utf-8")
|
error_msg = error_content.decode("utf-8")
|
||||||
logger.error(
|
logger.error(
|
||||||
f"API error: {response.status_code}, {error_msg}"
|
f"API error: {async_response.status_code}, {error_msg}"
|
||||||
)
|
)
|
||||||
if retries < MAX_RETRIES - 1:
|
if retries < max_retries - 1:
|
||||||
current_api_key = (
|
current_api_key = (
|
||||||
await self.key_manager.handle_api_failure(
|
await self.key_manager.handle_api_failure(
|
||||||
current_api_key
|
current_api_key
|
||||||
@@ -276,12 +283,12 @@ class ChatService:
|
|||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Max retries reached. Final error: {response.status_code}, {error_msg}"
|
f"Max retries reached. Final error: {async_response.status_code}, {error_msg}"
|
||||||
)
|
)
|
||||||
yield f"data: {json.dumps({'error': f'API error: {response.status_code}, {error_msg}'})}\n\n"
|
yield f"data: {json.dumps({'error': f'API error: {async_response.status_code}, {error_msg}'})}\n\n"
|
||||||
return
|
return
|
||||||
|
|
||||||
async for line in response.aiter_lines():
|
async for line in async_response.aiter_lines():
|
||||||
if line.startswith("data: "):
|
if line.startswith("data: "):
|
||||||
try:
|
try:
|
||||||
chunk = json.loads(line[6:])
|
chunk = json.loads(line[6:])
|
||||||
@@ -297,7 +304,7 @@ class ChatService:
|
|||||||
yield f"data: {json.dumps(openai_chunk)}\n\n"
|
yield f"data: {json.dumps(openai_chunk)}\n\n"
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
continue
|
continue
|
||||||
yield f"data: {json.dumps(self.convert_gemini_response_to_openai({}, model,stream=True, finish_reason='stop'))}\n\n"
|
yield f"data: {json.dumps(self.convert_gemini_response_to_openai({}, model, stream=True, finish_reason='stop'))}\n\n"
|
||||||
yield "data: [DONE]\n\n"
|
yield "data: [DONE]\n\n"
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -305,7 +312,7 @@ class ChatService:
|
|||||||
logger.warning(
|
logger.warning(
|
||||||
f"Read timeout occurred, attempting retry {retries + 1}"
|
f"Read timeout occurred, attempting retry {retries + 1}"
|
||||||
)
|
)
|
||||||
if retries < MAX_RETRIES - 1:
|
if retries < max_retries - 1:
|
||||||
current_api_key = await self.key_manager.handle_api_failure(
|
current_api_key = await self.key_manager.handle_api_failure(
|
||||||
current_api_key
|
current_api_key
|
||||||
)
|
)
|
||||||
@@ -322,7 +329,7 @@ class ChatService:
|
|||||||
logger.exception(
|
logger.exception(
|
||||||
f"Stream error: {str(e)}, attempting retry {retries + 1}"
|
f"Stream error: {str(e)}, attempting retry {retries + 1}"
|
||||||
)
|
)
|
||||||
if retries < MAX_RETRIES - 1:
|
if retries < max_retries - 1:
|
||||||
current_api_key = await self.key_manager.handle_api_failure(
|
current_api_key = await self.key_manager.handle_api_failure(
|
||||||
current_api_key
|
current_api_key
|
||||||
)
|
)
|
||||||
@@ -362,14 +369,8 @@ class ChatService:
|
|||||||
|
|
||||||
return f"""\n【代码执行】\n```{language}\n{code}\n```\n"""
|
return f"""\n【代码执行】\n```{language}\n{code}\n```\n"""
|
||||||
|
|
||||||
def format_execution_result(self, result_data: dict) -> str:
|
|
||||||
"""格式化执行结果输出"""
|
|
||||||
outcome = result_data.get("outcome", "")
|
|
||||||
output = result_data.get("output", "").strip()
|
|
||||||
return f"""\n【执行结果】\n> outcome: {outcome}\n\n【输出结果】\n```plaintext\n{output}\n```\n"""
|
|
||||||
|
|
||||||
async def generate_content(
|
async def generate_content(
|
||||||
self, model_name: str, request: GeminiRequest, api_key: str
|
self, model_name: str, request: GeminiRequest, api_key: str
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""调用Gemini API生成内容"""
|
"""调用Gemini API生成内容"""
|
||||||
url = f"{self.base_url}/models/{model_name}:generateContent?key={api_key}"
|
url = f"{self.base_url}/models/{model_name}:generateContent?key={api_key}"
|
||||||
@@ -392,7 +393,7 @@ class ChatService:
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
async def stream_generate_content(
|
async def stream_generate_content(
|
||||||
self, model_name: str, request: GeminiRequest, api_key: str
|
self, model_name: str, request: GeminiRequest, api_key: str
|
||||||
) -> AsyncGenerator:
|
) -> AsyncGenerator:
|
||||||
"""调用Gemini API流式生成内容"""
|
"""调用Gemini API流式生成内容"""
|
||||||
retries = 0
|
retries = 0
|
||||||
@@ -406,7 +407,7 @@ class ChatService:
|
|||||||
|
|
||||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||||
async with client.stream(
|
async with client.stream(
|
||||||
"POST", url, json=request.model_dump()
|
"POST", url, json=request.model_dump()
|
||||||
) as response:
|
) as response:
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
error_text = await response.text()
|
error_text = await response.text()
|
||||||
@@ -451,6 +452,3 @@ class ChatService:
|
|||||||
retries += 1
|
retries += 1
|
||||||
continue
|
continue
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def create_search_link(self, web):
|
|
||||||
return f'\n- [{web["title"]}]({web["uri"]})'
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
from typing import Union, List
|
||||||
|
|
||||||
import openai
|
import openai
|
||||||
from typing import Union, List, Dict, Any
|
from openai.types import CreateEmbeddingResponse
|
||||||
|
|
||||||
from app.core.logger import get_embeddings_logger
|
from app.core.logger import get_embeddings_logger
|
||||||
|
|
||||||
logger = get_embeddings_logger()
|
logger = get_embeddings_logger()
|
||||||
@@ -11,7 +14,7 @@ class EmbeddingService:
|
|||||||
|
|
||||||
async def create_embedding(
|
async def create_embedding(
|
||||||
self, input_text: Union[str, List[str]], model: str, api_key: str
|
self, input_text: Union[str, List[str]], model: str, api_key: str
|
||||||
) -> Dict[str, Any]:
|
) -> CreateEmbeddingResponse:
|
||||||
"""Create embeddings using OpenAI API"""
|
"""Create embeddings using OpenAI API"""
|
||||||
try:
|
try:
|
||||||
client = openai.OpenAI(api_key=api_key, base_url=self.base_url)
|
client = openai.OpenAI(api_key=api_key, base_url=self.base_url)
|
||||||
|
|||||||
Reference in New Issue
Block a user