-
-
Notifications
You must be signed in to change notification settings - Fork 10.2k
fix(otel): label retrieval and agent metrics correctly and emit gen_ai.provider.name #35151
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b8266b2
0b26c2c
72a176d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,17 +6,30 @@ | |
| from enum import Enum | ||
| from typing import Final | ||
|
|
||
| from litellm._logging import verbose_logger | ||
|
|
||
|
|
||
| class GenAIOperation(str, Enum): | ||
| """Values for ``gen_ai.operation.name``.""" | ||
| """Values for ``gen_ai.operation.name``. | ||
|
|
||
| The first block is the convention's own vocabulary. The ``LITELLM_`` members | ||
| are vendor values for operations the convention names nothing for; its note | ||
| on this attribute directs instrumentation to use a system-specific name in | ||
| exactly that case, the same allowance :func:`resolve_provider` relies on for | ||
| unmapped providers. They stay under the ``litellm.`` prefix so a value the | ||
| convention adds later can never collide with one of ours. | ||
| """ | ||
|
|
||
| CHAT = "chat" | ||
| TEXT_COMPLETION = "text_completion" | ||
| EMBEDDINGS = "embeddings" | ||
| GENERATE_CONTENT = "generate_content" | ||
| RETRIEVAL = "retrieval" # vector-store search / RAG query spans | ||
| CREATE_AGENT = "create_agent" # reserved for future agent spans | ||
| INVOKE_AGENT = "invoke_agent" # reserved for future agent spans | ||
| INVOKE_AGENT = "invoke_agent" # agent (A2A) message spans | ||
| EXECUTE_TOOL = "execute_tool" # MCP tool-call spans | ||
| LITELLM_VECTOR_STORE_MANAGEMENT = "litellm.vector_store_management" | ||
| LITELLM_VECTOR_STORE_FILE_MANAGEMENT = "litellm.vector_store_file_management" | ||
|
|
||
|
|
||
| class GenAIProvider(str, Enum): | ||
|
|
@@ -49,11 +62,17 @@ class MCPMethod(str, Enum): | |
|
|
||
|
|
||
| class GenAI: | ||
| """Canonical OTel GenAI span-attribute keys.""" | ||
| """Canonical OTel GenAI attribute keys. | ||
|
|
||
| ``SYSTEM`` is the one exception: the convention deprecated it in favor of | ||
| ``PROVIDER_NAME``, and it survives here only so already-shipped series keep | ||
| resolving for consumers that query it. Nothing new should use it. | ||
| """ | ||
|
|
||
| # request | ||
| OPERATION_NAME: Final = "gen_ai.operation.name" | ||
| PROVIDER_NAME: Final = "gen_ai.provider.name" | ||
| SYSTEM: Final = "gen_ai.system" | ||
| REQUEST_MODEL: Final = "gen_ai.request.model" | ||
| REQUEST_TEMPERATURE: Final = "gen_ai.request.temperature" | ||
| REQUEST_TOP_P: Final = "gen_ai.request.top_p" | ||
|
|
@@ -316,6 +335,35 @@ class Metric: | |
| "responses": GenAIOperation.CHAT, | ||
| "aresponses": GenAIOperation.CHAT, | ||
| "call_mcp_tool": GenAIOperation.EXECUTE_TOOL, | ||
| "vector_store_search": GenAIOperation.RETRIEVAL, | ||
| "avector_store_search": GenAIOperation.RETRIEVAL, | ||
| "query": GenAIOperation.RETRIEVAL, | ||
| "aquery": GenAIOperation.RETRIEVAL, | ||
| "send_message": GenAIOperation.INVOKE_AGENT, | ||
| "asend_message": GenAIOperation.INVOKE_AGENT, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Streaming agent still labeled chatMedium Severity
Reviewed by Cursor Bugbot for commit bb885fa. Configure here.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is it legit?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Legit, and worse than it reads. Fixed in The symptom is exactly right: a streamed A2A turn records as
Live proofOne proxy on the real streaming path, no mocks: a real A2A agent (built on the a2a-sdk's own server routes, streaming a task plus three status updates plus a completed status), registered in python agent_server.py 8959 # real A2A agent, /.well-known/agent-card.json + JSON-RPC
export OTEL_ENDPOINT="" OTEL_EXPORTER=console LITELLM_OTEL_V2=1 \
LITELLM_OTEL_INTEGRATION_ENABLE_METRICS=1
python litellm/proxy/proxy_cli.py --config config.yaml --port 4959 > run.log 2>&1 &
for i in 1 2 3; do
curl -sS -N -X POST http://127.0.0.1:4959/a2a/streaming-proof-agent \
-H 'Content-Type: application/json' -H 'Authorization: Bearer sk-1234' \
-d "{\"jsonrpc\":\"2.0\",\"id\":\"$i\",\"method\":\"message/stream\",\"params\":{\"message\":{\"role\":\"user\",\"parts\":[{\"kind\":\"text\",\"text\":\"streamed turn $i\"}],\"messageId\":\"m$i\"}}}"
done
curl -sS -X POST http://127.0.0.1:4959/a2a/streaming-proof-agent \
-H 'Content-Type: application/json' -H 'Authorization: Bearer sk-1234' \
-d '{"jsonrpc":"2.0","id":"4","method":"message/send","params":{"message":{"role":"user","parts":[{"kind":"text","text":"non-streamed turn"}],"messageId":"m4"}}}'All four calls HTTP 200 at every SHA, and the agent streamed its chunks back through the proxy each time. Datapoint attribute sets are counted by brace-matching each Before, at With only the map entry added, which is the fix the finding implies: Unchanged, because the call type was never arriving. After, at Not one datapoint reads TestsThree, each mutation-checked by reverting the production line and rerunning:
Reverting the map entry alone fails 3, reverting the stamping alone fails 1; both restored, One thing deliberately left aloneThe streamed datapoints carry no |
||
| "asend_message_streaming": GenAIOperation.INVOKE_AGENT, | ||
| "vector_store_create": GenAIOperation.LITELLM_VECTOR_STORE_MANAGEMENT, | ||
| "avector_store_create": GenAIOperation.LITELLM_VECTOR_STORE_MANAGEMENT, | ||
| "vector_store_retrieve": GenAIOperation.LITELLM_VECTOR_STORE_MANAGEMENT, | ||
| "avector_store_retrieve": GenAIOperation.LITELLM_VECTOR_STORE_MANAGEMENT, | ||
| "vector_store_list": GenAIOperation.LITELLM_VECTOR_STORE_MANAGEMENT, | ||
| "avector_store_list": GenAIOperation.LITELLM_VECTOR_STORE_MANAGEMENT, | ||
| "vector_store_update": GenAIOperation.LITELLM_VECTOR_STORE_MANAGEMENT, | ||
| "avector_store_update": GenAIOperation.LITELLM_VECTOR_STORE_MANAGEMENT, | ||
| "vector_store_delete": GenAIOperation.LITELLM_VECTOR_STORE_MANAGEMENT, | ||
| "avector_store_delete": GenAIOperation.LITELLM_VECTOR_STORE_MANAGEMENT, | ||
| "vector_store_file_create": GenAIOperation.LITELLM_VECTOR_STORE_FILE_MANAGEMENT, | ||
| "avector_store_file_create": GenAIOperation.LITELLM_VECTOR_STORE_FILE_MANAGEMENT, | ||
| "vector_store_file_list": GenAIOperation.LITELLM_VECTOR_STORE_FILE_MANAGEMENT, | ||
| "avector_store_file_list": GenAIOperation.LITELLM_VECTOR_STORE_FILE_MANAGEMENT, | ||
| "vector_store_file_retrieve": GenAIOperation.LITELLM_VECTOR_STORE_FILE_MANAGEMENT, | ||
| "avector_store_file_retrieve": GenAIOperation.LITELLM_VECTOR_STORE_FILE_MANAGEMENT, | ||
| "vector_store_file_content": GenAIOperation.LITELLM_VECTOR_STORE_FILE_MANAGEMENT, | ||
| "avector_store_file_content": GenAIOperation.LITELLM_VECTOR_STORE_FILE_MANAGEMENT, | ||
| "vector_store_file_update": GenAIOperation.LITELLM_VECTOR_STORE_FILE_MANAGEMENT, | ||
| "avector_store_file_update": GenAIOperation.LITELLM_VECTOR_STORE_FILE_MANAGEMENT, | ||
| "vector_store_file_delete": GenAIOperation.LITELLM_VECTOR_STORE_FILE_MANAGEMENT, | ||
| "avector_store_file_delete": GenAIOperation.LITELLM_VECTOR_STORE_FILE_MANAGEMENT, | ||
| } | ||
|
|
||
|
|
||
|
|
@@ -332,7 +380,21 @@ def resolve_provider(custom_llm_provider: str | None) -> str: | |
|
|
||
|
|
||
| def resolve_operation(call_type: str | None) -> GenAIOperation: | ||
| """Map a litellm ``call_type`` to a ``gen_ai.operation.name`` value.""" | ||
| """Map a litellm ``call_type`` to a ``gen_ai.operation.name`` value. | ||
|
|
||
| An unmapped call type still falls back to ``chat`` so every series keeps an | ||
| operation label, but it logs at debug rather than falling through silently: | ||
| a new call type mislabelled as ``chat`` mixes its latency and cost into | ||
| everyone's chat charts, which is invisible until someone reads the numbers. | ||
| """ | ||
| if not call_type: | ||
| return GenAIOperation.CHAT | ||
| return _OPERATION_BY_CALL_TYPE.get(call_type.lower(), GenAIOperation.CHAT) | ||
| mapped = _OPERATION_BY_CALL_TYPE.get(call_type.lower()) | ||
| if mapped is not None: | ||
| return mapped | ||
| verbose_logger.debug( | ||
| "otel: call_type %r has no gen_ai.operation.name mapping; labelling it %r. Add it to _OPERATION_BY_CALL_TYPE.", | ||
| call_type, | ||
| GenAIOperation.CHAT.value, | ||
| ) | ||
| return GenAIOperation.CHAT | ||


Uh oh!
There was an error while loading. Please reload this page.