The OpenHands SDK provides metrics tracking at two levels: individual LLM metrics and aggregated conversation-level costs:
You can access detailed metrics from each LLM instance using the llm.metrics object to track token usage, costs, and latencies per API call.
For a complete view, use conversation.conversation_stats to get aggregated costs across all LLMs used in a conversation, including the primary agent LLM and any auxiliary LLMs (such as those used by the context condenser).
import osfrom pydantic import SecretStrfrom openhands.sdk import ( LLM, Agent, Conversation, Event, LLMConvertibleEvent, get_logger,)from openhands.sdk.tool import Toolfrom openhands.tools.file_editor import FileEditorToolfrom openhands.tools.terminal import TerminalToollogger = get_logger(__name__)# Configure LLMapi_key = os.getenv("LLM_API_KEY")assert api_key is not None, "LLM_API_KEY environment variable is not set."model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929")base_url = os.getenv("LLM_BASE_URL")llm = LLM( usage_id="agent", model=model, base_url=base_url, api_key=SecretStr(api_key),)cwd = os.getcwd()tools = [ Tool(name=TerminalTool.name), Tool(name=FileEditorTool.name),]# Add MCP Toolsmcp_config = {"mcpServers": {"fetch": {"command": "uvx", "args": ["mcp-server-fetch"]}}}# Agentagent = Agent(llm=llm, tools=tools, mcp_config=mcp_config)llm_messages = [] # collect raw LLM messagesdef conversation_callback(event: Event): if isinstance(event, LLMConvertibleEvent): llm_messages.append(event.to_llm_message())# Conversationconversation = Conversation( agent=agent, callbacks=[conversation_callback], workspace=cwd,)logger.info("Starting conversation with MCP integration...")conversation.send_message( "Read https://github.com/OpenHands/OpenHands and write 3 facts " "about the project into FACTS.txt.")conversation.run()conversation.send_message("Great! Now delete that file.")conversation.run()print("=" * 100)print("Conversation finished. Got the following LLM messages:")for i, message in enumerate(llm_messages): print(f"Message {i}: {str(message)[:200]}")assert llm.metrics is not Noneprint( f"Conversation finished. Final LLM metrics with details: {llm.metrics.model_dump()}")# Report costcost = llm.metrics.accumulated_costprint(f"EXAMPLE_COST: {cost}")
You can run the example code as-is.
The model name should follow the LiteLLM convention: provider/model_name (e.g., anthropic/claude-sonnet-4-5-20250929, openai/gpt-4o).
The LLM_API_KEY should be the API key for your chosen provider.
ChatGPT Plus/Pro subscribers: You can use LLM.subscription_login() to authenticate with your ChatGPT account and access Codex models without consuming API credits. See the LLM Subscriptions guide for details.
The LLM Registry allows you to maintain a centralized registry of LLM instances, each identified by a unique usage_id. This is particularly useful for tracking costs across different LLMs used in your application.
Each LLM is created with a unique usage_id (e.g., “agent”, “condenser”) that serves as its identifier in the registry. The registry maintains references to all LLM instances, allowing you to:
Register LLMs: Add LLM instances to the registry with llm_registry.add(llm)
Retrieve LLMs: Get LLM instances by their usage ID with llm_registry.get("usage_id")
List Usage IDs: View all registered usage IDs with llm_registry.list_usage_ids()
Track Costs Separately: Each LLM’s metrics are tracked independently by its usage ID
This pattern is essential when using multiple LLMs in your application, such as having a primary agent LLM and a separate LLM for context condensing.
import osfrom pydantic import SecretStrfrom openhands.sdk import ( LLM, Agent, Conversation, Event, LLMConvertibleEvent, LLMRegistry, Message, TextContent, get_logger,)from openhands.sdk.tool import Toolfrom openhands.tools.terminal import TerminalToollogger = get_logger(__name__)# Configure LLM using LLMRegistryapi_key = os.getenv("LLM_API_KEY")assert api_key is not None, "LLM_API_KEY environment variable is not set."model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929")base_url = os.getenv("LLM_BASE_URL")# Create LLM instancemain_llm = LLM( usage_id="agent", model=model, base_url=base_url, api_key=SecretStr(api_key),)# Create LLM registry and add the LLMllm_registry = LLMRegistry()llm_registry.add(main_llm)# Get LLM from registryllm = llm_registry.get("agent")# Toolscwd = os.getcwd()tools = [Tool(name=TerminalTool.name)]# Agentagent = Agent(llm=llm, tools=tools)llm_messages = [] # collect raw LLM messagesdef conversation_callback(event: Event): if isinstance(event, LLMConvertibleEvent): llm_messages.append(event.to_llm_message())conversation = Conversation( agent=agent, callbacks=[conversation_callback], workspace=cwd)conversation.send_message("Please echo 'Hello!'")conversation.run()print("=" * 100)print("Conversation finished. Got the following LLM messages:")for i, message in enumerate(llm_messages): print(f"Message {i}: {str(message)[:200]}")print("=" * 100)print(f"LLM Registry usage IDs: {llm_registry.list_usage_ids()}")# Demonstrate getting the same LLM instance from registrysame_llm = llm_registry.get("agent")print(f"Same LLM instance: {llm is same_llm}")# Demonstrate requesting a completion directly from an LLMresp = llm.completion( messages=[ Message(role="user", content=[TextContent(text="Say hello in one word.")]) ])# Access the response content via OpenHands LLMResponsemsg = resp.messagetexts = [c.text for c in msg.content if isinstance(c, TextContent)]print(f"Direct completion response: {texts[0] if texts else str(msg)}")# Report costcost = llm.metrics.accumulated_costprint(f"EXAMPLE_COST: {cost}")
You can run the example code as-is.
The model name should follow the LiteLLM convention: provider/model_name (e.g., anthropic/claude-sonnet-4-5-20250929, openai/gpt-4o).
The LLM_API_KEY should be the API key for your chosen provider.
ChatGPT Plus/Pro subscribers: You can use LLM.subscription_login() to authenticate with your ChatGPT account and access Codex models without consuming API credits. See the LLM Subscriptions guide for details.
Beyond individual LLM metrics, you can access aggregated costs for an entire conversation using conversation.conversation_stats. This is particularly useful when your conversation involves multiple LLMs, such as the main agent LLM and auxiliary LLMs for tasks like context condensing.
The model name should follow the LiteLLM convention: provider/model_name (e.g., anthropic/claude-sonnet-4-5-20250929, openai/gpt-4o).
The LLM_API_KEY should be the API key for your chosen provider.
ChatGPT Plus/Pro subscribers: You can use LLM.subscription_login() to authenticate with your ChatGPT account and access Codex models without consuming API credits. See the LLM Subscriptions guide for details.
The conversation.conversation_stats object provides cost tracking across all LLMs used in a conversation. It is an instance of the ConversationStats class, which provides the following key features:
usage_to_metrics: A dictionary mapping usage IDs to their respective Metrics objects. This allows you to track costs separately for each LLM used in the conversation.
get_combined_metrics(): Returns a single Metrics object that aggregates costs across all LLMs used in the conversation. This gives you the total cost of the entire conversation.
get_metrics_for_usage(usage_id: str): Retrieves the Metrics object for a specific usage ID, allowing you to inspect costs for individual LLMs.
# Get combined metrics for the entire conversationtotal_metrics = conversation.conversation_stats.get_combined_metrics()print(f"Total cost: ${total_metrics.accumulated_cost:.6f}")# Get metrics for a specific LLM by usage IDagent_metrics = conversation.conversation_stats.get_metrics_for_usage("agent")print(f"Agent cost: ${agent_metrics.accumulated_cost:.6f}")# Access all usage IDs and their metricsfor usage_id, metrics in conversation.conversation_stats.usage_to_metrics.items(): print(f"{usage_id}: ${metrics.accumulated_cost:.6f}")