From 37ea8b8f5b2af8be0350a09dfe7b9be4286d3552 Mon Sep 17 00:00:00 2001 From: chengyongru Date: Tue, 21 Apr 2026 17:37:10 +0800 Subject: [PATCH] fix(retry): recognize ZhiPu 1302 rate-limit error for retry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ZhiPu API returns code 1302 with Chinese text "速率限制" instead of standard HTTP 429 + "rate limit", causing the retry engine to treat it as non-transient and fail immediately. --- nanobot/providers/base.py | 2 ++ tests/providers/test_provider_retry.py | 50 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/nanobot/providers/base.py b/nanobot/providers/base.py index 121052ef..d7a59bab 100644 --- a/nanobot/providers/base.py +++ b/nanobot/providers/base.py @@ -108,6 +108,7 @@ class LLMProvider(ABC): "connection", "server error", "temporarily unavailable", + "速率限制", ) _RETRYABLE_STATUS_CODES = frozenset({408, 409, 429}) _TRANSIENT_ERROR_KINDS = frozenset({"timeout", "connection"}) @@ -154,6 +155,7 @@ class LLMProvider(ABC): "temporarily unavailable", "overloaded", "concurrency limit", + "速率限制", ) _SENTINEL = object() diff --git a/tests/providers/test_provider_retry.py b/tests/providers/test_provider_retry.py index add5e224..4b72c163 100644 --- a/tests/providers/test_provider_retry.py +++ b/tests/providers/test_provider_retry.py @@ -546,6 +546,56 @@ async def test_chat_with_retry_normalizes_explicit_none_max_tokens() -> None: assert provider.last_kwargs["temperature"] == 0.7 +@pytest.mark.asyncio +async def test_chat_with_retry_retries_zhipu_1302_rate_limit(monkeypatch) -> None: + """ZhiPu returns code 1302 with Chinese rate-limit text instead of HTTP 429.""" + provider = ScriptedProvider([ + LLMResponse( + content='Error: {\'code\': \'1302\', \'message\': \'您的账户已达到速率限制,请您控制请求频率\'}', + finish_reason="error", + ), + LLMResponse(content="ok"), + ]) + delays: list[float] = [] + + async def _fake_sleep(delay: float) -> None: + delays.append(delay) + + monkeypatch.setattr("nanobot.providers.base.asyncio.sleep", _fake_sleep) + + response = await provider.chat_with_retry(messages=[{"role": "user", "content": "hello"}]) + + assert response.content == "ok" + assert provider.calls == 2 + assert delays == [1] + + +@pytest.mark.asyncio +async def test_chat_with_retry_retries_zhipu_1302_with_429_status(monkeypatch) -> None: + """ZhiPu 1302 error with HTTP 429 status should also retry.""" + provider = ScriptedProvider([ + LLMResponse( + content='Error: {\'code\': \'1302\', \'message\': \'您的账户已达到速率限制,请您控制请求频率\'}', + finish_reason="error", + error_status_code=429, + error_code="1302", + ), + LLMResponse(content="ok"), + ]) + delays: list[float] = [] + + async def _fake_sleep(delay: float) -> None: + delays.append(delay) + + monkeypatch.setattr("nanobot.providers.base.asyncio.sleep", _fake_sleep) + + response = await provider.chat_with_retry(messages=[{"role": "user", "content": "hello"}]) + + assert response.content == "ok" + assert provider.calls == 2 + assert delays == [1] + + @pytest.mark.asyncio async def test_chat_stream_with_retry_normalizes_explicit_none_max_tokens() -> None: """chat_stream_with_retry must apply the same None-guard as chat_with_retry."""