From c9504dc94291448d9046fabea8167e5359f897a2 Mon Sep 17 00:00:00 2001 From: x1051445024 <你的GitHub注册邮箱> Date: Wed, 1 Jul 2026 10:20:27 +0800 Subject: [PATCH 1/2] feat(provider): make outer OpenAI retry count configurable --- astrbot/core/config/default.py | 4 ++++ .../core/provider/sources/openai_source.py | 22 +++++++++++++++++-- tests/test_openai_source.py | 16 ++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 7fb847dccd..9b65c08ce6 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -104,6 +104,7 @@ "default_provider_id": "", "fallback_chat_models": [], "request_max_retries": 5, + "provider_error_retries": 1, "default_image_caption_provider_id": "", "image_caption_prompt": "Please describe the image using Chinese.", "provider_pool": ["*"], # "*" 表示使用所有可用的提供者 @@ -2803,6 +2804,9 @@ "request_max_retries": { "type": "int", }, + "provider_error_retries": { + "type": "int", + }, "wake_prefix": { "type": "string", }, diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index a49003af17..8f86f2a6ae 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -398,6 +398,24 @@ def __init__(self, provider_config, provider_settings) -> None: self.reasoning_key = "reasoning_content" + def _provider_error_retries(self) -> int: + """Return retry attempts for provider-level recovery paths. + + This outer retry loop handles payload mutation and key rotation (for + example context trimming, tool removal, image fallback, or switching to + another configured API key). Transport/status-code retries are handled + separately by ``request_max_retries`` in ``retry_provider_request``. + Keeping this value configurable prevents nested retry loops from + multiplying latency for proxy/aggregator providers that already perform + their own upstream retry and fallback. + """ + raw = self.provider_settings.get("provider_error_retries", 1) + try: + retries = int(raw) + except (TypeError, ValueError): + retries = 1 + return max(1, retries) + def _ollama_disable_thinking_enabled(self) -> bool: value = self.provider_config.get("ollama_disable_thinking", False) if isinstance(value, str): @@ -1188,7 +1206,7 @@ async def text_chat( payloads["tool_choice"] = tool_choice llm_response = None - max_retries = 10 + max_retries = self._provider_error_retries() available_api_keys = self.api_keys.copy() chosen_key = random.choice(available_api_keys) image_fallback_used = False @@ -1264,7 +1282,7 @@ async def text_chat_stream( if func_tool and not func_tool.empty(): payloads["tool_choice"] = tool_choice - max_retries = 10 + max_retries = self._provider_error_retries() available_api_keys = self.api_keys.copy() chosen_key = random.choice(available_api_keys) image_fallback_used = False diff --git a/tests/test_openai_source.py b/tests/test_openai_source.py index b8262090e4..558296ac18 100644 --- a/tests/test_openai_source.py +++ b/tests/test_openai_source.py @@ -120,6 +120,22 @@ def fake_import(name, globals=None, locals=None, fromlist=(), level=0): assert captured["httpx_module"] is openai_source_module.httpx +def test_provider_error_retries_defaults_and_coerces_values(): + provider = ProviderOpenAIOfficial.__new__(ProviderOpenAIOfficial) + + provider.provider_settings = {} + assert provider._provider_error_retries() == 1 + + provider.provider_settings = {"provider_error_retries": "3"} + assert provider._provider_error_retries() == 3 + + provider.provider_settings = {"provider_error_retries": 0} + assert provider._provider_error_retries() == 1 + + provider.provider_settings = {"provider_error_retries": "invalid"} + assert provider._provider_error_retries() == 1 + + @pytest.mark.asyncio async def test_get_models_retries_transient_request_error(monkeypatch): monkeypatch.setattr(request_retry, "REQUEST_RETRY_WAIT_MIN_S", 0) From 7a8a29b53ef1b32e5fb152904e8e05b412853854 Mon Sep 17 00:00:00 2001 From: x1051445024 <你的GitHub注册邮箱> Date: Wed, 1 Jul 2026 10:56:54 +0800 Subject: [PATCH 2/2] fix(provider): handle single outer retry success --- astrbot/core/provider/sources/openai_source.py | 9 ++++++--- tests/test_openai_source.py | 2 ++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index 8f86f2a6ae..666cde2b73 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -409,7 +409,8 @@ def _provider_error_retries(self) -> int: multiplying latency for proxy/aggregator providers that already perform their own upstream retry and fallback. """ - raw = self.provider_settings.get("provider_error_retries", 1) + provider_settings = getattr(self, "provider_settings", {}) or {} + raw = provider_settings.get("provider_error_retries", 1) try: retries = int(raw) except (TypeError, ValueError): @@ -1246,7 +1247,7 @@ async def text_chat( if success: break - if retry_cnt == max_retries - 1 or llm_response is None: + if llm_response is None: logger.error(f"API 调用失败,重试 {max_retries} 次仍然失败。") if last_exception is None: raise Exception("未知错误") @@ -1289,6 +1290,7 @@ async def text_chat_stream( last_exception = None retry_cnt = 0 + completed = False for retry_cnt in range(max_retries): try: self.client.api_key = chosen_key @@ -1298,6 +1300,7 @@ async def text_chat_stream( request_max_retries=request_max_retries, ): yield response + completed = True break except Exception as e: last_exception = e @@ -1323,7 +1326,7 @@ async def text_chat_stream( if success: break - if retry_cnt == max_retries - 1: + if not completed: logger.error(f"API 调用失败,重试 {max_retries} 次仍然失败。") if last_exception is None: raise Exception("未知错误") diff --git a/tests/test_openai_source.py b/tests/test_openai_source.py index 558296ac18..73332b1070 100644 --- a/tests/test_openai_source.py +++ b/tests/test_openai_source.py @@ -123,6 +123,8 @@ def fake_import(name, globals=None, locals=None, fromlist=(), level=0): def test_provider_error_retries_defaults_and_coerces_values(): provider = ProviderOpenAIOfficial.__new__(ProviderOpenAIOfficial) + assert provider._provider_error_retries() == 1 + provider.provider_settings = {} assert provider._provider_error_retries() == 1