Skip to content
Open
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/emit-explicit-served-tier.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 3 additions & 0 deletions posthog/ai/langchain/callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,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:
Expand All @@ -632,6 +633,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,
Expand Down
23 changes: 15 additions & 8 deletions posthog/ai/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,10 +521,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),
Expand Down Expand Up @@ -687,10 +688,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),
Expand Down Expand Up @@ -840,6 +842,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"],
Expand Down
1 change: 1 addition & 0 deletions posthog/test/ai/langchain/test_callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2864,6 +2864,7 @@ 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"


@pytest.mark.parametrize(
Expand Down
2 changes: 2 additions & 0 deletions posthog/test/ai/openai/test_openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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():
Expand Down
4 changes: 2 additions & 2 deletions posthog/test/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down