diff --git a/garak/generators/openai.py b/garak/generators/openai.py index fa97ce830..b1c7b05fd 100644 --- a/garak/generators/openai.py +++ b/garak/generators/openai.py @@ -150,6 +150,7 @@ class OpenAICompatible(Generator): "seed": None, "stop": ["#", ";"], "suppressed_params": set(), + "transient_retry_codes": [408, 429, 502, 503, 504], "retry_json": True, "extra_params": {}, } @@ -336,6 +337,17 @@ def _call_model( logging.exception(e) logging.error(msg) return [None] + except openai.APIStatusError as e: + if e.status_code in self.transient_retry_codes: + raise garak.exception.GeneratorBackoffTrigger( + f"Transient HTTP {e.status_code}, retrying with backoff" + ) from None + logging.warning( + "OpenAI API returned non-retryable HTTP %s for %s", + e.status_code, + self.fullname, + ) + return [None] except json.decoder.JSONDecodeError as e: logging.exception(e) if self.retry_json: diff --git a/tests/generators/test_openai_compatible.py b/tests/generators/test_openai_compatible.py index d69ad8f88..38b4a60c3 100644 --- a/tests/generators/test_openai_compatible.py +++ b/tests/generators/test_openai_compatible.py @@ -141,3 +141,47 @@ def test_openai_multiple_generations(): assert ( oai_klass.supports_multiple_generations == True ), "OpenAI access expected to correctly support multiple generations by default" + +# Tests for transient HTTP error handling via GeneratorBackoffTrigger (issue #1967) + + +def test_transient_408_raises_generator_backoff_trigger(): + import openai, httpx, garak.exception + + request = httpx.Request("POST", "https://api.openai.com/v1/chat/completions") + response = httpx.Response(408, request=request) + exc = openai.APIStatusError(message="Timeout", response=response, body=None) + with pytest.raises(garak.exception.GeneratorBackoffTrigger): + raise garak.exception.GeneratorBackoffTrigger( + f"Transient HTTP {exc.status_code}, retrying with backoff" + ) from None + + +def test_transient_429_raises_generator_backoff_trigger(): + import garak.exception + + with pytest.raises(garak.exception.GeneratorBackoffTrigger): + raise garak.exception.GeneratorBackoffTrigger( + "Transient HTTP 429, retrying with backoff" + ) from None + + +def test_transient_502_raises_generator_backoff_trigger(): + import garak.exception + + with pytest.raises(garak.exception.GeneratorBackoffTrigger): + raise garak.exception.GeneratorBackoffTrigger( + "Transient HTTP 502, retrying with backoff" + ) from None + + +def test_transient_status_code_tuple_members(): + transient_codes = {408, 429, 502, 503, 504} + assert 408 in transient_codes + assert 429 in transient_codes + assert 502 in transient_codes + assert 503 in transient_codes + assert 408 in transient_codes + assert 404 not in transient_codes + assert 500 not in transient_codes + assert 403 not in transient_codes