mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
fix(proxy/output-shaping): tolerate a non-string system block text in steering (#2435)
## Description
`apply_verbosity_steering` (the Anthropic output-shaping path) scans the
`system` block list to find and update an existing steering block:
```python
if isinstance(system, list):
for block in system:
if isinstance(block, dict) and block.get("text", "").startswith(_STEERING_SENTINEL):
```
`.get("text", "")` only substitutes the default when the key is
**absent**. A malformed client block with a null text (`{"type": "text",
"text": null}`) returns `None`, so `None.startswith(...)` raises
`AttributeError`. In the output-shaping treatment arm that call runs
inside `shape_request`, which is not individually guarded, so the
exception propagates and 502s the request.
The OpenAI chat sibling in the same module already defends against this
exact case (`isinstance(part.get("text"), str)`), so the Anthropic path
is the inconsistent one.
## Fix
Guard that the block text is a string before `startswith`, mirroring the
OpenAI sibling. Well-formed bodies are unchanged: the steering block is
still replaced idempotently when a level changes, or appended when
absent. The malformed block is left untouched.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- `headroom/proxy/output_steering.py`: string-guard the system block
text before `startswith` in `apply_verbosity_steering`.
- `tests/test_output_steering.py`: regression asserting a `system` list
containing a `{"text": null}` block does not crash and still appends the
steering block.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [ ] Manual testing performed
### Test Output
```text
$ python -m pytest tests/test_output_steering.py -q
9 passed
# with the fix reverted, the new test fails (AttributeError on None.startswith):
$ git stash push -- headroom/proxy/output_steering.py
$ python -m pytest "tests/test_output_steering.py::test_anthropic_steering_tolerates_non_string_system_block_text" -q
1 failed
$ uvx ruff@0.15.17 check headroom/proxy/output_steering.py tests/test_output_steering.py
All checks passed!
$ uvx mypy@1.20.2 --ignore-missing-imports headroom/proxy/output_steering.py
Success: no issues found in 1 source file
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.12, project venv (`uv sync --extra
proxy`), `uvx ruff@0.15.17` / `uvx mypy@1.20.2`, pytest in the venv.
- Exact command / steps: called the real `apply_verbosity_steering` with
`system=[{"type":"text","text":None},{"type":"text","text":"Real system
prompt."}]`; also confirmed the OpenAI sibling
`apply_openai_chat_verbosity_steering` handles the same shape.
- Observed result: pre-fix the Anthropic call raised `AttributeError:
'NoneType' object has no attribute 'startswith'` while the OpenAI
sibling returned True; post-fix the Anthropic call returns True, leaves
the malformed block as-is, appends the steering block, and stays
idempotent on a repeat. Ran against the actual module.
- Not tested: a live client that sends a null system block text end to
end.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
This commit is contained in:
parent
77b26c093c
commit
3e976712e7
2 changed files with 24 additions and 2 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue