From 7d99e8de19e2efd4acd89451d541b90fdad9d227 Mon Sep 17 00:00:00 2001 From: michael-ciridae Date: Thu, 14 May 2026 09:30:29 -0400 Subject: [PATCH] fix: guard null token usage fields in OpenAI converter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `extract_openai_usage_from_response` crashes with TypeError when the OpenAI API response includes null values for `cached_tokens` or `reasoning_tokens` in token detail objects. The `hasattr()` checks pass because the attributes exist — they're just None. This is common with OpenAI-compatible proxies/gateways that include the detail objects but set individual fields to null when not applicable. Add None checks before `> 0` comparisons and before assigning to the usage dict in streaming paths. Fixes #574 --- posthog/ai/openai/openai_converter.py | 28 +++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/posthog/ai/openai/openai_converter.py b/posthog/ai/openai/openai_converter.py index 5cb1a148..95090723 100644 --- a/posthog/ai/openai/openai_converter.py +++ b/posthog/ai/openai/openai_converter.py @@ -432,9 +432,9 @@ def extract_openai_usage_from_response(response: Any) -> TokenUsage: output_tokens=output_tokens, ) - if cached_tokens > 0: + if cached_tokens is not None and cached_tokens > 0: result["cache_read_input_tokens"] = cached_tokens - if reasoning_tokens > 0: + if reasoning_tokens is not None and reasoning_tokens > 0: result["reasoning_tokens"] = reasoning_tokens web_search_count = extract_openai_web_search_count(response) @@ -488,17 +488,17 @@ def extract_openai_usage_from_chunk( if hasattr(chunk.usage, "prompt_tokens_details") and hasattr( chunk.usage.prompt_tokens_details, "cached_tokens" ): - usage["cache_read_input_tokens"] = ( - chunk.usage.prompt_tokens_details.cached_tokens - ) + cached = chunk.usage.prompt_tokens_details.cached_tokens + if cached is not None: + usage["cache_read_input_tokens"] = cached # Handle reasoning tokens if hasattr(chunk.usage, "completion_tokens_details") and hasattr( chunk.usage.completion_tokens_details, "reasoning_tokens" ): - usage["reasoning_tokens"] = ( - chunk.usage.completion_tokens_details.reasoning_tokens - ) + reasoning = chunk.usage.completion_tokens_details.reasoning_tokens + if reasoning is not None: + usage["reasoning_tokens"] = reasoning # Capture raw usage metadata for backend processing # Serialize to dict here in the converter (not in utils) @@ -522,17 +522,17 @@ def extract_openai_usage_from_chunk( if hasattr(response_usage, "input_tokens_details") and hasattr( response_usage.input_tokens_details, "cached_tokens" ): - usage["cache_read_input_tokens"] = ( - response_usage.input_tokens_details.cached_tokens - ) + cached = response_usage.input_tokens_details.cached_tokens + if cached is not None: + usage["cache_read_input_tokens"] = cached # Handle reasoning tokens if hasattr(response_usage, "output_tokens_details") and hasattr( response_usage.output_tokens_details, "reasoning_tokens" ): - usage["reasoning_tokens"] = ( - response_usage.output_tokens_details.reasoning_tokens - ) + reasoning = response_usage.output_tokens_details.reasoning_tokens + if reasoning is not None: + usage["reasoning_tokens"] = reasoning # Extract web search count from the complete response if hasattr(chunk, "response"):