From db1e6095b64411e4bb225f7da3e274e33996d5a2 Mon Sep 17 00:00:00 2001 From: oyasumi Date: Wed, 19 Aug 2026 21:23:25 +0000 Subject: [PATCH] fix(llm): retry intermittent OpenRouter prompt rejections --- strix/core/execution.py | 29 +++++++++++++++++ tests/test_execution_transient_retry.py | 43 +++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/strix/core/execution.py b/strix/core/execution.py index bd99e7c3..31551d29 100644 --- a/strix/core/execution.py +++ b/strix/core/execution.py @@ -108,8 +108,12 @@ async def _compact_session( _MAX_TRANSIENT_MODEL_RETRIES = 5 +_MAX_OPENROUTER_PROMPT_POLICY_RETRIES = 3 _TRANSIENT_MODEL_RETRY_BASE_DELAY_S = 2.0 _TRANSIENT_MODEL_RETRY_MAX_DELAY_S = 90.0 +_OPENROUTER_PROMPT_POLICY_REJECTION = ( + "invalid prompt: your prompt was flagged as potentially violating our usage policy" +) def _model_error_status_code(exc: BaseException) -> int | None: @@ -117,6 +121,11 @@ def _model_error_status_code(exc: BaseException) -> int | None: return code if isinstance(code, int) else None +def _is_openrouter_prompt_policy_rejection(exc: BaseException) -> bool: + error_text = str(exc).lower() + return "openrouter" in error_text and _OPENROUTER_PROMPT_POLICY_REJECTION in error_text + + def _is_transient_model_error(exc: BaseException) -> bool: if codex.is_content_guardrail_error(exc): return False @@ -643,6 +652,7 @@ async def _run_cycle( # noqa: PLR0912, PLR0915 image_strips = 0 compactions = 0 model_retries = 0 + prompt_policy_retries = 0 while True: stream: Any = None pre_run_items: list[Any] = [] @@ -757,6 +767,25 @@ async def _run_cycle( # noqa: PLR0912, PLR0915 ) input_data = [] continue + if ( + prompt_policy_retries < _MAX_OPENROUTER_PROMPT_POLICY_RETRIES + and _is_openrouter_prompt_policy_rejection(exc) + ): + prompt_policy_retries += 1 + delay = _transient_model_retry_delay(prompt_policy_retries) + logger.warning( + "intermittent OpenRouter prompt-policy rejection for %s; replaying " + "unchanged turn (attempt %d/%d, backoff %.1fs): %r", + agent_id, + prompt_policy_retries, + _MAX_OPENROUTER_PROMPT_POLICY_RETRIES, + delay, + exc, + ) + await asyncio.sleep(delay) + if session is not None: + input_data = [] + continue if model_retries < _MAX_TRANSIENT_MODEL_RETRIES and _is_transient_model_error(exc): model_retries += 1 delay = _transient_model_retry_delay(model_retries) diff --git a/tests/test_execution_transient_retry.py b/tests/test_execution_transient_retry.py index d81e4e70..44ccdd2c 100644 --- a/tests/test_execution_transient_retry.py +++ b/tests/test_execution_transient_retry.py @@ -36,6 +36,15 @@ def _status_error(status: int) -> APIStatusError: ) +def _openrouter_prompt_policy_rejection() -> BadRequestError: + return BadRequestError( + "OpenrouterException - Message: Invalid prompt: your prompt was flagged as " + "potentially violating our usage policy. Please try again with a different prompt", + response=httpx.Response(400, request=_request()), + body=None, + ) + + def test_midstream_api_error_is_transient() -> None: assert execution._is_transient_model_error(_midstream_api_error()) is True @@ -79,6 +88,12 @@ def test_content_guardrail_is_not_retried() -> None: assert execution._is_transient_model_error(guardrail) is False +def test_openrouter_prompt_policy_rejection_has_dedicated_classification() -> None: + rejection = _openrouter_prompt_policy_rejection() + assert execution._is_openrouter_prompt_policy_rejection(rejection) is True + assert execution._is_transient_model_error(rejection) is False + + def test_client_errors_are_not_transient() -> None: bad_request = BadRequestError( "bad", response=httpx.Response(400, request=_request()), body=None @@ -150,6 +165,34 @@ async def test_run_cycle_retries_transient_midstream_error( assert attempts == 2 +@pytest.mark.asyncio +async def test_run_cycle_retries_openrouter_prompt_policy_rejection_three_times( + monkeypatch: pytest.MonkeyPatch, +) -> None: + streams = [ + _FakeStream(exc=_openrouter_prompt_policy_rejection()) + for _ in range(execution._MAX_OPENROUTER_PROMPT_POLICY_RETRIES) + ] + streams.append(_FakeStream()) + + result, attempts, _coordinator = await _run_once(monkeypatch, streams) + + assert result is streams[-1] + assert attempts == 4 + + +@pytest.mark.asyncio +async def test_run_cycle_gives_up_after_openrouter_prompt_policy_retry_limit( + monkeypatch: pytest.MonkeyPatch, +) -> None: + streams = [ + _FakeStream(exc=_openrouter_prompt_policy_rejection()) + for _ in range(execution._MAX_OPENROUTER_PROMPT_POLICY_RETRIES + 1) + ] + with pytest.raises(BadRequestError): + await _run_once(monkeypatch, streams) + + @pytest.mark.asyncio async def test_run_cycle_gives_up_after_max_retries( monkeypatch: pytest.MonkeyPatch,