Skip to content

Commit 4c1abdf

Browse files
authored
Store OpenAI Responses streaming response in events format understood by UI (#1479)
1 parent 6ac69e5 commit 4c1abdf

File tree

5 files changed

+40
-126
lines changed

5 files changed

+40
-126
lines changed

logfire/_internal/integrations/llm_providers/llm_provider.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,6 @@ def record_chunk(chunk: Any) -> None:
201201
duration = (timer() - start) / ONE_SECOND_IN_NANOSECONDS
202202
logire_llm.info(
203203
'streaming response from {request_data[model]!r} took {duration:.2f}s',
204-
**span_data,
205204
duration=duration,
206-
response_data=stream_state.get_response_data(),
205+
**stream_state.get_attributes(span_data),
207206
)

logfire/_internal/integrations/llm_providers/openai.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,15 @@ def get_endpoint_config(options: FinalRequestOptions) -> EndpointConfig:
5555
if is_current_agent_span('Responses API', 'Responses API with {gen_ai.request.model!r}'):
5656
return EndpointConfig(message_template='', span_data={})
5757

58+
stream = json_data.get('stream', False) # type: ignore
5859
span_data: dict[str, Any] = {
5960
'gen_ai.request.model': json_data['model'],
60-
}
61-
if json_data.get('stream'): # type: ignore
62-
span_data['request_data'] = json_data
63-
else:
64-
span_data['events'] = inputs_to_events(
61+
'request_data': {'model': json_data['model'], 'stream': stream},
62+
'events': inputs_to_events(
6563
json_data['input'], # type: ignore
6664
json_data.get('instructions'), # type: ignore
67-
)
65+
),
66+
}
6867

6968
return EndpointConfig(
7069
message_template='Responses API with {gen_ai.request.model!r}',
@@ -140,6 +139,11 @@ def get_response_data(self) -> Any:
140139

141140
return response
142141

142+
def get_attributes(self, span_data: dict[str, Any]) -> dict[str, Any]:
143+
response = self.get_response_data()
144+
span_data['events'] = span_data['events'] + responses_output_events(response)
145+
return span_data
146+
143147

144148
try:
145149
# ChatCompletionStreamState only exists in openai>=1.40.0

logfire/_internal/integrations/llm_providers/types.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ def record_chunk(self, chunk: Any) -> None:
1717
def get_response_data(self) -> Any:
1818
"""Returns the response data for including in the log."""
1919

20+
def get_attributes(self, span_data: dict[str, Any]) -> dict[str, Any]:
21+
"""Attributes to include in the log."""
22+
return dict(**span_data, response_data=self.get_response_data())
23+
2024

2125
class EndpointConfig(NamedTuple):
2226
"""The configuration for the endpoint of a provider based on request url."""

tests/otel_integrations/test_anthropic.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ def test_sync_message_empty_response_chunk(instrumented_client: anthropic.Anthro
343343
'logfire.tags': ('LLM',),
344344
'duration': 1.0,
345345
'response_data': '{"combined_chunk_content":"","chunk_count":0}',
346-
'logfire.json_schema': '{"type":"object","properties":{"request_data":{"type":"object"},"async":{},"duration":{},"response_data":{"type":"object"}}}',
346+
'logfire.json_schema': '{"type":"object","properties":{"duration":{},"request_data":{"type":"object"},"async":{},"response_data":{"type":"object"}}}',
347347
},
348348
},
349349
]
@@ -405,7 +405,7 @@ def test_sync_messages_stream(instrumented_client: anthropic.Anthropic, exporter
405405
'logfire.tags': ('LLM',),
406406
'duration': 1.0,
407407
'response_data': '{"combined_chunk_content":"The answer is secret","chunk_count":2}',
408-
'logfire.json_schema': '{"type":"object","properties":{"request_data":{"type":"object"},"async":{},"duration":{},"response_data":{"type":"object"}}}',
408+
'logfire.json_schema': '{"type":"object","properties":{"duration":{},"request_data":{"type":"object"},"async":{},"response_data":{"type":"object"}}}',
409409
},
410410
},
411411
]
@@ -470,7 +470,7 @@ async def test_async_messages_stream(
470470
'logfire.tags': ('LLM',),
471471
'duration': 1.0,
472472
'response_data': '{"combined_chunk_content":"The answer is secret","chunk_count":2}',
473-
'logfire.json_schema': '{"type":"object","properties":{"request_data":{"type":"object"},"async":{},"duration":{},"response_data":{"type":"object"}}}',
473+
'logfire.json_schema': '{"type":"object","properties":{"duration":{},"request_data":{"type":"object"},"async":{},"response_data":{"type":"object"}}}',
474474
},
475475
},
476476
]

