diff --git a/pricing.json b/pricing.json
index a21c20b..217786a 100644
--- a/pricing.json
+++ b/pricing.json
@@ -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,
diff --git a/relay-server.js b/relay-server.js
index c88a656..535b0da 100644
--- a/relay-server.js
+++ b/relay-server.js
@@ -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');
@@ -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(`
@@ -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 => ({
@@ -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 || '[]'),
})),
diff --git a/ui/src/pages/RelayDashboard.jsx b/ui/src/pages/RelayDashboard.jsx
index f06673a..62608e7 100644
--- a/ui/src/pages/RelayDashboard.jsx
+++ b/ui/src/pages/RelayDashboard.jsx
@@ -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'
@@ -331,6 +331,7 @@ export default function RelayDashboard() {