Skip to content

Commit f265154

Browse files
committed
rollout: isolate auditor-side input refusals per-seed too
The previous commits isolate target-side LLMInputError. Auditor-side LLMInputError (where the auditor's adversarial prompt itself trips the provider's content filter / Prompt Shields jailbreak detector) was still re-raised by _run_auditor_target_loop's auditor catch block, which aborted the whole rollout stage on the first occurrence. This was hit live on the May 12 PR #44 mix-1k validation run: at scenario 853 of 1000, the auditor's adversarial prompt for one seed was refused by Azure Prompt Shields with the same Bad request / 'flagged as potentially violating our usage policy' signature. Aaron flagged this auditor-side refusal pattern in the May 12 standup and worked around it in PR #45 with a tame auditor system prompt for benchmarking; the right fix in product code is the same per-seed isolation as the target-side case. Fix: catch LLMInputError separately from the other classified errors in _run_auditor_target_loop's auditor block. Record an '[AUDITOR INPUT REFUSED: ...]' system event in the transcript, set stop_reason='auditor_input_refused', and end the conversation cleanly so the worker moves on to the next seed. Auth/rate-limit/5xx errors still propagate (they're global pipeline problems, not seed-specific). Tests: - test_run_rollout_isolates_auditor_input_refusal_to_one_seed: 5-seed scenario batch where one seed's auditor call raises LLMInputError; all 5 transcripts are written, the refused seed carries stop_reason='auditor_input_refused', the batch returns successfully. All 51 rollout + exception-handling tests pass.
1 parent 0184d8d commit f265154

2 files changed

Lines changed: 139 additions & 4 deletions

File tree

p2m/stages/rollout.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -700,10 +700,32 @@ async def _run_auditor_target_loop(
700700
auditor_messages = [m for m in auditor_messages if m.content != _AUDITOR_RETRY_GUIDANCE]
701701
auditor_messages.append(auditor_response.message)
702702
break
703-
except (LLMAuthError, LLMInputError, LLMRateLimitError, LLMProviderError):
704-
# Transport/auth/rate-limit errors should propagate to the
705-
# runner's top-level handler — not be treated as bad auditor
706-
# output. The runner presents these with clean messages.
703+
except LLMInputError as exc:
704+
# Auditor-side input refusal (e.g. Azure Prompt Shields
705+
# rejecting the auditor's adversarial prompt as a suspected
706+
# jailbreak attempt - the default auditor system prompt is
707+
# jailbreak-shaped by design and reliably trips Prompt
708+
# Shields on a small fraction of seeds). This is per-seed
709+
# data, not a global pipeline error: a different seed will
710+
# lead the auditor down a different path and complete
711+
# normally. Record the refusal in the transcript and stop
712+
# the conversation cleanly so the worker can move on.
713+
transcript.add_event(TranscriptEvent(
714+
view=["system", "combined"],
715+
actor="system",
716+
edit=AddMessageEdit(
717+
message=TranscriptMessage(
718+
role="system",
719+
content=f"[AUDITOR INPUT REFUSED: {exc}]",
720+
),
721+
),
722+
))
723+
stop_reason = "auditor_input_refused"
724+
break
725+
except (LLMAuthError, LLMRateLimitError, LLMProviderError):
726+
# Auth/rate-limit/provider-5xx errors are global pipeline
727+
# problems, not seed-specific. Propagate so the runner can
728+
# surface a clean message and fail the stage fast.
707729
raise
708730
except Exception as exc:
709731
last_error = str(exc)

tests/test_rollout_stage.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,6 +1410,119 @@ async def fake_close(self_inner):
14101410
f"{completed_id} should have completed cleanly",
14111411
)
14121412

