diff --git a/app/database/services.py b/app/database/services.py index a92eeae..65c5f8d 100644 --- a/app/database/services.py +++ b/app/database/services.py @@ -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)}") diff --git a/app/service/chat/gemini_chat_service.py b/app/service/chat/gemini_chat_service.py index c8276ea..0c9f609 100644 --- a/app/service/chat/gemini_chat_service.py +++ b/app/service/chat/gemini_chat_service.py @@ -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, ) diff --git a/app/service/chat/openai_chat_service.py b/app/service/chat/openai_chat_service.py index 4553019..3dfb17e 100644 --- a/app/service/chat/openai_chat_service.py +++ b/app/service/chat/openai_chat_service.py @@ -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: diff --git a/app/service/chat/vertex_express_chat_service.py b/app/service/chat/vertex_express_chat_service.py index e8371a6..a54ffc9 100644 --- a/app/service/chat/vertex_express_chat_service.py +++ b/app/service/chat/vertex_express_chat_service.py @@ -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, ) diff --git a/app/service/embedding/embedding_service.py b/app/service/embedding/embedding_service.py index 43ad4d4..6954a5e 100644 --- a/app/service/embedding/embedding_service.py +++ b/app/service/embedding/embedding_service.py @@ -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, ) diff --git a/app/service/embedding/gemini_embedding_service.py b/app/service/embedding/gemini_embedding_service.py index 5628a20..62c697e 100644 --- a/app/service/embedding/gemini_embedding_service.py +++ b/app/service/embedding/gemini_embedding_service.py @@ -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: diff --git a/app/service/openai_compatiable/openai_compatiable_service.py b/app/service/openai_compatiable/openai_compatiable_service.py index d50b7da..6c09003 100644 --- a/app/service/openai_compatiable/openai_compatiable_service.py +++ b/app/service/openai_compatiable/openai_compatiable_service.py @@ -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" - - diff --git a/app/service/stats/stats_service.py b/app/service/stats/stats_service.py index 9e2cc42..93c91d0 100644 --- a/app/service/stats/stats_service.py +++ b/app/service/stats/stats_service.py @@ -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]: