@@ -263,7 +263,7 @@ def extract_openai_web_search_count(response: Any) -> int:
263263 1. Priority 1 (exact count): Check for output[].type == "web_search_call" (Responses API)
264264 2. Priority 2 (binary detection): Check for various web search indicators:
265265 - Root-level citations, search_results, or usage.search_context_size (Perplexity)
266- - Annotations with type "url_citation" in choices/output
266+ - Annotations with type "url_citation" in choices/output (including delta for streaming)
267267
268268 Args:
269269 response: The response from OpenAI API
@@ -275,6 +275,7 @@ def extract_openai_web_search_count(response: Any) -> int:
275275 # Priority 1: Check for exact count in Responses API output
276276 if hasattr (response , "output" ):
277277 web_search_count = 0
278+
278279 for item in response .output :
279280 if hasattr (item , "type" ) and item .type == "web_search_call" :
280281 web_search_count += 1
@@ -289,11 +290,13 @@ def extract_openai_web_search_count(response: Any) -> int:
289290 # Check root-level indicators (Perplexity)
290291 if hasattr (response , "citations" ):
291292 citations = getattr (response , "citations" )
293+
292294 if citations and len (citations ) > 0 :
293295 return 1
294296
295297 if hasattr (response , "search_results" ):
296298 search_results = getattr (response , "search_results" )
299+
297300 if search_results and len (search_results ) > 0 :
298301 return 1
299302
@@ -304,14 +307,36 @@ def extract_openai_web_search_count(response: Any) -> int:
304307 # Check for url_citation annotations in choices (Chat Completions)
305308 if hasattr (response , "choices" ):
306309 for choice in response .choices :
310+ # Check message.annotations (non-streaming or final chunk)
307311 if hasattr (choice , "message" ) and hasattr (choice .message , "annotations" ):
308312 annotations = choice .message .annotations
313+
309314 if annotations :
310315 for annotation in annotations :
311- if (
312- hasattr (annotation , "type" )
313- and annotation .type == "url_citation"
314- ):
316+ # Support both dict and object formats
317+ annotation_type = (
318+ annotation .get ("type" )
319+ if isinstance (annotation , dict )
320+ else getattr (annotation , "type" , None )
321+ )
322+
323+ if annotation_type == "url_citation" :
324+ return 1
325+
326+ # Check delta.annotations (streaming chunks)
327+ if hasattr (choice , "delta" ) and hasattr (choice .delta , "annotations" ):
328+ annotations = choice .delta .annotations
329+
330+ if annotations :
331+ for annotation in annotations :
332+ # Support both dict and object formats
333+ annotation_type = (
334+ annotation .get ("type" )
335+ if isinstance (annotation , dict )
336+ else getattr (annotation , "type" , None )
337+ )
338+
339+ if annotation_type == "url_citation" :
315340 return 1
316341
317342 # Check for url_citation annotations in output (Responses API)
@@ -321,12 +346,17 @@ def extract_openai_web_search_count(response: Any) -> int:
321346 for content_item in item .content :
322347 if hasattr (content_item , "annotations" ):
323348 annotations = content_item .annotations
349+
324350 if annotations :
325351 for annotation in annotations :
326- if (
327- hasattr (annotation , "type" )
328- and annotation .type == "url_citation"
329- ):
352+ # Support both dict and object formats
353+ annotation_type = (
354+ annotation .get ("type" )
355+ if isinstance (annotation , dict )
356+ else getattr (annotation , "type" , None )
357+ )
358+
359+ if annotation_type == "url_citation" :
330360 return 1
331361
332362 return 0
0 commit comments