1413+
async def test_run_rollout_isolates_auditor_input_refusal_to_one_seed(self) -> None:
1414+
"""One auditor_input_refused seed must not abort the rest of the batch.
1415+
1416+
Mirrors the May 12 PR #44 mix-1k failure: at scenario 853 of 1000
1417+
the auditor's adversarial prompt tripped Azure Prompt Shields'
1418+
jailbreak detector (the default auditor system prompt is
1419+
jailbreak-shaped by design - Aaron flagged this in the May 12
1420+
standup and worked around it in PR #45 with a tame variant). The
1421+
stage aborted because the auditor catch in _run_auditor_target_loop
1422+
re-raised LLMInputError. The fix routes auditor-side input errors
1423+
into a recorded transcript event with stop_reason=auditor_input_refused.
1424+
"""
1425+
seed_rows = [
1426+
{
1427+
"kind": "scenario",
1428+
"seed": {
1429+
"title": f"Title {i}",
1430+
"description": f"scenario seed {i}",
1431+
},
1432+
}
1433+
for i in range(5)
1434+
]
1435+
auditor_calls: list[str] = []
1436+
1437+
async def fake_generate(model, messages, options):
1438+
del options
1439+
# Identify which seed we're auditing by inspecting the auditor
1440+
# system prompt, which embeds the seed description verbatim.
1441+
description_marker = ""
1442+
for msg in messages:
1443+
content = msg.content if hasattr(msg, "content") else msg.get("content", "")
1444+
if "scenario seed" in content:
1445+
description_marker = content
1446+
break
1447+
auditor_calls.append(description_marker[:120])
1448+
if "scenario seed 2" in description_marker:
1449+
raise LLMInputError(
1450+
"Bad request: AzureException BadRequestError - Invalid "
1451+
"prompt: your prompt was flagged as potentially "
1452+
"violating our usage policy (Prompt Shields jailbreak)"
1453+
)
1454+
return ModelResponse(text="Where should I go?", model=str(model))
1455+
1456+
class FakeHostedSession:
1457+
runtime_mode = "hosted"
1458+
1459+
async def open(self) -> None:
1460+
return None
1461+
1462+
async def close(self) -> None:
1463+
return None
1464+
1465+
async def run_turn(self, messages):
1466+
user_text = ""
1467+
for msg in reversed(messages):
1468+
if msg.role == "user":
1469+
user_text = msg.text
1470+
break
1471+
return TurnResult(
1472+
text="OK.",
1473+
state_messages=list(messages) + [Message(role="assistant", content="OK.")],
1474+
interaction_messages=[
1475+
{"role": "user", "content": user_text},
1476+
{"role": "assistant", "content": "OK."},
1477+
],
1478+
raw={"response": {"content": "OK."}},
1479+
)
1480+
1481+
with TemporaryDirectory() as tmp_dir:
1482+
tmp_path = Path(tmp_dir)
1483+
seed_path = tmp_path / "seeds.jsonl"
1484+
out_dir = tmp_path / "run"
1485+
seed_path.write_text(
1486+
"\n".join(json.dumps(row) for row in seed_rows) + "\n",
1487+
encoding="utf-8",
1488+
)
1489+
1490+
with (
1491+
patch("p2m.stages.rollout.generate", new=fake_generate),
1492+
patch("p2m.stages.rollout._build_target_session", return_value=FakeHostedSession()),
1493+
):
1494+
result = await run_rollout(
1495+
seed_path=str(seed_path),
1496+
target=TargetConfig(model="azure/gpt-5.4-mini"),
1497+
evaluation=EvaluationConfig(
1498+
judge=JudgeConfig(model="azure/gpt-5.4"),
1499+
auditor=AuditorConfig(model="azure/gpt-5.4-mini"),
1500+
rollout=RolloutConfig(max_turns=1, concurrency=1),
1501+
),
1502+
save_dir=str(out_dir),
1503+
run_id="run-auditor-refusal",
1504+
)
1505+
1506+
transcript_rows = [
1507+
json.loads(line)
1508+
for line in (out_dir / "transcripts.jsonl").read_text(encoding="utf-8").splitlines()
1509+
]
1510+
1511+
# All 5 scenarios reached the auditor (no fail-fast on seed 3).
1512+
self.assertEqual(len(auditor_calls), 5)
1513+
self.assertEqual(len(transcript_rows), 5)
1514+
self.assertEqual(result["new_count"], 5)
1515+
1516+
by_seed = {row["seed_id"]: row for row in transcript_rows}
1517+
refused = by_seed["seed_000003"]
1518+
self.assertEqual(refused["stop_reason"], "auditor_input_refused")
1519+
refusal_events = [
1520+
event for event in refused["events"]
1521+
if event["edit"]["type"] == "add_message"
1522+
and "[AUDITOR INPUT REFUSED:" in event["edit"]["message"].get("content", "")
1523+
]
1524+
self.assertEqual(len(refusal_events), 1)
1525+
14131526
async def test_run_rollout_still_fails_fast_on_provider_5xx(self) -> None:
14141527
"""LLMProviderError (Azure 5xx) is global, not seed-specific.
14151528

0 commit comments

Comments
 (0)