Skip to content
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@


## 3.20.0 – 2025-03-13

1. Add support for OpenAI Responses API.

## 3.19.2 – 2025-03-11

1. Fix install requirements for analytics package
Expand Down
16 changes: 1 addition & 15 deletions mypy-baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,7 @@ posthog/sentry/posthog_integration.py:0: error: Statement is unreachable [unrea
posthog/ai/utils.py:0: error: Need type annotation for "output" (hint: "output: list[<type>] = ...") [var-annotated]
posthog/ai/utils.py:0: error: Function "builtins.any" is not valid as a type [valid-type]
posthog/ai/utils.py:0: note: Perhaps you meant "typing.Any" instead of "any"?
posthog/ai/utils.py:0: error: Incompatible types in assignment (expression has type "UUID", variable has type "str | None") [assignment]
posthog/ai/utils.py:0: error: Function "builtins.any" is not valid as a type [valid-type]
posthog/ai/utils.py:0: note: Perhaps you meant "typing.Any" instead of "any"?
posthog/ai/utils.py:0: error: Incompatible types in assignment (expression has type "UUID", variable has type "str | None") [assignment]
sentry_django_example/sentry_django_example/settings.py:0: error: Need type annotation for "ALLOWED_HOSTS" (hint: "ALLOWED_HOSTS: list[<type>] = ...") [var-annotated]
sentry_django_example/sentry_django_example/settings.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "None") [assignment]
posthog/ai/openai/openai_async.py:0: error: Incompatible types in assignment (expression has type "UUID", variable has type "str | None") [assignment]
posthog/ai/openai/openai_async.py:0: error: Incompatible types in assignment (expression has type "UUID", variable has type "str | None") [assignment]
posthog/ai/openai/openai_async.py:0: error: Incompatible types in assignment (expression has type "UUID", variable has type "str | None") [assignment]
posthog/ai/openai/openai.py:0: error: Incompatible types in assignment (expression has type "UUID", variable has type "str | None") [assignment]
posthog/ai/openai/openai.py:0: error: Incompatible types in assignment (expression has type "UUID", variable has type "str | None") [assignment]
posthog/ai/openai/openai.py:0: error: Incompatible types in assignment (expression has type "UUID", variable has type "str | None") [assignment]
posthog/ai/anthropic/anthropic_async.py:0: error: Incompatible types in assignment (expression has type "UUID", variable has type "str | None") [assignment]
posthog/ai/anthropic/anthropic_async.py:0: error: Incompatible types in assignment (expression has type "UUID", variable has type "str | None") [assignment]
posthog/ai/anthropic/anthropic_async.py:0: error: Incompatible types in assignment (expression has type "UUID", variable has type "str | None") [assignment]
posthog/ai/anthropic/anthropic.py:0: error: Incompatible types in assignment (expression has type "UUID", variable has type "str | None") [assignment]
posthog/ai/anthropic/anthropic.py:0: error: Incompatible types in assignment (expression has type "UUID", variable has type "str | None") [assignment]
posthog/ai/anthropic/anthropic.py:0: error: Incompatible types in assignment (expression has type "UUID", variable has type "str | None") [assignment]
sentry_django_example/sentry_django_example/settings.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "None") [assignment]
6 changes: 3 additions & 3 deletions posthog/ai/anthropic/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def create(
**kwargs: Arguments passed to Anthropic's messages.create
"""
if posthog_trace_id is None:
posthog_trace_id = uuid.uuid4()
posthog_trace_id = str(uuid.uuid4())

if kwargs.get("stream", False):
return self._create_streaming(
Expand Down Expand Up @@ -89,7 +89,7 @@ def stream(
**kwargs: Any,
):
if posthog_trace_id is None:
posthog_trace_id = uuid.uuid4()
posthog_trace_id = str(uuid.uuid4())

return self._create_streaming(
posthog_distinct_id,
Expand Down Expand Up @@ -167,7 +167,7 @@ def _capture_streaming_event(
output: str,
):
if posthog_trace_id is None:
posthog_trace_id = uuid.uuid4()
posthog_trace_id = str(uuid.uuid4())

event_properties = {
"$ai_provider": "anthropic",
Expand Down
6 changes: 3 additions & 3 deletions posthog/ai/anthropic/anthropic_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ async def create(
**kwargs: Arguments passed to Anthropic's messages.create
"""
if posthog_trace_id is None:
posthog_trace_id = uuid.uuid4()
posthog_trace_id = str(uuid.uuid4())

if kwargs.get("stream", False):
return await self._create_streaming(
Expand Down Expand Up @@ -89,7 +89,7 @@ async def stream(
**kwargs: Any,
):
if posthog_trace_id is None:
posthog_trace_id = uuid.uuid4()
posthog_trace_id = str(uuid.uuid4())

return await self._create_streaming(
posthog_distinct_id,
Expand Down Expand Up @@ -167,7 +167,7 @@ async def _capture_streaming_event(
output: str,
):
if posthog_trace_id is None:
posthog_trace_id = uuid.uuid4()
posthog_trace_id = str(uuid.uuid4())

event_properties = {
"$ai_provider": "anthropic",
Expand Down
175 changes: 171 additions & 4 deletions posthog/ai/openai/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,167 @@ def __init__(self, posthog_client: PostHogClient, **kwargs):
self.chat = WrappedChat(self)
self.embeddings = WrappedEmbeddings(self)
self.beta = WrappedBeta(self)
self.responses = WrappedResponses(self)


class WrappedResponses(openai.resources.responses.Responses):
_client: OpenAI

def create(
self,
posthog_distinct_id: Optional[str] = None,
posthog_trace_id: Optional[str] = None,
posthog_properties: Optional[Dict[str, Any]] = None,
posthog_privacy_mode: bool = False,
posthog_groups: Optional[Dict[str, Any]] = None,
**kwargs: Any,
):
if posthog_trace_id is None:
posthog_trace_id = str(uuid.uuid4())

if kwargs.get("stream", False):
return self._create_streaming(
posthog_distinct_id,
posthog_trace_id,
posthog_properties,
posthog_privacy_mode,
posthog_groups,
**kwargs,
)

return call_llm_and_track_usage(
posthog_distinct_id,
self._client._ph_client,
"openai",
posthog_trace_id,
posthog_properties,
posthog_privacy_mode,
posthog_groups,
self._client.base_url,
super().create,
**kwargs,
)

def _create_streaming(
self,
posthog_distinct_id: Optional[str],
posthog_trace_id: Optional[str],
posthog_properties: Optional[Dict[str, Any]],
posthog_privacy_mode: bool,
posthog_groups: Optional[Dict[str, Any]],
**kwargs: Any,
):
start_time = time.time()
usage_stats: Dict[str, int] = {}
final_content = []
response = super().create(**kwargs)

def generator():
nonlocal usage_stats
nonlocal final_content

try:
for chunk in response:
if hasattr(chunk, "type") and chunk.type == "response.completed":
res = chunk.response
if res.output and len(res.output) > 0:
final_content.append(res.output[0])
Comment on lines +98 to +99

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider handling all output items instead of just the first one (output[0])


if hasattr(chunk, "usage") and chunk.usage:
usage_stats = {
k: getattr(chunk.usage, k, 0)
for k in [
"input_tokens",
"output_tokens",
"total_tokens",
]
}

# Add support for cached tokens
if hasattr(chunk.usage, "output_tokens_details") and hasattr(
chunk.usage.output_tokens_details, "reasoning_tokens"
):
usage_stats["reasoning_tokens"] = chunk.usage.output_tokens_details.reasoning_tokens

if hasattr(chunk.usage, "input_tokens_details") and hasattr(
chunk.usage.input_tokens_details, "cached_tokens"
):
usage_stats["cache_read_input_tokens"] = chunk.usage.input_tokens_details.cached_tokens

yield chunk

finally:
end_time = time.time()
latency = end_time - start_time
output = final_content
self._capture_streaming_event(
posthog_distinct_id,
posthog_trace_id,
posthog_properties,
posthog_privacy_mode,
posthog_groups,
kwargs,
usage_stats,
latency,
output,
)

return generator()

def _capture_streaming_event(
self,
posthog_distinct_id: Optional[str],
posthog_trace_id: Optional[str],
posthog_properties: Optional[Dict[str, Any]],
posthog_privacy_mode: bool,
posthog_groups: Optional[Dict[str, Any]],
kwargs: Dict[str, Any],
usage_stats: Dict[str, int],
latency: float,
output: Any,
tool_calls: Optional[List[Dict[str, Any]]] = None,
):
if posthog_trace_id is None:
posthog_trace_id = str(uuid.uuid4())

event_properties = {
"$ai_provider": "openai",
"$ai_model": kwargs.get("model"),
"$ai_model_parameters": get_model_params(kwargs),
"$ai_input": with_privacy_mode(self._client._ph_client, posthog_privacy_mode, kwargs.get("input")),
"$ai_output_choices": with_privacy_mode(
self._client._ph_client,
posthog_privacy_mode,
output,
),
"$ai_http_status": 200,
"$ai_input_tokens": usage_stats.get("input_tokens", 0),
"$ai_output_tokens": usage_stats.get("output_tokens", 0),
"$ai_cache_read_input_tokens": usage_stats.get("cache_read_input_tokens", 0),
"$ai_reasoning_tokens": usage_stats.get("reasoning_tokens", 0),
"$ai_latency": latency,
"$ai_trace_id": posthog_trace_id,
"$ai_base_url": str(self._client.base_url),
**(posthog_properties or {}),
}

if tool_calls:
event_properties["$ai_tools"] = with_privacy_mode(
self._client._ph_client,
posthog_privacy_mode,
tool_calls,
)

if posthog_distinct_id is None:
event_properties["$process_person_profile"] = False

if hasattr(self._client._ph_client, "capture"):
self._client._ph_client.capture(
distinct_id=posthog_distinct_id or posthog_trace_id,
event="$ai_generation",
properties=event_properties,
groups=posthog_groups,
)


class WrappedChat(openai.resources.chat.Chat):
Expand All @@ -55,7 +216,7 @@ def create(
**kwargs: Any,
):
if posthog_trace_id is None:
posthog_trace_id = uuid.uuid4()
posthog_trace_id = str(uuid.uuid4())

if kwargs.get("stream", False):
return self._create_streaming(
Expand Down Expand Up @@ -121,6 +282,11 @@ def generator():
):
usage_stats["cache_read_input_tokens"] = chunk.usage.prompt_tokens_details.cached_tokens

if hasattr(chunk.usage, "output_tokens_details") and hasattr(
chunk.usage.output_tokens_details, "reasoning_tokens"
):
usage_stats["reasoning_tokens"] = chunk.usage.output_tokens_details.reasoning_tokens

if hasattr(chunk, "choices") and chunk.choices and len(chunk.choices) > 0:
if chunk.choices[0].delta and chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
Expand Down Expand Up @@ -171,11 +337,11 @@ def _capture_streaming_event(
kwargs: Dict[str, Any],
usage_stats: Dict[str, int],
latency: float,
output: str,
output: Any,
tool_calls: Optional[List[Dict[str, Any]]] = None,
):
if posthog_trace_id is None:
posthog_trace_id = uuid.uuid4()
posthog_trace_id = str(uuid.uuid4())

event_properties = {
"$ai_provider": "openai",
Expand All @@ -191,6 +357,7 @@ def _capture_streaming_event(
"$ai_input_tokens": usage_stats.get("prompt_tokens", 0),
"$ai_output_tokens": usage_stats.get("completion_tokens", 0),
"$ai_cache_read_input_tokens": usage_stats.get("cache_read_input_tokens", 0),
"$ai_reasoning_tokens": usage_stats.get("reasoning_tokens", 0),
"$ai_latency": latency,
"$ai_trace_id": posthog_trace_id,
"$ai_base_url": str(self._client.base_url),
Expand Down Expand Up @@ -241,7 +408,7 @@ def create(
The response from OpenAI's embeddings.create call.
"""
if posthog_trace_id is None:
posthog_trace_id = uuid.uuid4()
posthog_trace_id = str(uuid.uuid4())

start_time = time.time()
response = super().create(**kwargs)
Expand Down
Loading