Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .sampo/changesets/langchain-http-status-int.md
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 6 additions & 2 deletions posthog/ai/langchain/callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
61 changes: 61 additions & 0 deletions posthog/test/ai/langchain/test_callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading