diff --git a/headroom/proxy/output_steering.py b/headroom/proxy/output_steering.py index 2d86a86bc..7a39dae09 100644 --- a/headroom/proxy/output_steering.py +++ b/headroom/proxy/output_steering.py @@ -36,8 +36,13 @@ def apply_verbosity_steering(body: dict[str, Any], level: int) -> bool: return True if isinstance(system, list): for block in system: - if isinstance(block, dict) and block.get("text", "").startswith(_STEERING_SENTINEL): - if block["text"] == text: + # Guard the text is a string before ``startswith``: a malformed + # client block (``{"type": "text", "text": null}``) would otherwise + # raise ``AttributeError`` here and 500 the request. The OpenAI chat + # sibling below already guards this exact case. + block_text = block.get("text") if isinstance(block, dict) else None + if isinstance(block_text, str) and block_text.startswith(_STEERING_SENTINEL): + if block_text == text: return False block["text"] = text return True diff --git a/tests/test_output_steering.py b/tests/test_output_steering.py index 8b94f66c8..39a88a6b8 100644 --- a/tests/test_output_steering.py +++ b/tests/test_output_steering.py @@ -35,6 +35,23 @@ def test_anthropic_steering_preserves_cached_prefix_block() -> None: assert body["system"][1] == {"type": "text", "text": steering_text(2)} +def test_anthropic_steering_tolerates_non_string_system_block_text() -> None: + # A malformed client block ({"type": "text", "text": null}) must not crash + # `.startswith` and 500 the request; steering is still appended. The OpenAI + # chat sibling already guards this exact case. + body = { + "system": [ + {"type": "text", "text": None}, + {"type": "text", "text": "Real system prompt."}, + ] + } + + assert apply_verbosity_steering(body, 2) is True + # The malformed block is left as-is and a steering block is appended. + assert body["system"][0] == {"type": "text", "text": None} + assert body["system"][-1] == {"type": "text", "text": steering_text(2)} + + def test_openai_responses_steering_is_idempotent() -> None: body = {"instructions": "System."}