feat: flatten LiteLLM cache/reasoning usage sub-counts in _usage_to_dict#6033
Conversation
LiteLLM returns provider usage as-is, nesting cache-read / cache-creation / reasoning counts under provider-specific shapes (e.g. prompt_tokens_details.cached_tokens, Anthropic-style cache_read_input_tokens). Surface them as flat cached_prompt_tokens / reasoning_tokens / cache_creation_tokens keys so the span pipeline can read them; prompt / completion / total token counts are left untouched.
📝 WalkthroughWalkthroughThe LLM ChangesToken Usage Normalization
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
lib/crewai/src/crewai/llm.py (1)
1962-1967: ⚡ Quick winBe aware:
orchaining treats explicit zero as absent.The precedence chain uses
or, so ifcached_tokensis explicitly0, the chain continues and might select a non-zero value fromcached_prompt_tokensor other sources. Example:{cached_tokens: 0, cached_prompt_tokens: 5}yields5.This mirrors the existing pattern in
BaseLLM._track_token_usage_internal(as documented), and in practice providers typically populate only one field, so the risk is low. However, it's worth being aware of this behavior.📋 Consider adding test coverage for zero-value edge case
Add a test case to document the intended behavior when a field is explicitly
0:def test_zero_cached_tokens_with_alternative_source(): """Document behavior when primary source is 0 and alternative exists.""" usage = { "cached_tokens": 0, "cached_prompt_tokens": 5, } result = LLM._usage_to_dict(usage) # Current behavior: returns 5 (continues precedence chain) # Alternative: could return 0 (stop at first explicit value) assert result["cached_prompt_tokens"] == 5 # or 0, depending on intentThis would clarify whether
0means "no cached tokens" (continue checking) or "explicitly zero cached tokens" (stop here).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/crewai/src/crewai/llm.py` around lines 1962 - 1967, The current precedence chain for cached_prompt_tokens uses boolean "or" which treats 0 as falsy and skips it; update the selection logic in the block that computes cached_prompt_tokens (referencing variables/data keys cached_tokens, cached_prompt_tokens, cache_read_input_tokens and helper _nested) to explicitly check for None (e.g., use "is not None" or a sentinel) so an explicit 0 is preserved as a valid value; also add a unit test (e.g., in tests covering LLM._usage_to_dict or the relevant conversion path) that asserts when {"cached_tokens": 0, "cached_prompt_tokens": 5} the function returns cached_tokens == 0 (or documents the chosen behavior) to prevent regressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@lib/crewai/src/crewai/llm.py`:
- Around line 1962-1967: The current precedence chain for cached_prompt_tokens
uses boolean "or" which treats 0 as falsy and skips it; update the selection
logic in the block that computes cached_prompt_tokens (referencing
variables/data keys cached_tokens, cached_prompt_tokens, cache_read_input_tokens
and helper _nested) to explicitly check for None (e.g., use "is not None" or a
sentinel) so an explicit 0 is preserved as a valid value; also add a unit test
(e.g., in tests covering LLM._usage_to_dict or the relevant conversion path)
that asserts when {"cached_tokens": 0, "cached_prompt_tokens": 5} the function
returns cached_tokens == 0 (or documents the chosen behavior) to prevent
regressions.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 0c8cab37-1177-4c91-8bc9-2c60f0b0d113
📒 Files selected for processing (2)
lib/crewai/src/crewai/llm.pylib/crewai/tests/events/test_llm_usage_event.py
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 9123b32. Configure here.

LiteLLM returns provider usage as-is, nesting cache-read / cache-creation / reasoning counts under provider-specific shapes (e.g. prompt_tokens_details.cached_tokens, Anthropic-style cache_read_input_tokens). Surface them as flat cached_prompt_tokens / reasoning_tokens / cache_creation_tokens keys so the span pipeline can read them; prompt / completion / total token counts are left untouched.
Note
Low Risk
Observability-only normalization on usage payloads before events; behavior change is copying dicts instead of returning the same reference, with broad test coverage.
Overview
LiteLLM usage dicts are normalized before LLM call events and spans see them.
LLM._usage_to_dictno longer returns raw provider shapes unchanged; it copies dict/Pydantic/__dict__usage into a new dict and promotes cache-read, cache-creation, and reasoning counts from nested or Anthropic-style fields into top-levelcached_prompt_tokens,reasoning_tokens, andcache_creation_tokens, matching whatBaseLLM._track_token_usage_internaland the span pipeline already expect. Core counts (prompt_tokens,completion_tokens,total_tokens) are not rewritten, and plain usage without those buckets is left without the derived keys.Tests replace the old “dict pass-through” assertion with coverage that inputs are copied (not mutated), parametrized provider shapes normalize correctly, core totals stay intact, and missing buckets are omitted.
Reviewed by Cursor Bugbot for commit 9123b32. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit
Bug Fixes
Tests