mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
fix(streaming): preserve server_tool_use sse blocks (#1826)
## Description Buffered Anthropic responses currently fail late when they contain a `server_tool_use` block. `_response_to_sse()` raises after the upstream response is already fully buffered, so callers wait through the whole generation and then receive a 502 instead of the completed response. This adds explicit `server_tool_use` support in the buffered-to-SSE replay path, while keeping the existing rejection for truly unsupported Anthropic block types. Closes #1806. ## 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 - Added a `server_tool_use` branch in the Anthropic buffered-response SSE conversion loop. - Emitted the full `server_tool_use` block in `content_block_start` instead of raising during replay. - Added a focused regression that proves buffered `server_tool_use` blocks convert to SSE and round-trip with the block type intact. - Kept the existing reject-unknown test so unsupported future block types still fail loudly. - Applied the pinned Ruff formatter to three pre-existing files on the current base so the repo-wide lint job passes unchanged semantics. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_sse_thinking_blocks.py -q`) - [x] Linting passes (`uv run ruff check headroom/proxy/handlers/streaming.py tests/test_sse_thinking_blocks.py`) - [ ] Type checking passes (`uv run mypy headroom`) - [x] New tests added for new functionality when applicable - [ ] Manual testing performed ### Test Output ```text uv run pytest tests/test_sse_thinking_blocks.py -q 7 passed, 1 warning in 0.19s uv run ruff check headroom/proxy/handlers/streaming.py tests/test_sse_thinking_blocks.py All checks passed! uv run ruff check . All checks passed! uv run ruff format --check . 1046 files already formatted ``` ## Real Behavior Proof - Environment: Windows, project `uv` environment, focused handler-level regression. - Exact command / steps: run `tests/test_sse_thinking_blocks.py` on `origin/main` with the new `server_tool_use` regression present, then rerun the same file on this branch. - Observed result: base raises `Unsupported Anthropic content block type for SSE conversion: 'server_tool_use'`; head passes the focused file and preserves the `server_tool_use` block type through buffered SSE reconstruction, while the existing reject-unknown test still passes. - Not tested: live proxy traffic against Anthropic server-side tools. ## 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 - [x] 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 - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes No changelog entry is needed for this internal handler fix. The issue suggested a broader accept-all fallback, but this PR stays intentionally narrower: it handles the proven `server_tool_use` case and keeps the existing rejection for truly unsupported Anthropic block types. The extra formatting-only diff comes from the current base failing the pinned full-repo Ruff format check.
This commit is contained in:
parent
53a465b121
commit
4ac54934cb
2 changed files with 36 additions and 2 deletions
|
|
@ -544,10 +544,15 @@ class StreamingMixin:
|
|||
"data": block.get("data", ""),
|
||||
},
|
||||
}
|
||||
elif block.get("type") == "server_tool_use":
|
||||
block_start = {
|
||||
"type": "content_block_start",
|
||||
"index": idx,
|
||||
"content_block": block,
|
||||
}
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported Anthropic content block type for SSE conversion: "
|
||||
f"{block.get('type')!r}"
|
||||
f"Unsupported Anthropic content block type for SSE conversion: {block.get('type')!r}"
|
||||
)
|
||||
|
||||
events.append(
|
||||
|
|
|
|||
|
|
@ -186,6 +186,35 @@ def test_redacted_thinking_data_preserved() -> None:
|
|||
assert block["data"] == redacted_blob
|
||||
|
||||
|
||||
def test_response_to_sse_preserves_server_tool_use_blocks() -> None:
|
||||
parser = _Parser()
|
||||
response = {
|
||||
"id": "msg_1",
|
||||
"model": "claude-opus-4",
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "server_tool_use",
|
||||
"id": "srv_1",
|
||||
"name": "web_search",
|
||||
"input": {"query": "headroom"},
|
||||
}
|
||||
],
|
||||
"stop_reason": "end_turn",
|
||||
"usage": {"output_tokens": 1},
|
||||
}
|
||||
|
||||
sse_events = b"".join(parser._response_to_sse(response, "anthropic")).decode("utf-8")
|
||||
|
||||
assert '"type": "content_block_start"' in sse_events
|
||||
assert '"type": "server_tool_use"' in sse_events
|
||||
assert '"name": "web_search"' in sse_events
|
||||
|
||||
round_tripped = parser._parse_sse_to_response(sse_events, "anthropic")
|
||||
assert round_tripped is not None
|
||||
assert round_tripped["content"][0]["type"] == "server_tool_use"
|
||||
|
||||
|
||||
def test_response_to_sse_preserves_thinking_redacted_and_citations() -> None:
|
||||
parser = _Parser()
|
||||
redacted_blob = "ENC:" + ("y" * 200)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue