Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion hud/agents/openai/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
ResponseIncludable,
ResponseInputParam,
ResponseInputTextParam,
ResponseOutputRefusal,
ResponseOutputText,
ToolParam,
)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
)
Expand Down
39 changes: 39 additions & 0 deletions hud/agents/tests/test_openai_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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