Skip to content

Commit 5bad5c6

Browse files
feat(openai): Make tiktoken encoding name configurable + tiktoken usage opt-in (#3289)
Make tiktoken encoding name configurable + tiktoken usage opt-in --------- Co-authored-by: Ivana Kellyer <[email protected]>
1 parent 84a2afc commit 5bad5c6

File tree

4 files changed

+80
-64
lines changed

4 files changed

+80
-64
lines changed

Diff for: sentry_sdk/integrations/langchain.py

+25-30
Original file line numberDiff line numberDiff line change
@@ -27,28 +27,6 @@
2727
raise DidNotEnable("langchain not installed")
2828

2929

30-
try:
31-
import tiktoken # type: ignore
32-
33-
enc = tiktoken.get_encoding("cl100k_base")
34-
35-
def count_tokens(s):
36-
# type: (str) -> int
37-
return len(enc.encode_ordinary(s))
38-
39-
logger.debug("[langchain] using tiktoken to count tokens")
40-
except ImportError:
41-
logger.info(
42-
"The Sentry Python SDK requires 'tiktoken' in order to measure token usage from streaming langchain calls."
43-
"Please install 'tiktoken' if you aren't receiving accurate token usage in Sentry."
44-
"See https://docs.sentry.io/platforms/python/integrations/langchain/ for more information."
45-
)
46-
47-
def count_tokens(s):
48-
# type: (str) -> int
49-
return 1
50-
51-
5230
DATA_FIELDS = {
5331
"temperature": SPANDATA.AI_TEMPERATURE,
5432
"top_p": SPANDATA.AI_TOP_P,
@@ -78,10 +56,13 @@ class LangchainIntegration(Integration):
7856
# The most number of spans (e.g., LLM calls) that can be processed at the same time.
7957
max_spans = 1024
8058

81-
def __init__(self, include_prompts=True, max_spans=1024):
82-
# type: (LangchainIntegration, bool, int) -> None
59+
def __init__(
60+
self, include_prompts=True, max_spans=1024, tiktoken_encoding_name=None
61+
):
62+
# type: (LangchainIntegration, bool, int, Optional[str]) -> None
8363
self.include_prompts = include_prompts
8464
self.max_spans = max_spans
65+
self.tiktoken_encoding_name = tiktoken_encoding_name
8566

8667
@staticmethod
8768
def setup_once():
@@ -109,11 +90,23 @@ class SentryLangchainCallback(BaseCallbackHandler): # type: ignore[misc]
10990

11091
max_span_map_size = 0
11192

112-
def __init__(self, max_span_map_size, include_prompts):
113-
# type: (int, bool) -> None
93+
def __init__(self, max_span_map_size, include_prompts, tiktoken_encoding_name=None):
94+
# type: (int, bool, Optional[str]) -> None
11495
self.max_span_map_size = max_span_map_size
11596
self.include_prompts = include_prompts
11697

98+
self.tiktoken_encoding = None
99+
if tiktoken_encoding_name is not None:
100+
import tiktoken # type: ignore
101+
102+
self.tiktoken_encoding = tiktoken.get_encoding(tiktoken_encoding_name)
103+
104+
def count_tokens(self, s):
105+
# type: (str) -> int
106+
if self.tiktoken_encoding is not None:
107+
return len(self.tiktoken_encoding.encode_ordinary(s))
108+
return 0
109+
117110
def gc_span_map(self):
118111
# type: () -> None
119112

@@ -244,9 +237,9 @@ def on_chat_model_start(self, serialized, messages, *, run_id, **kwargs):
244237
if not watched_span.no_collect_tokens:
245238
for list_ in messages:
246239
for message in list_:
247-
self.span_map[run_id].num_prompt_tokens += count_tokens(
240+
self.span_map[run_id].num_prompt_tokens += self.count_tokens(
248241
message.content
249-
) + count_tokens(message.type)
242+
) + self.count_tokens(message.type)
250243

251244
def on_llm_new_token(self, token, *, run_id, **kwargs):
252245
# type: (SentryLangchainCallback, str, UUID, Any) -> Any
@@ -257,7 +250,7 @@ def on_llm_new_token(self, token, *, run_id, **kwargs):
257250
span_data = self.span_map[run_id]
258251
if not span_data or span_data.no_collect_tokens:
259252
return
260-
span_data.num_completion_tokens += count_tokens(token)
253+
span_data.num_completion_tokens += self.count_tokens(token)
261254

262255
def on_llm_end(self, response, *, run_id, **kwargs):
263256
# type: (SentryLangchainCallback, LLMResult, UUID, Any) -> Any
@@ -461,7 +454,9 @@ def new_configure(*args, **kwargs):
461454
if not already_added:
462455
new_callbacks.append(
463456
SentryLangchainCallback(
464-
integration.max_spans, integration.include_prompts
457+
integration.max_spans,
458+
integration.include_prompts,
459+
integration.tiktoken_encoding_name,
465460
)
466461
)
467462
return f(*args, **kwargs)

Diff for: sentry_sdk/integrations/openai.py

+25-32
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from sentry_sdk.scope import should_send_default_pii
1515
from sentry_sdk.integrations import DidNotEnable, Integration
1616
from sentry_sdk.utils import (
17-
logger,
1817
capture_internal_exceptions,
1918
event_from_exception,
2019
ensure_integration_enabled,
@@ -29,45 +28,33 @@
2928
except ImportError:
3029
raise DidNotEnable("OpenAI not installed")
3130

32-
try:
33-
import tiktoken # type: ignore
34-
35-
enc = None # lazy initialize
36-
37-
def count_tokens(s):
38-
# type: (str) -> int
39-
global enc
40-
if enc is None:
41-
enc = tiktoken.get_encoding("cl100k_base")
42-
return len(enc.encode_ordinary(s))
43-
44-
logger.debug("[OpenAI] using tiktoken to count tokens")
45-
except ImportError:
46-
logger.info(
47-
"The Sentry Python SDK requires 'tiktoken' in order to measure token usage from some OpenAI APIs"
48-
"Please install 'tiktoken' if you aren't receiving token usage in Sentry."
49-
"See https://docs.sentry.io/platforms/python/integrations/openai/ for more information."
50-
)
51-
52-
def count_tokens(s):
53-
# type: (str) -> int
54-
return 0
55-
5631

5732
class OpenAIIntegration(Integration):
5833
identifier = "openai"
5934
origin = f"auto.ai.{identifier}"
6035

61-
def __init__(self, include_prompts=True):
62-
# type: (OpenAIIntegration, bool) -> None
36+
def __init__(self, include_prompts=True, tiktoken_encoding_name=None):
37+
# type: (OpenAIIntegration, bool, Optional[str]) -> None
6338
self.include_prompts = include_prompts
6439

40+
self.tiktoken_encoding = None
41+
if tiktoken_encoding_name is not None:
42+
import tiktoken # type: ignore
43+
44+
self.tiktoken_encoding = tiktoken.get_encoding(tiktoken_encoding_name)
45+
6546
@staticmethod
6647
def setup_once():
6748
# type: () -> None
6849
Completions.create = _wrap_chat_completion_create(Completions.create)
6950
Embeddings.create = _wrap_embeddings_create(Embeddings.create)
7051

52+
def count_tokens(self, s):
53+
# type: (OpenAIIntegration, str) -> int
54+
if self.tiktoken_encoding is not None:
55+
return len(self.tiktoken_encoding.encode_ordinary(s))
56+
return 0
57+
7158

7259
def _capture_exception(exc):
7360
# type: (Any) -> None
@@ -80,9 +67,9 @@ def _capture_exception(exc):
8067

8168

8269
def _calculate_chat_completion_usage(
83-
messages, response, span, streaming_message_responses=None
70+
messages, response, span, streaming_message_responses, count_tokens
8471
):
85-
# type: (Iterable[ChatCompletionMessageParam], Any, Span, Optional[List[str]]) -> None
72+
# type: (Iterable[ChatCompletionMessageParam], Any, Span, Optional[List[str]], Callable[..., Any]) -> None
8673
completion_tokens = 0 # type: Optional[int]
8774
prompt_tokens = 0 # type: Optional[int]
8875
total_tokens = 0 # type: Optional[int]
@@ -173,7 +160,9 @@ def new_chat_completion(*args, **kwargs):
173160
"ai.responses",
174161
list(map(lambda x: x.message, res.choices)),
175162
)
176-
_calculate_chat_completion_usage(messages, res, span)
163+
_calculate_chat_completion_usage(
164+
messages, res, span, None, integration.count_tokens
165+
)
177166
span.__exit__(None, None, None)
178167
elif hasattr(res, "_iterator"):
179168
data_buf: list[list[str]] = [] # one for each choice
@@ -208,7 +197,11 @@ def new_iterator():
208197
span, SPANDATA.AI_RESPONSES, all_responses
209198
)
210199
_calculate_chat_completion_usage(
211-
messages, res, span, all_responses
200+
messages,
201+
res,
202+
span,
203+
all_responses,
204+
integration.count_tokens,
212205
)
213206
span.__exit__(None, None, None)
214207

@@ -266,7 +259,7 @@ def new_embeddings_create(*args, **kwargs):
266259
total_tokens = response.usage.total_tokens
267260

268261
if prompt_tokens == 0:
269-
prompt_tokens = count_tokens(kwargs["input"] or "")
262+
prompt_tokens = integration.count_tokens(kwargs["input"] or "")
270263

271264
record_token_usage(span, prompt_tokens, None, total_tokens or prompt_tokens)
272265

Diff for: tests/integrations/langchain/test_langchain.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ def _llm_type(self) -> str:
4646
return llm_type
4747

4848

49+
def tiktoken_encoding_if_installed():
50+
try:
51+
import tiktoken # type: ignore # noqa # pylint: disable=unused-import
52+
53+
return "cl100k_base"
54+
except ImportError:
55+
return None
56+
57+
4958
@pytest.mark.parametrize(
5059
"send_default_pii, include_prompts, use_unknown_llm_type",
5160
[
@@ -62,7 +71,12 @@ def test_langchain_agent(
6271
llm_type = "acme-llm" if use_unknown_llm_type else "openai-chat"
6372

6473
sentry_init(
65-
integrations=[LangchainIntegration(include_prompts=include_prompts)],
74+
integrations=[
75+
LangchainIntegration(
76+
include_prompts=include_prompts,
77+
tiktoken_encoding_name=tiktoken_encoding_if_installed(),
78+
)
79+
],
6680
traces_sample_rate=1.0,
6781
send_default_pii=send_default_pii,
6882
)

Diff for: tests/integrations/openai/test_openai.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,15 @@ def test_nonstreaming_chat_completion(
7878
assert span["measurements"]["ai_total_tokens_used"]["value"] == 30
7979

8080

81+
def tiktoken_encoding_if_installed():
82+
try:
83+
import tiktoken # type: ignore # noqa # pylint: disable=unused-import
84+
85+
return "cl100k_base"
86+
except ImportError:
87+
return None
88+
89+
8190
# noinspection PyTypeChecker
8291
@pytest.mark.parametrize(
8392
"send_default_pii, include_prompts",
@@ -87,7 +96,12 @@ def test_streaming_chat_completion(
8796
sentry_init, capture_events, send_default_pii, include_prompts
8897
):
8998
sentry_init(
90-
integrations=[OpenAIIntegration(include_prompts=include_prompts)],
99+
integrations=[
100+
OpenAIIntegration(
101+
include_prompts=include_prompts,
102+
tiktoken_encoding_name=tiktoken_encoding_if_installed(),
103+
)
104+
],
91105
traces_sample_rate=1.0,
92106
send_default_pii=send_default_pii,
93107
)

0 commit comments

Comments
 (0)