diff --git a/app/services/llm/base.py b/app/services/llm/base.py index 91f6c33..6bebef1 100644 --- a/app/services/llm/base.py +++ b/app/services/llm/base.py @@ -57,14 +57,33 @@ class BaseLLMProvider(ABC): """验证配置参数""" if not self.api_key: raise ConfigurationError("API密钥不能为空", "api_key") - + if not self.model_name: raise ConfigurationError("模型名称不能为空", "model_name") - - if self.model_name not in self.supported_models: - from .exceptions import ModelNotSupportedError - raise ModelNotSupportedError(self.model_name, self.provider_name) + + # 检查模型支持情况 + self._validate_model_support() + def _validate_model_support(self): + """验证模型支持情况""" + from app.config import config + from .exceptions import ModelNotSupportedError + from loguru import logger + + # 获取模型验证模式配置 + strict_model_validation = config.app.get('strict_model_validation', True) + + if self.model_name not in self.supported_models: + if strict_model_validation: + # 严格模式:抛出异常 + raise ModelNotSupportedError(self.model_name, self.provider_name) + else: + # 宽松模式:仅记录警告 + logger.warning( + f"模型 {self.model_name} 未在供应商 {self.provider_name} 的预定义支持列表中," + f"但已启用宽松验证模式。支持的模型列表: {self.supported_models}" + ) + def _initialize(self): """初始化提供商特定设置,子类可重写""" pass @@ -77,11 +96,15 @@ class BaseLLMProvider(ABC): def _handle_api_error(self, status_code: int, response_text: str) -> LLMServiceError: """处理API错误,返回适当的异常""" from .exceptions import APICallError, RateLimitError, AuthenticationError - + if status_code == 401: return AuthenticationError() elif status_code == 429: return RateLimitError() + elif status_code in [502, 503, 504]: + return APICallError(f"服务器错误 HTTP {status_code}", status_code, response_text) + elif status_code == 524: + return APICallError(f"服务器处理超时 HTTP {status_code}", status_code, response_text) else: return APICallError(f"HTTP {status_code}", status_code, response_text) diff --git a/app/services/llm/config_validator.py b/app/services/llm/config_validator.py index 0bfe287..31b902a 100644 --- a/app/services/llm/config_validator.py +++ b/app/services/llm/config_validator.py @@ -213,7 +213,8 @@ class LLMConfigValidator: "确保所有API密钥都已正确配置", "建议为每个提供商配置base_url以提高稳定性", "定期检查模型名称是否为最新版本", - "建议配置多个提供商作为备用方案" + "建议配置多个提供商作为备用方案", + "如果使用新发布的模型遇到MODEL_NOT_SUPPORTED错误,可以设置 strict_model_validation = false 启用宽松验证模式" ] } @@ -252,8 +253,8 @@ class LLMConfigValidator: """获取示例模型名称""" examples = { "gemini": { - "vision": ["gemini-2.0-flash-lite", "gemini-2.0-flash"], - "text": ["gemini-2.0-flash", "gemini-1.5-pro"] + "vision": ["gemini-2.5-flash", "gemini-2.0-flash-lite", "gemini-2.0-flash"], + "text": ["gemini-2.5-flash", "gemini-2.0-flash", "gemini-1.5-pro"] }, "openai": { "vision": [], diff --git a/app/services/llm/providers/gemini_openai_provider.py b/app/services/llm/providers/gemini_openai_provider.py index 45c30cb..e9c33ff 100644 --- a/app/services/llm/providers/gemini_openai_provider.py +++ b/app/services/llm/providers/gemini_openai_provider.py @@ -27,6 +27,7 @@ class GeminiOpenAIVisionProvider(VisionModelProvider): @property def supported_models(self) -> List[str]: return [ + "gemini-2.5-flash", "gemini-2.0-flash-lite", "gemini-2.0-flash", "gemini-1.5-pro", @@ -137,6 +138,7 @@ class GeminiOpenAITextProvider(TextModelProvider): @property def supported_models(self) -> List[str]: return [ + "gemini-2.5-flash", "gemini-2.0-flash-lite", "gemini-2.0-flash", "gemini-1.5-pro", diff --git a/app/services/llm/providers/gemini_provider.py b/app/services/llm/providers/gemini_provider.py index 9b571e6..949df21 100644 --- a/app/services/llm/providers/gemini_provider.py +++ b/app/services/llm/providers/gemini_provider.py @@ -27,6 +27,7 @@ class GeminiVisionProvider(VisionModelProvider): @property def supported_models(self) -> List[str]: return [ + "gemini-2.5-flash", "gemini-2.0-flash-lite", "gemini-2.0-flash", "gemini-1.5-pro", @@ -136,25 +137,72 @@ class GeminiVisionProvider(VisionModelProvider): return base64.b64encode(img_bytes).decode('utf-8') async def _make_api_call(self, payload: Dict[str, Any]) -> Dict[str, Any]: - """执行原生Gemini API调用""" + """执行原生Gemini API调用,包含重试机制""" + from app.config import config + url = f"{self.base_url}/models/{self.model_name}:generateContent?key={self.api_key}" - - response = await asyncio.to_thread( - requests.post, - url, - json=payload, - headers={ - "Content-Type": "application/json", - "User-Agent": "NarratoAI/1.0" - }, - timeout=120 - ) - - if response.status_code != 200: - error = self._handle_api_error(response.status_code, response.text) - raise error - - return response.json() + + max_retries = config.app.get('llm_max_retries', 3) + base_timeout = config.app.get('llm_vision_timeout', 120) + + for attempt in range(max_retries): + try: + # 根据尝试次数调整超时时间 + timeout = base_timeout * (attempt + 1) + logger.debug(f"Gemini API调用尝试 {attempt + 1}/{max_retries},超时设置: {timeout}秒") + + response = await asyncio.to_thread( + requests.post, + url, + json=payload, + headers={ + "Content-Type": "application/json", + "User-Agent": "NarratoAI/1.0" + }, + timeout=timeout + ) + + if response.status_code == 200: + return response.json() + + # 处理特定的错误状态码 + if response.status_code == 429: + # 速率限制,等待后重试 + wait_time = 30 * (attempt + 1) + logger.warning(f"Gemini API速率限制,等待 {wait_time} 秒后重试") + await asyncio.sleep(wait_time) + continue + elif response.status_code in [502, 503, 504, 524]: + # 服务器错误或超时,可以重试 + if attempt < max_retries - 1: + wait_time = 10 * (attempt + 1) + logger.warning(f"Gemini API服务器错误 {response.status_code},等待 {wait_time} 秒后重试") + await asyncio.sleep(wait_time) + continue + + # 其他错误,直接抛出 + error = self._handle_api_error(response.status_code, response.text) + raise error + + except requests.exceptions.Timeout: + if attempt < max_retries - 1: + wait_time = 15 * (attempt + 1) + logger.warning(f"Gemini API请求超时,等待 {wait_time} 秒后重试") + await asyncio.sleep(wait_time) + continue + else: + raise APICallError("Gemini API请求超时,已达到最大重试次数") + except requests.exceptions.RequestException as e: + if attempt < max_retries - 1: + wait_time = 10 * (attempt + 1) + logger.warning(f"Gemini API网络错误: {str(e)},等待 {wait_time} 秒后重试") + await asyncio.sleep(wait_time) + continue + else: + raise APICallError(f"Gemini API网络错误: {str(e)}") + + # 如果所有重试都失败了 + raise APICallError("Gemini API调用失败,已达到最大重试次数") def _parse_vision_response(self, response_data: Dict[str, Any]) -> str: """解析视觉分析响应""" @@ -192,6 +240,7 @@ class GeminiTextProvider(TextModelProvider): @property def supported_models(self) -> List[str]: return [ + "gemini-2.5-flash", "gemini-2.0-flash-lite", "gemini-2.0-flash", "gemini-1.5-pro", @@ -278,25 +327,72 @@ class GeminiTextProvider(TextModelProvider): return self._parse_text_response(response_data) async def _make_api_call(self, payload: Dict[str, Any]) -> Dict[str, Any]: - """执行原生Gemini API调用""" + """执行原生Gemini API调用,包含重试机制""" + from app.config import config + url = f"{self.base_url}/models/{self.model_name}:generateContent?key={self.api_key}" - - response = await asyncio.to_thread( - requests.post, - url, - json=payload, - headers={ - "Content-Type": "application/json", - "User-Agent": "NarratoAI/1.0" - }, - timeout=120 - ) - - if response.status_code != 200: - error = self._handle_api_error(response.status_code, response.text) - raise error - - return response.json() + + max_retries = config.app.get('llm_max_retries', 3) + base_timeout = config.app.get('llm_text_timeout', 180) # 文本生成任务使用更长的基础超时时间 + + for attempt in range(max_retries): + try: + # 根据尝试次数调整超时时间 + timeout = base_timeout * (attempt + 1) + logger.debug(f"Gemini文本API调用尝试 {attempt + 1}/{max_retries},超时设置: {timeout}秒") + + response = await asyncio.to_thread( + requests.post, + url, + json=payload, + headers={ + "Content-Type": "application/json", + "User-Agent": "NarratoAI/1.0" + }, + timeout=timeout + ) + + if response.status_code == 200: + return response.json() + + # 处理特定的错误状态码 + if response.status_code == 429: + # 速率限制,等待后重试 + wait_time = 30 * (attempt + 1) + logger.warning(f"Gemini API速率限制,等待 {wait_time} 秒后重试") + await asyncio.sleep(wait_time) + continue + elif response.status_code in [502, 503, 504, 524]: + # 服务器错误或超时,可以重试 + if attempt < max_retries - 1: + wait_time = 15 * (attempt + 1) + logger.warning(f"Gemini API服务器错误 {response.status_code},等待 {wait_time} 秒后重试") + await asyncio.sleep(wait_time) + continue + + # 其他错误,直接抛出 + error = self._handle_api_error(response.status_code, response.text) + raise error + + except requests.exceptions.Timeout: + if attempt < max_retries - 1: + wait_time = 20 * (attempt + 1) + logger.warning(f"Gemini文本API请求超时,等待 {wait_time} 秒后重试") + await asyncio.sleep(wait_time) + continue + else: + raise APICallError("Gemini文本API请求超时,已达到最大重试次数") + except requests.exceptions.RequestException as e: + if attempt < max_retries - 1: + wait_time = 15 * (attempt + 1) + logger.warning(f"Gemini文本API网络错误: {str(e)},等待 {wait_time} 秒后重试") + await asyncio.sleep(wait_time) + continue + else: + raise APICallError(f"Gemini文本API网络错误: {str(e)}") + + # 如果所有重试都失败了 + raise APICallError("Gemini文本API调用失败,已达到最大重试次数") def _parse_text_response(self, response_data: Dict[str, Any]) -> str: """解析文本生成响应""" diff --git a/config.example.toml b/config.example.toml index 877b71b..7bb37be 100644 --- a/config.example.toml +++ b/config.example.toml @@ -1,5 +1,19 @@ [app] project_version="0.6.8" + + # 模型验证模式配置 + # true: 严格模式,只允许使用预定义支持列表中的模型(默认) + # false: 宽松模式,允许使用任何模型名称,仅记录警告 + strict_model_validation = true + + # LLM API 超时配置(秒) + # 视觉模型基础超时时间 + llm_vision_timeout = 120 + # 文本模型基础超时时间(解说文案生成等复杂任务需要更长时间) + llm_text_timeout = 180 + # API 重试次数 + llm_max_retries = 3 + # 支持视频理解的大模型提供商 # gemini (谷歌, 需要 VPN) # siliconflow (硅基流动)