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
22 changes: 21 additions & 1 deletion langfuse/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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)
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 @@ -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"
}