Azure OpenAI streaming — extra prompt_filter_results and content_filter_results chunks
Copy-pasted OpenAI streaming code usually works on Azure. Then one day it emits a chunk that has no choices array — the code crashes and the outage is on you.
Quick fix (TL;DR)
prompt_filter_results and an empty choices array, and (2) per-token content_filter_results attached to delta chunks. Fix by (a) always guarding if not chunk.choices: continue, (b) checking choice.finish_reason == "content_filter" alongside stop/length, and (c) reading the filter results only after the stream completes for full context.Real error messages you'll see
These are the exact strings returned by the Azure OpenAI service and its SDKs when this error occurs. Copy-paste-searching any of them should land on this page.
Traceback (most recent call last):
File "app.py", line 42, in stream_response
delta = chunk.choices[0].delta
IndexError: list index out of rangedata: {"id":"chatcmpl-...","object":"chat.completion.chunk",
"created":1728000000,"model":"gpt-4o-2024-08-06",
"prompt_filter_results":[{"prompt_index":0,"content_filter_results":
{"hate":{"filtered":false,"severity":"safe"},...}}],
"choices":[]}data: {"id":"chatcmpl-...","object":"chat.completion.chunk",
"choices":[{"delta":{},"finish_reason":"content_filter","index":0,
"content_filter_results":{"sexual":{"filtered":true,"severity":"medium"}}}]}
Reference
Chunk shapes you will see on an Azure OpenAI stream
| Chunk type | When it arrives | choices array? | How to handle |
|---|---|---|---|
| Prompt filter results | First chunk of stream (Azure only) | Empty [] | if not chunk.choices: continue |
| Role delta | Early — usually 2nd chunk | One item with role only | Emit nothing to UI, remember the role |
| Content delta | Every subsequent chunk | One item with content | Append to buffer, emit to UI |
| Tool call delta | When model calls a tool | One item with tool_calls delta | Accumulate tool call fragments |
| Content filter mid-stream | Rare — response was filtered while generating | finish_reason=content_filter | Abort UI stream, show filter message |
| Final chunk | Last chunk | finish_reason set | Emit end-of-stream event |
| Usage chunk | After final chunk (when stream_options.include_usage=true) | Empty [], has usage | Record usage for billing |
Azure-specific fields not present on openai.com
| Field | Location | Purpose |
|---|---|---|
prompt_filter_results | Top-level of first chunk | Content filter analysis of the prompt |
content_filter_results | Per choice | Content filter analysis of the completion (or delta) |
prompt_filter_result | On non-stream response object | Per-prompt filter results (index-keyed for multi-prompt requests) |
Root causes, ranked by frequency
Based on developer reports across Azure OpenAI SDK forums, GitHub issues, and Microsoft Q&A during 2025–2026.
- 40%Code assumes
chunk.choices[0]exists. The prompt-filter chunk has an empty choices array. Any code that does not guard first crashes on the very first chunk. - 20%Streaming parser expects only OpenAI-shaped chunks. Third-party libraries pinned to old openai package versions do not know about content_filter chunks; they may skip them, mishandle them, or emit warnings that flood logs.
- 14%Client discards partial output on
content_filterwithout showing the user why. UX regression — user sees a spinner then nothing. - 10%Reasoning models (o1, o3) send no text tokens until reasoning is complete — the stream appears "stuck" for 20-60 seconds. Clients that timeout waiting for first token disconnect.
- 7%Usage chunk arriving after finish_reason. Client closes the stream on finish_reason=stop and misses the usage chunk. Billing / cost telemetry undercounts.
- 5%Tool-call deltas fragmented across chunks. arguments field arrives as many small deltas that must be concatenated — parsing each chunk as a complete tool call fails.
- 3%Content filter severity threshold mismatch. Client treats "low" severity as blocking even though the response was returned; premature abort.
- 1%Server-side buffering by API Management or gateway. Corporate gateways sometimes buffer SSE, defeating streaming — client sees all chunks at once.
Fixes — copy-paste solutions
Write a streaming parser that handles all Azure chunk shapes
Iterate the stream and emit structured events for tokens, tool calls, filter results, and end-of-stream. Guard against empty choices arrays on every chunk. This parser handles both Azure and openai.com correctly.
from typing import Iterator, Dict, Any from openai import AzureOpenAI client = AzureOpenAI(api_key="...", azure_endpoint="...", api_version="2024-10-21") def stream_chat(messages: list, deployment: str = "gpt4o-prod") -> Iterator[Dict[str, Any]]: """Yield structured events from an Azure OpenAI streaming completion. Events: {'type': 'prompt_filter', 'results': {...}} {'type': 'role', 'role': 'assistant'} {'type': 'token', 'content': '...'} {'type': 'tool_call_delta', 'index': 0, 'name': '...', 'arguments_delta': '...'} {'type': 'content_filter', 'category': '...', 'severity': '...'} {'type': 'finish', 'reason': 'stop|length|content_filter|tool_calls'} {'type': 'usage', 'prompt_tokens': int, 'completion_tokens': int, 'total_tokens': int} """ stream = client.chat.completions.create( model=deployment, messages=messages, stream=True, stream_options={"include_usage": True}, ) for chunk in stream: # Azure-specific: prompt filter results with empty choices pfr = getattr(chunk, "prompt_filter_results", None) if pfr: yield {"type": "prompt_filter", "results": pfr} # Usage chunk (empty choices, has usage) if chunk.usage: yield { "type": "usage", "prompt_tokens": chunk.usage.prompt_tokens, "completion_tokens": chunk.usage.completion_tokens, "total_tokens": chunk.usage.total_tokens, } # Guard the empty-choices case — critical for Azure if not chunk.choices: continue choice = chunk.choices[0] delta = choice.delta if delta and delta.role: yield {"type": "role", "role": delta.role} if delta and delta.content: yield {"type": "token", "content": delta.content} if delta and delta.tool_calls: for tc in delta.tool_calls: yield { "type": "tool_call_delta", "index": tc.index, "name": (tc.function.name if tc.function else None), "arguments_delta": (tc.function.arguments if tc.function else None), } # Azure-specific: per-choice content filter results cfr = getattr(choice, "content_filter_results", None) or {} for category, result in cfr.items(): if result.get("filtered"): yield { "type": "content_filter", "category": category, "severity": result.get("severity"), } if choice.finish_reason: yield {"type": "finish", "reason": choice.finish_reason} # Usage for event in stream_chat([{"role": "user", "content": "Hello"}]): if event["type"] == "token": print(event["content"], end="", flush=True) elif event["type"] == "finish": print(f"\n[Finished: {event['reason']}]") elif event["type"] == "usage": print(f"[Tokens: {event['total_tokens']}]")
choices == [].Accumulate tool-call deltas across chunks correctly
Each tool_call_delta contains a fragment of the JSON arguments string. Only the complete concatenation is valid JSON. Buffer per (index) and parse at finish_reason.
import json from typing import List, Dict, Any from azure_stream_parser import stream_chat # from previous fix def stream_with_tool_calls(messages: list, tools: list, deployment: str = "gpt4o-prod"): """Run a stream that may return tool calls; yield tokens and reconstruct tool calls.""" from openai import AzureOpenAI client = AzureOpenAI(api_key="...", azure_endpoint="...", api_version="2024-10-21") stream = client.chat.completions.create( model=deployment, messages=messages, tools=tools, stream=True, ) # Accumulators keyed by tool_call index tool_buffers: Dict[int, Dict[str, Any]] = {} for chunk in stream: if not chunk.choices: continue choice = chunk.choices[0] delta = choice.delta if delta and delta.content: yield {"type": "token", "content": delta.content} if delta and delta.tool_calls: for tc in delta.tool_calls: idx = tc.index buf = tool_buffers.setdefault(idx, {"id": None, "name": None, "arguments": ""}) if tc.id: buf["id"] = tc.id if tc.function: if tc.function.name: buf["name"] = tc.function.name if tc.function.arguments: buf["arguments"] += tc.function.arguments if choice.finish_reason == "tool_calls": # Finalise: parse each buffered arguments JSON for idx in sorted(tool_buffers): buf = tool_buffers[idx] try: args = json.loads(buf["arguments"]) except json.JSONDecodeError as e: # Model produced malformed JSON — surface for handling yield {"type": "tool_call_error", "index": idx, "raw": buf["arguments"], "error": str(e)} continue yield { "type": "tool_call", "index": idx, "id": buf["id"], "name": buf["name"], "arguments": args, }
Handle mid-stream content filter cleanly in the UI
The response streams normally then hits content_filter mid-way. The UI must discard whatever partial content was shown and display a clear filtered-state message rather than an ambiguous half-response.
from azure_stream_parser import stream_chat def handle_stream_for_ui(user_msg: str): """Emit UI events; roll back partial content if content_filter triggers mid-stream.""" buffer = [] ui_state = "streaming" for event in stream_chat([{"role": "user", "content": user_msg}]): if event["type"] == "prompt_filter": # Pre-generation filter — decide whether to proceed triggered = [k for k, v in event["results"][0]["content_filter_results"].items() if v.get("filtered")] if triggered: yield {"ui": "error", "message": f"Content policy: {triggered}"} return elif event["type"] == "token": buffer.append(event["content"]) yield {"ui": "append", "content": event["content"]} elif event["type"] == "content_filter": # Mid-stream filter — roll back what we streamed ui_state = "filtered" yield {"ui": "clear"} yield {"ui": "error", "message": f"The response was filtered ({event['category']}, {event['severity']}). " "Please rephrase your question."} return elif event["type"] == "finish": if event["reason"] == "content_filter" and ui_state == "streaming": # Filter fired at the very end (no per-token filter event) yield {"ui": "clear"} yield {"ui": "error", "message": "Response was filtered — please rephrase."} return yield {"ui": "done", "reason": event["reason"], "full_text": "".join(buffer)}
Prevention checklist
Ship these seven safeguards once and this error stops appearing in your logs.
- Always guard
if not chunk.choices: continueat the top of your stream loop. - Include
stream_options={"include_usage": true}when you need token accounting — it is off by default. - Parse tool call arguments only at
finish_reason=tool_calls, never per-chunk. - Distinguish all four finish_reasons in your UI: stop, length, content_filter, tool_calls.
- Log both
prompt_filter_resultsandcontent_filter_resultsto your observability stack — patterns reveal quality issues. - For reasoning models, set a first-token timeout of 60-90 seconds, not the default 10 — thinking time is long.
- Disable buffering in any gateway between client and Azure OpenAI (API Management, Front Door). SSE requires immediate flush.
Frequently asked questions
usage is always in the response object. On streaming, usage is only sent when you pass stream_options={"include_usage": true} and arrives as an extra chunk after the finish chunk. Many clients close the stream on finish_reason and miss this usage chunk.Related errors & hubs
Get the weekly AI-error digest
New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.