Repository navigation
Description
Description
What happened?
When a turn is stopped by an Azure OpenAI content filter, the hosted agent's
/responses HTTP output reports a completely successful turn. An HTTP
caller has no way to tell a filtered turn from a normal one.
The upstream chat completion clearly carries the signal. Captured from the
agent's own telemetry for the request in question:
{"index": 0, "finish_reason": "content_filter",
"message": {"content": "I'm sorry, but I cannot assist with that request."}}But the /responses payload returned to the caller for that same turn is:
HTTP 200
{
"status": "completed",
"error": null,
"id": "caresp_...",
"output": [
{"type": "message", "role": "assistant", "status": "completed",
"content": [{"type": "output_text",
"text": "I'm sorry, but I cannot assist with that request."}]}
]
}
Probing the full payload for any trace of the filter finds none:
| probe | present |
|---|---|
finish_reason |
❌ |
content_filter |
❌ |
incomplete |
❌ |
refusal |
❌ |
Top-level keys are agent_reference, agent_session_id, background, completed_at, created_at, id, model, object, output, parallel_tool_calls, response_id, status, usage — the finish_reason: "content_filter" from the
underlying completion is dropped, and both the response and the output item
say "completed".
What did you expect to happen?
The /responses output should preserve the fact that the turn was
content-filtered — e.g. status: "incomplete" with an
incomplete_details.reason, or a finish_reason on the output item, or an
error — so a caller can react to it.
Why this matters in practice
We run a hosted agent behind a small HTTP relay. When a user's message trips
the filter, the relay hands them back the raw
"I'm sorry, but I cannot assist with that request." string, which is
out-of-character for the agent's persona and unexplained — the user re-sent the
identical message, which deterministically tripped the same filter again.
Because nothing in the response distinguishes this case, the only workaround
available to us is matching on the refusal text, which is obviously
brittle: it breaks on wording changes and localisation, and risks false
positives on legitimate short replies.
Note this is not #7025 (ChatResponse.finish_reason always None on
Responses-based clients, closed 2026-07-17). This report is about the
hosting/server side — what /responses emits to an HTTP caller. Related,
but a different layer.
It is also distinct from #8385 (OpenAIContentFilterException raising
ValueError on the ContentFiltered inner code): on this hosted path no
exception surfaces at all, because the service returns a normal completion with
finish_reason: content_filter rather than an error.
Versions
agent-framework-core1.18.0agent-framework-foundry1.13.0agent-framework-foundry-hosting1.0.0b260910agent-framework-openai1.14.3azure-ai-agentserver-core2.1.0,-responses2.2.0b1
Agent hosted with ResponsesHostServer(agent, history_source="agent") on
Azure AI Foundry, gpt-5.4, called over the OpenAI-compatible /responses
protocol.
Repro
- Host any agent with
ResponsesHostServeron Foundry. - POST a prompt to its
/responsesendpoint that trips the deployment's
content filter (a violent or sexual plot summary will do — ours was an
ordinary historical-drama recap that scoredhighonsexual). - Observe
HTTP 200,status: "completed", noerror, and the canned
refusal asoutput_text, while the agent's telemetry for the same request
showsfinish_reason: "content_filter".
Metadata
Metadata
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
github-actions commentedon Sep 17, 2026
🤖 Automated triage reproduction notes (agent-authored — trust but verify)
Agent analysis
Repro:
python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py::_handle_inner_agentat lines 848-859 ignoresAgentResponseUpdate.finish_reason, and_handle_responseat lines 695-701 consequently emitsresponse.completed. It triggers when an agent update contains refusal text withfinish_reason="content_filter". Minimal repro: use the existing ASGI harness with a mock agent emitting that update and POST a non-streaming request to/responses; the response returns HTTP 200 withstatus="completed".python/packages/foundry_hosting/tests/test_responses.py::TestNonStreaming::test_content_filter_finish_reason_marks_response_incomplete1.0.0b2609101.0.0b260910anishmehta24 commentedon Sep 17, 2026
I'd like to take this one. Plan: track
AgentResponseUpdate.finish_reasonin the hosted/responsespath (both the plain-agent and workflow branches) and end the response withresponse.incomplete+incomplete_details.reason="content_filter"instead ofresponse.completedwhen a turn is content-filtered, mirroring the OpenAI Responses shape. Tests for streaming and non-streaming. PR shortly.Python: mark hosted /responses incomplete on content_filter and lengt…
Python: mark hosted /responses incomplete on content_filter and lengt…
Python: mark hosted /responses incomplete on content_filter and lengt…