This commit is contained in:
devin-ai-integration[bot] 2026-09-23 14:44:34 +00:00 • committed by GitHub
commit 94d332502b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 50 additions and 10 deletions

View file

@ -88,17 +88,22 @@ def _flatten_messages_for_compression(messages: list[dict[str, object]]) -> list
at its part), so merging text across a non-text part would move a later
breakpoint to the other side of it. Rows with non-text parts are sent
unchanged and pass through the service untouched.
An explicit ``"tool_calls": null`` (what OpenAI SDK message objects
serialize to) is dropped: the service iterates the field and older
releases fail on ``None``.
"""
flattened: Final[list[dict[str, object]]] = []
for msg in messages:
content = msg.get("content")
if is_all_text_parts(content):
text = content_to_text(content)
if text:
flattened.append({**msg, "content": text})
continue
flattened.append(msg)
return flattened
return [_compression_row(msg) for msg in messages]
def _compression_row(message: Mapping[str, object]) -> dict[str, object]:
content: Final = message.get("content")
text: Final = content_to_text(content) if is_all_text_parts(content) else ""
return {
key: text if key == "content" and text else value
for key, value in message.items()
if key != "tool_calls" or value is not None
}
def _restore_content_shapes(

View file

@ -2008,6 +2008,41 @@ async def test_apply_guardrail_sends_textless_parts_rows_unflattened(
assert wire_messages[1]["content"] == "D" * 5000
@pytest.mark.asyncio
async def test_apply_guardrail_drops_null_tool_calls_but_keeps_real_ones(
guardrail: HeadroomGuardrail,
):
real_tool_calls = [{"id": "call_1", "type": "function", "function": {"name": "lookup", "arguments": "{}"}}]
history = [
{"role": "user", "content": "E" * 5000},
{"role": "assistant", "content": None, "tool_calls": real_tool_calls},
{"role": "tool", "tool_call_id": "call_1", "content": "result"},
{"role": "assistant", "content": "summary", "tool_calls": None, "function_call": None},
]
inputs = GenericGuardrailAPIInputs(
texts=["E" * 5000],
structured_messages=json.loads(json.dumps(history))
+ [{"role": "assistant", "content": "last turn"}, {"role": "user", "content": "and now?"}],
)
mock_response = _make_compress_response(json.loads(json.dumps(history)))
with patch.object(
guardrail.async_handler,
"post",
new_callable=AsyncMock,
return_value=mock_response,
) as mock_post:
await guardrail.apply_guardrail(
inputs=inputs,
request_data={"model": "claude-fable-5"},
input_type="request",
)
wire_messages = mock_post.call_args.kwargs["json"]["messages"]
assert wire_messages[1]["tool_calls"] == real_tool_calls
assert wire_messages[3] == {"role": "assistant", "content": "summary", "function_call": None}
@pytest.mark.asyncio
async def test_fail_open_returns_original_parts_shapes():
guardrail = _make_guardrail(unreachable_fallback="fail_open")