Compare commits

..

1 Commits

Author SHA1 Message Date
Thiago Moretto
2df8973658 fix: use the internal token usage tracker when using litellm 2026-01-06 12:06:25 -03:00
6 changed files with 6 additions and 125 deletions

View File

@@ -55,7 +55,7 @@ Each webhook sends a list of events:
}
```
The `data` object structure varies by event type. Refer to the [event list](https://github.com/crewAIInc/crewAI/tree/main/lib/crewai/src/crewai/events/types) on GitHub.
The `data` object structure varies by event type. Refer to the [event list](https://github.com/crewAIInc/crewAI/tree/main/src/crewai/utilities/events) on GitHub.
As requests are sent over HTTP, the order of events can't be guaranteed. If you need ordering, use the `timestamp` field.

View File

@@ -26,9 +26,3 @@ ACTION_REGEX: Final[re.Pattern[str]] = re.compile(
ACTION_INPUT_ONLY_REGEX: Final[re.Pattern[str]] = re.compile(
r"\s*Action\s*\d*\s*Input\s*\d*\s*:\s*(.*)", re.DOTALL
)
# Regex to match "Action: None" or similar non-action values (None, N/A, etc.)
# This captures the action value and any text that follows it
ACTION_NONE_REGEX: Final[re.Pattern[str]] = re.compile(
r"Action\s*\d*\s*:\s*(none|n/a|na|no action|no_action)(?:\s*[-:(]?\s*(.*))?",
re.IGNORECASE | re.DOTALL,
)

View File

@@ -12,7 +12,6 @@ from json_repair import repair_json # type: ignore[import-untyped]
from crewai.agents.constants import (
ACTION_INPUT_ONLY_REGEX,
ACTION_INPUT_REGEX,
ACTION_NONE_REGEX,
ACTION_REGEX,
FINAL_ANSWER_ACTION,
MISSING_ACTION_AFTER_THOUGHT_ERROR_MESSAGE,
@@ -119,34 +118,6 @@ def parse(text: str) -> AgentAction | AgentFinish:
thought=thought, tool=clean_action, tool_input=safe_tool_input, text=text
)
# Check for "Action: None" or similar non-action values
# This handles cases where the LLM indicates it cannot/should not use a tool
action_none_match = ACTION_NONE_REGEX.search(text)
if action_none_match:
# Extract any additional content after "Action: None"
additional_content = action_none_match.group(2)
if additional_content:
additional_content = additional_content.strip()
# Remove trailing parenthesis if present (from patterns like "Action: None (reason)")
if additional_content.startswith("(") and ")" in additional_content:
additional_content = additional_content.split(")", 1)[-1].strip()
elif additional_content.startswith(")"):
additional_content = additional_content[1:].strip()
# Build the final answer from thought and any additional content
final_answer = thought
if additional_content:
if final_answer:
final_answer = f"{final_answer}\n\n{additional_content}"
else:
final_answer = additional_content
# If we still have no content, use a generic message
if not final_answer:
final_answer = "I cannot perform this action with the available tools."
return AgentFinish(thought=thought, output=final_answer, text=text)
if not ACTION_REGEX.search(text):
raise OutputParserError(
f"{MISSING_ACTION_AFTER_THOUGHT_ERROR_MESSAGE}\n{_I18N.slice('final_answer_format')}",

View File

@@ -1185,6 +1185,8 @@ class LLM(BaseLLM):
start_time=0,
end_time=0,
)
self._track_token_usage_internal(usage_info)
# --- 4) Check for tool calls
tool_calls = getattr(response_message, "tool_calls", [])

View File

@@ -360,92 +360,3 @@ def test_integration_valid_and_invalid():
# TODO: ADD TEST TO MAKE SURE ** REMOVAL DOESN'T MESS UP ANYTHING
# Tests for Action: None handling (Issue #4186)
def test_action_none_basic():
"""Test that 'Action: None' is parsed as AgentFinish."""
text = "Thought: I cannot use any tool for this.\nAction: None"
result = parser.parse(text)
assert isinstance(result, AgentFinish)
assert "I cannot use any tool for this." in result.output
def test_action_none_with_reason_in_parentheses():
"""Test 'Action: None (reason)' format."""
text = "Thought: The tool is not available.\nAction: None (direct response required)"
result = parser.parse(text)
assert isinstance(result, AgentFinish)
assert "The tool is not available." in result.output
def test_action_none_lowercase():
"""Test that 'Action: none' (lowercase) is handled."""
text = "Thought: I should respond directly.\nAction: none"
result = parser.parse(text)
assert isinstance(result, AgentFinish)
assert "I should respond directly." in result.output
def test_action_na():
"""Test that 'Action: N/A' is handled."""
text = "Thought: No action needed here.\nAction: N/A"
result = parser.parse(text)
assert isinstance(result, AgentFinish)
assert "No action needed here." in result.output
def test_action_na_lowercase():
"""Test that 'Action: n/a' (lowercase) is handled."""
text = "Thought: This requires a direct answer.\nAction: n/a"
result = parser.parse(text)
assert isinstance(result, AgentFinish)
assert "This requires a direct answer." in result.output
def test_action_none_with_dash_separator():
"""Test 'Action: None - reason' format."""
text = "Thought: I need to provide a direct response.\nAction: None - direct response"
result = parser.parse(text)
assert isinstance(result, AgentFinish)
assert "I need to provide a direct response." in result.output
def test_action_none_with_additional_content():
"""Test 'Action: None' with additional content after."""
text = "Thought: I analyzed the request.\nAction: None\nHere is my direct response to your question."
result = parser.parse(text)
assert isinstance(result, AgentFinish)
assert "I analyzed the request." in result.output
def test_action_no_action():
"""Test that 'Action: no action' is handled."""
text = "Thought: I will respond without using tools.\nAction: no action"
result = parser.parse(text)
assert isinstance(result, AgentFinish)
assert "I will respond without using tools." in result.output
def test_action_none_without_thought():
"""Test 'Action: None' without a thought prefix."""
text = "Action: None"
result = parser.parse(text)
assert isinstance(result, AgentFinish)
assert result.output == "I cannot perform this action with the available tools."
def test_action_none_preserves_original_text():
"""Test that the original text is preserved in the result."""
text = "Thought: I cannot delegate this task.\nAction: None"
result = parser.parse(text)
assert isinstance(result, AgentFinish)
assert result.text == text
def test_action_none_with_colon_separator():
"""Test 'Action: None: reason' format."""
text = "Thought: Direct response needed.\nAction: None: providing direct answer"
result = parser.parse(text)
assert isinstance(result, AgentFinish)
assert "Direct response needed." in result.output

View File

@@ -72,6 +72,9 @@ def test_llm_call_with_string_input_and_callbacks():
assert len(result.strip()) > 0
assert usage_metrics.successful_requests == 1
usage_metrics_internal = llm.get_token_usage_summary()
assert usage_metrics_internal == usage_metrics
@pytest.mark.vcr()
def test_llm_call_with_message_list():