mirror of
https://github.com/usestrix/strix.git
synced 2026-08-28 05:25:00 +00:00
Merge fa52c344a6 into 717ffc8f4c
This commit is contained in:
commit
c8d1389c11
2 changed files with 113 additions and 2 deletions
|
|
@ -120,8 +120,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:
|
||||
|
|
@ -129,6 +133,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
|
||||
|
|
@ -657,6 +666,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] = []
|
||||
|
|
@ -733,8 +743,29 @@ async def _run_cycle( # noqa: PLR0912, PLR0915
|
|||
await coordinator.trigger_budget_stop()
|
||||
raise
|
||||
except Exception as exc:
|
||||
prompt_policy_rejection = _is_openrouter_prompt_policy_rejection(exc)
|
||||
if (
|
||||
image_strips < 3
|
||||
prompt_policy_rejection
|
||||
and prompt_policy_retries < _MAX_OPENROUTER_PROMPT_POLICY_RETRIES
|
||||
):
|
||||
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 (
|
||||
not prompt_policy_rejection
|
||||
and image_strips < 3
|
||||
and session is not None
|
||||
and getattr(exc, "status_code", None) in _INPUT_REJECTION_CODES
|
||||
):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -109,6 +124,8 @@ def _patch_fast_backoff(monkeypatch: pytest.MonkeyPatch) -> None:
|
|||
async def _run_once(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
streams: list[_FakeStream],
|
||||
*,
|
||||
session: Any = None,
|
||||
) -> Any:
|
||||
_patch_fast_backoff(monkeypatch)
|
||||
calls = {"n": 0}
|
||||
|
|
@ -131,7 +148,7 @@ async def _run_once(
|
|||
run_config=cast("RunConfig", object()),
|
||||
context={},
|
||||
max_turns=5,
|
||||
session=None,
|
||||
session=session,
|
||||
interactive=False,
|
||||
event_sink=None,
|
||||
hooks=None,
|
||||
|
|
@ -150,6 +167,69 @@ 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_openrouter_prompt_policy_retry_preserves_session_images(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
class _Session:
|
||||
async def get_items(self) -> list[Any]:
|
||||
return []
|
||||
|
||||
image_strips = 0
|
||||
|
||||
async def _strip_images(_session: Any) -> bool:
|
||||
nonlocal image_strips
|
||||
image_strips += 1
|
||||
return True
|
||||
|
||||
async def _no_compaction(*_args: Any, **_kwargs: Any) -> bool:
|
||||
return False
|
||||
|
||||
async def _no_salvage(*_args: Any, **_kwargs: Any) -> None:
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(execution, "strip_all_images_from_session", _strip_images)
|
||||
monkeypatch.setattr(execution, "_compact_session", _no_compaction)
|
||||
monkeypatch.setattr(execution, "_salvage_stream_to_session", _no_salvage)
|
||||
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, session=_Session())
|
||||
|
||||
assert image_strips == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_cycle_gives_up_after_max_retries(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue