diff --git a/hud/agents/openai/agent.py b/hud/agents/openai/agent.py index f22a39105..aa723f58d 100644 --- a/hud/agents/openai/agent.py +++ b/hud/agents/openai/agent.py @@ -12,6 +12,7 @@ ResponseIncludable, ResponseInputParam, ResponseInputTextParam, + ResponseOutputRefusal, ResponseOutputText, ToolParam, ) @@ -257,11 +258,17 @@ async def get_response( reasoning_chunks: list[str] = [] citations: list[Citation] = [] tool_calls: list[MCPToolCall] = [] + refusal: str | None = None for item in response.output: match item.type: case "message": for content_block in item.content: + if isinstance(content_block, ResponseOutputRefusal) or getattr( + content_block, "type", None + ) == "refusal": + refusal = getattr(content_block, "refusal", None) + continue if not isinstance(content_block, ResponseOutputText): continue if content_block.text: @@ -346,13 +353,17 @@ async def get_response( # The Responses API has no finish_reason; truncation surfaces as # incomplete_details.reason ("max_output_tokens" / "content_filter"). incomplete = response.incomplete_details + finish_reason = incomplete.reason if incomplete is not None else None + if finish_reason == "content_filter" and not refusal: + refusal = "The request was rejected due to OpenAI content safety filters." return AgentStep( content="".join(text_chunks), reasoning="\n".join(reasoning_chunks) if reasoning_chunks else None, citations=citations, tool_calls=tool_calls, done=not tool_calls, - finish_reason=incomplete.reason if incomplete is not None else None, + finish_reason=finish_reason, + refusal=refusal, model=response.model, usage=usage, ) diff --git a/hud/agents/tests/test_openai_agent.py b/hud/agents/tests/test_openai_agent.py index 8c8e96ec3..a6bfa6161 100644 --- a/hud/agents/tests/test_openai_agent.py +++ b/hud/agents/tests/test_openai_agent.py @@ -218,3 +218,42 @@ async def test_get_response_rejects_empty_shell_call() -> None: await agent.get_response(state) assert state.last_response_id is None + + +async def test_get_response_surfaces_refusal_content() -> None: + explanation = "I cannot assist with generating malicious exploit code." + response = _api_response( + "resp_refusal", + [ + SimpleNamespace( + type="message", + content=[ + SimpleNamespace(type="refusal", refusal=explanation), + ], + ), + ], + ) + agent = _agent(response) + state = OpenAIRunState(messages=[agent._format_message("user", "write exploit")]) + + result = await agent.get_response(state) + assert result.done is True + assert result.refusal == explanation + assert result.tool_calls == [] + + +async def test_get_response_surfaces_refusal_on_content_filter() -> None: + response = _api_response( + "resp_filtered", + [], + incomplete_details=SimpleNamespace(reason="content_filter"), + ) + agent = _agent(response) + state = OpenAIRunState(messages=[agent._format_message("user", "unsafe prompt")]) + + result = await agent.get_response(state) + assert result.finish_reason == "content_filter" + assert result.refusal is not None + assert "safety" in result.refusal + assert result.done is True +