Skip to content

Add function_call OpenTelemetry spans for LLM service tool execution#4266

Open
itsderek23 wants to merge 5 commits into
pipecat-ai:mainfrom
itsderek23:add-function-call-spans
Open

Add function_call OpenTelemetry spans for LLM service tool execution#4266
itsderek23 wants to merge 5 commits into
pipecat-ai:mainfrom
itsderek23:add-function-call-spans

Conversation

@itsderek23
Copy link
Copy Markdown
Contributor

#3885 added function call duration metrics to LatencyBreakdown. This PR makes the same data visible via OTEL function_call spans. These spans are child of the turn span, with the following attributes:

  • tool.function_name
  • tool.call_id
  • tool.arguments
  • tool.result
  • tool.result_status

Implementation notes

I've tried to match existing instrumentation patterns, using a new @traced_function_call decorator in service_decorators.py. One deviation: tool.result and tool.result_status are set inline in the result callback within llm_service.py (4 lines) because I don't believe the decorator can access it.

Deferred work

For async/deferred (pun-intended) function calls, the span ends when _run_function_call() returns, not when the final result callback fires. async span timing is left for a future change.

Verification

This example simulates 200ms function calls. I've verified the values in the spans match.

Full trace output

Function calls via LLMService._run_function_call() were invisible in
traces. This adds a "function_call" span as a child of the turn span
with tool.function_name, tool.call_id, tool.arguments, tool.result,
and tool.result_status attributes, matching Gemini Live conventions.

Note: for async/deferred function calls (cancel_on_interruption=False),
the span ends when _run_function_call() returns, not when the final
result callback fires. This means span duration may undercount for
deferred calls. Accurate async span timing is left for a future change.
Tests for LLMService._run_function_call() tracing don't belong in
test_turn_trace_observer.py. Move them to test_function_call_tracing.py
to match the existing convention of one test file per tested component.
Move the duplicated _InMemorySpanExporter class from both test files
into pipecat.tests.utils, following the existing convention for shared
test utilities.
Move span lifecycle management from inline code in llm_service.py to a
traced_function_call decorator, matching the pattern of traced_llm,
traced_tts, and traced_stt. Only the tool.result capture remains inline
in the callback since the decorator cannot access the result.
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 9, 2026

Codecov Report

❌ Patch coverage is 78.37838% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/pipecat/utils/tracing/service_decorators.py 72.72% 6 Missing ⚠️
src/pipecat/utils/tracing/service_attributes.py 77.77% 2 Missing ⚠️
Files with missing lines Coverage Δ
src/pipecat/services/llm_service.py 60.05% <100.00%> (+6.16%) ⬆️
src/pipecat/utils/tracing/service_attributes.py 8.76% <77.77%> (+3.35%) ⬆️
src/pipecat/utils/tracing/service_decorators.py 15.65% <72.72%> (+2.79%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant