From 5dd8142cc0f9c5f2f7b615c0f1cc15a12c047b9c Mon Sep 17 00:00:00 2001 From: SweetenedSuzuka <188338189+SweetenedSuzuka@users.noreply.github.com> Date: Fri, 14 Aug 2026 00:55:39 +0800 Subject: [PATCH 1/2] fix(provider): disable OpenAI SDK built-in retries under AstrBot retry layer (#9663) AsyncOpenAI/AsyncAzureOpenAI inherit the SDK default max_retries=2, which nests underneath AstrBot's tenacity-based retry_provider_request(). As a result provider_settings.request_max_retries no longer reflects the true attempt count (e.g. request_max_retries=1 still sends up to 3 HTTP requests) and delays fallback-provider switchover. Pass max_retries=0 so AstrBot's retry layer is the single source of truth for retries. --- .../core/provider/sources/openai_source.py | 6 ++++++ tests/test_openai_source.py | 21 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index f7870b7137..e984b076b6 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -377,6 +377,9 @@ def __init__(self, provider_config, provider_settings) -> None: default_headers=self.custom_headers, base_url=provider_config.get("api_base", ""), timeout=self.timeout, + # Retry is handled by retry_provider_request(); disable the + # SDK built-in retry to avoid stacking request attempts. + max_retries=0, http_client=self._create_http_client(provider_config), ) else: @@ -386,6 +389,9 @@ def __init__(self, provider_config, provider_settings) -> None: base_url=provider_config.get("api_base", None), default_headers=self.custom_headers, timeout=self.timeout, + # Retry is handled by retry_provider_request(); disable the + # SDK built-in retry to avoid stacking request attempts. + max_retries=0, http_client=self._create_http_client(provider_config), ) diff --git a/tests/test_openai_source.py b/tests/test_openai_source.py index 911b76131f..b719178e7f 100644 --- a/tests/test_openai_source.py +++ b/tests/test_openai_source.py @@ -5,6 +5,7 @@ import httpx import pytest +from openai import AsyncAzureOpenAI, AsyncOpenAI from openai.types.chat.chat_completion import ChatCompletion from openai.types.chat.chat_completion_chunk import ChatCompletionChunk from PIL import Image as PILImage @@ -60,6 +61,26 @@ def _make_groq_provider(overrides: dict | None = None) -> ProviderGroq: ) +@pytest.mark.asyncio +async def test_openai_client_disables_sdk_builtin_retries(): + provider = _make_provider() + try: + assert isinstance(provider.client, AsyncOpenAI) + assert provider.client.max_retries == 0 + finally: + await provider.terminate() + + +@pytest.mark.asyncio +async def test_azure_client_disables_sdk_builtin_retries(): + provider = _make_provider({"api_version": "2024-02-01"}) + try: + assert isinstance(provider.client, AsyncAzureOpenAI) + assert provider.client.max_retries == 0 + finally: + await provider.terminate() + + def test_create_http_client_uses_openai_httpx_module(monkeypatch): captured: dict[str, object] = {} fake_httpx_module = object() From 5735f0c281d82b7d5952ad8868f95514458d4c71 Mon Sep 17 00:00:00 2001 From: SweetenedSuzuka <188338189+SweetenedSuzuka@users.noreply.github.com> Date: Fri, 14 Aug 2026 01:10:57 +0800 Subject: [PATCH 2/2] test(provider): add retry-count regression test, parameterize client retry tests Address review feedback on #9669: merge the near-identical official/Azure client construction tests into a parametrized test, and add a regression test asserting _query() performs exactly request_max_retries attempts through retry_provider_request() when the call keeps failing, guarding against multiplicative retries being reintroduced. --- tests/test_openai_source.py | 42 ++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/tests/test_openai_source.py b/tests/test_openai_source.py index b719178e7f..ac36299f23 100644 --- a/tests/test_openai_source.py +++ b/tests/test_openai_source.py @@ -61,22 +61,50 @@ def _make_groq_provider(overrides: dict | None = None) -> ProviderGroq: ) +@pytest.mark.parametrize( + ("overrides", "expected_client"), + [ + ({}, AsyncOpenAI), + ({"api_version": "2024-02-01"}, AsyncAzureOpenAI), + ], +) @pytest.mark.asyncio -async def test_openai_client_disables_sdk_builtin_retries(): - provider = _make_provider() +async def test_provider_client_disables_sdk_builtin_retries(overrides, expected_client): + provider = _make_provider(overrides) try: - assert isinstance(provider.client, AsyncOpenAI) + assert isinstance(provider.client, expected_client) assert provider.client.max_retries == 0 finally: await provider.terminate() @pytest.mark.asyncio -async def test_azure_client_disables_sdk_builtin_retries(): - provider = _make_provider({"api_version": "2024-02-01"}) +async def test_query_attempts_exactly_request_max_retries_times(monkeypatch): + monkeypatch.setattr(request_retry, "REQUEST_RETRY_WAIT_MIN_S", 0) + monkeypatch.setattr(request_retry, "REQUEST_RETRY_WAIT_MAX_S", 0) + + provider = _make_provider() try: - assert isinstance(provider.client, AsyncAzureOpenAI) - assert provider.client.max_retries == 0 + calls = 0 + + async def failing_create(**kwargs): + nonlocal calls + calls += 1 + raise httpx.ConnectError("temporary connection failure") + + monkeypatch.setattr(provider.client.chat.completions, "create", failing_create) + + with pytest.raises(httpx.ConnectError): + await provider._query( + payloads={ + "model": "gpt-4o-mini", + "messages": [{"role": "user", "content": "hi"}], + }, + tools=None, + request_max_retries=2, + ) + + assert calls == 2 finally: await provider.terminate()