Skip to content
Merged
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
36 changes: 35 additions & 1 deletion langfuse/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from dataclasses import dataclass
from datetime import datetime
from inspect import isawaitable, isclass
from typing import Any, Optional, cast
from typing import Any, Dict, Optional, cast

from openai import _types as openai_types
from openai._types import NotGiven
Expand Down Expand Up @@ -494,6 +494,36 @@ def _extract_chat_response(kwargs: Any) -> Any:
return response


def _parse_reasoning_model_parameters(
resource: OpenAiDefinition, kwargs: Any
) -> Dict[str, Any]:
"""Collect reasoning-model request params as flat model parameters.

Chat Completions accepts `reasoning_effort` and `verbosity` at the top level,
while the Responses API nests them under `reasoning` and `text`.
"""
if resource.object in ("Responses", "AsyncResponses"):
reasoning = kwargs.get("reasoning", None)
text = kwargs.get("text", None)
candidates = {
"reasoning_effort": _get_attr_or_item(reasoning, "effort"),
"reasoning_summary": _get_attr_or_item(reasoning, "summary"),
"verbosity": _get_attr_or_item(text, "verbosity"),
"max_output_tokens": kwargs.get("max_output_tokens", None),
}
else:
candidates = {
"reasoning_effort": kwargs.get("reasoning_effort", None),
"verbosity": kwargs.get("verbosity", None),
}

return {
key: value
for key, value in candidates.items()
if value is not None and not _is_not_given(value)
}


def _get_langfuse_data_from_kwargs(resource: OpenAiDefinition, kwargs: Any) -> Any:
default_name = (
"OpenAI-embedding" if resource.type == "embedding" else "OpenAI-generation"
Expand Down Expand Up @@ -646,6 +676,10 @@ def _get_langfuse_data_from_kwargs(resource: OpenAiDefinition, kwargs: Any) -> A
if parsed_service_tier is not None:
modelParameters["service_tier"] = parsed_service_tier

modelParameters.update(_parse_reasoning_model_parameters(resource, kwargs))
if "max_output_tokens" in modelParameters:
modelParameters.pop("max_tokens", None)

langfuse_prompt = kwargs.get("langfuse_prompt", None)

return {
Expand Down
71 changes: 71 additions & 0 deletions tests/unit/test_openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -1167,6 +1167,77 @@ async def test_openai_async_stream_captures_service_tier_from_chunks(
assert model_parameters["temperature"] == 0


def test_chat_completion_captures_reasoning_parameters(
langfuse_memory_client, get_span, json_attr
):
openai_client = lf_openai.OpenAI(api_key="test")
response = _make_chat_response()

with patch.object(openai_client.chat.completions, "_post", return_value=response):
openai_client.chat.completions.create(
name="unit-openai-reasoning-params",
model="gpt-5",
messages=[{"role": "user", "content": "1 + 1 = ?"}],
reasoning_effort="minimal",
verbosity="low",
)

langfuse_memory_client.flush()
span = get_span("unit-openai-reasoning-params")

model_parameters = json_attr(
span, LangfuseOtelSpanAttributes.OBSERVATION_MODEL_PARAMETERS
)
assert model_parameters["reasoning_effort"] == "minimal"
assert model_parameters["verbosity"] == "low"


def test_chat_completion_reasoning_parameters_absent_by_default(
langfuse_memory_client, get_span, json_attr
):
from openai._types import NOT_GIVEN

openai_client = lf_openai.OpenAI(api_key="test")
response = _make_chat_response()

with patch.object(openai_client.chat.completions, "_post", return_value=response):
openai_client.chat.completions.create(
name="unit-openai-reasoning-params-absent",
model="gpt-4o-mini",
messages=[{"role": "user", "content": "1 + 1 = ?"}],
reasoning_effort=NOT_GIVEN,
)

langfuse_memory_client.flush()
span = get_span("unit-openai-reasoning-params-absent")

model_parameters = json_attr(
span, LangfuseOtelSpanAttributes.OBSERVATION_MODEL_PARAMETERS
)
assert "reasoning_effort" not in model_parameters
assert "verbosity" not in model_parameters


def test_responses_kwargs_capture_reasoning_parameters():
data = lf_openai_module._get_langfuse_data_from_kwargs(
SimpleNamespace(type="chat", object="Responses"),
{
"model": "gpt-5",
"input": "1 + 1 = ?",
"reasoning": {"effort": "high", "summary": "auto"},
"text": {"verbosity": "high", "format": {"type": "text"}},
"max_output_tokens": 256,
},
)

model_parameters = data["model_parameters"]
assert model_parameters["reasoning_effort"] == "high"
assert model_parameters["reasoning_summary"] == "auto"
assert model_parameters["verbosity"] == "high"
assert model_parameters["max_output_tokens"] == 256
assert "max_tokens" not in model_parameters


def test_embedding_model_parameters_do_not_include_service_tier(
langfuse_memory_client, get_span, json_attr
):
Expand Down
Loading