Add prompt_tokens and completion_tokens to collector API and UI

Supports the token breakdown (prompt/completion) now sent by agent-sync
from LibreChat's transactions collection. Updates models, collector
endpoint, usage stats, CSV export, and agent card/modal display.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
nickviljoen 2026-02-24 09:35:26 +02:00
parent 62138e9142
commit cba9e57db9
5 changed files with 41 additions and 11 deletions

View file

@ -60,9 +60,9 @@ Create `.env` file with:
- `AiAgent`: Core agent model with comprehensive fields (includes `discipline`, `rating`)
- `UsageTimelineEntry`: Daily usage data including `message_count` and `token_count`
- `UserCreate/UserResponse`: User management models
- `AiAgentCreate/AiAgentResponse`: API request/response models (includes `total_tokens`, `discipline`, `rating`, `rating_count`)
- `AgentCollectorCreate`: Collector API input model (includes `total_tokens`, `discipline`)
- `AgentUsageStatsResponse`: Usage statistics response (includes `total_tokens`)
- `AiAgentCreate/AiAgentResponse`: API request/response models (includes `total_tokens`, `prompt_tokens`, `completion_tokens`, `discipline`, `rating`, `rating_count`)
- `AgentCollectorCreate`: Collector API input model (includes `total_tokens`, `prompt_tokens`, `completion_tokens`, `discipline`)
- `AgentUsageStatsResponse`: Usage statistics response (includes `total_tokens`, `prompt_tokens`, `completion_tokens`)
**crud.py**: Database operations using Motor (async MongoDB driver):
- User CRUD: authentication, creation, management
@ -142,13 +142,14 @@ Located in `templates/` directory:
- Discipline passed through collector API; rating is human-only (not in collector)
### Token Usage Tracking
- `total_tokens` field on agents tracks cumulative LLM token consumption
- `total_tokens`, `prompt_tokens`, `completion_tokens` fields on agents track cumulative LLM token consumption
- `prompt_tokens` (input) and `completion_tokens` (output) provide cost breakdown detail
- `token_count` per day in usage timeline entries alongside `message_count`
- Token badge displayed on agent cards (gold/coins icon)
- Usage modal shows Total Tokens stat alongside messages/conversations/users
- Token badge displayed on agent cards (gold/coins icon) with prompt/completion breakdown in tooltip
- Usage modal shows Total Tokens stat with In/Out breakdown alongside messages/conversations/users
- Dual-axis chart (messages left axis, tokens right axis) when token data exists
- Sort agents by Total Tokens
- CSV export includes `total_tokens` column
- CSV export includes `total_tokens`, `prompt_tokens`, `completion_tokens` columns
### High Usage Email Notifications
- Entirely optional — silently disabled when Mailgun env vars are not set

View file

@ -157,6 +157,10 @@ async def create_agent(agent_data: dict, user_id: str, skip_time_check: bool = F
agent_doc["rating"] = None
if "total_tokens" not in agent_doc:
agent_doc["total_tokens"] = None
if "prompt_tokens" not in agent_doc:
agent_doc["prompt_tokens"] = None
if "completion_tokens" not in agent_doc:
agent_doc["completion_tokens"] = None
result = await agents_collection.insert_one(agent_doc)
agent_doc["_id"] = result.inserted_id
return agent_doc

20
main.py
View file

@ -236,7 +236,9 @@ def create_agent_response(agent: dict) -> models.AiAgentResponse:
total_messages=agent.get("total_messages"),
first_used=agent.get("first_used"),
last_used=agent.get("last_used"),
total_tokens=agent.get("total_tokens")
total_tokens=agent.get("total_tokens"),
prompt_tokens=agent.get("prompt_tokens"),
completion_tokens=agent.get("completion_tokens")
)
async def verify_agent_collector_api_key(x_api_key: str = Header(alias="X-API-Key")):
@ -296,6 +298,8 @@ def map_agent_collector_to_internal(collector_data: models.AgentCollectorCreate)
"first_used": collector_data.first_used,
"last_used": collector_data.last_used,
"total_tokens": collector_data.total_tokens,
"prompt_tokens": collector_data.prompt_tokens,
"completion_tokens": collector_data.completion_tokens,
}
@ -1150,6 +1154,8 @@ async def export_agents_csv(current_user: dict = Depends(require_admin)):
"last_edited_by",
"created_by",
"total_tokens",
"prompt_tokens",
"completion_tokens",
"discipline",
"rating",
"rating_count"
@ -1188,6 +1194,8 @@ async def export_agents_csv(current_user: dict = Depends(require_admin)):
"last_edited_by": agent.get("last_edited_by", ""),
"created_by": agent.get("created_by", ""),
"total_tokens": str(agent.get("total_tokens", "")) if agent.get("total_tokens") is not None else "",
"prompt_tokens": str(agent.get("prompt_tokens", "")) if agent.get("prompt_tokens") is not None else "",
"completion_tokens": str(agent.get("completion_tokens", "")) if agent.get("completion_tokens") is not None else "",
"discipline": agent.get("discipline", ""),
"rating": str(agent.get("rating", "")) if agent.get("rating") is not None else "",
"rating_count": str(agent.get("rating_count", "")) if agent.get("rating_count") is not None else ""
@ -1403,6 +1411,8 @@ async def create_agent_collector(
"first_used": internal_data.get("first_used"),
"last_used": internal_data.get("last_used"),
"total_tokens": internal_data.get("total_tokens"),
"prompt_tokens": internal_data.get("prompt_tokens"),
"completion_tokens": internal_data.get("completion_tokens"),
"updated_at": datetime.utcnow()
}
@ -1518,7 +1528,9 @@ async def get_agent_usage(
usage_by_period=usage_by_period,
conversation_count=agent.get("conversation_count"),
unique_users=agent.get("unique_users"),
total_tokens=agent.get("total_tokens")
total_tokens=agent.get("total_tokens"),
prompt_tokens=agent.get("prompt_tokens"),
completion_tokens=agent.get("completion_tokens")
)
else:
# FALLBACK TO CALCULATED DATA (old system)
@ -1544,7 +1556,9 @@ async def get_agent_usage(
usage_by_period=usage_by_period,
conversation_count=None, # Old system doesn't track this
unique_users=None, # Old system doesn't track this
total_tokens=None # Old system doesn't track this
total_tokens=None, # Old system doesn't track this
prompt_tokens=None,
completion_tokens=None
)
except HTTPException:
raise

