Skip to content

Commit 09c5ca7

Browse files
committed
feat(integrations): add litellm integration
1 parent 3a43dc9 commit 09c5ca7

File tree

2 files changed

+282
-0
lines changed

2 files changed

+282
-0
lines changed

sentry_sdk/integrations/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ def iter_default_integrations(with_auto_enabling_integrations):
9797
"sentry_sdk.integrations.langchain.LangchainIntegration",
9898
"sentry_sdk.integrations.langgraph.LanggraphIntegration",
9999
"sentry_sdk.integrations.litestar.LitestarIntegration",
100+
"sentry_sdk.integrations.litellm.LiteLLMIntegration",
100101
"sentry_sdk.integrations.loguru.LoguruIntegration",
101102
"sentry_sdk.integrations.openai.OpenAIIntegration",
102103
"sentry_sdk.integrations.pymongo.PyMongoIntegration",

sentry_sdk/integrations/litellm.py

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
from typing import TYPE_CHECKING
2+
3+
import sentry_sdk
4+
from sentry_sdk import consts
5+
from sentry_sdk.ai.monitoring import record_token_usage
6+
from sentry_sdk.ai.utils import get_start_span_function, set_data_normalized
7+
from sentry_sdk.consts import SPANDATA
8+
from sentry_sdk.integrations import DidNotEnable, Integration
9+
from sentry_sdk.scope import should_send_default_pii
10+
from sentry_sdk.utils import event_from_exception
11+
12+
if TYPE_CHECKING:
13+
from typing import Any, Dict
14+
from datetime import datetime
15+
16+
try:
17+
import litellm
18+
except ImportError:
19+
raise DidNotEnable("LiteLLM not installed")
20+
21+
22+
def _get_provider_from_model(model):
23+
# type: (str) -> str
24+
"""Extract provider name from model string using LiteLLM's logic"""
25+
if not model:
26+
return "unknown"
27+
28+
# Common provider prefixes/patterns
29+
if model.startswith("gpt-") or model.startswith("o1-") or "openai/" in model:
30+
return "openai"
31+
elif model.startswith("claude-") or "anthropic/" in model:
32+
return "anthropic"
33+
elif (
34+
model.startswith("gemini-")
35+
or "google/" in model
36+
or model.startswith("vertex_ai/")
37+
):
38+
return "google"
39+
elif "cohere/" in model or model.startswith("command-"):
40+
return "cohere"
41+
elif "azure/" in model:
42+
return "azure"
43+
elif "bedrock/" in model:
44+
return "bedrock"
45+
elif "ollama/" in model:
46+
return "ollama"
47+
else:
48+
# Try to use LiteLLM's internal provider detection if available
49+
try:
50+
if hasattr(litellm, "get_llm_provider"):
51+
provider_info = litellm.get_llm_provider(model)
52+
if isinstance(provider_info, tuple) and len(provider_info) > 1:
53+
return provider_info[1] or "unknown"
54+
return "unknown"
55+
except Exception:
56+
return "unknown"
57+
58+
59+
def _input_callback(
60+
kwargs, # type: Dict[str, Any]
61+
):
62+
# type: (...) -> None
63+
"""Handle the start of a request."""
64+
integration = sentry_sdk.get_client().get_integration(LiteLLMIntegration)
65+
66+
if integration is None:
67+
return
68+
69+
# Get key parameters
70+
model = kwargs.get("model", "")
71+
messages = kwargs.get("messages", [])
72+
operation = "chat" if messages else "embeddings"
73+
74+
# Start a new span/transaction
75+
span = get_start_span_function()(
76+
op=(
77+
consts.OP.GEN_AI_CHAT
78+
if operation == "chat"
79+
else consts.OP.GEN_AI_EMBEDDINGS
80+
),
81+
name=f"{operation} {model}",
82+
origin=LiteLLMIntegration.origin,
83+
)
84+
span.__enter__()
85+
86+
# Store span for later
87+
kwargs["_sentry_span"] = span
88+
89+
# Set basic data
90+
set_data_normalized(span, SPANDATA.GEN_AI_SYSTEM, "litellm")
91+
set_data_normalized(span, SPANDATA.GEN_AI_OPERATION_NAME, operation)
92+
set_data_normalized(
93+
span, "gen_ai.litellm.provider", _get_provider_from_model(model)
94+
)
95+
96+
# Record messages if allowed
97+
if messages and should_send_default_pii() and integration.include_prompts:
98+
set_data_normalized(
99+
span, SPANDATA.GEN_AI_REQUEST_MESSAGES, messages, unpack=False
100+
)
101+
102+
# Record other parameters
103+
params = {
104+
"model": SPANDATA.GEN_AI_REQUEST_MODEL,
105+
"stream": SPANDATA.GEN_AI_RESPONSE_STREAMING,
106+
"max_tokens": SPANDATA.GEN_AI_REQUEST_MAX_TOKENS,
107+
"presence_penalty": SPANDATA.GEN_AI_REQUEST_PRESENCE_PENALTY,
108+
"frequency_penalty": SPANDATA.GEN_AI_REQUEST_FREQUENCY_PENALTY,
109+
"temperature": SPANDATA.GEN_AI_REQUEST_TEMPERATURE,
110+
"top_p": SPANDATA.GEN_AI_REQUEST_TOP_P,
111+
}
112+
for key, attribute in params.items():
113+
value = kwargs.get(key)
114+
if value is not None:
115+
set_data_normalized(span, attribute, value)
116+
117+
# Record LiteLLM-specific parameters
118+
litellm_params = {
119+
"api_base": kwargs.get("api_base"),
120+
"api_version": kwargs.get("api_version"),
121+
"custom_llm_provider": kwargs.get("custom_llm_provider"),
122+
}
123+
for key, value in litellm_params.items():
124+
if value is not None:
125+
set_data_normalized(span, f"gen_ai.litellm.{key}", value)
126+
127+
128+
def _success_callback(
129+
kwargs, # type: Dict[str, Any]
130+
completion_response, # type: Any
131+
start_time, # type: datetime
132+
end_time, # type: datetime
133+
):
134+
# type: (...) -> None
135+
"""Handle successful completion."""
136+
137+
span = kwargs.get("_sentry_span")
138+
if span is None:
139+
return
140+
141+
integration = sentry_sdk.get_client().get_integration(LiteLLMIntegration)
142+
if integration is None:
143+
return
144+
145+
try:
146+
# Record model information
147+
if hasattr(completion_response, "model"):
148+
set_data_normalized(
149+
span, SPANDATA.GEN_AI_RESPONSE_MODEL, completion_response.model
150+
)
151+
152+
# Record response content if allowed
153+
if should_send_default_pii() and integration.include_prompts:
154+
if hasattr(completion_response, "choices"):
155+
response_messages = []
156+
for choice in completion_response.choices:
157+
if hasattr(choice, "message"):
158+
if hasattr(choice.message, "model_dump"):
159+
response_messages.append(choice.message.model_dump())
160+
elif hasattr(choice.message, "dict"):
161+
response_messages.append(choice.message.dict())
162+
else:
163+
# Fallback for basic message objects
164+
msg = {}
165+
if hasattr(choice.message, "role"):
166+
msg["role"] = choice.message.role
167+
if hasattr(choice.message, "content"):
168+
msg["content"] = choice.message.content
169+
if hasattr(choice.message, "tool_calls"):
170+
msg["tool_calls"] = choice.message.tool_calls
171+
response_messages.append(msg)
172+
173+
if response_messages:
174+
set_data_normalized(
175+
span, SPANDATA.GEN_AI_RESPONSE_TEXT, response_messages
176+
)
177+
178+
# Record token usage
179+
if hasattr(completion_response, "usage"):
180+
usage = completion_response.usage
181+
record_token_usage(
182+
span,
183+
input_tokens=getattr(usage, "prompt_tokens", None),
184+
output_tokens=getattr(usage, "completion_tokens", None),
185+
total_tokens=getattr(usage, "total_tokens", None),
186+
)
187+
188+
finally:
189+
# Always finish the span and clean up
190+
span.__exit__(None, None, None)
191+
192+
193+
def _failure_callback(
194+
kwargs, # type: Dict[str, Any]
195+
exception, # type: Exception
196+
start_time, # type: datetime
197+
end_time, # type: datetime
198+
):
199+
# type: (...) -> None
200+
"""Handle request failure."""
201+
span = kwargs.get("_sentry_span")
202+
203+
try:
204+
# Capture the exception
205+
event, hint = event_from_exception(
206+
exception,
207+
client_options=sentry_sdk.get_client().options,
208+
mechanism={"type": "litellm", "handled": False},
209+
)
210+
sentry_sdk.capture_event(event, hint=hint)
211+
finally:
212+
# Always finish the span and clean up
213+
span.__exit__(None, None, None)
214+
215+
216+
class LiteLLMIntegration(Integration):
217+
"""
218+
LiteLLM integration for Sentry.
219+
220+
This integration automatically captures LiteLLM API calls and sends them to Sentry
221+
for monitoring and error tracking. It supports all 100+ LLM providers that LiteLLM
222+
supports, including OpenAI, Anthropic, Google, Cohere, and many others.
223+
224+
Features:
225+
- Automatic exception capture for all LiteLLM calls
226+
- Token usage tracking across all providers
227+
- Provider detection and attribution
228+
- Input/output message capture (configurable)
229+
- Streaming response support
230+
- Cost tracking integration
231+
232+
Usage:
233+
234+
```python
235+
import litellm
236+
import sentry_sdk
237+
238+
# Initialize Sentry with the LiteLLM integration
239+
sentry_sdk.init(
240+
dsn="your-dsn",
241+
integrations=[
242+
sentry_sdk.integrations.LiteLLMIntegration(
243+
include_prompts=True # Set to False to exclude message content
244+
)
245+
]
246+
)
247+
248+
# All LiteLLM calls will now be monitored
249+
response = litellm.completion(
250+
model="gpt-3.5-turbo",
251+
messages=[{"role": "user", "content": "Hello!"}]
252+
)
253+
```
254+
255+
Configuration:
256+
- include_prompts (bool): Whether to include prompts and responses in spans.
257+
Defaults to True. Set to False to exclude potentially sensitive data.
258+
"""
259+
260+
identifier = "litellm"
261+
origin = f"auto.ai.{identifier}"
262+
263+
def __init__(self, include_prompts=True):
264+
# type: (LiteLLMIntegration, bool) -> None
265+
self.include_prompts = include_prompts
266+
267+
@staticmethod
268+
def setup_once():
269+
# type: () -> None
270+
"""Set up LiteLLM callbacks for monitoring."""
271+
litellm.input_callback = litellm.input_callback or []
272+
if _input_callback not in litellm.input_callback:
273+
litellm.input_callback.append(_input_callback)
274+
275+
litellm.success_callback = litellm.success_callback or []
276+
if _success_callback not in litellm.success_callback:
277+
litellm.success_callback.append(_success_callback)
278+
279+
litellm.failure_callback = litellm.failure_callback or []
280+
if _failure_callback not in litellm.failure_callback:
281+
litellm.failure_callback.append(_failure_callback)

0 commit comments

Comments
 (0)