Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion config/tool/tool-reasoning.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ env:
# These values will be loaded from the .env file at runtime
OPENAI_API_KEY: "${oc.env:OPENROUTER_API_KEY}"
OPENAI_BASE_URL: "${oc.env:OPENROUTER_BASE_URL,https://openrouter.ai/api/v1}"
OPENAI_MODEL_NAME: "${oc.env:OPENROUTER_MODEL_NAME,anthropic/claude-3-7-sonnet:thinking}"
OPENAI_MODEL_NAME: "${oc.env:OPENROUTER_MODEL_NAME,anthropic/claude-3.7-sonnet:thinking}"
ANTHROPIC_API_KEY: "${oc.env:ANTHROPIC_API_KEY}"
ANTHROPIC_BASE_URL: "${oc.env:ANTHROPIC_BASE_URL,https://api.anthropic.com}"
ANTHROPIC_MODEL_NAME: "${oc.env:ANTHROPIC_MODEL_NAME,claude-3-7-sonnet-20250219}"
Expand Down
6 changes: 5 additions & 1 deletion docs/mkdocs/docs/contribute_tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ async def tool_name(param: str) -> str:
Explanation of the tool, its parameters, and return value.
"""
tool_result = ... # Your logic here
return tool_result
usage = ... # Usage of billing tools
return ["text": tool_result, "usage": usage]

if __name__ == "__main__":
mcp.run(transport="stdio")
Expand Down Expand Up @@ -75,6 +76,9 @@ sub_agents:

---

### Step 4: Update Tool Usage Pricing

You need to add your price per unit of usage to `"tool"` in `utils/usage/pricing.json`. This needs to be in the appropriate string format; it can be a formula or a conditional statement. If you need to add other formats, please make the corresponding modifications at `utils/usage/calculate_usage_from_log.py`.

!!! info "Documentation Info"
**Last Updated:** September 2025 · **Doc Contributor:** Team @ MiroMind AI
12 changes: 12 additions & 0 deletions src/core/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,12 @@ async def run_sub_agent(
sub_agent_name
].execute_tool_call(server_name, tool_name, arguments)

self.task_log.log_step(
"tool_usage",
str(tool_result["usage"]),
)
tool_result.pop("usage", None)

call_end_time = time.time()
call_duration_ms = int((call_end_time - call_start_time) * 1000)

Expand Down Expand Up @@ -899,6 +905,12 @@ async def run_main_agent(
)
)

self.task_log.log_step(
"tool_usage",
str(tool_result["usage"]),
)
tool_result.pop("usage", None)

call_end_time = time.time()
call_duration_ms = int((call_end_time - call_start_time) * 1000)

Expand Down
35 changes: 27 additions & 8 deletions src/llm/provider_client_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@ class LLMProviderClientBase(ABC):
client: Any = dataclasses.field(init=False)
# Usage tracking - cumulative for each agent session
total_input_tokens: int = dataclasses.field(init=False, default=0)
total_input_cached_tokens: int = dataclasses.field(init=False, default=0)
total_input_cached_read_tokens: int = dataclasses.field(init=False, default=0)
total_input_cached_write_tokens: int = dataclasses.field(init=False, default=0)
total_output_tokens: int = dataclasses.field(init=False, default=0)
total_output_reasoning_tokens: int = dataclasses.field(init=False, default=0)
total_fee: float = dataclasses.field(init=False, default=0)

def __post_init__(self):
# Explicitly assign from cfg object
Expand Down Expand Up @@ -208,11 +210,17 @@ async def create_message(
usage = self._extract_usage_from_response(response)
if usage:
self.total_input_tokens += usage.get("input_tokens", 0)
self.total_input_cached_tokens += usage.get("cached_tokens", 0)
self.total_input_cached_read_tokens += usage.get(
"cached_read_tokens", 0
)
self.total_input_cached_write_tokens += usage.get(
"cached_write_tokens", 0
)
self.total_output_tokens += usage.get("output_tokens", 0)
self.total_output_reasoning_tokens += usage.get(
"reasoning_tokens", 0
)
self.total_fee += usage.get("fee", 0)
except Exception as e:
logger.warning(f"Failed to accumulate usage: {e}")

Expand Down Expand Up @@ -341,9 +349,11 @@ def _extract_usage_from_response(self, response):
if not hasattr(response, "usage"):
return {
"input_tokens": 0,
"cached_tokens": 0,
"cached_read_tokens": 0,
"cached_write_tokens": 0,
"output_tokens": 0,
"reasoning_tokens": 0,
"fee": 0,
}

usage = response.usage
Expand All @@ -358,9 +368,11 @@ def _extract_usage_from_response(self, response):

usage_dict = {
"input_tokens": getattr(usage, "prompt_tokens", 0),
"cached_tokens": prompt_tokens_details.get("cached_tokens", 0),
"cached_read_tokens": prompt_tokens_details.get("cached_tokens", 0),
"cached_write_tokens": 0,
"output_tokens": getattr(usage, "completion_tokens", 0),
"reasoning_tokens": completion_tokens_details.get("reasoning_tokens", 0),
"fee": getattr(usage, "cost", 0),
}

return usage_dict
Expand All @@ -369,20 +381,27 @@ def get_usage_log(self) -> str:
"""Get cumulative usage for current agent session as formatted string"""
# Format: [Provider | Model] Total Input: X, Cache Input: Y, Output: Z, ...
provider_model = f"[{self.provider_class} | {self.model_name}]"
input_uncached = self.total_input_tokens - self.total_input_cached_tokens
input_uncached = (
self.total_input_tokens
- self.total_input_cached_read_tokens
- self.total_input_cached_write_tokens
)
output_response = self.total_output_tokens - self.total_output_reasoning_tokens
total_tokens = self.total_input_tokens + self.total_output_tokens

return (
f"Usage log: {provider_model}, "
f"Total Input: {self.total_input_tokens} (Cached: {self.total_input_cached_tokens}, Uncached: {input_uncached}), "
f"Total Input: {self.total_input_tokens} (Cached Read: {self.total_input_cached_read_tokens}, Cached Write: {self.total_input_cached_write_tokens}, Uncached: {input_uncached}), "
f"Total Output: {self.total_output_tokens} (Reasoning: {self.total_output_reasoning_tokens}, Response: {output_response}), "
f"Total Tokens: {total_tokens}"
f"Total Tokens: {total_tokens}, "
f"Total Fee: {self.total_fee}"
)

def reset_usage_stats(self):
"""Reset usage stats for new agent session"""
self.total_input_tokens = 0
self.total_input_cached_tokens = 0
self.total_input_cached_read_tokens = 0
self.total_input_cached_write_tokens = 0
self.total_output_tokens = 0
self.total_output_reasoning_tokens = 0
self.total_fee = 0
8 changes: 6 additions & 2 deletions src/llm/providers/claude_anthropic_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,9 +190,11 @@ def _extract_usage_from_response(self, response):
if not hasattr(response, "usage"):
return {
"input_tokens": 0,
"cached_tokens": 0,
"cached_read_tokens": 0,
"cached_write_tokens": 0,
"output_tokens": 0,
"reasoning_tokens": 0,
"fee": 0,
}

usage = response.usage
Expand All @@ -205,9 +207,11 @@ def _extract_usage_from_response(self, response):
"input_tokens": cache_creation_input_tokens
+ cache_read_input_tokens
+ input_tokens,
"cached_tokens": cache_read_input_tokens,
"cached_read_tokens": cache_read_input_tokens,
"cached_write_tokens": cache_creation_input_tokens,
"output_tokens": output_tokens,
"reasoning_tokens": 0,
"fee": getattr(usage, "cost", 0),
}

return usage_dict
Expand Down
42 changes: 36 additions & 6 deletions src/tool/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ async def execute_tool_call(self, server_name, tool_name, arguments) -> Any:
"server_name": server_name,
"tool_name": tool_name,
"error": error_message,
"usage": {},
}

logger.info(
Expand All @@ -342,22 +343,26 @@ async def execute_tool_call(self, server_name, tool_name, arguments) -> Any:
"server_name": server_name,
"tool_name": tool_name,
"result": f"Tool '{tool_name}' returned empty result - this may be expected (e.g., delete operations) or indicate an issue with tool execution",
"usage": {},
}

return {
"server_name": server_name,
"tool_name": tool_name,
"result": tool_result,
"usage": {},
}
except Exception as e:
return {
"server_name": server_name,
"tool_name": tool_name,
"error": f"Tool call failed: {str(e)}",
"usage": {},
}
else:
try:
result_content = None
usage = {}
if isinstance(server_params, StdioServerParameters):
async with stdio_client(
update_server_params_with_context_var(server_params)
Expand All @@ -371,8 +376,18 @@ async def execute_tool_call(self, server_name, tool_name, arguments) -> Any:
tool_name, arguments=arguments
)
# Safely extract result content without changing original format
if tool_result.content and len(tool_result.content) > 0:
text_content = tool_result.content[-1].text
if tool_result.structuredContent:
text_content = tool_result.structuredContent["text"]
usage = tool_result.structuredContent.get(
"usage", {}
)
if (
not usage
and server_name == "tool-searching-serper"
):
usage = {"SERPER": 1}
logger.info(f"Tool result content: {text_content}")
logger.info(f"Tool result usage: {usage}")
if (
text_content is not None
and text_content.strip()
Expand All @@ -386,7 +401,7 @@ async def execute_tool_call(self, server_name, tool_name, arguments) -> Any:
result_content = f"Tool '{tool_name}' completed but returned no content - this may be expected or indicate an issue"

# If result is empty, log warning
if not tool_result.content:
if not tool_result.structuredContent:
logger.error(
f"Tool '{tool_name}' returned empty content, tool_result.content: {tool_result.content}"
)
Expand All @@ -400,6 +415,7 @@ async def execute_tool_call(self, server_name, tool_name, arguments) -> Any:
"server_name": server_name,
"tool_name": tool_name,
"error": f"Tool execution failed: {str(tool_error)}",
"usage": {},
}
elif isinstance(server_params, str) and server_params.startswith(
("http://", "https://")
Expand All @@ -414,8 +430,18 @@ async def execute_tool_call(self, server_name, tool_name, arguments) -> Any:
tool_name, arguments=arguments
)
# Safely extract result content without changing original format
if tool_result.content and len(tool_result.content) > 0:
text_content = tool_result.content[-1].text
if tool_result.structuredContent:
text_content = tool_result.structuredContent["text"]
usage = tool_result.structuredContent.get(
"usage", {}
)
if (
not usage
and server_name == "tool-searching-serper"
):
usage = {"SERPER": 1}
logger.info(f"Tool result content: {text_content}")
logger.info(f"Tool result usage: {usage}")
if (
text_content is not None
and text_content.strip()
Expand All @@ -429,7 +455,7 @@ async def execute_tool_call(self, server_name, tool_name, arguments) -> Any:
result_content = f"Tool '{tool_name}' completed but returned no content - this may be expected or indicate an issue"

# If result is empty, log warning
if not tool_result.content:
if not tool_result.structuredContent:
logger.error(
f"Tool '{tool_name}' returned empty content, tool_result.content: {tool_result.content}"
)
Expand All @@ -443,6 +469,7 @@ async def execute_tool_call(self, server_name, tool_name, arguments) -> Any:
"server_name": server_name,
"tool_name": tool_name,
"error": f"Tool execution failed: {str(tool_error)}",
"usage": {},
}
else:
raise TypeError(
Expand Down Expand Up @@ -470,6 +497,7 @@ async def execute_tool_call(self, server_name, tool_name, arguments) -> Any:
"server_name": server_name,
"tool_name": tool_name,
"result": result_content, # Return extracted text content
"usage": usage,
}

except Exception as outer_e: # Rename this to outer_e to avoid shadowing
Expand Down Expand Up @@ -501,6 +529,7 @@ async def execute_tool_call(self, server_name, tool_name, arguments) -> Any:
"server_name": server_name,
"tool_name": tool_name,
"result": result.text_content, # Return extracted text content
"usage": {},
}
except (
Exception
Expand All @@ -514,4 +543,5 @@ async def execute_tool_call(self, server_name, tool_name, arguments) -> Any:
"server_name": server_name,
"tool_name": tool_name,
"error": f"Tool call failed: {error_message}",
"usage": {},
}
Loading