From 4de3ca7092dcd8148085a1020dae2d612f9317db Mon Sep 17 00:00:00 2001 From: Alex Castillo Date: Wed, 21 Oct 2026 12:43:59 -0400 Subject: [PATCH] fix(ai): record an integer $ai_http_status in the LangChain callback handler Co-Authored-By: Claude Sonnet 5.5 --- .../changesets/langchain-http-status-int.md | 5 ++ posthog/ai/langchain/callbacks.py | 8 ++- posthog/test/ai/langchain/test_callbacks.py | 61 +++++++++++++++++++ 3 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 .sampo/changesets/langchain-http-status-int.md diff --git a/.sampo/changesets/langchain-http-status-int.md b/.sampo/changesets/langchain-http-status-int.md new file mode 100644 index 000000000..37e2f623c --- /dev/null +++ b/.sampo/changesets/langchain-http-status-int.md @@ -0,0 +1,5 @@ +--- +pypi/posthog: patch +--- + +The LangChain callback handler now records `$ai_http_status` as an integer for failed generations. An error that carries a string `code` and no status code (such as OpenAI's `rate_limit_exceeded`) gave a string status; it now gives `0`, as an error without a status already did. diff --git a/posthog/ai/langchain/callbacks.py b/posthog/ai/langchain/callbacks.py index 3b9e188b0..5f996fb6c 100644 --- a/posthog/ai/langchain/callbacks.py +++ b/posthog/ai/langchain/callbacks.py @@ -1036,8 +1036,12 @@ def _get_http_status(error: BaseException) -> int: # OpenAI: https://github.com/openai/openai-python/blob/main/src/openai/_exceptions.py # Anthropic: https://github.com/anthropics/anthropic-sdk-python/blob/main/src/anthropic/_exceptions.py # Google: https://github.com/googleapis/python-api-core/blob/main/google/api_core/exceptions.py - status_code = getattr(error, "status_code", getattr(error, "code", 0)) - return status_code + # `code` is a string on some errors (OpenAI's "rate_limit_exceeded"), so only ints count + for attribute in ("status_code", "code"): + value = getattr(error, attribute, None) + if isinstance(value, int) and not isinstance(value, bool): + return value + return 0 def _get_langchain_run_name( diff --git a/posthog/test/ai/langchain/test_callbacks.py b/posthog/test/ai/langchain/test_callbacks.py index 2556294ae..b33e154fc 100644 --- a/posthog/test/ai/langchain/test_callbacks.py +++ b/posthog/test/ai/langchain/test_callbacks.py @@ -2953,3 +2953,64 @@ def test_stop_reason_resolution( props = mock_client.capture.call_args.kwargs["properties"] assert props.get("$ai_stop_reason") == expected + + +class TestGetHttpStatus: + def test_uses_the_status_code_of_an_http_error(self): + from posthog.ai.langchain.callbacks import _get_http_status + + error = Exception("rate limited") + error.status_code = 429 # type: ignore[attr-defined] + + assert _get_http_status(error) == 429 + + def test_uses_an_integer_code_when_there_is_no_status_code(self): + from posthog.ai.langchain.callbacks import _get_http_status + + error = Exception("quota") + error.code = 503 # type: ignore[attr-defined] + + assert _get_http_status(error) == 503 + + def test_ignores_a_string_code(self): + """OpenAI's APIError carries code="rate_limit_exceeded" and no status code.""" + from posthog.ai.langchain.callbacks import _get_http_status + + error = Exception("Rate limit reached") + error.code = "rate_limit_exceeded" # type: ignore[attr-defined] + + assert _get_http_status(error) == 0 + + def test_falls_back_to_zero_without_a_status(self): + from posthog.ai.langchain.callbacks import _get_http_status + + assert _get_http_status(Exception("boom")) == 0 + + def test_an_error_event_records_an_integer_status(self): + import httpx + import openai + from unittest.mock import MagicMock + from uuid import uuid4 + + from posthog.ai.langchain import CallbackHandler + + client = MagicMock() + client.privacy_mode = False + callback = CallbackHandler(client) + run_id = uuid4() + callback._set_llm_metadata( + serialized={}, + run_id=run_id, + messages=[{"role": "user", "content": "test"}], + metadata={"ls_provider": "openai", "ls_model_name": "gpt-5.6-sol"}, + ) + error = openai.APIError( + "Rate limit reached", + request=httpx.Request("POST", "https://api.openai.com/v1/chat/completions"), + body={"code": "rate_limit_exceeded", "message": "Rate limit reached"}, + ) + + callback._pop_run_and_capture_generation(run_id, None, error) + + properties = client.capture.call_args_list[0][1]["properties"] + assert properties["$ai_http_status"] == 0