Skip to content

Commit 270d20c

Browse files
committed
feat: add support for parse via responses
1 parent 4426dd9 commit 270d20c

File tree

5 files changed

+191
-1
lines changed

5 files changed

+191
-1
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 4.7.0 - 2025-06-10
2+
3+
- feat: add support for parse endpoint in responses API (no longer beta)
4+
15
## 4.6.2 - 2025-06-09
26

37
- fix: replace `import posthog` with direct method imports

posthog/ai/openai/openai.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,42 @@ def _capture_streaming_event(
230230
groups=posthog_groups,
231231
)
232232

233+
def parse(
234+
self,
235+
posthog_distinct_id: Optional[str] = None,
236+
posthog_trace_id: Optional[str] = None,
237+
posthog_properties: Optional[Dict[str, Any]] = None,
238+
posthog_privacy_mode: bool = False,
239+
posthog_groups: Optional[Dict[str, Any]] = None,
240+
**kwargs: Any,
241+
):
242+
"""
243+
Parse structured output using OpenAI's 'responses.parse' method, but also track usage in PostHog.
244+
245+
Args:
246+
posthog_distinct_id: Optional ID to associate with the usage event.
247+
posthog_trace_id: Optional trace UUID for linking events.
248+
posthog_properties: Optional dictionary of extra properties to include in the event.
249+
posthog_privacy_mode: Whether to anonymize the input and output.
250+
posthog_groups: Optional dictionary of groups to associate with the event.
251+
**kwargs: Any additional parameters for the OpenAI Responses Parse API.
252+
253+
Returns:
254+
The response from OpenAI's responses.parse call.
255+
"""
256+
return call_llm_and_track_usage(
257+
posthog_distinct_id,
258+
self._client._ph_client,
259+
"openai",
260+
posthog_trace_id,
261+
posthog_properties,
262+
posthog_privacy_mode,
263+
posthog_groups,
264+
self._client.base_url,
265+
self._original.parse,
266+
**kwargs,
267+
)
268+
233269

234270
class WrappedChat:
235271
"""Wrapper for OpenAI chat that tracks usage in PostHog."""

posthog/ai/openai/openai_async.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,42 @@ async def _capture_streaming_event(
229229
properties=event_properties,
230230
groups=posthog_groups,
231231
)
232+
233+
async def parse(
234+
self,
235+
posthog_distinct_id: Optional[str] = None,
236+
posthog_trace_id: Optional[str] = None,
237+
posthog_properties: Optional[Dict[str, Any]] = None,
238+
posthog_privacy_mode: bool = False,
239+
posthog_groups: Optional[Dict[str, Any]] = None,
240+
**kwargs: Any,
241+
):
242+
"""
243+
Parse structured output using OpenAI's 'responses.parse' method, but also track usage in PostHog.
244+
245+
Args:
246+
posthog_distinct_id: Optional ID to associate with the usage event.
247+
posthog_trace_id: Optional trace UUID for linking events.
248+
posthog_properties: Optional dictionary of extra properties to include in the event.
249+
posthog_privacy_mode: Whether to anonymize the input and output.
250+
posthog_groups: Optional dictionary of groups to associate with the event.
251+
**kwargs: Any additional parameters for the OpenAI Responses Parse API.
252+
253+
Returns:
254+
The response from OpenAI's responses.parse call.
255+
"""
256+
return await call_llm_and_track_usage_async(
257+
posthog_distinct_id,
258+
self._client._ph_client,
259+
"openai",
260+
posthog_trace_id,
261+
posthog_properties,
262+
posthog_privacy_mode,
263+
posthog_groups,
264+
self._client.base_url,
265+
self._original.parse,
266+
**kwargs,
267+
)
232268

233269

234270
class WrappedChat:

posthog/test/ai/openai/test_openai.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
ResponseOutputMessage,
2727
ResponseOutputText,
2828
ResponseUsage,
29+
ParsedResponse,
30+
)
31+
from openai.types.responses.parsed_response import (
32+
ParsedResponseOutputMessage,
33+
ParsedResponseOutputText,
2934
)
3035

3136
from posthog.ai.openai import OpenAI
@@ -115,6 +120,51 @@ def mock_openai_response_with_responses_api():
115120
)
116121

117122