tests/otel_integrations/test_openai.py

Lines changed: 22 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1489,8 +1489,11 @@ def test_responses_stream(exporter: TestExporter) -> None:
14891489
'code.filepath': 'test_openai.py',
14901490
'code.function': 'test_responses_stream',
14911491
'code.lineno': 123,
1492-
'request_data': {'input': 'What is four plus five?', 'model': 'gpt-4.1', 'stream': True},
1492+
'request_data': {'model': 'gpt-4.1', 'stream': True},
14931493
'gen_ai.request.model': 'gpt-4.1',
1494+
'events': [
1495+
{'event.name': 'gen_ai.user.message', 'content': 'What is four plus five?', 'role': 'user'}
1496+
],
14941497
'async': False,
14951498
'logfire.msg_template': 'Responses API with {gen_ai.request.model!r}',
14961499
'logfire.msg': "Responses API with 'gpt-4.1'",
@@ -1499,6 +1502,7 @@ def test_responses_stream(exporter: TestExporter) -> None:
14991502
'properties': {
15001503
'request_data': {'type': 'object'},
15011504
'gen_ai.request.model': {},
1505+
'events': {'type': 'array'},
15021506
'async': {},
15031507
},
15041508
},
@@ -1521,131 +1525,30 @@ def test_responses_stream(exporter: TestExporter) -> None:
15211525
'code.filepath': 'test_openai.py',
15221526
'code.function': 'test_responses_stream',
15231527
'code.lineno': 123,
1524-
'request_data': {'input': 'What is four plus five?', 'model': 'gpt-4.1', 'stream': True},
1528+
'request_data': {'model': 'gpt-4.1', 'stream': True},
15251529
'gen_ai.request.model': 'gpt-4.1',
15261530
'async': False,
15271531
'duration': 1.0,
1528-
'response_data': {
1529-
'id': 'resp_079fceed100a827c0068e011e9cefc81969ea6a843546705e6',
1530-
'created_at': 1759515113.0,
1531-
'error': None,
1532-
'incomplete_details': None,
1533-
'instructions': None,
1534-
'metadata': {},
1535-
'model': 'gpt-4.1-2025-04-14',
1536-
'object': 'response',
1537-
'output': [
1538-
{
1539-
'id': 'msg_079fceed100a827c0068e011ea7d388196a588ec8cf09b1364',
1540-
'content': [
1541-
{
1542-
'annotations': [],
1543-
'text': 'Four plus five equals **nine**.',
1544-
'type': 'output_text',
1545-
'logprobs': [],
1546-
'parsed': None,
1547-
}
1548-
],
1549-
'role': 'assistant',
1550-
'status': 'completed',
1551-
'type': 'message',
1552-
}
1553-
],
1554-
'parallel_tool_calls': True,
1555-
'temperature': 1.0,
1556-
'tool_choice': 'auto',
1557-
'tools': [],
1558-
'top_p': 1.0,
1559-
'background': False,
1560-
'conversation': None,
1561-
'max_output_tokens': None,
1562-
'max_tool_calls': None,
1563-
'previous_response_id': None,
1564-
'prompt': None,
1565-
'prompt_cache_key': None,
1566-
'reasoning': {'effort': None, 'generate_summary': None, 'summary': None},
1567-
'safety_identifier': None,
1568-
'service_tier': 'default',
1569-
'status': 'completed',
1570-
'text': {'format': {'type': 'text'}, 'verbosity': 'medium'},
1571-
'top_logprobs': 0,
1572-
'truncation': 'disabled',
1573-
'usage': {
1574-
'input_tokens': 13,
1575-
'input_tokens_details': {'cached_tokens': 0},
1576-
'output_tokens': 9,
1577-
'output_tokens_details': {'reasoning_tokens': 0},
1578-
'total_tokens': 22,
1532+
'events': [
1533+
{
1534+
'event.name': 'gen_ai.user.message',
1535+
'content': 'What is four plus five?',
1536+
'role': 'user',
15791537
},
1580-
'user': None,
1581-
'store': True,
1582-
},
1538+
{
1539+
'event.name': 'gen_ai.assistant.message',
1540+
'content': 'Four plus five equals **nine**.',
1541+
'role': 'assistant',
1542+
},
1543+
],
15831544
'logfire.json_schema': {
15841545
'type': 'object',
15851546
'properties': {
15861547
'request_data': {'type': 'object'},
15871548
'gen_ai.request.model': {},
15881549
'async': {},
1550+
'events': {'type': 'array'},
15891551
'duration': {},
1590-
'response_data': {
1591-
'type': 'object',
1592-
'title': 'ParsedResponse[NoneType]',
1593-
'x-python-datatype': 'PydanticModel',
1594-
'properties': {
1595-
'output': {
1596-
'type': 'array',
1597-
'items': {
1598-
'type': 'object',
1599-
'title': 'ParsedResponseOutputMessage[NoneType]',
1600-
'x-python-datatype': 'PydanticModel',
1601-
'properties': {
1602-
'content': {
1603-
'type': 'array',
1604-
'items': {
1605-
'type': 'object',
1606-
'title': 'ParsedResponseOutputText[NoneType]',
1607-
'x-python-datatype': 'PydanticModel',
1608-
},
1609-
}
1610-
},
1611-
},
1612-
},
1613-
'reasoning': {
1614-
'type': 'object',
1615-
'title': 'Reasoning',
1616-
'x-python-datatype': 'PydanticModel',
1617-
},
1618-
'text': {
1619-
'type': 'object',
1620-
'title': 'ResponseTextConfig',
1621-
'x-python-datatype': 'PydanticModel',
1622-
'properties': {
1623-
'format': {
1624-
'type': 'object',
1625-
'title': 'ResponseFormatText',
1626-
'x-python-datatype': 'PydanticModel',
1627-
}
1628-
},
1629-
},
1630-
'usage': {
1631-
'type': 'object',
1632-
'title': 'ResponseUsage',
1633-
'x-python-datatype': 'PydanticModel',
1634-
'properties': {
1635-
'input_tokens_details': {
1636-
'type': 'object',
1637-
'title': 'InputTokensDetails',
1638-
'x-python-datatype': 'PydanticModel',
1639-
},
1640-
'output_tokens_details': {
1641-
'type': 'object',
1642-
'title': 'OutputTokensDetails',
1643-
'x-python-datatype': 'PydanticModel',
1644-
},
1645-
},
1646-
},
1647-
},
1648-
},
16491552
},
16501553
},
16511554
'logfire.tags': ('LLM',),
@@ -2315,6 +2218,7 @@ def test_responses_api(exporter: TestExporter) -> None:
23152218
'code.function': 'test_responses_api',
23162219
'code.lineno': 123,
23172220
'async': False,
2221+
'request_data': {'model': 'gpt-4.1', 'stream': False},
23182222
'logfire.msg_template': 'Responses API with {gen_ai.request.model!r}',
23192223
'logfire.msg': "Responses API with 'gpt-4.1'",
23202224
'logfire.tags': ('LLM',),
@@ -2348,6 +2252,7 @@ def test_responses_api(exporter: TestExporter) -> None:
23482252
'type': 'object',
23492253
'properties': {
23502254
'gen_ai.request.model': {},
2255+
'request_data': {'type': 'object'},
23512256
'events': {'type': 'array'},
23522257
'async': {},
23532258
'gen_ai.system': {},
@@ -2370,6 +2275,7 @@ def test_responses_api(exporter: TestExporter) -> None:
23702275
'code.function': 'test_responses_api',
23712276
'code.lineno': 123,
23722277
'async': False,
2278+
'request_data': {'model': 'gpt-4.1', 'stream': False},
23732279
'logfire.msg_template': 'Responses API with {gen_ai.request.model!r}',
23742280
'logfire.msg': "Responses API with 'gpt-4.1'",
23752281
'logfire.tags': ('LLM',),
@@ -2414,6 +2320,7 @@ def test_responses_api(exporter: TestExporter) -> None:
24142320
'type': 'object',
24152321
'properties': {
24162322
'gen_ai.request.model': {},
2323+
'request_data': {'type': 'object'},
24172324
'events': {'type': 'array'},
24182325
'async': {},
24192326
'gen_ai.system': {},

0 commit comments

Comments
 (0)