mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-08-22 08:45:01 +08:00
fix(api): 统一错误日志时间戳并传递 request_datetime
- 统一 add_error_log 的 request_time:优先使用 request_datetime, 否则使用 datetime.now(),去除 timezone.utc,避免与请求日志时区不一致 - 在 Gemini/OpenAI/Vertex/Embedding 等服务的异常处理处补充传入 request_datetime,使错误日志与请求日志可一一对应 - stats: 移除失败记录的错误日志时间窗匹配与 error_log_id 附带,降低查询开销 与误关联风险;建议通过统一时间戳(key + request_time)或独立错误日志 查询接口完成关联 - 调整部分导入顺序与长行换行等代码风格,无功能改动 BREAKING CHANGE: 统计详情接口不再返回 error_log_id 字段。需要关联错误日志的 客户端请改为基于 key 与 request_time 在错误日志接口中检索。
This commit is contained in:
@@ -141,9 +141,7 @@ async def add_error_log(
|
||||
model_name=model_name,
|
||||
error_code=error_code,
|
||||
request_msg=request_msg_json,
|
||||
request_time=(
|
||||
request_datetime if request_datetime else datetime.now(timezone.utc)
|
||||
),
|
||||
request_time=(request_datetime if request_datetime else datetime.now()),
|
||||
)
|
||||
await database.execute(query)
|
||||
logger.info(f"Added error log for key: {redact_key_for_logging(gemini_key)}")
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
# app/services/chat_service.py
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import re
|
||||
import datetime
|
||||
import time
|
||||
from typing import Any, AsyncGenerator, Dict, List
|
||||
|
||||
from app.config.config import settings
|
||||
from app.core.constants import GEMINI_2_FLASH_EXP_SAFETY_SETTINGS
|
||||
from app.database.services import add_error_log, add_request_log, get_file_api_key
|
||||
from app.domain.gemini_models import GeminiRequest
|
||||
from app.handler.response_handler import GeminiResponseHandler
|
||||
from app.handler.stream_optimizer import gemini_optimizer
|
||||
from app.log.logger import get_gemini_logger
|
||||
from app.service.client.api_client import GeminiApiClient
|
||||
from app.service.key.key_manager import KeyManager
|
||||
from app.database.services import add_error_log, add_request_log, get_file_api_key
|
||||
from app.utils.helpers import redact_key_for_logging
|
||||
|
||||
logger = get_gemini_logger()
|
||||
@@ -28,6 +29,7 @@ def _has_image_parts(contents: List[Dict[str, Any]]) -> bool:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _extract_file_references(contents: List[Dict[str, Any]]) -> List[str]:
|
||||
"""從內容中提取文件引用"""
|
||||
file_names = []
|
||||
@@ -42,7 +44,9 @@ def _extract_file_references(contents: List[Dict[str, Any]]) -> List[str]:
|
||||
file_uri = file_data["fileUri"]
|
||||
# 從 URI 中提取文件名
|
||||
# 1. https://generativelanguage.googleapis.com/v1beta/files/{file_id}
|
||||
match = re.match(rf"{re.escape(settings.BASE_URL)}/(files/.*)", file_uri)
|
||||
match = re.match(
|
||||
rf"{re.escape(settings.BASE_URL)}/(files/.*)", file_uri
|
||||
)
|
||||
if not match:
|
||||
logger.warning(f"Invalid file URI: {file_uri}")
|
||||
continue
|
||||
@@ -51,19 +55,36 @@ def _extract_file_references(contents: List[Dict[str, Any]]) -> List[str]:
|
||||
logger.info(f"Found file reference: {file_id}")
|
||||
return file_names
|
||||
|
||||
|
||||
def _clean_json_schema_properties(obj: Any) -> Any:
|
||||
"""清理JSON Schema中Gemini API不支持的字段"""
|
||||
if not isinstance(obj, dict):
|
||||
return obj
|
||||
|
||||
|
||||
# Gemini API不支持的JSON Schema字段
|
||||
unsupported_fields = {
|
||||
"exclusiveMaximum", "exclusiveMinimum", "const", "examples",
|
||||
"contentEncoding", "contentMediaType", "if", "then", "else",
|
||||
"allOf", "anyOf", "oneOf", "not", "definitions", "$schema",
|
||||
"$id", "$ref", "$comment", "readOnly", "writeOnly"
|
||||
"exclusiveMaximum",
|
||||
"exclusiveMinimum",
|
||||
"const",
|
||||
"examples",
|
||||
"contentEncoding",
|
||||
"contentMediaType",
|
||||
"if",
|
||||
"then",
|
||||
"else",
|
||||
"allOf",
|
||||
"anyOf",
|
||||
"oneOf",
|
||||
"not",
|
||||
"definitions",
|
||||
"$schema",
|
||||
"$id",
|
||||
"$ref",
|
||||
"$comment",
|
||||
"readOnly",
|
||||
"writeOnly",
|
||||
}
|
||||
|
||||
|
||||
cleaned = {}
|
||||
for key, value in obj.items():
|
||||
if key in unsupported_fields:
|
||||
@@ -74,13 +95,13 @@ def _clean_json_schema_properties(obj: Any) -> Any:
|
||||
cleaned[key] = [_clean_json_schema_properties(item) for item in value]
|
||||
else:
|
||||
cleaned[key] = value
|
||||
|
||||
|
||||
return cleaned
|
||||
|
||||
|
||||
def _build_tools(model: str, payload: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""构建工具"""
|
||||
|
||||
|
||||
def _has_function_call(contents: List[Dict[str, Any]]) -> bool:
|
||||
"""检查内容中是否包含 functionCall"""
|
||||
if not contents or not isinstance(contents, list):
|
||||
@@ -95,7 +116,7 @@ def _build_tools(model: str, payload: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
if isinstance(part, dict) and "functionCall" in part:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _merge_tools(tools: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
record = dict()
|
||||
for item in tools:
|
||||
@@ -146,16 +167,18 @@ def _build_tools(model: str, payload: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
and not _has_image_parts(payload.get("contents", []))
|
||||
):
|
||||
tool["codeExecution"] = {}
|
||||
|
||||
|
||||
if model.endswith("-search"):
|
||||
tool["googleSearch"] = {}
|
||||
|
||||
|
||||
real_model = _get_real_model(model)
|
||||
if real_model in settings.URL_CONTEXT_MODELS and settings.URL_CONTEXT_ENABLED:
|
||||
tool["urlContext"] = {}
|
||||
|
||||
# 解决 "Tool use with function calling is unsupported" 问题
|
||||
if tool.get("functionDeclarations") or _has_function_call(payload.get("contents", [])):
|
||||
if tool.get("functionDeclarations") or _has_function_call(
|
||||
payload.get("contents", [])
|
||||
):
|
||||
tool.pop("googleSearch", None)
|
||||
tool.pop("codeExecution", None)
|
||||
tool.pop("urlContext", None)
|
||||
@@ -189,10 +212,16 @@ def _filter_empty_parts(contents: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
|
||||
filtered_contents = []
|
||||
for content in contents:
|
||||
if not content or "parts" not in content or not isinstance(content.get("parts"), list):
|
||||
if (
|
||||
not content
|
||||
or "parts" not in content
|
||||
or not isinstance(content.get("parts"), list)
|
||||
):
|
||||
continue
|
||||
|
||||
valid_parts = [part for part in content["parts"] if isinstance(part, dict) and part]
|
||||
valid_parts = [
|
||||
part for part in content["parts"] if isinstance(part, dict) and part
|
||||
]
|
||||
|
||||
if valid_parts:
|
||||
new_content = content.copy()
|
||||
@@ -241,30 +270,32 @@ def _build_payload(model: str, request: GeminiRequest) -> Dict[str, Any]:
|
||||
if model.endswith("-image") or model.endswith("-image-generation"):
|
||||
payload.pop("systemInstruction")
|
||||
payload["generationConfig"]["responseModalities"] = ["Text", "Image"]
|
||||
|
||||
|
||||
# 处理思考配置:优先使用客户端提供的配置,否则使用默认配置
|
||||
client_thinking_config = None
|
||||
if request.generationConfig and request.generationConfig.thinkingConfig:
|
||||
client_thinking_config = request.generationConfig.thinkingConfig
|
||||
|
||||
|
||||
if client_thinking_config is not None:
|
||||
# 客户端提供了思考配置,直接使用
|
||||
payload["generationConfig"]["thinkingConfig"] = client_thinking_config
|
||||
else:
|
||||
# 客户端没有提供思考配置,使用默认配置
|
||||
# 客户端没有提供思考配置,使用默认配置
|
||||
if model.endswith("-non-thinking"):
|
||||
if "gemini-2.5-pro" in model:
|
||||
payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": 128}
|
||||
else:
|
||||
payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": 0}
|
||||
payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": 0}
|
||||
elif _get_real_model(model) in settings.THINKING_BUDGET_MAP:
|
||||
if settings.SHOW_THINKING_PROCESS:
|
||||
payload["generationConfig"]["thinkingConfig"] = {
|
||||
"thinkingBudget": settings.THINKING_BUDGET_MAP.get(model,1000),
|
||||
"includeThoughts": True
|
||||
"thinkingBudget": settings.THINKING_BUDGET_MAP.get(model, 1000),
|
||||
"includeThoughts": True,
|
||||
}
|
||||
else:
|
||||
payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": settings.THINKING_BUDGET_MAP.get(model,1000)}
|
||||
payload["generationConfig"]["thinkingConfig"] = {
|
||||
"thinkingBudget": settings.THINKING_BUDGET_MAP.get(model, 1000)
|
||||
}
|
||||
|
||||
return payload
|
||||
|
||||
@@ -311,11 +342,15 @@ class GeminiChatService:
|
||||
logger.info(f"Request contains file references: {file_names}")
|
||||
file_api_key = await get_file_api_key(file_names[0])
|
||||
if file_api_key:
|
||||
logger.info(f"Found API key for file {file_names[0]}: {redact_key_for_logging(file_api_key)}")
|
||||
logger.info(
|
||||
f"Found API key for file {file_names[0]}: {redact_key_for_logging(file_api_key)}"
|
||||
)
|
||||
api_key = file_api_key # 使用文件的 API key
|
||||
else:
|
||||
logger.warning(f"No API key found for file {file_names[0]}, using default key: {redact_key_for_logging(api_key)}")
|
||||
|
||||
logger.warning(
|
||||
f"No API key found for file {file_names[0]}, using default key: {redact_key_for_logging(api_key)}"
|
||||
)
|
||||
|
||||
payload = _build_payload(model, request)
|
||||
start_time = time.perf_counter()
|
||||
request_datetime = datetime.datetime.now()
|
||||
@@ -344,7 +379,8 @@ class GeminiChatService:
|
||||
error_type="gemini-chat-non-stream",
|
||||
error_log=error_log_msg,
|
||||
error_code=status_code,
|
||||
request_msg=payload
|
||||
request_msg=payload,
|
||||
request_datetime=request_datetime,
|
||||
)
|
||||
raise e
|
||||
finally:
|
||||
@@ -356,7 +392,7 @@ class GeminiChatService:
|
||||
is_success=is_success,
|
||||
status_code=status_code,
|
||||
latency_ms=latency_ms,
|
||||
request_time=request_datetime
|
||||
request_time=request_datetime,
|
||||
)
|
||||
|
||||
async def count_tokens(
|
||||
@@ -364,7 +400,9 @@ class GeminiChatService:
|
||||
) -> Dict[str, Any]:
|
||||
"""计算token数量"""
|
||||
# countTokens API只需要contents
|
||||
payload = {"contents": _filter_empty_parts(request.model_dump().get("contents", []))}
|
||||
payload = {
|
||||
"contents": _filter_empty_parts(request.model_dump().get("contents", []))
|
||||
}
|
||||
start_time = time.perf_counter()
|
||||
request_datetime = datetime.datetime.now()
|
||||
is_success = False
|
||||
@@ -392,7 +430,7 @@ class GeminiChatService:
|
||||
error_type="gemini-count-tokens",
|
||||
error_log=error_log_msg,
|
||||
error_code=status_code,
|
||||
request_msg=payload
|
||||
request_msg=payload,
|
||||
)
|
||||
raise e
|
||||
finally:
|
||||
@@ -404,7 +442,7 @@ class GeminiChatService:
|
||||
is_success=is_success,
|
||||
status_code=status_code,
|
||||
latency_ms=latency_ms,
|
||||
request_time=request_datetime
|
||||
request_time=request_datetime,
|
||||
)
|
||||
|
||||
async def stream_generate_content(
|
||||
@@ -417,11 +455,15 @@ class GeminiChatService:
|
||||
logger.info(f"Request contains file references: {file_names}")
|
||||
file_api_key = await get_file_api_key(file_names[0])
|
||||
if file_api_key:
|
||||
logger.info(f"Found API key for file {file_names[0]}: {redact_key_for_logging(file_api_key)}")
|
||||
logger.info(
|
||||
f"Found API key for file {file_names[0]}: {redact_key_for_logging(file_api_key)}"
|
||||
)
|
||||
api_key = file_api_key # 使用文件的 API key
|
||||
else:
|
||||
logger.warning(f"No API key found for file {file_names[0]}, using default key: {redact_key_for_logging(api_key)}")
|
||||
|
||||
logger.warning(
|
||||
f"No API key found for file {file_names[0]}, using default key: {redact_key_for_logging(api_key)}"
|
||||
)
|
||||
|
||||
retries = 0
|
||||
max_retries = settings.MAX_RETRIES
|
||||
payload = _build_payload(model, request)
|
||||
@@ -482,20 +524,23 @@ class GeminiChatService:
|
||||
error_type="gemini-chat-stream",
|
||||
error_log=error_log_msg,
|
||||
error_code=status_code,
|
||||
request_msg=payload
|
||||
request_msg=payload,
|
||||
request_datetime=request_datetime,
|
||||
)
|
||||
|
||||
api_key = await self.key_manager.handle_api_failure(current_attempt_key, retries)
|
||||
api_key = await self.key_manager.handle_api_failure(
|
||||
current_attempt_key, retries
|
||||
)
|
||||
if api_key:
|
||||
logger.info(f"Switched to new API key: {redact_key_for_logging(api_key)}")
|
||||
logger.info(
|
||||
f"Switched to new API key: {redact_key_for_logging(api_key)}"
|
||||
)
|
||||
else:
|
||||
logger.error(f"No valid API key available after {retries} retries.")
|
||||
break
|
||||
|
||||
if retries >= max_retries:
|
||||
logger.error(
|
||||
f"Max retries ({max_retries}) reached for streaming."
|
||||
)
|
||||
logger.error(f"Max retries ({max_retries}) reached for streaming.")
|
||||
break
|
||||
finally:
|
||||
end_time = time.perf_counter()
|
||||
@@ -506,5 +551,5 @@ class GeminiChatService:
|
||||
is_success=is_success,
|
||||
status_code=status_code,
|
||||
latency_ms=latency_ms,
|
||||
request_time=request_datetime
|
||||
request_time=request_datetime,
|
||||
)
|
||||
|
||||
@@ -559,6 +559,7 @@ class OpenAIChatService:
|
||||
error_log=error_log_msg,
|
||||
error_code=status_code,
|
||||
request_msg=payload,
|
||||
request_datetime=request_datetime,
|
||||
)
|
||||
|
||||
if self.key_manager:
|
||||
@@ -672,6 +673,7 @@ class OpenAIChatService:
|
||||
error_log=error_log_msg,
|
||||
error_code=status_code,
|
||||
request_msg={"image_data_truncated": image_data[:1000]},
|
||||
request_datetime=request_datetime,
|
||||
)
|
||||
yield f"data: {json.dumps({'error': error_log_msg})}\n\n"
|
||||
yield "data: [DONE]\n\n"
|
||||
@@ -722,6 +724,7 @@ class OpenAIChatService:
|
||||
error_log=error_log_msg,
|
||||
error_code=status_code,
|
||||
request_msg={"image_data_truncated": image_data[:1000]},
|
||||
request_datetime=request_datetime,
|
||||
)
|
||||
raise e
|
||||
finally:
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
# app/services/chat_service.py
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import re
|
||||
import datetime
|
||||
import time
|
||||
from typing import Any, AsyncGenerator, Dict, List
|
||||
|
||||
from app.config.config import settings
|
||||
from app.core.constants import GEMINI_2_FLASH_EXP_SAFETY_SETTINGS
|
||||
from app.database.services import add_error_log, add_request_log
|
||||
from app.domain.gemini_models import GeminiRequest
|
||||
from app.handler.response_handler import GeminiResponseHandler
|
||||
from app.handler.stream_optimizer import gemini_optimizer
|
||||
from app.log.logger import get_gemini_logger
|
||||
from app.service.client.api_client import GeminiApiClient
|
||||
from app.service.key.key_manager import KeyManager
|
||||
from app.database.services import add_error_log, add_request_log
|
||||
from app.utils.helpers import redact_key_for_logging
|
||||
|
||||
logger = get_gemini_logger()
|
||||
@@ -33,15 +34,31 @@ def _clean_json_schema_properties(obj: Any) -> Any:
|
||||
"""清理JSON Schema中Gemini API不支持的字段"""
|
||||
if not isinstance(obj, dict):
|
||||
return obj
|
||||
|
||||
|
||||
# Gemini API不支持的JSON Schema字段
|
||||
unsupported_fields = {
|
||||
"exclusiveMaximum", "exclusiveMinimum", "const", "examples",
|
||||
"contentEncoding", "contentMediaType", "if", "then", "else",
|
||||
"allOf", "anyOf", "oneOf", "not", "definitions", "$schema",
|
||||
"$id", "$ref", "$comment", "readOnly", "writeOnly"
|
||||
"exclusiveMaximum",
|
||||
"exclusiveMinimum",
|
||||
"const",
|
||||
"examples",
|
||||
"contentEncoding",
|
||||
"contentMediaType",
|
||||
"if",
|
||||
"then",
|
||||
"else",
|
||||
"allOf",
|
||||
"anyOf",
|
||||
"oneOf",
|
||||
"not",
|
||||
"definitions",
|
||||
"$schema",
|
||||
"$id",
|
||||
"$ref",
|
||||
"$comment",
|
||||
"readOnly",
|
||||
"writeOnly",
|
||||
}
|
||||
|
||||
|
||||
cleaned = {}
|
||||
for key, value in obj.items():
|
||||
if key in unsupported_fields:
|
||||
@@ -52,13 +69,13 @@ def _clean_json_schema_properties(obj: Any) -> Any:
|
||||
cleaned[key] = [_clean_json_schema_properties(item) for item in value]
|
||||
else:
|
||||
cleaned[key] = value
|
||||
|
||||
|
||||
return cleaned
|
||||
|
||||
|
||||
def _build_tools(model: str, payload: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""构建工具"""
|
||||
|
||||
|
||||
def _has_function_call(contents: List[Dict[str, Any]]) -> bool:
|
||||
"""检查内容中是否包含 functionCall"""
|
||||
if not contents or not isinstance(contents, list):
|
||||
@@ -73,7 +90,7 @@ def _build_tools(model: str, payload: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
if isinstance(part, dict) and "functionCall" in part:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _merge_tools(tools: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
record = dict()
|
||||
for item in tools:
|
||||
@@ -124,16 +141,18 @@ def _build_tools(model: str, payload: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
and not _has_image_parts(payload.get("contents", []))
|
||||
):
|
||||
tool["codeExecution"] = {}
|
||||
|
||||
|
||||
if model.endswith("-search"):
|
||||
tool["googleSearch"] = {}
|
||||
|
||||
|
||||
real_model = _get_real_model(model)
|
||||
if real_model in settings.URL_CONTEXT_MODELS and settings.URL_CONTEXT_ENABLED:
|
||||
tool["urlContext"] = {}
|
||||
|
||||
# 解决 "Tool use with function calling is unsupported" 问题
|
||||
if tool.get("functionDeclarations") or _has_function_call(payload.get("contents", [])):
|
||||
if tool.get("functionDeclarations") or _has_function_call(
|
||||
payload.get("contents", [])
|
||||
):
|
||||
tool.pop("googleSearch", None)
|
||||
tool.pop("codeExecution", None)
|
||||
tool.pop("urlContext", None)
|
||||
@@ -167,7 +186,7 @@ def _build_payload(model: str, request: GeminiRequest) -> Dict[str, Any]:
|
||||
if request.generationConfig.maxOutputTokens is None:
|
||||
# 如果未指定最大输出长度,则不传递该字段,解决截断的问题
|
||||
request_dict["generationConfig"].pop("maxOutputTokens")
|
||||
|
||||
|
||||
payload = {
|
||||
"contents": request_dict.get("contents", []),
|
||||
"tools": _build_tools(model, request_dict),
|
||||
@@ -179,30 +198,32 @@ def _build_payload(model: str, request: GeminiRequest) -> Dict[str, Any]:
|
||||
if model.endswith("-image") or model.endswith("-image-generation"):
|
||||
payload.pop("systemInstruction")
|
||||
payload["generationConfig"]["responseModalities"] = ["Text", "Image"]
|
||||
|
||||
|
||||
# 处理思考配置:优先使用客户端提供的配置,否则使用默认配置
|
||||
client_thinking_config = None
|
||||
if request.generationConfig and request.generationConfig.thinkingConfig:
|
||||
client_thinking_config = request.generationConfig.thinkingConfig
|
||||
|
||||
|
||||
if client_thinking_config is not None:
|
||||
# 客户端提供了思考配置,直接使用
|
||||
payload["generationConfig"]["thinkingConfig"] = client_thinking_config
|
||||
else:
|
||||
# 客户端没有提供思考配置,使用默认配置
|
||||
# 客户端没有提供思考配置,使用默认配置
|
||||
if model.endswith("-non-thinking"):
|
||||
if "gemini-2.5-pro" in model:
|
||||
payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": 128}
|
||||
else:
|
||||
payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": 0}
|
||||
payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": 0}
|
||||
elif _get_real_model(model) in settings.THINKING_BUDGET_MAP:
|
||||
if settings.SHOW_THINKING_PROCESS:
|
||||
payload["generationConfig"]["thinkingConfig"] = {
|
||||
"thinkingBudget": settings.THINKING_BUDGET_MAP.get(model,1000),
|
||||
"includeThoughts": True
|
||||
"thinkingBudget": settings.THINKING_BUDGET_MAP.get(model, 1000),
|
||||
"includeThoughts": True,
|
||||
}
|
||||
else:
|
||||
payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": settings.THINKING_BUDGET_MAP.get(model,1000)}
|
||||
payload["generationConfig"]["thinkingConfig"] = {
|
||||
"thinkingBudget": settings.THINKING_BUDGET_MAP.get(model, 1000)
|
||||
}
|
||||
|
||||
return payload
|
||||
|
||||
@@ -271,7 +292,8 @@ class GeminiChatService:
|
||||
error_type="gemini-chat-non-stream",
|
||||
error_log=error_log_msg,
|
||||
error_code=status_code,
|
||||
request_msg=payload
|
||||
request_msg=payload,
|
||||
request_datetime=request_datetime,
|
||||
)
|
||||
raise e
|
||||
finally:
|
||||
@@ -283,7 +305,7 @@ class GeminiChatService:
|
||||
is_success=is_success,
|
||||
status_code=status_code,
|
||||
latency_ms=latency_ms,
|
||||
request_time=request_datetime
|
||||
request_time=request_datetime,
|
||||
)
|
||||
|
||||
async def stream_generate_content(
|
||||
@@ -301,7 +323,7 @@ class GeminiChatService:
|
||||
request_datetime = datetime.datetime.now()
|
||||
start_time = time.perf_counter()
|
||||
current_attempt_key = api_key
|
||||
final_api_key = current_attempt_key # Update final key used
|
||||
final_api_key = current_attempt_key # Update final key used
|
||||
try:
|
||||
async for line in self.api_client.stream_generate_content(
|
||||
payload, model, current_attempt_key
|
||||
@@ -350,20 +372,23 @@ class GeminiChatService:
|
||||
error_type="gemini-chat-stream",
|
||||
error_log=error_log_msg,
|
||||
error_code=status_code,
|
||||
request_msg=payload
|
||||
request_msg=payload,
|
||||
request_datetime=request_datetime,
|
||||
)
|
||||
|
||||
api_key = await self.key_manager.handle_api_failure(current_attempt_key, retries)
|
||||
api_key = await self.key_manager.handle_api_failure(
|
||||
current_attempt_key, retries
|
||||
)
|
||||
if api_key:
|
||||
logger.info(f"Switched to new API key: {redact_key_for_logging(api_key)}")
|
||||
logger.info(
|
||||
f"Switched to new API key: {redact_key_for_logging(api_key)}"
|
||||
)
|
||||
else:
|
||||
logger.error(f"No valid API key available after {retries} retries.")
|
||||
break
|
||||
|
||||
if retries >= max_retries:
|
||||
logger.error(
|
||||
f"Max retries ({max_retries}) reached for streaming."
|
||||
)
|
||||
logger.error(f"Max retries ({max_retries}) reached for streaming.")
|
||||
break
|
||||
finally:
|
||||
end_time = time.perf_counter()
|
||||
@@ -374,5 +399,5 @@ class GeminiChatService:
|
||||
is_success=is_success,
|
||||
status_code=status_code,
|
||||
latency_ms=latency_ms,
|
||||
request_time=request_datetime
|
||||
request_time=request_datetime,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import datetime
|
||||
import time
|
||||
import re
|
||||
import time
|
||||
from typing import List, Union
|
||||
|
||||
import openai
|
||||
@@ -8,8 +8,8 @@ from openai import APIStatusError
|
||||
from openai.types import CreateEmbeddingResponse
|
||||
|
||||
from app.config.config import settings
|
||||
from app.log.logger import get_embeddings_logger
|
||||
from app.database.services import add_error_log, add_request_log
|
||||
from app.log.logger import get_embeddings_logger
|
||||
|
||||
logger = get_embeddings_logger()
|
||||
|
||||
@@ -27,12 +27,20 @@ class EmbeddingService:
|
||||
response = None
|
||||
error_log_msg = ""
|
||||
if isinstance(input_text, list):
|
||||
request_msg_log = {"input_truncated": [str(item)[:100] + "..." if len(str(item)) > 100 else str(item) for item in input_text[:5]]}
|
||||
request_msg_log = {
|
||||
"input_truncated": [
|
||||
str(item)[:100] + "..." if len(str(item)) > 100 else str(item)
|
||||
for item in input_text[:5]
|
||||
]
|
||||
}
|
||||
if len(input_text) > 5:
|
||||
request_msg_log["input_truncated"].append("...")
|
||||
request_msg_log["input_truncated"].append("...")
|
||||
else:
|
||||
request_msg_log = {"input_truncated": input_text[:1000] + "..." if len(input_text) > 1000 else input_text}
|
||||
|
||||
request_msg_log = {
|
||||
"input_truncated": (
|
||||
input_text[:1000] + "..." if len(input_text) > 1000 else input_text
|
||||
)
|
||||
}
|
||||
|
||||
try:
|
||||
client = openai.OpenAI(api_key=api_key, base_url=settings.BASE_URL)
|
||||
@@ -66,13 +74,14 @@ class EmbeddingService:
|
||||
error_type="openai-embedding",
|
||||
error_log=error_log_msg,
|
||||
error_code=status_code,
|
||||
request_msg=request_msg_log
|
||||
)
|
||||
request_msg=request_msg_log,
|
||||
request_datetime=request_datetime,
|
||||
)
|
||||
await add_request_log(
|
||||
model_name=model,
|
||||
api_key=api_key,
|
||||
is_success=is_success,
|
||||
status_code=status_code,
|
||||
latency_ms=latency_ms,
|
||||
request_time=request_datetime
|
||||
request_time=request_datetime,
|
||||
)
|
||||
|
||||
@@ -84,6 +84,7 @@ class GeminiEmbeddingService:
|
||||
error_log=error_log_msg,
|
||||
error_code=status_code,
|
||||
request_msg=payload,
|
||||
request_datetime=request_datetime,
|
||||
)
|
||||
raise e
|
||||
finally:
|
||||
@@ -133,6 +134,7 @@ class GeminiEmbeddingService:
|
||||
error_log=error_log_msg,
|
||||
error_code=status_code,
|
||||
request_msg=payload,
|
||||
request_datetime=request_datetime,
|
||||
)
|
||||
raise e
|
||||
finally:
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import re
|
||||
@@ -11,20 +10,21 @@ from app.database.services import (
|
||||
add_request_log,
|
||||
)
|
||||
from app.domain.openai_models import ChatRequest, ImageGenerationRequest
|
||||
from app.log.logger import get_openai_compatible_logger
|
||||
from app.service.client.api_client import OpenaiApiClient
|
||||
from app.service.key.key_manager import KeyManager
|
||||
from app.utils.helpers import redact_key_for_logging
|
||||
from app.log.logger import get_openai_compatible_logger
|
||||
|
||||
logger = get_openai_compatible_logger()
|
||||
|
||||
|
||||
class OpenAICompatiableService:
|
||||
|
||||
def __init__(self, base_url: str, key_manager: KeyManager = None):
|
||||
self.key_manager = key_manager
|
||||
self.base_url = base_url
|
||||
self.api_client = OpenaiApiClient(base_url, settings.TIME_OUT)
|
||||
|
||||
|
||||
async def get_models(self, api_key: str) -> Dict[str, Any]:
|
||||
return await self.api_client.get_models(api_key)
|
||||
|
||||
@@ -37,10 +37,12 @@ class OpenAICompatiableService:
|
||||
request_dict = request.model_dump()
|
||||
# 移除值为null的
|
||||
request_dict = {k: v for k, v in request_dict.items() if v is not None}
|
||||
del request_dict["top_k"] # 删除top_k参数,目前不支持该参数
|
||||
del request_dict["top_k"] # 删除top_k参数,目前不支持该参数
|
||||
if request.stream:
|
||||
return self._handle_stream_completion(request.model, request_dict, api_key)
|
||||
return await self._handle_normal_completion(request.model, request_dict, api_key)
|
||||
return await self._handle_normal_completion(
|
||||
request.model, request_dict, api_key
|
||||
)
|
||||
|
||||
async def generate_images(
|
||||
self,
|
||||
@@ -153,6 +155,7 @@ class OpenAICompatiableService:
|
||||
error_log=error_log_msg,
|
||||
error_code=status_code,
|
||||
request_msg=payload,
|
||||
request_datetime=request_datetime,
|
||||
)
|
||||
|
||||
if self.key_manager:
|
||||
@@ -160,15 +163,17 @@ class OpenAICompatiableService:
|
||||
current_attempt_key, retries
|
||||
)
|
||||
if api_key:
|
||||
logger.info(f"Switched to new API key: {redact_key_for_logging(api_key)}")
|
||||
logger.info(
|
||||
f"Switched to new API key: {redact_key_for_logging(api_key)}"
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
f"No valid API key available after {retries} retries."
|
||||
)
|
||||
break
|
||||
break
|
||||
else:
|
||||
logger.error("KeyManager not available for retry logic.")
|
||||
break
|
||||
break
|
||||
|
||||
if retries >= max_retries:
|
||||
logger.error(f"Max retries ({max_retries}) reached for streaming.")
|
||||
@@ -187,5 +192,3 @@ class OpenAICompatiableService:
|
||||
if not is_success and retries >= max_retries:
|
||||
yield f"data: {json.dumps({'error': 'Streaming failed after retries'})}\n\n"
|
||||
yield "data: [DONE]\n\n"
|
||||
|
||||
|
||||
|
||||
@@ -178,9 +178,6 @@ class StatsService:
|
||||
|
||||
results = await database.fetch_all(query)
|
||||
|
||||
# 为失败调用尝试查找匹配的错误日志ID(时间窗口 +/- 5 分钟)
|
||||
from app.database.models import ErrorLog # 延迟导入避免循环依赖
|
||||
|
||||
details: list[dict] = []
|
||||
for row in results:
|
||||
status = "failure"
|
||||
@@ -196,30 +193,6 @@ class StatsService:
|
||||
"latency_ms": row["latency_ms"],
|
||||
}
|
||||
|
||||
# 如果失败,尝试附带一个相关的错误日志ID,便于前端拉取详情
|
||||
if status == "failure" and row["key"]:
|
||||
try:
|
||||
ts = row["timestamp"]
|
||||
start_win = ts - datetime.timedelta(minutes=5)
|
||||
end_win = ts + datetime.timedelta(minutes=5)
|
||||
err_query = (
|
||||
select(ErrorLog.id)
|
||||
.where(
|
||||
ErrorLog.gemini_key == row["key"],
|
||||
ErrorLog.request_time >= start_win,
|
||||
ErrorLog.request_time <= end_win,
|
||||
)
|
||||
.order_by(ErrorLog.request_time.desc())
|
||||
.limit(1)
|
||||
)
|
||||
err = await database.fetch_one(err_query)
|
||||
if err:
|
||||
record["error_log_id"] = err["id"]
|
||||
except Exception as _e:
|
||||
logger.debug(
|
||||
f"No matching error log found for key ending ...{row['key'][-4:] if row['key'] else ''}: {_e}"
|
||||
)
|
||||
|
||||
details.append(record)
|
||||
|
||||
logger.info(
|
||||
@@ -260,8 +233,6 @@ class StatsService:
|
||||
|
||||
results = await database.fetch_all(query)
|
||||
|
||||
from app.database.models import ErrorLog
|
||||
|
||||
details: list[dict] = []
|
||||
for row in results:
|
||||
status = "failure"
|
||||
@@ -277,29 +248,6 @@ class StatsService:
|
||||
"latency_ms": row["latency_ms"],
|
||||
}
|
||||
|
||||
if status == "failure" and row["key"]:
|
||||
try:
|
||||
ts = row["timestamp"]
|
||||
start_win = ts - datetime.timedelta(minutes=5)
|
||||
end_win = ts + datetime.timedelta(minutes=5)
|
||||
err_query = (
|
||||
select(ErrorLog.id)
|
||||
.where(
|
||||
ErrorLog.gemini_key == row["key"],
|
||||
ErrorLog.request_time >= start_win,
|
||||
ErrorLog.request_time < end_win,
|
||||
)
|
||||
.order_by(ErrorLog.request_time.desc())
|
||||
.limit(1)
|
||||
)
|
||||
err = await database.fetch_one(err_query)
|
||||
if err:
|
||||
record["error_log_id"] = err["id"]
|
||||
except Exception as _e:
|
||||
logger.debug(
|
||||
f"No matching error log found for key ending ...{row['key'][-4:] if row['key'] else ''}: {_e}"
|
||||
)
|
||||
|
||||
details.append(record)
|
||||
|
||||
logger.info(
|
||||
@@ -312,7 +260,9 @@ class StatsService:
|
||||
)
|
||||
raise
|
||||
|
||||
async def get_attention_keys_last_24h(self, include_keys: set[str], limit: int = 20, status_code: int = 429) -> list[dict]:
|
||||
async def get_attention_keys_last_24h(
|
||||
self, include_keys: set[str], limit: int = 20, status_code: int = 429
|
||||
) -> list[dict]:
|
||||
"""返回最近24小时内指定状态码(默认429)最多的Key列表,仅包含include_keys中的Key。
|
||||
|
||||
Returns: [{"key": str, "count": int, "status_code": int}, ...] 按次数降序
|
||||
@@ -344,7 +294,9 @@ class StatsService:
|
||||
if row["key"]
|
||||
]
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get attention keys ({status_code}) in last 24h: {e}")
|
||||
logger.error(
|
||||
f"Failed to get attention keys ({status_code}) in last 24h: {e}"
|
||||
)
|
||||
return []
|
||||
|
||||
async def get_key_usage_details_last_24h(self, key: str) -> Union[dict, None]:
|
||||
|
||||
Reference in New Issue
Block a user