Skip to content

Commit 11056d7

Browse files
committed
fix(litellm): Set operation name from call type instead of chat fallback
The litellm integration always fell back to the chat operation name, so text completions, embeddings and responses calls were all recorded as chat. Map the operation from the call type instead, and skip instrumentation for call types we do not know.
1 parent 064542d commit 11056d7

2 files changed

Lines changed: 140 additions & 19 deletions

File tree

sentry_sdk/integrations/litellm.py

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
if TYPE_CHECKING:
2424
from datetime import datetime
25-
from typing import Any, Dict, List
25+
from typing import Any, Dict, List, Tuple
2626

2727
try:
2828
import litellm # type: ignore[import-not-found]
@@ -35,6 +35,19 @@
3535
# to every callback, so it lives and dies with the request.
3636
_SPAN_KEY = "_sentry_span"
3737

38+
# Call types whose gen_ai operation name we can determine accurately. Everything
39+
# else is not instrumented, since guessing records wrong data.
40+
_CALL_TYPE_OPERATIONS: "Dict[Any, Tuple[str, str]]" = {
41+
"completion": ("chat", consts.OP.GEN_AI_CHAT),
42+
"acompletion": ("chat", consts.OP.GEN_AI_CHAT),
43+
"text_completion": ("text_completion", consts.OP.GEN_AI_TEXT_COMPLETION),
44+
"atext_completion": ("text_completion", consts.OP.GEN_AI_TEXT_COMPLETION),
45+
"embedding": ("embeddings", consts.OP.GEN_AI_EMBEDDINGS),
46+
"aembedding": ("embeddings", consts.OP.GEN_AI_EMBEDDINGS),
47+
"responses": ("responses", consts.OP.GEN_AI_RESPONSES),
48+
"aresponses": ("responses", consts.OP.GEN_AI_RESPONSES),
49+
}
50+
3851

3952
def _store_span(kwargs: "Dict[str, Any]", span: "Any") -> None:
4053
kwargs[_SPAN_KEY] = span
@@ -83,6 +96,12 @@ def _input_callback(kwargs: "Dict[str, Any]") -> None:
8396
if integration is None:
8497
return
8598

99+
call_type = kwargs.get("call_type", None)
100+
if call_type not in _CALL_TYPE_OPERATIONS:
101+
return
102+
103+
operation, span_op = _CALL_TYPE_OPERATIONS[call_type]
104+
86105
# Get key parameters
87106
full_model = kwargs.get("model", "")
88107
try:
@@ -91,33 +110,21 @@ def _input_callback(kwargs: "Dict[str, Any]") -> None:
91110
model = full_model
92111
provider = "unknown"
93112

94-
call_type = kwargs.get("call_type", None)
95-
if call_type == "embedding" or call_type == "aembedding":
96-
operation = "embeddings"
97-
else:
98-
operation = "chat"
113+
span_name = f"{operation} {model}"
99114

100115
# Start a new span/transaction
101116
if has_span_streaming_enabled(client.options):
102117
span = sentry_sdk.traces.start_span(
103-
name=f"{operation} {model}",
118+
name=span_name,
104119
attributes={
105-
"sentry.op": (
106-
consts.OP.GEN_AI_CHAT
107-
if operation == "chat"
108-
else consts.OP.GEN_AI_EMBEDDINGS
109-
),
120+
"sentry.op": span_op,
110121
"sentry.origin": LiteLLMIntegration.origin,
111122
},
112123
)
113124
else:
114125
span = get_start_span_function()(
115-
op=(
116-
consts.OP.GEN_AI_CHAT
117-
if operation == "chat"
118-
else consts.OP.GEN_AI_EMBEDDINGS
119-
),
120-
name=f"{operation} {model}",
126+
op=span_op,
127+
name=span_name,
121128
origin=LiteLLMIntegration.origin,
122129
)
123130
span.__enter__()

tests/integrations/litellm/test_litellm.py

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ async def __call__(self, *args, **kwargs):
3434
from litellm.litellm_core_utils.logging_worker import GLOBAL_LOGGING_WORKER
3535
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
3636
from openai import AsyncOpenAI, OpenAI
37-
from openai.types import CompletionUsage
37+
from openai.types import Completion, CompletionUsage
38+
from openai.types.completion_choice import CompletionChoice
3839

