From 2a25497f417e30c591865ef36fc8b4011b09ea3f Mon Sep 17 00:00:00 2001 From: Jaden Earl Date: Tue, 12 May 2026 16:03:20 -0600 Subject: [PATCH] Handle pydantic url_citation annotations from newer OpenAI SDK The OpenAI SDK now returns url_citation annotations as pydantic Annotation objects instead of dicts. Coerce via model_dump() so the existing dict-based extraction path continues to work for both old and new SDK shapes. Co-Authored-By: Claude Opus 4.7 (1M context) --- forecasting_tools/ai_models/general_llm.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/forecasting_tools/ai_models/general_llm.py b/forecasting_tools/ai_models/general_llm.py index e60783b9..cbe5c140 100644 --- a/forecasting_tools/ai_models/general_llm.py +++ b/forecasting_tools/ai_models/general_llm.py @@ -359,9 +359,10 @@ def _extract_citations( return typeguard.check_type(citations, list[str]) # OpenRouter returns Perplexity citations as url_citation annotations - # rather than in model_extra["citations"]. URLs may be duplicated - # (e.g. once with titles, once without). We deduplicate while - # preserving first-seen order so urls[i] corresponds to [i+1]. + # rather than in model_extra["citations"]. Annotations may arrive as + # dicts (older SDKs) or pydantic objects (newer SDKs). URLs may be + # duplicated (e.g. once with titles, once without). We deduplicate + # while preserving first-seen order so urls[i] corresponds to [i+1]. message = choices[0].message annotations = getattr(message, "annotations", None) if not annotations: @@ -369,6 +370,8 @@ def _extract_citations( seen: set[str] = set() unique_urls: list[str] = [] for annotation in annotations: + if hasattr(annotation, "model_dump"): + annotation = annotation.model_dump() if not isinstance(annotation, dict): continue if annotation.get("type") != "url_citation":