Skip to content

Commit 462baef

Browse files
CopilotTomeHiratachenmoneygithub
authored
Fix TypeError when tracking usage with Anthropic models returning Pydantic objects (#8978)
* Initial plan * Fix TypeError when merging Anthropic CacheCreation objects in usage tracker Co-authored-by: TomeHirata <33407409+TomeHirata@users.noreply.github.com> * Enhance _flatten_usage_entry to convert Pydantic models on first add Co-authored-by: TomeHirata <33407409+TomeHirata@users.noreply.github.com> * Fix potential TypeError when both usage entries are None Co-authored-by: TomeHirata <33407409+TomeHirata@users.noreply.github.com> * simplify * small fix * lint * robust version handling --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: TomeHirata <33407409+TomeHirata@users.noreply.github.com> Co-authored-by: chenmoneygithub <chen.qian@databricks.com>
1 parent 9b467b5 commit 462baef

File tree

2 files changed

+116
-7
lines changed

2 files changed

+116
-7
lines changed

dspy/utils/usage_tracker.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from contextlib import contextmanager
55
from typing import Any, Generator
66

7+
from pydantic import BaseModel
8+
79
from dspy.dsp.utils.settings import settings
810

911

@@ -21,15 +23,18 @@ def __init__(self):
2123
self.usage_data = defaultdict(list)
2224

2325
def _flatten_usage_entry(self, usage_entry: dict[str, Any]) -> dict[str, Any]:
24-
result = dict(usage_entry)
25-
26-
if completion_tokens_details := result.get("completion_tokens_details"):
27-
result["completion_tokens_details"] = dict(completion_tokens_details)
28-
if prompt_tokens_details := result.get("prompt_tokens_details"):
29-
result["prompt_tokens_details"] = dict(prompt_tokens_details)
26+
result = {}
27+
for key, value in usage_entry.items():
28+
if isinstance(value, BaseModel):
29+
# Convert Pydantic models to dicts, like `PromptTokensDetailsWrapper` from litellm.
30+
result[key] = value.model_dump()
31+
else:
32+
result[key] = value
3033
return result
3134

32-
def _merge_usage_entries(self, usage_entry1: dict[str, Any] | None, usage_entry2: dict[str, Any] | None) -> dict[str, Any]:
35+
def _merge_usage_entries(
36+
self, usage_entry1: dict[str, Any] | None, usage_entry2: dict[str, Any] | None
37+
) -> dict[str, Any]:
3338
if usage_entry1 is None or len(usage_entry1) == 0:
3439
return dict(usage_entry2)
3540
if usage_entry2 is None or len(usage_entry2) == 0:

tests/utils/test_usage_tracker.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from pydantic import BaseModel
2+
13
import dspy
24
from dspy.utils.usage_tracker import UsageTracker, track_usage
35

@@ -221,3 +223,105 @@ def test_merge_usage_entries_with_none_values():
221223
assert total_usage["gpt-4o-mini"]["completion_tokens_details"]["audio_tokens"] == 1
222224
assert total_usage["gpt-4o-mini"]["completion_tokens_details"]["accepted_prediction_tokens"] == 1
223225
assert total_usage["gpt-4o-mini"]["completion_tokens_details"]["rejected_prediction_tokens"] == 1
226+
227+
228+
def test_merge_usage_entries_with_pydantic_models():
229+
"""Test merging usage entries with Pydantic model objects, like `PromptTokensDetailsWrapper` from litellm."""
230+
tracker = UsageTracker()
231+
232+
# Here we define a simplified version of the Pydantic models from litellm to avoid the dependency change on litellm.
233+
class CacheCreationTokenDetails(BaseModel):
234+
ephemeral_5m_input_tokens: int
235+
ephemeral_1h_input_tokens: int
236+
237+
class PromptTokensDetailsWrapper(BaseModel):
238+
audio_tokens: int | None
239+
cached_tokens: int
240+
text_tokens: int | None
241+
image_tokens: int | None
242+
cache_creation_tokens: int
243+
cache_creation_token_details: CacheCreationTokenDetails
244+
245+
# Add usage entries for different models
246+
usage_entries = [
247+
{
248+
"model": "gpt-4o-mini",
249+
"usage": {
250+
"prompt_tokens": 1117,
251+
"completion_tokens": 46,
252+
"total_tokens": 1163,
253+
"prompt_tokens_details": PromptTokensDetailsWrapper(
254+
audio_tokens=None,
255+
cached_tokens=3,
256+
text_tokens=None,
257+
image_tokens=None,
258+
cache_creation_tokens=0,
259+
cache_creation_token_details=CacheCreationTokenDetails(
260+
ephemeral_5m_input_tokens=5, ephemeral_1h_input_tokens=0
261+
),
262+
),
263+
"completion_tokens_details": {},
264+
},
265+
},
266+
{
267+
"model": "gpt-4o-mini",
268+
"usage": {
269+
"prompt_tokens": 800,
270+
"completion_tokens": 100,
271+
"total_tokens": 900,
272+
"prompt_tokens_details": PromptTokensDetailsWrapper(
273+
audio_tokens=None,
274+
cached_tokens=3,
275+
text_tokens=None,
276+
image_tokens=None,
277+
cache_creation_tokens=0,
278+
cache_creation_token_details=CacheCreationTokenDetails(
279+
ephemeral_5m_input_tokens=5, ephemeral_1h_input_tokens=0
280+
),
281+
),
282+
"completion_tokens_details": None,
283+
},
284+
},
285+
{
286+
"model": "gpt-4o-mini",
287+
"usage": {
288+
"prompt_tokens": 800,
289+
"completion_tokens": 100,
290+
"total_tokens": 900,
291+
"prompt_tokens_details": PromptTokensDetailsWrapper(
292+
audio_tokens=None,
293+
cached_tokens=3,
294+
text_tokens=None,
295+
image_tokens=None,
296+
cache_creation_tokens=0,
297+
cache_creation_token_details=CacheCreationTokenDetails(
298+
ephemeral_5m_input_tokens=5, ephemeral_1h_input_tokens=0
299+
),
300+
),
301+
"completion_tokens_details": {
302+
"reasoning_tokens": 1,
303+
"audio_tokens": 1,
304+
"accepted_prediction_tokens": 1,
305+
"rejected_prediction_tokens": 1,
306+
},
307+
},
308+
},
309+
]
310+
311+
for entry in usage_entries:
312+
tracker.add_usage(entry["model"], entry["usage"])
313+
314+
total_usage = tracker.get_total_tokens()
315+
316+
assert total_usage["gpt-4o-mini"]["prompt_tokens"] == 2717
317+
assert total_usage["gpt-4o-mini"]["completion_tokens"] == 246
318+
assert total_usage["gpt-4o-mini"]["total_tokens"] == 2963
319+
assert total_usage["gpt-4o-mini"]["prompt_tokens_details"]["cached_tokens"] == 9
320+
assert (
321+
total_usage["gpt-4o-mini"]["prompt_tokens_details"]["cache_creation_token_details"]["ephemeral_5m_input_tokens"]
322+
== 15
323+
)
324+
assert total_usage["gpt-4o-mini"]["completion_tokens_details"]["reasoning_tokens"] == 1
325+
assert total_usage["gpt-4o-mini"]["completion_tokens_details"]["audio_tokens"] == 1
326+
assert total_usage["gpt-4o-mini"]["completion_tokens_details"]["accepted_prediction_tokens"] == 1
327+
assert total_usage["gpt-4o-mini"]["completion_tokens_details"]["rejected_prediction_tokens"] == 1

0 commit comments

Comments
 (0)