3940
from sentry_sdk import start_transaction
4041
from sentry_sdk._types import BLOB_DATA_SUBSTITUTE
@@ -2651,6 +2652,7 @@ def test_response_without_usage(
26512652
kwargs = {
26522653
"model": "gpt-3.5-turbo",
26532654
"messages": messages,
2655+
"call_type": "completion",
26542656
}
26552657

26562658
_input_callback(kwargs)
@@ -2674,6 +2676,7 @@ def test_response_without_usage(
26742676
kwargs = {
26752677
"model": "gpt-3.5-turbo",
26762678
"messages": messages,
2679+
"call_type": "completion",
26772680
}
26782681

26792682
_input_callback(kwargs)
@@ -2733,6 +2736,7 @@ def test_litellm_message_truncation(sentry_init, capture_events):
27332736
kwargs = {
27342737
"model": "gpt-3.5-turbo",
27352738
"messages": messages,
2739+
"call_type": "completion",
27362740
}
27372741

27382742
_input_callback(kwargs)
@@ -3847,3 +3851,113 @@ def test_embeddings_data_collection(
38473851
assert span_data[SPANDATA.GEN_AI_OPERATION_NAME] == "embeddings"
38483852
assert span_data[SPANDATA.GEN_AI_REQUEST_MODEL] == "text-embedding-ada-002"
38493853
assert span_data[SPANDATA.GEN_AI_USAGE_INPUT_TOKENS] == 5
3854+
3855+
3856+
def test_text_completion_operation_name(
3857+
sentry_init,
3858+
capture_events,
3859+
get_model_response,
3860+
reset_litellm_executor,
3861+
):
3862+
"""text_completion calls get the text_completion op and record their prompt."""
3863+
sentry_init(
3864+
integrations=[LiteLLMIntegration(include_prompts=True)],
3865+
disabled_integrations=[StdlibIntegration],
3866+
traces_sample_rate=1.0,
3867+
send_default_pii=True,
3868+
stream_gen_ai_spans=False,
3869+
)
3870+
events = capture_events()
3871+
3872+
client = OpenAI(api_key="test-key")
3873+
3874+
model_response = get_model_response(
3875+
Completion(
3876+
id="cmpl-test",
3877+
choices=[
3878+
CompletionChoice(finish_reason="stop", index=0, text="Test response")
3879+
],
3880+
created=1234567890,
3881+
model="gpt-3.5-turbo-instruct",
3882+
object="text_completion",
3883+
usage=CompletionUsage(
3884+
prompt_tokens=10,
3885+
completion_tokens=20,
3886+
total_tokens=30,
3887+
),
3888+
),
3889+
serialize_pydantic=True,
3890+
request_headers={"X-Stainless-Raw-Response": "true"},
3891+
)
3892+
3893+
with mock.patch.object(
3894+
client.completions._client._client,
3895+
"send",
3896+
return_value=model_response,
3897+
), start_transaction(name="litellm test"):
3898+
litellm.text_completion(
3899+
model="gpt-3.5-turbo-instruct",
3900+
prompt="Hello!",
3901+
client=client,
3902+
)
3903+
3904+
litellm_utils.executor.shutdown(wait=True)
3905+
3906+
(event,) = events
3907+
(span,) = [s for s in event["spans"] if s["origin"] == "auto.ai.litellm"]
3908+
3909+
assert span["op"] == OP.GEN_AI_TEXT_COMPLETION
3910+
assert span["description"] == "text_completion gpt-3.5-turbo-instruct"
3911+
assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "text_completion"
3912+
assert json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) == [
3913+
{"role": "user", "content": "Hello!"}
3914+
]
3915+
3916+
3917+
def test_responses_operation_name(
3918+
sentry_init,
3919+
capture_events,
3920+
get_model_response,
3921+
nonstreaming_responses_model_response,
3922+
reset_litellm_executor,
3923+
):
3924+
"""Responses API calls get the responses op and record their input."""
3925+
sentry_init(
3926+
integrations=[LiteLLMIntegration(include_prompts=True)],
3927+
disabled_integrations=[StdlibIntegration],
3928+
traces_sample_rate=1.0,
3929+
send_default_pii=True,
3930+
stream_gen_ai_spans=False,
3931+
)
3932+
events = capture_events()
3933+
3934+
client = HTTPHandler()
3935+
3936+
model_response = get_model_response(
3937+
nonstreaming_responses_model_response,
3938+
serialize_pydantic=True,
3939+
)
3940+
3941+
with mock.patch.object(
3942+
client,
3943+
"post",
3944+
return_value=model_response,
3945+
), start_transaction(name="litellm test"):
3946+
litellm.responses(
3947+
model="gpt-4",
3948+
input="Hello!",
3949+
client=client,
3950+
api_key="test-key",
3951+
)
3952+
3953+
litellm_utils.executor.shutdown(wait=True)
3954+
3955+
(event,) = events
3956+
(span,) = [s for s in event["spans"] if s["origin"] == "auto.ai.litellm"]
3957+
3958+
assert span["op"] == OP.GEN_AI_RESPONSES
3959+
assert span["description"] == "responses gpt-4"
3960+
assert span["data"][SPANDATA.GEN_AI_OPERATION_NAME] == "responses"
3961+
assert json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES]) == [
3962+
{"role": "user", "content": "Hello!"}
3963+
]

0 commit comments

Comments
 (0)