From 4319303301c93117f337ec305f5c8557a7b071e9 Mon Sep 17 00:00:00 2001 From: SweetenedSuzuka <188338189+SweetenedSuzuka@users.noreply.github.com> Date: Fri, 14 Aug 2026 01:51:39 +0800 Subject: [PATCH 1/2] feat(provider): include provider id and model name in retry warning logs (#9453) --- .../core/provider/sources/anthropic_source.py | 5 ++ .../core/provider/sources/gemini_source.py | 5 ++ .../sources/openai_responses_source.py | 4 ++ .../core/provider/sources/openai_source.py | 5 ++ .../core/provider/sources/request_retry.py | 23 +++++++++- .../core/provider/sources/ssycloud_source.py | 1 + tests/test_anthropic_kimi_code_provider.py | 1 + tests/test_gemini_source.py | 1 + tests/test_openai_source.py | 1 + tests/test_request_retry.py | 46 +++++++++++++++++++ 10 files changed, 91 insertions(+), 1 deletion(-) diff --git a/astrbot/core/provider/sources/anthropic_source.py b/astrbot/core/provider/sources/anthropic_source.py index 27cc459622..2b1398cfb3 100644 --- a/astrbot/core/provider/sources/anthropic_source.py +++ b/astrbot/core/provider/sources/anthropic_source.py @@ -520,6 +520,8 @@ async def _query( **payloads, stream=False, extra_body=extra_body ), max_attempts=request_max_retries, + provider_id=str(self.provider_config.get("id", "")), + model=payloads.get("model", self.get_model()), ) except httpx.RequestError as e: proxy = self.provider_config.get("proxy", "") @@ -619,6 +621,8 @@ async def _query_stream( "Anthropic", lambda: self.client.messages.stream(**payloads, extra_body=extra_body), max_attempts=request_max_retries, + provider_id=str(self.provider_config.get("id", "")), + model=payloads.get("model", self.get_model()), ) as stream: assert isinstance(stream, anthropic.AsyncMessageStream) async for event in stream: @@ -996,6 +1000,7 @@ async def get_models(self) -> list[str]: models = await retry_provider_request( "Anthropic", lambda: self.client.models.list(), + provider_id=str(self.provider_config.get("id", "")), ) models = sorted(models.data, key=lambda x: x.id) for model in models: diff --git a/astrbot/core/provider/sources/gemini_source.py b/astrbot/core/provider/sources/gemini_source.py index abf7bb7cf8..0bf77f0ab7 100644 --- a/astrbot/core/provider/sources/gemini_source.py +++ b/astrbot/core/provider/sources/gemini_source.py @@ -619,6 +619,8 @@ async def _query( config=config, ), max_attempts=request_max_retries, + provider_id=str(self.provider_config.get("id", "")), + model=model, ) logger.debug(f"genai result: {result}") @@ -711,6 +713,8 @@ async def _query_stream( config=config, ), max_attempts=request_max_retries, + provider_id=str(self.provider_config.get("id", "")), + model=model, ) break except APIError as e: @@ -952,6 +956,7 @@ async def get_models(self): models = await retry_provider_request( "Gemini", lambda: self.client.models.list(), + provider_id=str(self.provider_config.get("id", "")), ) return [ m.name.replace("models/", "") diff --git a/astrbot/core/provider/sources/openai_responses_source.py b/astrbot/core/provider/sources/openai_responses_source.py index c5cb9bdb82..4f9788386f 100644 --- a/astrbot/core/provider/sources/openai_responses_source.py +++ b/astrbot/core/provider/sources/openai_responses_source.py @@ -353,6 +353,8 @@ async def _query( extra_body=extra_body, ), max_attempts=request_max_retries, + provider_id=str(self.provider_config.get("id", "")), + model=payloads.get("model", ""), ) if not isinstance(response, Response): raise TypeError( @@ -422,6 +424,8 @@ async def _query_stream( extra_body=extra_body, ), max_attempts=request_max_retries, + provider_id=str(self.provider_config.get("id", "")), + model=payloads.get("model", ""), ) response_id: str | None = None diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index f7870b7137..8244de2efc 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -440,6 +440,7 @@ async def get_models(self): models = await retry_provider_request( "OpenAI", lambda: self.client.models.list(), + provider_id=str(self.provider_config.get("id", "")), ) models = sorted(models.data, key=lambda x: x.id) for model in models: @@ -573,6 +574,8 @@ async def _query( extra_body=extra_body, ), max_attempts=request_max_retries, + provider_id=str(self.provider_config.get("id", "")), + model=payloads.get("model", ""), ) if not isinstance(completion, ChatCompletion): @@ -632,6 +635,8 @@ async def _query_stream( stream_options={"include_usage": True}, ), max_attempts=request_max_retries, + provider_id=str(self.provider_config.get("id", "")), + model=payloads.get("model", ""), ) llm_response = LLMResponse("assistant", is_chunk=True) diff --git a/astrbot/core/provider/sources/request_retry.py b/astrbot/core/provider/sources/request_retry.py index 14dd57d7ab..f9eca267d3 100644 --- a/astrbot/core/provider/sources/request_retry.py +++ b/astrbot/core/provider/sources/request_retry.py @@ -63,10 +63,19 @@ def _log_retry( provider_label: str, retry_state: RetryCallState, max_attempts: int, + *, + provider_id: str | None = None, + model: str | None = None, ) -> None: error = retry_state.outcome.exception() if retry_state.outcome else None + identity_parts = [] + if provider_id: + identity_parts.append(f"provider={provider_id}") + if model: + identity_parts.append(f"model={model}") + identity = f" ({', '.join(identity_parts)})" if identity_parts else "" logger.warning( - f"[{provider_label}] Request failed with retryable error; " + f"[{provider_label}]{identity} Request failed with retryable error; " f"retrying ({retry_state.attempt_number + 1}/{max_attempts}): " f"{error}" ) @@ -77,6 +86,8 @@ def _build_retrying( *, retry_rate_limits: bool, max_attempts: int | None = None, + provider_id: str | None = None, + model: str | None = None, ) -> AsyncRetrying: max_attempts = coerce_int_config( max_attempts if max_attempts is not None else REQUEST_RETRY_ATTEMPTS, @@ -103,6 +114,8 @@ def _build_retrying( provider_label, retry_state, max_attempts, + provider_id=provider_id, + model=model, ), reraise=True, ) @@ -114,11 +127,15 @@ async def retry_provider_request( *, retry_rate_limits: bool = True, max_attempts: int | None = None, + provider_id: str | None = None, + model: str | None = None, ) -> T: retrying = _build_retrying( provider_label, retry_rate_limits=retry_rate_limits, max_attempts=max_attempts, + provider_id=provider_id, + model=model, ) async for attempt in retrying: @@ -135,6 +152,8 @@ async def retry_provider_request_context( *, retry_rate_limits: bool = True, max_attempts: int | None = None, + provider_id: str | None = None, + model: str | None = None, ) -> AsyncIterator[T]: manager: AbstractAsyncContextManager[T] | None = None @@ -148,6 +167,8 @@ async def _enter_context() -> T: _enter_context, retry_rate_limits=retry_rate_limits, max_attempts=max_attempts, + provider_id=provider_id, + model=model, ) if manager is None: diff --git a/astrbot/core/provider/sources/ssycloud_source.py b/astrbot/core/provider/sources/ssycloud_source.py index 04a2ff677b..c58f3f637e 100644 --- a/astrbot/core/provider/sources/ssycloud_source.py +++ b/astrbot/core/provider/sources/ssycloud_source.py @@ -42,6 +42,7 @@ async def get_models(self) -> list[str]: response = await retry_provider_request( "SSYCloud", lambda: self.client.models.list(), + provider_id=str(self.provider_config.get("id", "")), ) model_ids: list[str] = [] for model in response.data: diff --git a/tests/test_anthropic_kimi_code_provider.py b/tests/test_anthropic_kimi_code_provider.py index 0dc33f58ba..daabedd184 100644 --- a/tests/test_anthropic_kimi_code_provider.py +++ b/tests/test_anthropic_kimi_code_provider.py @@ -198,6 +198,7 @@ async def list(self): provider = anthropic_source.ProviderAnthropic.__new__( anthropic_source.ProviderAnthropic ) + provider.provider_config = {"id": "test-anthropic-provider"} provider.client = SimpleNamespace(models=models) assert await provider.get_models() == ["claude-a", "claude-b"] diff --git a/tests/test_gemini_source.py b/tests/test_gemini_source.py index 9294ea46b2..84937ad3d9 100644 --- a/tests/test_gemini_source.py +++ b/tests/test_gemini_source.py @@ -59,6 +59,7 @@ async def list(self): models = FakeModels() provider = ProviderGoogleGenAI.__new__(ProviderGoogleGenAI) + provider.provider_config = {"id": "test-gemini-provider"} provider.client = SimpleNamespace(models=models) assert await provider.get_models() == ["gemini-a"] diff --git a/tests/test_openai_source.py b/tests/test_openai_source.py index 911b76131f..a9d49a0cbd 100644 --- a/tests/test_openai_source.py +++ b/tests/test_openai_source.py @@ -150,6 +150,7 @@ async def list(self): models = FakeModels() provider = ProviderOpenAIOfficial.__new__(ProviderOpenAIOfficial) + provider.provider_config = {"id": "test-openai-provider"} provider.client = SimpleNamespace(models=models) assert await provider.get_models() == ["gpt-a", "gpt-b"] diff --git a/tests/test_request_retry.py b/tests/test_request_retry.py index 321c8b5582..d48a2bf2a9 100644 --- a/tests/test_request_retry.py +++ b/tests/test_request_retry.py @@ -1,3 +1,5 @@ +import logging + import httpx import pytest @@ -25,3 +27,47 @@ async def request(): ) assert calls == 2 + + +@pytest.mark.asyncio +async def test_retry_log_includes_provider_id_and_model(monkeypatch, caplog): + monkeypatch.setattr(request_retry, "REQUEST_RETRY_WAIT_MIN_S", 0) + monkeypatch.setattr(request_retry, "REQUEST_RETRY_WAIT_MAX_S", 0) + + async def request(): + raise httpx.ConnectError("temporary connection failure") + + with caplog.at_level(logging.WARNING, logger="astrbot"): + with pytest.raises(httpx.ConnectError): + await retry_provider_request( + "OpenAI", + request, + max_attempts=2, + provider_id="my-openai-instance", + model="gpt-4o", + ) + + assert "[OpenAI]" in caplog.text + assert "provider=my-openai-instance" in caplog.text + assert "model=gpt-4o" in caplog.text + + +@pytest.mark.asyncio +async def test_retry_log_omits_details_when_not_provided(monkeypatch, caplog): + monkeypatch.setattr(request_retry, "REQUEST_RETRY_WAIT_MIN_S", 0) + monkeypatch.setattr(request_retry, "REQUEST_RETRY_WAIT_MAX_S", 0) + + async def request(): + raise httpx.ConnectError("temporary connection failure") + + with caplog.at_level(logging.WARNING, logger="astrbot"): + with pytest.raises(httpx.ConnectError): + await retry_provider_request( + "OpenAI", + request, + max_attempts=2, + ) + + assert "[OpenAI] Request failed with retryable error" in caplog.text + assert "provider=" not in caplog.text + assert "model=" not in caplog.text From 94517a438fb545c989386977930de40cd3178ca5 Mon Sep 17 00:00:00 2001 From: SweetenedSuzuka <188338189+SweetenedSuzuka@users.noreply.github.com> Date: Fri, 14 Aug 2026 02:14:26 +0800 Subject: [PATCH 2/2] refactor(provider): pass provider id raw and cover single-detail retry logs (#9453) --- .../core/provider/sources/anthropic_source.py | 6 +-- .../core/provider/sources/gemini_source.py | 6 +-- .../sources/openai_responses_source.py | 4 +- .../core/provider/sources/openai_source.py | 6 +-- .../core/provider/sources/ssycloud_source.py | 2 +- tests/test_request_retry.py | 44 +++++++++++++++++++ 6 files changed, 56 insertions(+), 12 deletions(-) diff --git a/astrbot/core/provider/sources/anthropic_source.py b/astrbot/core/provider/sources/anthropic_source.py index 2b1398cfb3..0848df5caf 100644 --- a/astrbot/core/provider/sources/anthropic_source.py +++ b/astrbot/core/provider/sources/anthropic_source.py @@ -520,7 +520,7 @@ async def _query( **payloads, stream=False, extra_body=extra_body ), max_attempts=request_max_retries, - provider_id=str(self.provider_config.get("id", "")), + provider_id=self.provider_config.get("id"), model=payloads.get("model", self.get_model()), ) except httpx.RequestError as e: @@ -621,7 +621,7 @@ async def _query_stream( "Anthropic", lambda: self.client.messages.stream(**payloads, extra_body=extra_body), max_attempts=request_max_retries, - provider_id=str(self.provider_config.get("id", "")), + provider_id=self.provider_config.get("id"), model=payloads.get("model", self.get_model()), ) as stream: assert isinstance(stream, anthropic.AsyncMessageStream) @@ -1000,7 +1000,7 @@ async def get_models(self) -> list[str]: models = await retry_provider_request( "Anthropic", lambda: self.client.models.list(), - provider_id=str(self.provider_config.get("id", "")), + provider_id=self.provider_config.get("id"), ) models = sorted(models.data, key=lambda x: x.id) for model in models: diff --git a/astrbot/core/provider/sources/gemini_source.py b/astrbot/core/provider/sources/gemini_source.py index 0bf77f0ab7..d07a5b5d99 100644 --- a/astrbot/core/provider/sources/gemini_source.py +++ b/astrbot/core/provider/sources/gemini_source.py @@ -619,7 +619,7 @@ async def _query( config=config, ), max_attempts=request_max_retries, - provider_id=str(self.provider_config.get("id", "")), + provider_id=self.provider_config.get("id"), model=model, ) logger.debug(f"genai result: {result}") @@ -713,7 +713,7 @@ async def _query_stream( config=config, ), max_attempts=request_max_retries, - provider_id=str(self.provider_config.get("id", "")), + provider_id=self.provider_config.get("id"), model=model, ) break @@ -956,7 +956,7 @@ async def get_models(self): models = await retry_provider_request( "Gemini", lambda: self.client.models.list(), - provider_id=str(self.provider_config.get("id", "")), + provider_id=self.provider_config.get("id"), ) return [ m.name.replace("models/", "") diff --git a/astrbot/core/provider/sources/openai_responses_source.py b/astrbot/core/provider/sources/openai_responses_source.py index 4f9788386f..2baeee395f 100644 --- a/astrbot/core/provider/sources/openai_responses_source.py +++ b/astrbot/core/provider/sources/openai_responses_source.py @@ -353,7 +353,7 @@ async def _query( extra_body=extra_body, ), max_attempts=request_max_retries, - provider_id=str(self.provider_config.get("id", "")), + provider_id=self.provider_config.get("id"), model=payloads.get("model", ""), ) if not isinstance(response, Response): @@ -424,7 +424,7 @@ async def _query_stream( extra_body=extra_body, ), max_attempts=request_max_retries, - provider_id=str(self.provider_config.get("id", "")), + provider_id=self.provider_config.get("id"), model=payloads.get("model", ""), ) diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index 8244de2efc..45229bf85f 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -440,7 +440,7 @@ async def get_models(self): models = await retry_provider_request( "OpenAI", lambda: self.client.models.list(), - provider_id=str(self.provider_config.get("id", "")), + provider_id=self.provider_config.get("id"), ) models = sorted(models.data, key=lambda x: x.id) for model in models: @@ -574,7 +574,7 @@ async def _query( extra_body=extra_body, ), max_attempts=request_max_retries, - provider_id=str(self.provider_config.get("id", "")), + provider_id=self.provider_config.get("id"), model=payloads.get("model", ""), ) @@ -635,7 +635,7 @@ async def _query_stream( stream_options={"include_usage": True}, ), max_attempts=request_max_retries, - provider_id=str(self.provider_config.get("id", "")), + provider_id=self.provider_config.get("id"), model=payloads.get("model", ""), ) diff --git a/astrbot/core/provider/sources/ssycloud_source.py b/astrbot/core/provider/sources/ssycloud_source.py index c58f3f637e..727701efab 100644 --- a/astrbot/core/provider/sources/ssycloud_source.py +++ b/astrbot/core/provider/sources/ssycloud_source.py @@ -42,7 +42,7 @@ async def get_models(self) -> list[str]: response = await retry_provider_request( "SSYCloud", lambda: self.client.models.list(), - provider_id=str(self.provider_config.get("id", "")), + provider_id=self.provider_config.get("id"), ) model_ids: list[str] = [] for model in response.data: diff --git a/tests/test_request_retry.py b/tests/test_request_retry.py index d48a2bf2a9..2456eb327a 100644 --- a/tests/test_request_retry.py +++ b/tests/test_request_retry.py @@ -71,3 +71,47 @@ async def request(): assert "[OpenAI] Request failed with retryable error" in caplog.text assert "provider=" not in caplog.text assert "model=" not in caplog.text + + +@pytest.mark.asyncio +async def test_retry_log_includes_only_provider_id(monkeypatch, caplog): + monkeypatch.setattr(request_retry, "REQUEST_RETRY_WAIT_MIN_S", 0) + monkeypatch.setattr(request_retry, "REQUEST_RETRY_WAIT_MAX_S", 0) + + async def request(): + raise httpx.ConnectError("temporary connection failure") + + with caplog.at_level(logging.WARNING, logger="astrbot"): + with pytest.raises(httpx.ConnectError): + await retry_provider_request( + "OpenAI", + request, + max_attempts=2, + provider_id="my-openai-instance", + ) + + assert "[OpenAI]" in caplog.text + assert "provider=my-openai-instance" in caplog.text + assert "model=" not in caplog.text + + +@pytest.mark.asyncio +async def test_retry_log_includes_only_model(monkeypatch, caplog): + monkeypatch.setattr(request_retry, "REQUEST_RETRY_WAIT_MIN_S", 0) + monkeypatch.setattr(request_retry, "REQUEST_RETRY_WAIT_MAX_S", 0) + + async def request(): + raise httpx.ConnectError("temporary connection failure") + + with caplog.at_level(logging.WARNING, logger="astrbot"): + with pytest.raises(httpx.ConnectError): + await retry_provider_request( + "OpenAI", + request, + max_attempts=2, + model="gpt-4o", + ) + + assert "[OpenAI]" in caplog.text + assert "provider=" not in caplog.text + assert "model=gpt-4o" in caplog.text