123+
@pytest.fixture
124+
def mock_parsed_response():
125+
return ParsedResponse(
126+
id="test",
127+
model="gpt-4o-2024-08-06",
128+
object="response",
129+
created_at=1741476542,
130+
status="completed",
131+
error=None,
132+
incomplete_details=None,
133+
instructions=None,
134+
max_output_tokens=None,
135+
tools=[],
136+
tool_choice="auto",
137+
output=[
138+
ParsedResponseOutputMessage(
139+
id="msg_123",
140+
type="message",
141+
role="assistant",
142+
status="completed",
143+
content=[
144+
ParsedResponseOutputText(
145+
type="output_text",
146+
text='{"name": "Science Fair", "date": "Friday", "participants": ["Alice", "Bob"]}',
147+
annotations=[],
148+
parsed={"name": "Science Fair", "date": "Friday", "participants": ["Alice", "Bob"]},
149+
)
150+
],
151+
)
152+
],
153+
output_parsed={"name": "Science Fair", "date": "Friday", "participants": ["Alice", "Bob"]},
154+
parallel_tool_calls=True,
155+
previous_response_id=None,
156+
usage=ResponseUsage(
157+
input_tokens=15,
158+
output_tokens=20,
159+
input_tokens_details={"prompt_tokens": 15, "cached_tokens": 0},
160+
output_tokens_details={"reasoning_tokens": 5},
161+
total_tokens=35,
162+
),
163+
user=None,
164+
metadata={},
165+
)
166+
167+
118168
@pytest.fixture
119169
def mock_embedding_response():
120170
return CreateEmbeddingResponse(
@@ -646,3 +696,67 @@ def test_responses_api(mock_client, mock_openai_response_with_responses_api):
646696
assert props["$ai_http_status"] == 200
647697
assert props["foo"] == "bar"
648698
assert isinstance(props["$ai_latency"], float)
699+
700+
701+
def test_responses_parse(mock_client, mock_parsed_response):
702+
with patch(
703+
"openai.resources.responses.Responses.parse",
704+
return_value=mock_parsed_response,
705+
):
706+
client = OpenAI(api_key="test-key", posthog_client=mock_client)
707+
response = client.responses.parse(
708+
model="gpt-4o-2024-08-06",
709+
input=[
710+
{"role": "system", "content": "Extract the event information."},
711+
{
712+
"role": "user",
713+
"content": "Alice and Bob are going to a science fair on Friday.",
714+
},
715+
],
716+
text={
717+
"format": {
718+
"type": "json_schema",
719+
"json_schema": {
720+
"name": "event",
721+
"schema": {
722+
"type": "object",
723+
"properties": {
724+
"name": {"type": "string"},
725+
"date": {"type": "string"},
726+
"participants": {"type": "array", "items": {"type": "string"}},
727+
},
728+
"required": ["name", "date", "participants"],
729+
},
730+
},
731+
}
732+
},
733+
posthog_distinct_id="test-id",
734+
posthog_properties={"foo": "bar"},
735+
)
736+
737+
assert response == mock_parsed_response
738+
assert mock_client.capture.call_count == 1
739+
740+
call_args = mock_client.capture.call_args[1]
741+
props = call_args["properties"]
742+
743+
assert call_args["distinct_id"] == "test-id"
744+
assert call_args["event"] == "$ai_generation"
745+
assert props["$ai_provider"] == "openai"
746+
assert props["$ai_model"] == "gpt-4o-2024-08-06"
747+
assert props["$ai_input"] == [
748+
{"role": "system", "content": "Extract the event information."},
749+
{
750+
"role": "user",
751+
"content": "Alice and Bob are going to a science fair on Friday.",
752+
},
753+
]
754+
assert props["$ai_output_choices"] == [
755+
{"role": "assistant", "content": '{"name": "Science Fair", "date": "Friday", "participants": ["Alice", "Bob"]}'}
756+
]
757+
assert props["$ai_input_tokens"] == 15
758+
assert props["$ai_output_tokens"] == 20
759+
assert props["$ai_reasoning_tokens"] == 5
760+
assert props["$ai_http_status"] == 200
761+
assert props["foo"] == "bar"
762+
assert isinstance(props["$ai_latency"], float)

posthog/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
VERSION = "4.6.2"
1+
VERSION = "4.7.0"
22

33
if __name__ == "__main__":
44
print(VERSION, end="") # noqa: T201

0 commit comments

Comments
 (0)