mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
fix(langfuse_otel): keep every reasoning summary in the derived observation output
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
dfd956a788
commit
98964dca87
2 changed files with 58 additions and 30 deletions
|
|
@ -155,7 +155,13 @@ class LangfuseOtelLogger(OpenTelemetry):
|
|||
|
||||
expanded: Final = tuple(_default_tag(key) for key in default_tags) if isinstance(default_tags, list) else ()
|
||||
candidates: Final = (
|
||||
(tuple(tag for tag in caller_tags if isinstance(tag, str)) if isinstance(caller_tags, list) else ())
|
||||
(
|
||||
(caller_tags,)
|
||||
if isinstance(caller_tags, str)
|
||||
else tuple(tag for tag in caller_tags if isinstance(tag, str))
|
||||
if isinstance(caller_tags, list)
|
||||
else ()
|
||||
)
|
||||
+ (tuple(tag for tag in request_tags if isinstance(tag, str)) if isinstance(request_tags, list) else ())
|
||||
+ tuple(tag for tag in expanded if tag is not None)
|
||||
)
|
||||
|
|
@ -447,35 +453,35 @@ def _extract_output_items(response_obj) -> str | None:
|
|||
output: Final = response_obj.get("output", [])
|
||||
if not output:
|
||||
return None
|
||||
output_items: Final = tuple(_output_item(item) for item in output)
|
||||
rendered: Final = tuple(item for item in output_items if item is not None)
|
||||
rendered: Final = tuple(entry for item in output for entry in _output_items(item))
|
||||
return safe_dumps(list(rendered)) if rendered else None
|
||||
|
||||
|
||||
def _output_item(item) -> dict | None:
|
||||
def _output_items(item) -> tuple[dict, ...]:
|
||||
if not hasattr(item, "type"):
|
||||
return None
|
||||
return ()
|
||||
if item.type == "reasoning" and hasattr(item, "summary"):
|
||||
return next(
|
||||
(
|
||||
{"role": "reasoning_summary", "content": summary.text}
|
||||
for summary in item.summary
|
||||
if hasattr(summary, "text")
|
||||
),
|
||||
None,
|
||||
return tuple(
|
||||
{"role": "reasoning_summary", "content": summary.text}
|
||||
for summary in item.summary
|
||||
if hasattr(summary, "text")
|
||||
)
|
||||
if item.type == "message":
|
||||
return {
|
||||
"role": getattr(item, "role", "assistant"),
|
||||
"content": getattr(getattr(item, "content", [{}])[0], "text", ""),
|
||||
}
|
||||
return (
|
||||
{
|
||||
"role": getattr(item, "role", "assistant"),
|
||||
"content": getattr(getattr(item, "content", [{}])[0], "text", ""),
|
||||
},
|
||||
)
|
||||
if item.type == "function_call":
|
||||
arguments: Final = getattr(item, "arguments", "{}")
|
||||
return {
|
||||
"id": getattr(item, "id", ""),
|
||||
"name": getattr(item, "name", ""),
|
||||
"call_id": getattr(item, "call_id", ""),
|
||||
"type": "function_call",
|
||||
"arguments": safe_json_loads(arguments, default={}) if isinstance(arguments, str) else arguments,
|
||||
}
|
||||
return None
|
||||
return (
|
||||
{
|
||||
"id": getattr(item, "id", ""),
|
||||
"name": getattr(item, "name", ""),
|
||||
"call_id": getattr(item, "call_id", ""),
|
||||
"type": "function_call",
|
||||
"arguments": safe_json_loads(arguments, default={}) if isinstance(arguments, str) else arguments,
|
||||
},
|
||||
)
|
||||
return ()
|
||||
|
|
|
|||
|
|
@ -774,7 +774,11 @@ class TestLangfuseOtelResponsesAPI:
|
|||
Summary(
|
||||
text="Let me analyze this problem step by step...",
|
||||
type="summary_text",
|
||||
)
|
||||
),
|
||||
Summary(
|
||||
text="Now checking the forecast data.",
|
||||
type="summary_text",
|
||||
),
|
||||
],
|
||||
),
|
||||
ResponseOutputMessage(
|
||||
|
|
@ -816,17 +820,26 @@ class TestLangfuseOtelResponsesAPI:
|
|||
output_json = output_calls[0].args[2]
|
||||
output_data = json.loads(output_json)
|
||||
|
||||
# Verify output contains reasoning and message
|
||||
# Verify output contains both reasoning summaries and the message
|
||||
assert isinstance(output_data, list)
|
||||
assert len(output_data) == 2
|
||||
assert len(output_data) == 3
|
||||
|
||||
# Verify reasoning summary
|
||||
assert output_data[0]["role"] == "reasoning_summary"
|
||||
assert output_data[0]["content"] == "Let me analyze this problem step by step..."
|
||||
assert output_data[1]["role"] == "reasoning_summary"
|
||||
assert output_data[1]["content"] == "Now checking the forecast data."
|
||||
|
||||
# Verify message
|
||||
assert output_data[1]["role"] == "assistant"
|
||||
assert output_data[1]["content"] == "The weather in San Francisco is sunny, 20°C."
|
||||
assert output_data[2]["role"] == "assistant"
|
||||
assert output_data[2]["content"] == "The weather in San Francisco is sunny, 20°C."
|
||||
|
||||
trace_output_calls = [
|
||||
call
|
||||
for call in mock_safe_set_attribute.call_args_list
|
||||
if call.args[1] == LangfuseSpanAttributes.TRACE_OUTPUT.value
|
||||
]
|
||||
assert len(trace_output_calls) > 0, "trace.output should be set"
|
||||
assert trace_output_calls[0].args[2] == output_json
|
||||
|
||||
def test_responses_api_with_function_calls(self):
|
||||
"""Test Langfuse OTEL logger with Responses API function_call output."""
|
||||
|
|
@ -1067,6 +1080,15 @@ class TestDerivedTraceFields:
|
|||
"user_api_key_alias:k1",
|
||||
]
|
||||
|
||||
def test_caller_tags_string_is_a_single_tag(self):
|
||||
attributes = _emitted(
|
||||
{
|
||||
"call_type": "acompletion",
|
||||
"litellm_params": {"metadata": {"tags": "single"}},
|
||||
}
|
||||
)
|
||||
assert json.loads(attributes["langfuse.trace.tags"]) == ["single"]
|
||||
|
||||
def test_no_tags_anywhere_leaves_trace_tags_unset(self):
|
||||
attributes = _emitted(
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue