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
24 changes: 15 additions & 9 deletions litellm/integrations/langfuse/langfuse_otel.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,19 +196,25 @@ def _set_observation_output(span: Span, response_obj):
"content": getattr(getattr(item, "content", [{}])[0], "text", ""),
}
)
elif item_type == "function_call":
arguments_str = getattr(item, "arguments", "{}")
arguments_obj = (
safe_json_loads(arguments_str, default={})
if isinstance(arguments_str, str)
else arguments_str
)
elif item_type in ("function_call", "custom_tool_call"):
# A custom (freeform) tool carries its payload as a raw string under `input`,
# where a function call carries a JSON string under `arguments`.
if item_type == "custom_tool_call":
payload_key, payload_value = "input", getattr(item, "input", "")
else:
arguments_str = getattr(item, "arguments", "{}")
payload_key = "arguments"
payload_value = (
safe_json_loads(arguments_str, default={})
if isinstance(arguments_str, str)
else arguments_str
)
langfuse_tool_call = {
"id": getattr(item, "id", ""),
"name": getattr(item, "name", ""),
"call_id": getattr(item, "call_id", ""),
"type": "function_call",
"arguments": arguments_obj,
"type": item_type,
payload_key: payload_value,
}
output_items_data.append(langfuse_tool_call)
if output_items_data:
Expand Down
53 changes: 53 additions & 0 deletions tests/unit/integrations/test_langfuse_otel.py
Original file line number Diff line number Diff line change
Expand Up @@ -1005,6 +1005,59 @@ def test_responses_api_function_call_with_redacted_arguments(self):
assert output_data[0]["name"] == "get_weather"
assert output_data[0]["arguments"] == {}

def test_responses_api_with_custom_tool_calls(self):
"""Test Langfuse OTEL logger with Responses API custom_tool_call output."""
from litellm.types.integrations.langfuse_otel import LangfuseSpanAttributes
from litellm.types.responses.main import CustomToolCallOutputItem

# Create Responses API response with a custom (freeform) tool call
response_obj = ResponsesAPIResponse(
id="response-790",
created_at=1625247700,
output=[
CustomToolCallOutputItem(
id="ctc-123",
type="custom_tool_call",
name="exec",
call_id="call-def",
input="console.log('hello')",
status="completed",
)
],
)

kwargs = {
"call_type": "responses",
"messages": [{"role": "user", "content": "Print hello"}],
"model": "gpt-4o",
"optional_params": {},
}

mock_span = MagicMock()

with patch(
"litellm.integrations.arize._utils.safe_set_attribute"
) as mock_safe_set_attribute:
LangfuseOtelLogger._set_langfuse_specific_attributes(
mock_span, kwargs, response_obj
)

output_calls = [
call
for call in mock_safe_set_attribute.call_args_list
if call.args[1] == LangfuseSpanAttributes.OBSERVATION_OUTPUT.value
]

assert len(output_calls) > 0, "observation.output should be set"
output_data = json.loads(output_calls[0].args[2])
assert isinstance(output_data, list)
assert len(output_data) == 1
assert output_data[0]["type"] == "custom_tool_call"
assert output_data[0]["id"] == "ctc-123"
assert output_data[0]["name"] == "exec"
assert output_data[0]["call_id"] == "call-def"
assert output_data[0]["input"] == "console.log('hello')"


if __name__ == "__main__":
pytest.main([__file__])
Loading