Skip to content

Commit 6dc0253

Browse files
authored
feat(openai): capture service_tier in model_parameters (#1751)
1 parent 5d949cf commit 6dc0253

4 files changed

Lines changed: 376 additions & 18 deletions

File tree

langfuse/openai.py

Lines changed: 79 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,12 @@ def _get_langfuse_data_from_kwargs(resource: OpenAiDefinition, kwargs: Any) -> A
598598

599599
parsed_n = kwargs.get("n", 1) if not isinstance(kwargs.get("n", 1), NotGiven) else 1
600600

601+
parsed_service_tier = (
602+
kwargs.get("service_tier", None)
603+
if not isinstance(kwargs.get("service_tier", None), NotGiven)
604+
else None
605+
)
606+
601607
if resource.type == "embedding":
602608
parsed_dimensions = (
603609
kwargs.get("dimensions", None)
@@ -634,6 +640,9 @@ def _get_langfuse_data_from_kwargs(resource: OpenAiDefinition, kwargs: Any) -> A
634640
if parsed_seed is not None:
635641
modelParameters["seed"] = parsed_seed
636642

643+
if parsed_service_tier is not None:
644+
modelParameters["service_tier"] = parsed_service_tier
645+
637646
langfuse_prompt = kwargs.get("langfuse_prompt", None)
638647

639648
return {
@@ -657,6 +666,7 @@ def _create_langfuse_update(
657666
model: Optional[str] = None,
658667
usage: Optional[Any] = None,
659668
metadata: Optional[Any] = None,
669+
model_parameters: Optional[Any] = None,
660670
) -> Any:
661671
update = {
662672
"output": completion,
@@ -668,6 +678,9 @@ def _create_langfuse_update(
668678
if metadata is not None:
669679
update["metadata"] = metadata
670680

681+
if model_parameters is not None:
682+
update["model_parameters"] = model_parameters
683+
671684
if usage is not None:
672685
update["usage_details"] = _parse_usage(usage)
673686
update["cost_details"] = _parse_cost(usage)
@@ -721,7 +734,7 @@ def _parse_cost(usage: Optional[Any] = None) -> Any:
721734

722735

723736
def _extract_streamed_response_api_response(chunks: Any) -> Any:
724-
completion, model, usage = None, None, None
737+
completion, model, usage, service_tier = None, None, None, None
725738
metadata = {}
726739

727740
for raw_chunk in chunks:
@@ -731,6 +744,7 @@ def _extract_streamed_response_api_response(chunks: Any) -> Any:
731744

732745
response = raw_response.__dict__
733746
model = response.get("model")
747+
service_tier = response.get("service_tier", None) or service_tier
734748

735749
for key, val in response.items():
736750
if key not in ["created_at", "model", "output", "usage", "text"]:
@@ -739,18 +753,19 @@ def _extract_streamed_response_api_response(chunks: Any) -> Any:
739753
if key == "output":
740754
completion = _extract_response_api_completion(val)
741755

742-
return (model, completion, usage, metadata)
756+
return (model, completion, usage, metadata, service_tier)
743757

744758

745759
def _extract_streamed_openai_response(resource: Any, chunks: Any) -> Any:
746760
completion: Any = defaultdict(lambda: None) if resource.type == "chat" else ""
747-
model, usage, finish_reason = None, None, None
761+
model, usage, finish_reason, service_tier = None, None, None, None
748762

749763
for chunk in chunks:
750764
if _is_openai_v1():
751765
chunk = chunk.__dict__
752766

753767
model = model or chunk.get("model", None) or None
768+
service_tier = service_tier or chunk.get("service_tier", None) or None
754769
chunk_usage = chunk.get("usage", None)
755770
if chunk_usage is not None:
756771
usage = chunk_usage
@@ -884,16 +899,18 @@ def get_response_for_chat() -> Any:
884899
get_response_for_chat() if resource.type == "chat" else completion,
885900
usage,
886901
{"finish_reason": finish_reason} if finish_reason is not None else None,
902+
service_tier,
887903
)
888904

889905

890906
def _get_langfuse_data_from_default_response(
891907
resource: OpenAiDefinition, response: Any
892908
) -> Any:
893909
if response is None:
894-
return None, "<NoneType response returned from OpenAI>", None
910+
return None, "<NoneType response returned from OpenAI>", None, None
895911

896912
model = response.get("model", None) or None
913+
service_tier = response.get("service_tier", None) or None
897914

898915
completion = None
899916

@@ -942,7 +959,23 @@ def _get_langfuse_data_from_default_response(
942959

943960
usage = _parse_usage(response.get("usage", None))
944961

945-
return (model, completion, usage)
962+
return (model, completion, usage, service_tier)
963+
964+
965+
def _merge_service_tier_into_model_parameters(
966+
model_parameters: Optional[Any], service_tier: Optional[Any]
967+
) -> Optional[Any]:
968+
"""Merge the response-side service tier into the request-side model parameters.
969+
970+
The response value is authoritative because OpenAI returns the tier that
971+
actually processed the request (e.g. when the request specified "auto").
972+
Returns None when there is nothing to update so callers can skip the
973+
update and keep the request-side model parameters untouched.
974+
"""
975+
if service_tier is None:
976+
return None
977+
978+
return {**(model_parameters or {}), "service_tier": service_tier}
946979

947980

948981
def _is_openai_v1() -> bool:
@@ -999,9 +1032,10 @@ def _finalize_stream_response(
9991032
items: list[Any],
10001033
generation: LangfuseGeneration,
10011034
completion_start_time: Optional[datetime],
1035+
model_parameters: Optional[Any] = None,
10021036
) -> None:
10031037
try:
1004-
model, completion, usage, metadata = (
1038+
model, completion, usage, metadata, service_tier = (
10051039
_extract_streamed_response_api_response(items)
10061040
if resource.object == "Responses" or resource.object == "AsyncResponses"
10071041
else _extract_streamed_openai_response(resource, items)
@@ -1014,6 +1048,9 @@ def _finalize_stream_response(
10141048
model=model,
10151049
usage=usage,
10161050
metadata=metadata,
1051+
model_parameters=_merge_service_tier_into_model_parameters(
1052+
model_parameters, service_tier
1053+
),
10171054
)
10181055
except Exception:
10191056
pass
@@ -1026,12 +1063,14 @@ def _instrument_openai_stream(
10261063
resource: OpenAiDefinition,
10271064
response: Any,
10281065
generation: LangfuseGeneration,
1066+
model_parameters: Optional[Any] = None,
10291067
) -> Any:
10301068
if not hasattr(response, "_iterator"):
10311069
return LangfuseResponseGeneratorSync(
10321070
resource=resource,
10331071
response=response,
10341072
generation=generation,
1073+
model_parameters=model_parameters,
10351074
)
10361075

10371076
items: list[Any] = []
@@ -1051,6 +1090,7 @@ def finalize_once() -> None:
10511090
items=items,
10521091
generation=generation,
10531092
completion_start_time=completion_start_time,
1093+
model_parameters=model_parameters,
10541094
)
10551095

10561096
response._langfuse_finalize_once = finalize_once # type: ignore[attr-defined]
@@ -1085,12 +1125,14 @@ def _instrument_openai_async_stream(
10851125
resource: OpenAiDefinition,
10861126
response: Any,
10871127
generation: LangfuseGeneration,
1128+
model_parameters: Optional[Any] = None,
10881129
) -> Any:
10891130
if not hasattr(response, "_iterator"):
10901131
return LangfuseResponseGeneratorAsync(
10911132
resource=resource,
10921133
response=response,
10931134
generation=generation,
1135+
model_parameters=model_parameters,
10941136
)
10951137

10961138
items: list[Any] = []
@@ -1110,6 +1152,7 @@ async def finalize_once() -> None:
11101152
items=items,
11111153
generation=generation,
11121154
completion_start_time=completion_start_time,
1155+
model_parameters=model_parameters,
11131156
)
11141157

11151158
response._langfuse_finalize_once = finalize_once # type: ignore[attr-defined]
@@ -1243,21 +1286,25 @@ def _wrap(
12431286
resource=open_ai_resource,
12441287
response=openai_response,
12451288
generation=generation,
1289+
model_parameters=langfuse_data.get("model_parameters", None),
12461290
)
12471291
elif _is_streaming_response(openai_response):
12481292
return LangfuseResponseGeneratorSync(
12491293
resource=open_ai_resource,
12501294
response=openai_response,
12511295
generation=generation,
1296+
model_parameters=langfuse_data.get("model_parameters", None),
12521297
)
12531298

12541299
else:
12551300
parsed_response = _unwrap_raw_response(openai_response)
1256-
model, completion, usage = _get_langfuse_data_from_default_response(
1257-
open_ai_resource,
1258-
(parsed_response and parsed_response.__dict__)
1259-
if _is_openai_v1()
1260-
else parsed_response,
1301+
model, completion, usage, service_tier = (
1302+
_get_langfuse_data_from_default_response(
1303+
open_ai_resource,
1304+
(parsed_response and parsed_response.__dict__)
1305+
if _is_openai_v1()
1306+
else parsed_response,
1307+
)
12611308
)
12621309

12631310
generation.update(
@@ -1267,6 +1314,9 @@ def _wrap(
12671314
cost_details=_parse_cost(parsed_response.usage)
12681315
if hasattr(parsed_response, "usage")
12691316
else None,
1317+
model_parameters=_merge_service_tier_into_model_parameters(
1318+
langfuse_data.get("model_parameters", None), service_tier
1319+
),
12701320
).end()
12711321

12721322
return openai_response
@@ -1325,21 +1375,25 @@ async def _wrap_async(
13251375
resource=open_ai_resource,
13261376
response=openai_response,
13271377
generation=generation,
1378+
model_parameters=langfuse_data.get("model_parameters", None),
13281379
)
13291380
elif _is_streaming_response(openai_response):
13301381
return LangfuseResponseGeneratorAsync(
13311382
resource=open_ai_resource,
13321383
response=openai_response,
13331384
generation=generation,
1385+
model_parameters=langfuse_data.get("model_parameters", None),
13341386
)
13351387

13361388
else:
13371389
parsed_response = _unwrap_raw_response(openai_response)
1338-
model, completion, usage = _get_langfuse_data_from_default_response(
1339-
open_ai_resource,
1340-
(parsed_response and parsed_response.__dict__)
1341-
if _is_openai_v1()
1342-
else parsed_response,
1390+
model, completion, usage, service_tier = (
1391+
_get_langfuse_data_from_default_response(
1392+
open_ai_resource,
1393+
(parsed_response and parsed_response.__dict__)
1394+
if _is_openai_v1()
1395+
else parsed_response,
1396+
)
13431397
)
13441398
generation.update(
13451399
model=model,
@@ -1349,6 +1403,9 @@ async def _wrap_async(
13491403
cost_details=_parse_cost(parsed_response.usage)
13501404
if hasattr(parsed_response, "usage")
13511405
else None,
1406+
model_parameters=_merge_service_tier_into_model_parameters(
1407+
langfuse_data.get("model_parameters", None), service_tier
1408+
),
13521409
).end()
13531410

13541411
return openai_response
@@ -1397,12 +1454,14 @@ def __init__(
13971454
resource: Any,
13981455
response: Any,
13991456
generation: Any,
1457+
model_parameters: Optional[Any] = None,
14001458
) -> None:
14011459
self.items: list[Any] = []
14021460

14031461
self.resource = resource
14041462
self.response = response
14051463
self.generation = generation
1464+
self.model_parameters = model_parameters
14061465
self.completion_start_time: Optional[datetime] = None
14071466
self._is_finalized = False
14081467

@@ -1458,6 +1517,7 @@ def _finalize(self) -> None:
14581517
items=self.items,
14591518
generation=self.generation,
14601519
completion_start_time=self.completion_start_time,
1520+
model_parameters=self.model_parameters,
14611521
)
14621522

14631523

@@ -1468,12 +1528,14 @@ def __init__(
14681528
resource: Any,
14691529
response: Any,
14701530
generation: Any,
1531+
model_parameters: Optional[Any] = None,
14711532
) -> None:
14721533
self.items: list[Any] = []
14731534

14741535
self.resource = resource
14751536
self.response = response
14761537
self.generation = generation
1538+
self.model_parameters = model_parameters
14771539
self.completion_start_time: Optional[datetime] = None
14781540
self._is_finalized = False
14791541

@@ -1520,6 +1582,7 @@ async def _finalize(self) -> None:
15201582
items=self.items,
15211583
generation=self.generation,
15221584
completion_start_time=self.completion_start_time,
1585+
model_parameters=self.model_parameters,
15231586
)
15241587

15251588
async def close(self) -> None:

tests/e2e/test_decorators.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -940,6 +940,7 @@ async def level_1_function(*args, **kwargs):
940940
assert generation.end_time is not None
941941
assert generation.start_time < generation.end_time
942942
assert generation.model_parameters == {
943+
"service_tier": "default",
943944
"temperature": 0,
944945
"top_p": 1,
945946
"frequency_penalty": 0,

0 commit comments

Comments
 (0)