diff --git a/.sampo/changesets/doughty-iceseeker-tursas.md b/.sampo/changesets/doughty-iceseeker-tursas.md new file mode 100644 index 00000000..e2cb0890 --- /dev/null +++ b/.sampo/changesets/doughty-iceseeker-tursas.md @@ -0,0 +1,5 @@ +--- +pypi/posthog: patch +--- + +Capture pre-calculated total cost from OpenAI Agents Responses API usage. diff --git a/posthog/ai/openai_agents/processor.py b/posthog/ai/openai_agents/processor.py index d9a0de6e..affeec85 100644 --- a/posthog/ai/openai_agents/processor.py +++ b/posthog/ai/openai_agents/processor.py @@ -682,6 +682,7 @@ def _handle_response_span( # Try to extract usage from response usage = getattr(response, "usage", None) if response else None + total_cost_usd = getattr(usage, "cost", None) if usage else None input_tokens = 0 output_tokens = 0 if usage: @@ -703,6 +704,9 @@ def _handle_response_span( "$ai_total_tokens": input_tokens + output_tokens, } + if total_cost_usd is not None: + properties["$ai_total_cost_usd"] = total_cost_usd + # Extract output content from response if response: output_items = getattr(response, "output", None) diff --git a/posthog/test/ai/openai_agents/test_processor.py b/posthog/test/ai/openai_agents/test_processor.py index 99ad7b43..3ea619e6 100644 --- a/posthog/test/ai/openai_agents/test_processor.py +++ b/posthog/test/ai/openai_agents/test_processor.py @@ -536,6 +536,7 @@ def test_response_span_with_output_and_total_tokens( mock_response.usage = MagicMock() mock_response.usage.input_tokens = 25 mock_response.usage.output_tokens = 10 + mock_response.usage.cost = None span_data = ResponseSpanData( response=mock_response, @@ -554,6 +555,29 @@ def test_response_span_with_output_and_total_tokens( {"type": "message", "content": "Hello!"} ] assert call_kwargs["properties"]["$ai_response_id"] == "resp_123" + assert "$ai_total_cost_usd" not in call_kwargs["properties"] + + @pytest.mark.parametrize("total_cost_usd", [0.01234, 0.0]) + def test_response_span_includes_total_cost( + self, processor, mock_client, mock_span, total_cost_usd + ): + """Test ResponseSpanData includes a pre-calculated total cost.""" + mock_response = MagicMock() + mock_response.id = "resp_123" + mock_response.model = "gpt-4o" + mock_response.output = [] + mock_response.usage = MagicMock() + mock_response.usage.input_tokens = 25 + mock_response.usage.output_tokens = 10 + mock_response.usage.cost = total_cost_usd + + mock_span.span_data = ResponseSpanData(response=mock_response) + + processor.on_span_start(mock_span) + processor.on_span_end(mock_span) + + call_kwargs = mock_client.capture.call_args[1] + assert call_kwargs["properties"]["$ai_total_cost_usd"] == total_cost_usd def test_speech_span_with_pass_through_properties( self, processor, mock_client, mock_span