From efee11d180c1218c8e3c9fa1ec5f7fe7c413a545 Mon Sep 17 00:00:00 2001 From: Bernat Torres Date: Tue, 8 Sep 2026 14:37:41 +0200 Subject: [PATCH 1/2] feat(ai): emit the served tier as the explicit $ai_service_tier property Cost processing prices only from this property: its writers assert served values, unlike $ai_model_parameters.service_tier, which older releases populated from the request. Co-Authored-By: Claude Fable 5 --- posthog/ai/langchain/callbacks.py | 3 +++ posthog/ai/utils.py | 23 ++++++++++++++------- posthog/test/ai/langchain/test_callbacks.py | 1 + posthog/test/ai/openai/test_openai.py | 2 ++ posthog/test/test_utils.py | 4 ++-- 5 files changed, 23 insertions(+), 10 deletions(-) diff --git a/posthog/ai/langchain/callbacks.py b/posthog/ai/langchain/callbacks.py index bab85cec..6b8085a8 100644 --- a/posthog/ai/langchain/callbacks.py +++ b/posthog/ai/langchain/callbacks.py @@ -618,6 +618,7 @@ def _capture_generation( ): # The served tier comes from the response, because a requested tier can be refused. model_params = run.model_params + served_tier = None if isinstance(output, LLMResult) and isinstance(output.llm_output, dict): served_tier = output.llm_output.get("service_tier") if served_tier is not None: @@ -631,6 +632,8 @@ def _capture_generation( "$ai_provider": run.provider, "$ai_model": run.model, "$ai_model_parameters": model_params, + # The explicit served-tier signal cost processing prices from. + **({"$ai_service_tier": served_tier} if served_tier is not None else {}), "$ai_input": with_privacy_mode( self._ph_client, self._privacy_mode, diff --git a/posthog/ai/utils.py b/posthog/ai/utils.py index 011db77e..bef213c3 100644 --- a/posthog/ai/utils.py +++ b/posthog/ai/utils.py @@ -492,10 +492,11 @@ def call_llm_and_track_usage( tag("$ai_provider", provider) tag("$ai_model", kwargs.get("model") or getattr(response, "model", None)) - tag( - "$ai_model_parameters", - get_model_params(kwargs, getattr(response, "service_tier", None)), - ) + served_service_tier = getattr(response, "service_tier", None) + tag("$ai_model_parameters", get_model_params(kwargs, served_service_tier)) + if served_service_tier is not None: + # The explicit served-tier signal cost processing prices from. + tag("$ai_service_tier", served_service_tier) tag( "$ai_input", with_privacy_mode(ph_client, posthog_privacy_mode, sanitized_messages), @@ -658,10 +659,11 @@ async def call_llm_and_track_usage_async( tag("$ai_provider", provider) tag("$ai_model", kwargs.get("model") or getattr(response, "model", None)) - tag( - "$ai_model_parameters", - get_model_params(kwargs, getattr(response, "service_tier", None)), - ) + served_service_tier = getattr(response, "service_tier", None) + tag("$ai_model_parameters", get_model_params(kwargs, served_service_tier)) + if served_service_tier is not None: + # The explicit served-tier signal cost processing prices from. + tag("$ai_service_tier", served_service_tier) tag( "$ai_input", with_privacy_mode(ph_client, posthog_privacy_mode, sanitized_messages), @@ -811,6 +813,11 @@ def capture_streaming_event( "$ai_model_parameters": get_model_params( event_data["kwargs"], event_data.get("service_tier") ), + **( + {"$ai_service_tier": event_data["service_tier"]} + if event_data.get("service_tier") is not None + else {} + ), "$ai_input": with_privacy_mode( ph_client, event_data["privacy_mode"], diff --git a/posthog/test/ai/langchain/test_callbacks.py b/posthog/test/ai/langchain/test_callbacks.py index fd2ed8dc..7e75a3fd 100644 --- a/posthog/test/ai/langchain/test_callbacks.py +++ b/posthog/test/ai/langchain/test_callbacks.py @@ -2864,3 +2864,4 @@ def test_served_service_tier_merges_into_model_parameters(mock_client): props = mock_client.capture.call_args.kwargs["properties"] assert props["$ai_model_parameters"]["service_tier"] == "flex" assert props["$ai_model_parameters"]["temperature"] == 0.5 + assert props["$ai_service_tier"] == "flex" diff --git a/posthog/test/ai/openai/test_openai.py b/posthog/test/ai/openai/test_openai.py index a8aa9c61..1aee497d 100644 --- a/posthog/test/ai/openai/test_openai.py +++ b/posthog/test/ai/openai/test_openai.py @@ -2683,6 +2683,7 @@ def test_served_service_tier_lands_in_model_parameters( props = mock_client.capture.call_args[1]["properties"] assert props["$ai_model_parameters"]["service_tier"] == "flex" + assert props["$ai_service_tier"] == "flex" def test_response_without_service_tier_omits_it(mock_client, mock_openai_response): @@ -2699,6 +2700,7 @@ def test_response_without_service_tier_omits_it(mock_client, mock_openai_respons props = mock_client.capture.call_args[1]["properties"] assert "service_tier" not in props["$ai_model_parameters"] + assert "$ai_service_tier" not in props def test_streaming_state_tracks_served_service_tier(): diff --git a/posthog/test/test_utils.py b/posthog/test/test_utils.py index e075ff4a..8473f6e1 100644 --- a/posthog/test/test_utils.py +++ b/posthog/test/test_utils.py @@ -112,8 +112,8 @@ def test_guess_timezone_preserves_recent_naive_local_instant_as_utc(self): utc_now = datetime(2026, 1, 15, 12, 30, 45, 123456, tzinfo=timezone.utc) with mock.patch("posthog.utils.datetime", wraps=datetime) as mock_datetime: - mock_datetime.now.side_effect = ( - lambda tz=None: local_now if tz is None else utc_now.astimezone(tz) + mock_datetime.now.side_effect = lambda tz=None: ( + local_now if tz is None else utc_now.astimezone(tz) ) normalized = utils.guess_timezone(local_now) From 6878e10f89e25df980b709a28f2b06afd114440c Mon Sep 17 00:00:00 2001 From: Bernat Torres Date: Tue, 8 Sep 2026 14:38:00 +0200 Subject: [PATCH 2/2] chore: add changeset Co-Authored-By: Claude Fable 5 --- .sampo/changesets/emit-explicit-served-tier.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .sampo/changesets/emit-explicit-served-tier.md diff --git a/.sampo/changesets/emit-explicit-served-tier.md b/.sampo/changesets/emit-explicit-served-tier.md new file mode 100644 index 00000000..01b031f9 --- /dev/null +++ b/.sampo/changesets/emit-explicit-served-tier.md @@ -0,0 +1,5 @@ +--- +pypi/posthog: minor +--- + +OpenAI and LangChain generations now also emit the served service tier as the explicit `$ai_service_tier` event property, next to the copy inside `$ai_model_parameters`. Cost processing prices tiered calls only from the explicit property, whose writers assert response-derived values.