From 99998462178dc5f7e914928131f139e8cba3ca0f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 24 Sep 2026 13:10:23 +0000 Subject: [PATCH] fix(openai): capture reasoning_effort and verbosity in model parameters Co-authored-by: Hassieb Pakzad --- langfuse/openai.py | 36 +++++++++++++++++++- tests/unit/test_openai.py | 71 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/langfuse/openai.py b/langfuse/openai.py index 3e480f541..16e7f1c1f 100644 --- a/langfuse/openai.py +++ b/langfuse/openai.py @@ -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 @@ -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" @@ -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 { diff --git a/tests/unit/test_openai.py b/tests/unit/test_openai.py index 681be4fbf..cacdae5c2 100644 --- a/tests/unit/test_openai.py +++ b/tests/unit/test_openai.py @@ -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 ):