diff --git a/tests/agent/transports/test_chat_completions.py b/tests/agent/transports/test_chat_completions.py index 8dbc5b885fa..588b019d7b2 100644 --- a/tests/agent/transports/test_chat_completions.py +++ b/tests/agent/transports/test_chat_completions.py @@ -900,6 +900,28 @@ class TestChatCompletionsNormalize: assert nr.content == "declined" assert nr.provider_data == {"refusal": "declined"} + def test_explicit_content_filter_finish_reason_passes_through(self, transport): + """OpenRouter (and other OpenAI-compatible providers) surface an + upstream Claude / moderation refusal as ``finish_reason="content_filter"`` + — often with empty content and no ``message.refusal`` field. The + transport must pass that finish reason straight through so the loop's + content_filter refusal handler fires; no ``message.refusal`` required. + This is the OpenRouter coverage path (OpenRouter uses the default + chat_completions transport).""" + r = SimpleNamespace( + choices=[SimpleNamespace( + message=SimpleNamespace( + content=None, tool_calls=None, reasoning_content=None, + refusal=None, + ), + finish_reason="content_filter", + )], + usage=None, + ) + nr = transport.normalize_response(r) + assert nr.finish_reason == "content_filter" + assert nr.content is None + def test_refusal_does_not_clobber_existing_content(self, transport): """If the model emitted partial text *and* a refusal, keep the visible text as content but still flag the refusal via content_filter."""