Skip to content

Commit 64546a9

Browse files
fix(openai): Token reporting
1 parent 7d9bf9a commit 64546a9

File tree

3 files changed

+42
-16
lines changed

3 files changed

+42
-16
lines changed

sentry_sdk/ai/_openai_completions_api.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44

55
if TYPE_CHECKING:
66
from sentry_sdk._types import TextPart
7+
from typing import Union
78

89
from openai.types.chat import (
910
ChatCompletionMessageParam,
1011
ChatCompletionSystemMessageParam,
12+
ChatCompletionContentPartParam,
1113
)
1214

1315

@@ -24,6 +26,25 @@ def _get_system_instructions(
2426
return [message for message in messages if _is_system_instruction(message)]
2527

2628

29+
def _get_text_items(
30+
content: "Union[str, Iterable[ChatCompletionContentPartParam]]",
31+
) -> "list[str]":
32+
if isinstance(content, str):
33+
return [content]
34+
35+
if not isinstance(content, Iterable):
36+
return []
37+
38+
text_items = []
39+
for part in content:
40+
if isinstance(part, dict) and part.get("type") == "text":
41+
text = part.get("text", None)
42+
if text is not None:
43+
text_items.append(text)
44+
45+
return text_items
46+
47+
2748
def _transform_system_instructions(
2849
system_instructions: "list[ChatCompletionSystemMessageParam]",
2950
) -> "list[TextPart]":
@@ -34,15 +55,12 @@ def _transform_system_instructions(
3455
continue
3556

3657
content = instruction.get("content")
58+
if content is None:
59+
continue
3760

38-
if isinstance(content, str):
39-
instruction_text_parts.append({"type": "text", "content": content})
40-
41-
elif isinstance(content, list):
42-
for part in content:
43-
if isinstance(part, dict) and part.get("type") == "text":
44-
text = part.get("text", None)
45-
if text is not None:
46-
instruction_text_parts.append({"type": "text", "content": text})
61+
text_parts: "list[TextPart]" = [
62+
{"type": "text", "content": content} for content in _get_text_items(content)
63+
]
64+
instruction_text_parts += text_parts
4765

4866
return instruction_text_parts

sentry_sdk/integrations/openai.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
_is_system_instruction as _is_system_instruction_completions,
1818
_get_system_instructions as _get_system_instructions_completions,
1919
_transform_system_instructions,
20+
_get_text_items,
2021
)
2122
from sentry_sdk.ai._openai_responses_api import (
2223
_is_system_instruction as _is_system_instruction_responses,
@@ -181,10 +182,17 @@ def _calculate_token_usage(
181182
# Manually count tokens
182183
if input_tokens == 0:
183184
for message in messages or []:
184-
if isinstance(message, dict) and "content" in message:
185-
input_tokens += count_tokens(message["content"])
186-
elif isinstance(message, str):
185+
if isinstance(message, str):
187186
input_tokens += count_tokens(message)
187+
continue
188+
elif isinstance(message, dict):
189+
message_content = message.get("content")
190+
if message_content is None:
191+
continue
192+
# Deliberate use of Completions function for both Completions and Responses input format.
193+
text_items = _get_text_items(message_content)
194+
input_tokens += sum(count_tokens(text) for text in text_items)
195+
continue
188196

189197
if output_tokens == 0:
190198
if streaming_message_responses is not None:

tests/integrations/openai/test_openai.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -621,8 +621,8 @@ def test_streaming_chat_completion(sentry_init, capture_events, messages, reques
621621
assert span["data"]["gen_ai.usage.total_tokens"] == 9
622622
else:
623623
assert span["data"]["gen_ai.usage.output_tokens"] == 2
624-
assert span["data"]["gen_ai.usage.input_tokens"] == 1
625-
assert span["data"]["gen_ai.usage.total_tokens"] == 3
624+
assert span["data"]["gen_ai.usage.input_tokens"] == 12
625+
assert span["data"]["gen_ai.usage.total_tokens"] == 14
626626
except ImportError:
627627
pass # if tiktoken is not installed, we can't guarantee token usage will be calculated properly
628628

@@ -865,8 +865,8 @@ async def test_streaming_chat_completion_async(
865865
assert span["data"]["gen_ai.usage.total_tokens"] == 9
866866
else:
867867
assert span["data"]["gen_ai.usage.output_tokens"] == 2
868-
assert span["data"]["gen_ai.usage.input_tokens"] == 1
869-
assert span["data"]["gen_ai.usage.total_tokens"] == 3
868+
assert span["data"]["gen_ai.usage.input_tokens"] == 12
869+
assert span["data"]["gen_ai.usage.total_tokens"] == 14
870870

871871
except ImportError:
872872
pass # if tiktoken is not installed, we can't guarantee token usage will be calculated properly

0 commit comments

Comments
 (0)