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:
snaily
2025-08-18 17:26:53 +08:00
parent 669123f348
commit 2001bfdcd9
8 changed files with 187 additions and 150 deletions
+1 -3
View File
@@ -141,9 +141,7 @@ async def add_error_log(
model_name=model_name, model_name=model_name,
error_code=error_code, error_code=error_code,
request_msg=request_msg_json, request_msg=request_msg_json,
request_time=( request_time=(request_datetime if request_datetime else datetime.now()),
request_datetime if request_datetime else datetime.now(timezone.utc)
),
) )
await database.execute(query) await database.execute(query)
logger.info(f"Added error log for key: {redact_key_for_logging(gemini_key)}") logger.info(f"Added error log for key: {redact_key_for_logging(gemini_key)}")
+74 -29
View File
@@ -1,19 +1,20 @@
# app/services/chat_service.py # app/services/chat_service.py
import datetime
import json import json
import re import re
import datetime
import time import time
from typing import Any, AsyncGenerator, Dict, List from typing import Any, AsyncGenerator, Dict, List
from app.config.config import settings from app.config.config import settings
from app.core.constants import GEMINI_2_FLASH_EXP_SAFETY_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.domain.gemini_models import GeminiRequest
from app.handler.response_handler import GeminiResponseHandler from app.handler.response_handler import GeminiResponseHandler
from app.handler.stream_optimizer import gemini_optimizer from app.handler.stream_optimizer import gemini_optimizer
from app.log.logger import get_gemini_logger from app.log.logger import get_gemini_logger
from app.service.client.api_client import GeminiApiClient from app.service.client.api_client import GeminiApiClient
from app.service.key.key_manager import KeyManager 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 from app.utils.helpers import redact_key_for_logging
logger = get_gemini_logger() logger = get_gemini_logger()
@@ -28,6 +29,7 @@ def _has_image_parts(contents: List[Dict[str, Any]]) -> bool:
return True return True
return False return False
def _extract_file_references(contents: List[Dict[str, Any]]) -> List[str]: def _extract_file_references(contents: List[Dict[str, Any]]) -> List[str]:
"""從內容中提取文件引用""" """從內容中提取文件引用"""
file_names = [] file_names = []
@@ -42,7 +44,9 @@ def _extract_file_references(contents: List[Dict[str, Any]]) -> List[str]:
file_uri = file_data["fileUri"] file_uri = file_data["fileUri"]
# 從 URI 中提取文件名 # 從 URI 中提取文件名
# 1. https://generativelanguage.googleapis.com/v1beta/files/{file_id} # 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: if not match:
logger.warning(f"Invalid file URI: {file_uri}") logger.warning(f"Invalid file URI: {file_uri}")
continue continue
@@ -51,6 +55,7 @@ def _extract_file_references(contents: List[Dict[str, Any]]) -> List[str]:
logger.info(f"Found file reference: {file_id}") logger.info(f"Found file reference: {file_id}")
return file_names return file_names
def _clean_json_schema_properties(obj: Any) -> Any: def _clean_json_schema_properties(obj: Any) -> Any:
"""清理JSON Schema中Gemini API不支持的字段""" """清理JSON Schema中Gemini API不支持的字段"""
if not isinstance(obj, dict): if not isinstance(obj, dict):
@@ -58,10 +63,26 @@ def _clean_json_schema_properties(obj: Any) -> Any:
# Gemini API不支持的JSON Schema字段 # Gemini API不支持的JSON Schema字段
unsupported_fields = { unsupported_fields = {
"exclusiveMaximum", "exclusiveMinimum", "const", "examples", "exclusiveMaximum",
"contentEncoding", "contentMediaType", "if", "then", "else", "exclusiveMinimum",
"allOf", "anyOf", "oneOf", "not", "definitions", "$schema", "const",
"$id", "$ref", "$comment", "readOnly", "writeOnly" "examples",
"contentEncoding",
"contentMediaType",
"if",
"then",
"else",
"allOf",
"anyOf",
"oneOf",
"not",
"definitions",
"$schema",
"$id",
"$ref",
"$comment",
"readOnly",
"writeOnly",
} }
cleaned = {} cleaned = {}
@@ -155,7 +176,9 @@ def _build_tools(model: str, payload: Dict[str, Any]) -> List[Dict[str, Any]]:
tool["urlContext"] = {} tool["urlContext"] = {}
# 解决 "Tool use with function calling is unsupported" 问题 # 解决 "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("googleSearch", None)
tool.pop("codeExecution", None) tool.pop("codeExecution", None)
tool.pop("urlContext", None) tool.pop("urlContext", None)
@@ -189,10 +212,16 @@ def _filter_empty_parts(contents: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
filtered_contents = [] filtered_contents = []
for content in 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 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: if valid_parts:
new_content = content.copy() new_content = content.copy()
@@ -260,11 +289,13 @@ def _build_payload(model: str, request: GeminiRequest) -> Dict[str, Any]:
elif _get_real_model(model) in settings.THINKING_BUDGET_MAP: elif _get_real_model(model) in settings.THINKING_BUDGET_MAP:
if settings.SHOW_THINKING_PROCESS: if settings.SHOW_THINKING_PROCESS:
payload["generationConfig"]["thinkingConfig"] = { payload["generationConfig"]["thinkingConfig"] = {
"thinkingBudget": settings.THINKING_BUDGET_MAP.get(model,1000), "thinkingBudget": settings.THINKING_BUDGET_MAP.get(model, 1000),
"includeThoughts": True "includeThoughts": True,
} }
else: 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 return payload
@@ -311,10 +342,14 @@ class GeminiChatService:
logger.info(f"Request contains file references: {file_names}") logger.info(f"Request contains file references: {file_names}")
file_api_key = await get_file_api_key(file_names[0]) file_api_key = await get_file_api_key(file_names[0])
if file_api_key: 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 api_key = file_api_key # 使用文件的 API key
else: 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) payload = _build_payload(model, request)
start_time = time.perf_counter() start_time = time.perf_counter()
@@ -344,7 +379,8 @@ class GeminiChatService:
error_type="gemini-chat-non-stream", error_type="gemini-chat-non-stream",
error_log=error_log_msg, error_log=error_log_msg,
error_code=status_code, error_code=status_code,
request_msg=payload request_msg=payload,
request_datetime=request_datetime,
) )
raise e raise e
finally: finally:
@@ -356,7 +392,7 @@ class GeminiChatService:
is_success=is_success, is_success=is_success,
status_code=status_code, status_code=status_code,
latency_ms=latency_ms, latency_ms=latency_ms,
request_time=request_datetime request_time=request_datetime,
) )
async def count_tokens( async def count_tokens(
@@ -364,7 +400,9 @@ class GeminiChatService:
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""计算token数量""" """计算token数量"""
# countTokens API只需要contents # 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() start_time = time.perf_counter()
request_datetime = datetime.datetime.now() request_datetime = datetime.datetime.now()
is_success = False is_success = False
@@ -392,7 +430,7 @@ class GeminiChatService:
error_type="gemini-count-tokens", error_type="gemini-count-tokens",
error_log=error_log_msg, error_log=error_log_msg,
error_code=status_code, error_code=status_code,
request_msg=payload request_msg=payload,
) )
raise e raise e
finally: finally:
@@ -404,7 +442,7 @@ class GeminiChatService:
is_success=is_success, is_success=is_success,
status_code=status_code, status_code=status_code,
latency_ms=latency_ms, latency_ms=latency_ms,
request_time=request_datetime request_time=request_datetime,
) )
async def stream_generate_content( async def stream_generate_content(
@@ -417,10 +455,14 @@ class GeminiChatService:
logger.info(f"Request contains file references: {file_names}") logger.info(f"Request contains file references: {file_names}")
file_api_key = await get_file_api_key(file_names[0]) file_api_key = await get_file_api_key(file_names[0])
if file_api_key: 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 api_key = file_api_key # 使用文件的 API key
else: 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 retries = 0
max_retries = settings.MAX_RETRIES max_retries = settings.MAX_RETRIES
@@ -482,20 +524,23 @@ class GeminiChatService:
error_type="gemini-chat-stream", error_type="gemini-chat-stream",
error_log=error_log_msg, error_log=error_log_msg,
error_code=status_code, 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: 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: else:
logger.error(f"No valid API key available after {retries} retries.") logger.error(f"No valid API key available after {retries} retries.")
break break
if retries >= max_retries: if retries >= max_retries:
logger.error( logger.error(f"Max retries ({max_retries}) reached for streaming.")
f"Max retries ({max_retries}) reached for streaming."
)
break break
finally: finally:
end_time = time.perf_counter() end_time = time.perf_counter()
@@ -506,5 +551,5 @@ class GeminiChatService:
is_success=is_success, is_success=is_success,
status_code=status_code, status_code=status_code,
latency_ms=latency_ms, latency_ms=latency_ms,
request_time=request_datetime request_time=request_datetime,
) )
+3
View File
@@ -559,6 +559,7 @@ class OpenAIChatService:
error_log=error_log_msg, error_log=error_log_msg,
error_code=status_code, error_code=status_code,
request_msg=payload, request_msg=payload,
request_datetime=request_datetime,
) )
if self.key_manager: if self.key_manager:
@@ -672,6 +673,7 @@ class OpenAIChatService:
error_log=error_log_msg, error_log=error_log_msg,
error_code=status_code, error_code=status_code,
request_msg={"image_data_truncated": image_data[:1000]}, request_msg={"image_data_truncated": image_data[:1000]},
request_datetime=request_datetime,
) )
yield f"data: {json.dumps({'error': error_log_msg})}\n\n" yield f"data: {json.dumps({'error': error_log_msg})}\n\n"
yield "data: [DONE]\n\n" yield "data: [DONE]\n\n"
@@ -722,6 +724,7 @@ class OpenAIChatService:
error_log=error_log_msg, error_log=error_log_msg,
error_code=status_code, error_code=status_code,
request_msg={"image_data_truncated": image_data[:1000]}, request_msg={"image_data_truncated": image_data[:1000]},
request_datetime=request_datetime,
) )
raise e raise e
finally: finally:
+45 -20
View File
@@ -1,19 +1,20 @@
# app/services/chat_service.py # app/services/chat_service.py
import datetime
import json import json
import re import re
import datetime
import time import time
from typing import Any, AsyncGenerator, Dict, List from typing import Any, AsyncGenerator, Dict, List
from app.config.config import settings from app.config.config import settings
from app.core.constants import GEMINI_2_FLASH_EXP_SAFETY_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.domain.gemini_models import GeminiRequest
from app.handler.response_handler import GeminiResponseHandler from app.handler.response_handler import GeminiResponseHandler
from app.handler.stream_optimizer import gemini_optimizer from app.handler.stream_optimizer import gemini_optimizer
from app.log.logger import get_gemini_logger from app.log.logger import get_gemini_logger
from app.service.client.api_client import GeminiApiClient from app.service.client.api_client import GeminiApiClient
from app.service.key.key_manager import KeyManager 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 from app.utils.helpers import redact_key_for_logging
logger = get_gemini_logger() logger = get_gemini_logger()
@@ -36,10 +37,26 @@ def _clean_json_schema_properties(obj: Any) -> Any:
# Gemini API不支持的JSON Schema字段 # Gemini API不支持的JSON Schema字段
unsupported_fields = { unsupported_fields = {
"exclusiveMaximum", "exclusiveMinimum", "const", "examples", "exclusiveMaximum",
"contentEncoding", "contentMediaType", "if", "then", "else", "exclusiveMinimum",
"allOf", "anyOf", "oneOf", "not", "definitions", "$schema", "const",
"$id", "$ref", "$comment", "readOnly", "writeOnly" "examples",
"contentEncoding",
"contentMediaType",
"if",
"then",
"else",
"allOf",
"anyOf",
"oneOf",
"not",
"definitions",
"$schema",
"$id",
"$ref",
"$comment",
"readOnly",
"writeOnly",
} }
cleaned = {} cleaned = {}
@@ -133,7 +150,9 @@ def _build_tools(model: str, payload: Dict[str, Any]) -> List[Dict[str, Any]]:
tool["urlContext"] = {} tool["urlContext"] = {}
# 解决 "Tool use with function calling is unsupported" 问题 # 解决 "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("googleSearch", None)
tool.pop("codeExecution", None) tool.pop("codeExecution", None)
tool.pop("urlContext", None) tool.pop("urlContext", None)
@@ -198,11 +217,13 @@ def _build_payload(model: str, request: GeminiRequest) -> Dict[str, Any]:
elif _get_real_model(model) in settings.THINKING_BUDGET_MAP: elif _get_real_model(model) in settings.THINKING_BUDGET_MAP:
if settings.SHOW_THINKING_PROCESS: if settings.SHOW_THINKING_PROCESS:
payload["generationConfig"]["thinkingConfig"] = { payload["generationConfig"]["thinkingConfig"] = {
"thinkingBudget": settings.THINKING_BUDGET_MAP.get(model,1000), "thinkingBudget": settings.THINKING_BUDGET_MAP.get(model, 1000),
"includeThoughts": True "includeThoughts": True,
} }
else: 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 return payload
@@ -271,7 +292,8 @@ class GeminiChatService:
error_type="gemini-chat-non-stream", error_type="gemini-chat-non-stream",
error_log=error_log_msg, error_log=error_log_msg,
error_code=status_code, error_code=status_code,
request_msg=payload request_msg=payload,
request_datetime=request_datetime,
) )
raise e raise e
finally: finally:
@@ -283,7 +305,7 @@ class GeminiChatService:
is_success=is_success, is_success=is_success,
status_code=status_code, status_code=status_code,
latency_ms=latency_ms, latency_ms=latency_ms,
request_time=request_datetime request_time=request_datetime,
) )
async def stream_generate_content( async def stream_generate_content(
@@ -301,7 +323,7 @@ class GeminiChatService:
request_datetime = datetime.datetime.now() request_datetime = datetime.datetime.now()
start_time = time.perf_counter() start_time = time.perf_counter()
current_attempt_key = api_key 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: try:
async for line in self.api_client.stream_generate_content( async for line in self.api_client.stream_generate_content(
payload, model, current_attempt_key payload, model, current_attempt_key
@@ -350,20 +372,23 @@ class GeminiChatService:
error_type="gemini-chat-stream", error_type="gemini-chat-stream",
error_log=error_log_msg, error_log=error_log_msg,
error_code=status_code, 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: 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: else:
logger.error(f"No valid API key available after {retries} retries.") logger.error(f"No valid API key available after {retries} retries.")
break break
if retries >= max_retries: if retries >= max_retries:
logger.error( logger.error(f"Max retries ({max_retries}) reached for streaming.")
f"Max retries ({max_retries}) reached for streaming."
)
break break
finally: finally:
end_time = time.perf_counter() end_time = time.perf_counter()
@@ -374,5 +399,5 @@ class GeminiChatService:
is_success=is_success, is_success=is_success,
status_code=status_code, status_code=status_code,
latency_ms=latency_ms, latency_ms=latency_ms,
request_time=request_datetime request_time=request_datetime,
) )
+18 -9
View File
@@ -1,6 +1,6 @@
import datetime import datetime
import time
import re import re
import time
from typing import List, Union from typing import List, Union
import openai import openai
@@ -8,8 +8,8 @@ from openai import APIStatusError
from openai.types import CreateEmbeddingResponse from openai.types import CreateEmbeddingResponse
from app.config.config import settings 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.database.services import add_error_log, add_request_log
from app.log.logger import get_embeddings_logger
logger = get_embeddings_logger() logger = get_embeddings_logger()
@@ -27,12 +27,20 @@ class EmbeddingService:
response = None response = None
error_log_msg = "" error_log_msg = ""
if isinstance(input_text, list): 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: if len(input_text) > 5:
request_msg_log["input_truncated"].append("...") request_msg_log["input_truncated"].append("...")
else: 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: try:
client = openai.OpenAI(api_key=api_key, base_url=settings.BASE_URL) client = openai.OpenAI(api_key=api_key, base_url=settings.BASE_URL)
@@ -66,13 +74,14 @@ class EmbeddingService:
error_type="openai-embedding", error_type="openai-embedding",
error_log=error_log_msg, error_log=error_log_msg,
error_code=status_code, error_code=status_code,
request_msg=request_msg_log request_msg=request_msg_log,
) request_datetime=request_datetime,
)
await add_request_log( await add_request_log(
model_name=model, model_name=model,
api_key=api_key, api_key=api_key,
is_success=is_success, is_success=is_success,
status_code=status_code, status_code=status_code,
latency_ms=latency_ms, latency_ms=latency_ms,
request_time=request_datetime request_time=request_datetime,
) )
@@ -84,6 +84,7 @@ class GeminiEmbeddingService:
error_log=error_log_msg, error_log=error_log_msg,
error_code=status_code, error_code=status_code,
request_msg=payload, request_msg=payload,
request_datetime=request_datetime,
) )
raise e raise e
finally: finally:
@@ -133,6 +134,7 @@ class GeminiEmbeddingService:
error_log=error_log_msg, error_log=error_log_msg,
error_code=status_code, error_code=status_code,
request_msg=payload, request_msg=payload,
request_datetime=request_datetime,
) )
raise e raise e
finally: finally:
@@ -1,4 +1,3 @@
import datetime import datetime
import json import json
import re import re
@@ -11,13 +10,14 @@ from app.database.services import (
add_request_log, add_request_log,
) )
from app.domain.openai_models import ChatRequest, ImageGenerationRequest 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.client.api_client import OpenaiApiClient
from app.service.key.key_manager import KeyManager from app.service.key.key_manager import KeyManager
from app.utils.helpers import redact_key_for_logging from app.utils.helpers import redact_key_for_logging
from app.log.logger import get_openai_compatible_logger
logger = get_openai_compatible_logger() logger = get_openai_compatible_logger()
class OpenAICompatiableService: class OpenAICompatiableService:
def __init__(self, base_url: str, key_manager: KeyManager = None): def __init__(self, base_url: str, key_manager: KeyManager = None):
@@ -37,10 +37,12 @@ class OpenAICompatiableService:
request_dict = request.model_dump() request_dict = request.model_dump()
# 移除值为null的 # 移除值为null的
request_dict = {k: v for k, v in request_dict.items() if v is not None} 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: if request.stream:
return self._handle_stream_completion(request.model, request_dict, api_key) 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( async def generate_images(
self, self,
@@ -153,6 +155,7 @@ class OpenAICompatiableService:
error_log=error_log_msg, error_log=error_log_msg,
error_code=status_code, error_code=status_code,
request_msg=payload, request_msg=payload,
request_datetime=request_datetime,
) )
if self.key_manager: if self.key_manager:
@@ -160,7 +163,9 @@ class OpenAICompatiableService:
current_attempt_key, retries current_attempt_key, retries
) )
if api_key: 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: else:
logger.error( logger.error(
f"No valid API key available after {retries} retries." f"No valid API key available after {retries} retries."
@@ -187,5 +192,3 @@ class OpenAICompatiableService:
if not is_success and retries >= max_retries: if not is_success and retries >= max_retries:
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"
+6 -54
View File
@@ -178,9 +178,6 @@ class StatsService:
results = await database.fetch_all(query) results = await database.fetch_all(query)
# 为失败调用尝试查找匹配的错误日志ID(时间窗口 +/- 5 分钟)
from app.database.models import ErrorLog # 延迟导入避免循环依赖
details: list[dict] = [] details: list[dict] = []
for row in results: for row in results:
status = "failure" status = "failure"
@@ -196,30 +193,6 @@ class StatsService:
"latency_ms": row["latency_ms"], "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) details.append(record)
logger.info( logger.info(
@@ -260,8 +233,6 @@ class StatsService:
results = await database.fetch_all(query) results = await database.fetch_all(query)
from app.database.models import ErrorLog
details: list[dict] = [] details: list[dict] = []
for row in results: for row in results:
status = "failure" status = "failure"
@@ -277,29 +248,6 @@ class StatsService:
"latency_ms": row["latency_ms"], "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) details.append(record)
logger.info( logger.info(
@@ -312,7 +260,9 @@ class StatsService:
) )
raise 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。 """返回最近24小时内指定状态码(默认429)最多的Key列表,仅包含include_keys中的Key。
Returns: [{"key": str, "count": int, "status_code": int}, ...] 按次数降序 Returns: [{"key": str, "count": int, "status_code": int}, ...] 按次数降序
@@ -344,7 +294,9 @@ class StatsService:
if row["key"] if row["key"]
] ]
except Exception as e: 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 [] return []
async def get_key_usage_details_last_24h(self, key: str) -> Union[dict, None]: async def get_key_usage_details_last_24h(self, key: str) -> Union[dict, None]: