"""Message preprocessors for SWE-bench agent rollouts.
Preprocessors transform messages after Anthropic-to-OpenAI translation and
before they reach AReaL's OpenAI client. They remove volatile Claude Code
metadata that otherwise breaks prefix-based parent matching in concat export.
"""
import json
import re
from typing import Protocol, runtime_checkable
@runtime_checkable
class MessagePreprocessor(Protocol):
"""Protocol for message preprocessors."""
def __call__(self, messages: list[dict]) -> list[dict]: ...
class StripAnthropicBillingHeader:
"""Remove per-request Anthropic billing header lines from system prompts."""
_PATTERN = re.compile(r"^x-anthropic-billing-header:[^\n]*\n?", re.MULTILINE)
def __call__(self, messages: list[dict]) -> list[dict]:
if not messages:
return messages
first = messages[0]
if first.get("role") == "system" and isinstance(first.get("content"), str):
first["content"] = self._PATTERN.sub("", first["content"])
return messages
class NormalizeSystemReminder:
"""Remove volatile ``currentDate`` lines from system reminders."""
_PATTERN = re.compile(
r"# currentDate\nToday's date is \d{4}-\d{2}-\d{2}\.\n?",
)
def __call__(self, messages: list[dict]) -> list[dict]:
for msg in messages:
content = msg.get("content")
if isinstance(content, str) and "currentDate" in content:
msg["content"] = self._PATTERN.sub("", content)
return messages
class StripAllSystemReminders:
"""Remove all ``<system-reminder>...</system-reminder>`` blocks."""
_PATTERN = re.compile(r"<system-reminder>[\s\S]*?</system-reminder>")
def __call__(self, messages: list[dict]) -> list[dict]:
for msg in messages:
content = msg.get("content")
if isinstance(content, str) and "<system-reminder>" in content:
msg["content"] = self._PATTERN.sub("", content)
return messages
class StripAnthropicCacheFields:
"""Strip Anthropic-specific fields that are not preserved in stored output."""
def __call__(self, messages: list[dict]) -> list[dict]:
for msg in messages:
msg.pop("cache_control", None)
msg.pop("thinking_blocks", None)
if "tool_calls" in msg:
for tc in msg["tool_calls"]:
if isinstance(tc, dict):
tc.pop("cache_control", None)
tc.pop("provider_specific_fields", None)
fn = tc.get("function")
if isinstance(fn, dict):
fn.pop("cache_control", None)
return messages
class NormalizeToolCallArguments:
"""Normalize tool_call arguments for deterministic comparison."""
_TOOL_ARG_DEFAULTS: dict[tuple[str, str], object] = {
("Edit", "replace_all"): False,
}
@classmethod
def _normalize_tool_arguments(
cls,
tool_name: str | None,
args_dict: dict,
) -> dict:
if tool_name is not None:
for (tn, field), default in cls._TOOL_ARG_DEFAULTS.items():
if tn == tool_name and args_dict.get(field) == default:
args_dict.pop(field, None)
return args_dict
def __call__(self, messages: list[dict]) -> list[dict]:
for msg in messages:
if "tool_calls" in msg:
for tc in msg["tool_calls"]:
if isinstance(tc, dict):
fn = tc.get("function")
if isinstance(fn, dict):
tool_name = fn.get("name")
args = fn.get("arguments")
if isinstance(args, str):
try:
parsed = json.loads(args)
parsed = self._normalize_tool_arguments(
tool_name,
parsed,
)
fn["arguments"] = json.dumps(
parsed,
sort_keys=True,
ensure_ascii=False,
)
except (json.JSONDecodeError, TypeError):
pass
if msg.get("role") == "assistant" and "content" not in msg:
msg["content"] = ""
return messages