View file

@ -140,6 +140,8 @@ class AiAgentResponse(BaseModel):
first_used: Optional[str] = None
last_used: Optional[str] = None
total_tokens: Optional[int] = None
prompt_tokens: Optional[int] = None
completion_tokens: Optional[int] = None
# Agent Collector API Models (for compatibility with agent_collector app)
class AgentCollectorCreate(BaseModel):
@ -169,6 +171,8 @@ class AgentCollectorCreate(BaseModel):
first_used: Optional[str] = None # ISO 8601 datetime string
last_used: Optional[str] = None # ISO 8601 datetime string
total_tokens: Optional[int] = Field(default=None, ge=0)
prompt_tokens: Optional[int] = Field(default=None, ge=0)
completion_tokens: Optional[int] = Field(default=None, ge=0)
class AgentCollectorResponse(BaseModel):
status: str = "success"
@ -201,3 +205,5 @@ class AgentUsageStatsResponse(BaseModel):
conversation_count: Optional[int] = None
unique_users: Optional[int] = None
total_tokens: Optional[int] = None
prompt_tokens: Optional[int] = None
completion_tokens: Optional[int] = None

View file

@ -722,7 +722,7 @@ function displayAgents(agentsToShow) {
${agent.quality_audit_status ? '<span class="badge bg-success" title="Quality Audited"><i class="fas fa-certificate"></i></span>' : ''}
${agent.quality_audit_status && agent.risk_factor ? getRiskFactorBadge(agent.risk_factor) : ''}
${agent.total_messages ? `<span class="badge bg-info text-white" title="Total Messages"><i class="fas fa-comments me-1"></i>${agent.total_messages.toLocaleString()}</span>` : ''}
${agent.total_tokens ? `<span class="badge bg-warning text-dark" title="Total Tokens"><i class="fas fa-coins me-1"></i>${agent.total_tokens.toLocaleString()}</span>` : ''}
${agent.total_tokens ? `<span class="badge bg-warning text-dark" title="Total Tokens${agent.prompt_tokens != null ? '\nPrompt: ' + agent.prompt_tokens.toLocaleString() : ''}${agent.completion_tokens != null ? '\nCompletion: ' + agent.completion_tokens.toLocaleString() : ''}"><i class="fas fa-coins me-1"></i>${agent.total_tokens.toLocaleString()}</span>` : ''}
${agent.unique_users ? `<span class="badge bg-primary" title="Unique Users"><i class="fas fa-users me-1"></i>${agent.unique_users}</span>` : ''}
${agent.url ? `<a href="${agent.url}" target="_blank" class="btn btn-sm btn-success" onclick="event.stopPropagation();" title="Start a conversation with this agent"><i class="fas fa-external-link-alt me-1"></i>Open Agent</a>` : ''}
</div>
@ -1389,6 +1389,11 @@ async function loadUsageChart(agentName, period = 'daily', startDate = null, end
<div class="border rounded p-2">
<h6 class="mb-1 text-warning">${stats.total_tokens != null ? stats.total_tokens.toLocaleString() : 'N/A'}</h6>
<small class="text-muted">Total Tokens</small>
${stats.prompt_tokens != null || stats.completion_tokens != null ? `
<div class="mt-1" style="font-size: 0.7rem;">
<span class="text-muted">In: ${stats.prompt_tokens != null ? stats.prompt_tokens.toLocaleString() : '—'}</span> |
<span class="text-muted">Out: ${stats.completion_tokens != null ? stats.completion_tokens.toLocaleString() : '—'}</span>
</div>` : ''}
</div>
</div>
<div class="col">