diff --git a/langfuse/openai.py b/langfuse/openai.py index 94d7d1393..309b27dea 100644 --- a/langfuse/openai.py +++ b/langfuse/openai.py @@ -193,6 +193,22 @@ class OpenAiDefinition: sync=False, min_version="1.66.0", ), + OpenAiDefinition( + module="openai.resources.responses", + object="Responses", + method="retrieve", + type="chat", + sync=True, + min_version="1.66.0", + ), + OpenAiDefinition( + module="openai.resources.responses", + object="AsyncResponses", + method="retrieve", + type="chat", + sync=False, + min_version="1.66.0", + ), OpenAiDefinition( module="openai.resources.embeddings", object="Embeddings", @@ -550,7 +566,11 @@ def _get_langfuse_data_from_kwargs(resource: OpenAiDefinition, kwargs: Any) -> A prompt = None - if resource.type == "completion": + if resource.method == "retrieve": + # retrieve() only knows the id of a response created earlier; the model, + # output and usage are filled in from the response itself. + prompt = {"response_id": kwargs.get("response_id", None)} + elif resource.type == "completion": prompt = kwargs.get("prompt", None) elif resource.object == "Responses" or resource.object == "AsyncResponses": prompt = _extract_responses_prompt(kwargs) diff --git a/tests/unit/test_openai.py b/tests/unit/test_openai.py index 681be4fbf..83006d70d 100644 --- a/tests/unit/test_openai.py +++ b/tests/unit/test_openai.py @@ -1399,3 +1399,74 @@ def test_with_raw_response_streaming_passes_through_untraced( span.name != "OpenAI-generation" for span in memory_exporter.get_finished_spans() ) + + +def test_response_retrieve_exports_generation_span( + langfuse_memory_client, get_span, json_attr +): + """Background responses complete through retrieve(), which must be traced.""" + openai_client = lf_openai.OpenAI(api_key="test") + response = SimpleNamespace( + model="gpt-4o-mini", + output=[{"role": "assistant", "content": "2"}], + usage=SimpleNamespace(input_tokens=3, output_tokens=1, total_tokens=4), + ) + + with patch.object(openai_client.responses, "_get", return_value=response): + result = openai_client.responses.retrieve( + response_id="resp_123", + name="unit-openai-response-retrieve", + ) + + assert result is response + + langfuse_memory_client.flush() + span = get_span("unit-openai-response-retrieve") + + assert span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] == "generation" + assert ( + span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_MODEL] == "gpt-4o-mini" + ) + assert json_attr(span, LangfuseOtelSpanAttributes.OBSERVATION_INPUT) == { + "response_id": "resp_123" + } + assert json_attr(span, LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT) == { + "role": "assistant", + "content": "2", + } + assert json_attr(span, LangfuseOtelSpanAttributes.OBSERVATION_USAGE_DETAILS) == { + "input_tokens": 3, + "output_tokens": 1, + "total_tokens": 4, + } + + +@pytest.mark.asyncio +async def test_async_response_retrieve_exports_generation_span( + langfuse_memory_client, get_span, json_attr +): + openai_client = lf_openai.AsyncOpenAI(api_key="test") + response = SimpleNamespace( + model="gpt-4o-mini", + output=[{"role": "assistant", "content": "2"}], + usage=SimpleNamespace(input_tokens=3, output_tokens=1, total_tokens=4), + ) + + async def _get(*args, **kwargs): + return response + + with patch.object(openai_client.responses, "_get", side_effect=_get): + result = await openai_client.responses.retrieve( + response_id="resp_123", + name="unit-openai-async-response-retrieve", + ) + + assert result is response + + langfuse_memory_client.flush() + span = get_span("unit-openai-async-response-retrieve") + + assert span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] == "generation" + assert json_attr(span, LangfuseOtelSpanAttributes.OBSERVATION_INPUT) == { + "response_id": "resp_123" + }