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
6 changes: 6 additions & 0 deletions pricing.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
"cacheWrite": "cache write price (Anthropic: 1.25x input, OpenAI: ~same as input)"
}
},
"claude-opus-4-7": {
"input": 5,
"output": 25,
"cacheRead": 0.5,
"cacheWrite": 6.25
},
"claude-opus-4-6": {
"input": 5,
"output": 25,
Expand Down
29 changes: 26 additions & 3 deletions relay-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ const path = require('path');
const os = require('os');
const fs = require('fs');
const crypto = require('crypto');
const { calculateCost } = require('./pricing');

// Estimate USD cost for a single chat's token usage. A chat may list multiple
// models without a per-model token split, so tokens are divided evenly across
// the models used. Unpriced models contribute 0 (matches Cost Analysis behavior).
function estimateChatCost(modelsJson, inputTokens, outputTokens) {
let models = [];
try { models = JSON.parse(modelsJson || '[]'); } catch {}
if (models.length === 0) return 0;
const n = models.length;
let cost = 0;
for (const m of models) {
cost += calculateCost(m, (inputTokens || 0) / n, (outputTokens || 0) / n) || 0;
}
return cost;
}

const CACHE_DIR = path.join(os.homedir(), '.agentlytics');
const RELAY_DB_PATH = path.join(CACHE_DIR, 'relay.db');
Expand Down Expand Up @@ -164,18 +180,23 @@ function createRelayApp() {
userEditorMap[r.username][r.source] = r.count;
}

const perUserModels = db.prepare(`
SELECT rcs.username, rcs.models
const perChatStats = db.prepare(`
SELECT rcs.username, rcs.models, rcs.total_input_tokens, rcs.total_output_tokens
FROM relay_chat_stats rcs
`).all();
const userModelMap = {};
for (const r of perUserModels) {
const userCostMap = {};
let totalCost = 0;
for (const r of perChatStats) {
if (!userModelMap[r.username]) userModelMap[r.username] = {};
try {
for (const m of JSON.parse(r.models || '[]')) {
userModelMap[r.username][m] = (userModelMap[r.username][m] || 0) + 1;
}
} catch {}
const cost = estimateChatCost(r.models, r.total_input_tokens, r.total_output_tokens);
userCostMap[r.username] = (userCostMap[r.username] || 0) + cost;
totalCost += cost;
}

const totalTokens = db.prepare(`
Expand All @@ -199,6 +220,7 @@ function createRelayApp() {
totalMessages: totalTokens.messages,
totalInputTokens: totalTokens.inputTokens,
totalOutputTokens: totalTokens.outputTokens,
totalCost,
editors: editorBreakdown.map(e => ({ source: e.source, count: e.count, users: e.users })),
topModels: Object.entries(modelFreq).sort((a, b) => b[1] - a[1]).slice(0, 15).map(([name, count]) => ({ name, count })),
users: perUser.map(u => ({
Expand All @@ -210,6 +232,7 @@ function createRelayApp() {
totalMessages: u.totalMessages,
totalInputTokens: u.totalInputTokens,
totalOutputTokens: u.totalOutputTokens,
totalCost: userCostMap[u.username] || 0,
topModels: Object.entries(userModelMap[u.username] || {}).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([name, count]) => ({ name, count })),
sharedProjects: JSON.parse((users.find(x => x.username === u.username) || {}).projects || '[]'),
})),
Expand Down
6 changes: 4 additions & 2 deletions ui/src/pages/RelayDashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import EditorIcon from '../components/EditorIcon'
import SectionTitle from '../components/SectionTitle'
import ChatSidebar from '../components/ChatSidebar'
import LiveFeed from '../components/LiveFeed'
import { editorColor, editorLabel, formatNumber, formatDate } from '../lib/constants'
import { editorColor, editorLabel, formatNumber, formatCost, formatDate } from '../lib/constants'
import { fetchRelayTeamStats, fetchRelaySearch, fetchRelaySession, mergeRelayUsers } from '../lib/api'
import AnimatedLoader from '../components/AnimatedLoader'
import PageHeader from '../components/PageHeader'
Expand Down Expand Up @@ -331,6 +331,7 @@ export default function RelayDashboard() {
<KpiCard label="projects" value={stats.totalProjects} />
<KpiCard label="messages" value={formatNumber(stats.totalMessages)} />
<KpiCard label="tokens" value={formatNumber(totalTok)} sub={`${formatNumber(tokPerSession)}/session`} />
<KpiCard label="total cost" value={formatCost(stats.totalCost)} sub="estimated" />
</div>

{/* Token overview */}
Expand Down Expand Up @@ -525,12 +526,13 @@ export default function RelayDashboard() {
))}
</div>
</div>
<div className="grid grid-cols-4 gap-1 text-center mb-2">
<div className="grid grid-cols-5 gap-1 text-center mb-2">
{[
[u.sessions, 'sessions'],
[formatNumber(u.totalMessages), 'messages'],
[u.projects, 'projects'],
[formatNumber(uTok), 'tokens'],
[formatCost(u.totalCost), 'cost'],
].map(([v, l]) => (
<div key={l} className="p-1 rounded-sm" style={{ background: 'var(--c-code-bg)' }}>
<div className="text-[11px] font-bold" style={{ color: 'var(--c-white)' }}>{v}</div>
Expand Down