-
Notifications
You must be signed in to change notification settings - Fork 8.4k
fix(crew): count each LLM instance once when summing usage metrics #7260
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
parthiban-sivakumar
wants to merge
3
commits into
crewAIInc:main
Choose a base branch
from
parthiban-sivakumar:parthiban/fix/usage-metrics-shared-llm
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+157
−30
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
c9fa1d7
fix(crew): count each LLM instance once when summing usage metrics
parthiban-sivakumar 4c30aa4
fix(crew): record usage per LLM call instead of reading lifetime coun…
parthiban-sivakumar 9cf2bb4
fix(crew): measure usage per LLM instance across a run, not per agent
parthiban-sivakumar File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| """Tests for crew-level token usage aggregation. | ||
|
|
||
| ``crew.usage_metrics`` reports what a kickoff consumed, measured as the growth | ||
| of each LLM instance's counters across the run. Reading those counters as | ||
| absolute totals instead both multiplied usage across agents sharing an | ||
| instance and carried earlier runs into later ones. | ||
|
|
||
| Baselines are taken per distinct instance rather than per agent, so a shared | ||
| instance is measured once and no per-agent window exists to overlap when tasks | ||
| run concurrently. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from crewai import Agent, Crew, Task, LLM | ||
| from crewai.types.usage_metrics import UsageMetrics | ||
|
|
||
|
|
||
| class _Counter: | ||
| """Stands in for an LLM instance's cumulative lifetime counters.""" | ||
|
|
||
| def __init__(self) -> None: | ||
| self.total = 0 | ||
|
|
||
| def bind(self, llm: LLM) -> LLM: | ||
| llm.get_token_usage_summary = lambda: UsageMetrics( # type: ignore[method-assign] | ||
| total_tokens=self.total, | ||
| prompt_tokens=self.total, | ||
| successful_requests=1 if self.total else 0, | ||
| ) | ||
| return llm | ||
|
|
||
| def consume(self, tokens: int) -> None: | ||
| self.total += tokens | ||
|
|
||
|
|
||
| def _crew(*llms: LLM) -> Crew: | ||
| agents = [ | ||
| Agent(role=f"Role {i}", goal="goal", backstory="backstory", llm=llm) | ||
| for i, llm in enumerate(llms) | ||
| ] | ||
| tasks = [ | ||
| Task(description=f"task {i}", expected_output="out", agent=agent) | ||
| for i, agent in enumerate(agents) | ||
| ] | ||
| return Crew(agents=agents, tasks=tasks) | ||
|
|
||
|
|
||
| def test_agents_sharing_one_llm_are_counted_once() -> None: | ||
| """Three agents on one instance report that instance's usage, not 3x it.""" | ||
| counter = _Counter() | ||
| llm = counter.bind(LLM(model="gpt-4o")) | ||
| crew = _crew(llm, llm, llm) | ||
|
|
||
| crew._snapshot_usage_baselines() | ||
| counter.consume(100) | ||
|
|
||
| assert crew.calculate_usage_metrics().total_tokens == 100 | ||
|
|
||
|
|
||
| def test_distinct_llm_instances_are_summed() -> None: | ||
| """Separate instances still add up, even for the same model.""" | ||
| first, second = _Counter(), _Counter() | ||
| crew = _crew(first.bind(LLM(model="gpt-4o")), second.bind(LLM(model="gpt-4o"))) | ||
|
|
||
| crew._snapshot_usage_baselines() | ||
| first.consume(100) | ||
| second.consume(50) | ||
|
|
||
| assert crew.calculate_usage_metrics().total_tokens == 150 | ||
|
|
||
|
|
||
| def test_a_later_run_excludes_the_previous_one() -> None: | ||
| """Usage is the growth across this run, not the instance's lifetime.""" | ||
| counter = _Counter() | ||
| crew = _crew(counter.bind(LLM(model="gpt-4o"))) | ||
|
|
||
| crew._snapshot_usage_baselines() | ||
| counter.consume(100) | ||
| assert crew.calculate_usage_metrics().total_tokens == 100 | ||
|
|
||
| crew._snapshot_usage_baselines() | ||
| counter.consume(50) | ||
| assert crew.calculate_usage_metrics().total_tokens == 50 | ||
|
|
||
|
|
||
| def test_manager_sharing_an_agent_llm_is_counted_once() -> None: | ||
| """A manager on the same instance as its agents adds no extra usage.""" | ||
| counter = _Counter() | ||
| llm = counter.bind(LLM(model="gpt-4o")) | ||
| crew = _crew(llm) | ||
| crew.manager_agent = Agent( | ||
| role="Manager", goal="coordinate", backstory="backstory", llm=llm | ||
| ) | ||
|
|
||
| crew._snapshot_usage_baselines() | ||
| counter.consume(100) | ||
|
|
||
| assert crew.calculate_usage_metrics().total_tokens == 100 | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Make the fake request counter grow on every
consume()call.successful_requestsremains1after the second consume. The later-run case therefore models a zero request delta even though it models new usage. Track request count in_Counter.consume()and assert the second kickoff reports one successful request.Proposed test-double update
class _Counter: def __init__(self) -> None: self.total = 0 + self.requests = 0 def bind(self, llm: LLM) -> LLM: llm.get_token_usage_summary = lambda: UsageMetrics( total_tokens=self.total, prompt_tokens=self.total, - successful_requests=1 if self.total else 0, + successful_requests=self.requests, ) return llm def consume(self, tokens: int) -> None: self.total += tokens + self.requests += 1🤖 Prompt for AI Agents