mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
refactor: unify token usage stats across all session types (#274)
- Rewrite SQL queries to join bot_history_messages with bot_sessions, supporting chat/heartbeat/schedule usage from a single source - Update Go handler and CLI command to use unified queries - Fix daily chart stacking: each session type gets its own bar group - Add total input/output trend lines to the daily token chart - Fix summary cards reactivity by restricting aggregation to allDays range - Fix cache chart reactive dependency tracking by inlining data access - Add i18n keys for schedule, totalInput, totalOutput - Default time range changed to 7 days - Regenerate sqlc, swagger, and SDK
This commit is contained in:
@@ -1033,6 +1033,10 @@
|
||||
"chatOutput": "Chat Output",
|
||||
"heartbeatInput": "Heartbeat Input",
|
||||
"heartbeatOutput": "Heartbeat Output",
|
||||
"scheduleInput": "Schedule Input",
|
||||
"scheduleOutput": "Schedule Output",
|
||||
"totalInput": "Total Input",
|
||||
"totalOutput": "Total Output",
|
||||
"cacheRead": "Cache Read",
|
||||
"cacheWrite": "Cache Write",
|
||||
"noCache": "No Cache",
|
||||
@@ -1041,6 +1045,13 @@
|
||||
"modelDistribution": "Model Usage Distribution",
|
||||
"filterByModel": "Filter by Model",
|
||||
"allModels": "All Models",
|
||||
"sessionType": "Session Type",
|
||||
"allTypes": "All Types",
|
||||
"chat": "Chat",
|
||||
"heartbeat": "Heartbeat",
|
||||
"schedule": "Schedule",
|
||||
"inputTokens": "Input Tokens",
|
||||
"outputTokens": "Output Tokens",
|
||||
"dateFrom": "From",
|
||||
"dateTo": "To",
|
||||
"chartPie": "Pie",
|
||||
|
||||
@@ -1029,6 +1029,10 @@
|
||||
"chatOutput": "对话输出",
|
||||
"heartbeatInput": "心跳输入",
|
||||
"heartbeatOutput": "心跳输出",
|
||||
"scheduleInput": "定时任务输入",
|
||||
"scheduleOutput": "定时任务输出",
|
||||
"totalInput": "总输入",
|
||||
"totalOutput": "总输出",
|
||||
"cacheRead": "缓存读取",
|
||||
"cacheWrite": "缓存写入",
|
||||
"noCache": "无缓存",
|
||||
@@ -1037,6 +1041,13 @@
|
||||
"modelDistribution": "模型用量分布",
|
||||
"filterByModel": "按模型筛选",
|
||||
"allModels": "全部模型",
|
||||
"sessionType": "会话类型",
|
||||
"allTypes": "全部类型",
|
||||
"chat": "对话",
|
||||
"heartbeat": "心跳",
|
||||
"schedule": "定时任务",
|
||||
"inputTokens": "输入 Tokens",
|
||||
"outputTokens": "输出 Tokens",
|
||||
"dateFrom": "开始日期",
|
||||
"dateTo": "结束日期",
|
||||
"chartPie": "饼图",
|
||||
|
||||
@@ -75,6 +75,29 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<Label>{{ $t('usage.sessionType') }}</Label>
|
||||
<Select v-model="selectedSessionType">
|
||||
<SelectTrigger class="w-40">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">
|
||||
{{ $t('usage.allTypes') }}
|
||||
</SelectItem>
|
||||
<SelectItem value="chat">
|
||||
{{ $t('usage.chat') }}
|
||||
</SelectItem>
|
||||
<SelectItem value="heartbeat">
|
||||
{{ $t('usage.heartbeat') }}
|
||||
</SelectItem>
|
||||
<SelectItem value="schedule">
|
||||
{{ $t('usage.schedule') }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="modelOptions.length > 0"
|
||||
class="space-y-1.5"
|
||||
@@ -194,7 +217,7 @@
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Chart 1: Daily token usage stacked area -->
|
||||
<!-- Chart: Daily token usage -->
|
||||
<Card>
|
||||
<CardHeader class="pb-2">
|
||||
<CardTitle class="text-base">
|
||||
@@ -210,7 +233,7 @@
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Chart 2: Cache breakdown stacked bar -->
|
||||
<!-- Chart: Cache breakdown -->
|
||||
<Card>
|
||||
<CardHeader class="pb-2">
|
||||
<CardTitle class="text-base">
|
||||
@@ -226,7 +249,7 @@
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Chart 3: Cache hit rate line -->
|
||||
<!-- Chart: Cache hit rate -->
|
||||
<Card>
|
||||
<CardHeader class="pb-2">
|
||||
<CardTitle class="text-base">
|
||||
@@ -292,8 +315,9 @@ use([CanvasRenderer, LineChart, BarChart, PieChart, GridComponent, TooltipCompon
|
||||
const { t } = useI18n()
|
||||
|
||||
const selectedBotId = useSyncedQueryParam('bot', '')
|
||||
const timeRange = useSyncedQueryParam('range', '30')
|
||||
const timeRange = useSyncedQueryParam('range', '7')
|
||||
const selectedModelId = useSyncedQueryParam('model', 'all')
|
||||
const selectedSessionType = useSyncedQueryParam('type', 'all')
|
||||
const modelChartType = ref('pie')
|
||||
|
||||
function daysAgo(days: number): string {
|
||||
@@ -362,6 +386,38 @@ const modelOptions = computed(() =>
|
||||
byModelData.value.filter(m => m.model_id),
|
||||
)
|
||||
|
||||
type SessionType = 'chat' | 'heartbeat' | 'schedule'
|
||||
|
||||
const sessionTypeFilter = computed(() =>
|
||||
selectedSessionType.value === 'all' ? null : selectedSessionType.value as SessionType,
|
||||
)
|
||||
|
||||
interface TypedDayMaps {
|
||||
chat: Map<string, HandlersDailyTokenUsage>
|
||||
heartbeat: Map<string, HandlersDailyTokenUsage>
|
||||
schedule: Map<string, HandlersDailyTokenUsage>
|
||||
}
|
||||
|
||||
function buildDayMap(rows: HandlersDailyTokenUsage[] | undefined) {
|
||||
const map = new Map<string, HandlersDailyTokenUsage>()
|
||||
for (const r of rows ?? []) {
|
||||
if (r.day) map.set(r.day, r)
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
const dayMaps = computed<TypedDayMaps>(() => ({
|
||||
chat: buildDayMap(usageData.value?.chat),
|
||||
heartbeat: buildDayMap(usageData.value?.heartbeat),
|
||||
schedule: buildDayMap(usageData.value?.schedule),
|
||||
}))
|
||||
|
||||
const activeTypes = computed<SessionType[]>(() => {
|
||||
const filter = sessionTypeFilter.value
|
||||
if (filter) return [filter]
|
||||
return ['chat', 'heartbeat', 'schedule']
|
||||
})
|
||||
|
||||
const allDays = computed(() => {
|
||||
const from = new Date(dateFrom.value + 'T00:00:00')
|
||||
const toExclusive = new Date(dateTo.value + 'T00:00:00')
|
||||
@@ -383,26 +439,22 @@ const allDays = computed(() => {
|
||||
const hasData = computed(() => {
|
||||
const chat = usageData.value?.chat ?? []
|
||||
const heartbeat = usageData.value?.heartbeat ?? []
|
||||
return chat.length > 0 || heartbeat.length > 0 || byModelData.value.length > 0
|
||||
const schedule = usageData.value?.schedule ?? []
|
||||
return chat.length > 0 || heartbeat.length > 0 || schedule.length > 0 || byModelData.value.length > 0
|
||||
})
|
||||
|
||||
function buildDayMap(rows: HandlersDailyTokenUsage[] | undefined) {
|
||||
const map = new Map<string, HandlersDailyTokenUsage>()
|
||||
for (const r of rows ?? []) {
|
||||
if (r.day) map.set(r.day, r)
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
const summary = computed(() => {
|
||||
const chatMap = buildDayMap(usageData.value?.chat)
|
||||
const hbMap = buildDayMap(usageData.value?.heartbeat)
|
||||
const days = allDays.value
|
||||
const types = activeTypes.value
|
||||
const maps = dayMaps.value
|
||||
let totalInput = 0
|
||||
let totalOutput = 0
|
||||
let totalCacheRead = 0
|
||||
let totalReasoning = 0
|
||||
for (const m of [chatMap, hbMap]) {
|
||||
for (const r of m.values()) {
|
||||
for (const day of days) {
|
||||
for (const tp of types) {
|
||||
const r = maps[tp].get(day)
|
||||
if (!r) continue
|
||||
totalInput += r.input_tokens ?? 0
|
||||
totalOutput += r.output_tokens ?? 0
|
||||
totalCacheRead += r.cache_read_tokens ?? 0
|
||||
@@ -418,6 +470,11 @@ const summary = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const sessionTypeInputLabel = (type: SessionType) =>
|
||||
t(`usage.${type}Input`)
|
||||
const sessionTypeOutputLabel = (type: SessionType) =>
|
||||
t(`usage.${type}Output`)
|
||||
|
||||
function modelLabel(m: HandlersModelTokenUsage) {
|
||||
return `${m.model_name || m.model_slug} (${m.provider_name})`
|
||||
}
|
||||
@@ -464,7 +521,7 @@ const modelBarOption = computed(() => {
|
||||
const names = models.map(m => modelLabel(m))
|
||||
return {
|
||||
tooltip: { trigger: 'axis' as const },
|
||||
legend: { data: [t('usage.chatInput'), t('usage.chatOutput')] },
|
||||
legend: { data: [t('usage.inputTokens'), t('usage.outputTokens')] },
|
||||
grid: { left: 60, right: 20, bottom: 60, top: 40 },
|
||||
xAxis: {
|
||||
type: 'category' as const,
|
||||
@@ -474,13 +531,13 @@ const modelBarOption = computed(() => {
|
||||
yAxis: { type: 'value' as const },
|
||||
series: [
|
||||
{
|
||||
name: t('usage.chatInput'),
|
||||
name: t('usage.inputTokens'),
|
||||
type: 'bar' as const,
|
||||
stack: 'tokens',
|
||||
data: models.map(m => m.input_tokens ?? 0),
|
||||
},
|
||||
{
|
||||
name: t('usage.chatOutput'),
|
||||
name: t('usage.outputTokens'),
|
||||
type: 'bar' as const,
|
||||
stack: 'tokens',
|
||||
data: models.map(m => m.output_tokens ?? 0),
|
||||
@@ -495,56 +552,90 @@ const modelChartOption = computed(() =>
|
||||
|
||||
const dailyTokensOption = computed(() => {
|
||||
const days = allDays.value
|
||||
const chatMap = buildDayMap(usageData.value?.chat)
|
||||
const hbMap = buildDayMap(usageData.value?.heartbeat)
|
||||
const types = activeTypes.value
|
||||
const maps = dayMaps.value
|
||||
|
||||
const legendItems: string[] = []
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const series: any[] = []
|
||||
|
||||
for (const type of types) {
|
||||
const inputName = sessionTypeInputLabel(type)
|
||||
const outputName = sessionTypeOutputLabel(type)
|
||||
legendItems.push(inputName, outputName)
|
||||
const map = maps[type]
|
||||
|
||||
series.push({
|
||||
name: inputName,
|
||||
type: 'bar' as const,
|
||||
stack: type,
|
||||
data: days.map(d => map.get(d)?.input_tokens ?? 0),
|
||||
})
|
||||
series.push({
|
||||
name: outputName,
|
||||
type: 'bar' as const,
|
||||
stack: type,
|
||||
data: days.map(d => map.get(d)?.output_tokens ?? 0),
|
||||
})
|
||||
}
|
||||
|
||||
const totalInputLabel = t('usage.totalInput')
|
||||
const totalOutputLabel = t('usage.totalOutput')
|
||||
legendItems.push(totalInputLabel, totalOutputLabel)
|
||||
|
||||
series.push({
|
||||
name: totalInputLabel,
|
||||
type: 'line' as const,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
data: days.map(d => {
|
||||
let sum = 0
|
||||
for (const tp of types) sum += maps[tp].get(d)?.input_tokens ?? 0
|
||||
return sum
|
||||
}),
|
||||
})
|
||||
series.push({
|
||||
name: totalOutputLabel,
|
||||
type: 'line' as const,
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 4,
|
||||
data: days.map(d => {
|
||||
let sum = 0
|
||||
for (const tp of types) sum += maps[tp].get(d)?.output_tokens ?? 0
|
||||
return sum
|
||||
}),
|
||||
})
|
||||
|
||||
return {
|
||||
tooltip: { trigger: 'axis' as const },
|
||||
legend: {
|
||||
data: [t('usage.chatInput'), t('usage.chatOutput'), t('usage.heartbeatInput'), t('usage.heartbeatOutput')],
|
||||
data: legendItems,
|
||||
bottom: 0,
|
||||
left: 'center',
|
||||
itemGap: 16,
|
||||
itemGap: 12,
|
||||
},
|
||||
grid: { left: 60, right: 20, bottom: 50, top: 20 },
|
||||
grid: { left: 60, right: 20, bottom: 55, top: 20 },
|
||||
xAxis: { type: 'category' as const, data: days },
|
||||
yAxis: { type: 'value' as const },
|
||||
series: [
|
||||
{
|
||||
name: t('usage.chatInput'),
|
||||
type: 'line' as const,
|
||||
stack: 'input',
|
||||
areaStyle: {},
|
||||
data: days.map(d => chatMap.get(d)?.input_tokens ?? 0),
|
||||
},
|
||||
{
|
||||
name: t('usage.heartbeatInput'),
|
||||
type: 'line' as const,
|
||||
stack: 'input',
|
||||
areaStyle: {},
|
||||
data: days.map(d => hbMap.get(d)?.input_tokens ?? 0),
|
||||
},
|
||||
{
|
||||
name: t('usage.chatOutput'),
|
||||
type: 'line' as const,
|
||||
stack: 'output',
|
||||
areaStyle: {},
|
||||
data: days.map(d => chatMap.get(d)?.output_tokens ?? 0),
|
||||
},
|
||||
{
|
||||
name: t('usage.heartbeatOutput'),
|
||||
type: 'line' as const,
|
||||
stack: 'output',
|
||||
areaStyle: {},
|
||||
data: days.map(d => hbMap.get(d)?.output_tokens ?? 0),
|
||||
},
|
||||
],
|
||||
series,
|
||||
}
|
||||
})
|
||||
|
||||
const cacheBreakdownOption = computed(() => {
|
||||
const days = allDays.value
|
||||
const chatMap = buildDayMap(usageData.value?.chat)
|
||||
const hbMap = buildDayMap(usageData.value?.heartbeat)
|
||||
const types = activeTypes.value
|
||||
const maps = dayMaps.value
|
||||
|
||||
function sumField(day: string, field: 'cache_read_tokens' | 'cache_write_tokens' | 'input_tokens') {
|
||||
let total = 0
|
||||
for (const tp of types) {
|
||||
total += (maps[tp].get(day)?.[field] ?? 0) as number
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
return {
|
||||
tooltip: { trigger: 'axis' as const },
|
||||
legend: {
|
||||
@@ -561,32 +652,22 @@ const cacheBreakdownOption = computed(() => {
|
||||
name: t('usage.cacheRead'),
|
||||
type: 'bar' as const,
|
||||
stack: 'cache',
|
||||
data: days.map(d => {
|
||||
const c = chatMap.get(d)
|
||||
const h = hbMap.get(d)
|
||||
return (c?.cache_read_tokens ?? 0) + (h?.cache_read_tokens ?? 0)
|
||||
}),
|
||||
data: days.map(d => sumField(d, 'cache_read_tokens')),
|
||||
},
|
||||
{
|
||||
name: t('usage.cacheWrite'),
|
||||
type: 'bar' as const,
|
||||
stack: 'cache',
|
||||
data: days.map(d => {
|
||||
const c = chatMap.get(d)
|
||||
const h = hbMap.get(d)
|
||||
return (c?.cache_write_tokens ?? 0) + (h?.cache_write_tokens ?? 0)
|
||||
}),
|
||||
data: days.map(d => sumField(d, 'cache_write_tokens')),
|
||||
},
|
||||
{
|
||||
name: t('usage.noCache'),
|
||||
type: 'bar' as const,
|
||||
stack: 'cache',
|
||||
data: days.map(d => {
|
||||
const c = chatMap.get(d)
|
||||
const h = hbMap.get(d)
|
||||
const totalInput = (c?.input_tokens ?? 0) + (h?.input_tokens ?? 0)
|
||||
const cacheRead = (c?.cache_read_tokens ?? 0) + (h?.cache_read_tokens ?? 0)
|
||||
const cacheWrite = (c?.cache_write_tokens ?? 0) + (h?.cache_write_tokens ?? 0)
|
||||
const totalInput = sumField(d, 'input_tokens')
|
||||
const cacheRead = sumField(d, 'cache_read_tokens')
|
||||
const cacheWrite = sumField(d, 'cache_write_tokens')
|
||||
return Math.max(0, totalInput - cacheRead - cacheWrite)
|
||||
}),
|
||||
},
|
||||
@@ -596,8 +677,17 @@ const cacheBreakdownOption = computed(() => {
|
||||
|
||||
const cacheHitRateOption = computed(() => {
|
||||
const days = allDays.value
|
||||
const chatMap = buildDayMap(usageData.value?.chat)
|
||||
const hbMap = buildDayMap(usageData.value?.heartbeat)
|
||||
const types = activeTypes.value
|
||||
const maps = dayMaps.value
|
||||
|
||||
function sumField(day: string, field: 'cache_read_tokens' | 'input_tokens') {
|
||||
let total = 0
|
||||
for (const tp of types) {
|
||||
total += (maps[tp].get(day)?.[field] ?? 0) as number
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'axis' as const,
|
||||
@@ -615,10 +705,8 @@ const cacheHitRateOption = computed(() => {
|
||||
type: 'line' as const,
|
||||
smooth: true,
|
||||
data: days.map(d => {
|
||||
const c = chatMap.get(d)
|
||||
const h = hbMap.get(d)
|
||||
const totalInput = (c?.input_tokens ?? 0) + (h?.input_tokens ?? 0)
|
||||
const cacheRead = (c?.cache_read_tokens ?? 0) + (h?.cache_read_tokens ?? 0)
|
||||
const totalInput = sumField(d, 'input_tokens')
|
||||
const cacheRead = sumField(d, 'cache_read_tokens')
|
||||
return totalInput > 0 ? parseFloat(((cacheRead / totalInput) * 100).toFixed(1)) : 0
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
-- name: GetMessageTokenUsageByDay :many
|
||||
-- name: GetTokenUsageByDayAndType :many
|
||||
SELECT
|
||||
COALESCE(s.type, 'chat')::text AS session_type,
|
||||
date_trunc('day', m.created_at)::date AS day,
|
||||
COALESCE(SUM((m.usage->>'inputTokens')::bigint), 0)::bigint AS input_tokens,
|
||||
COALESCE(SUM((m.usage->>'outputTokens')::bigint), 0)::bigint AS output_tokens,
|
||||
@@ -7,32 +8,16 @@ SELECT
|
||||
COALESCE(SUM((m.usage->'inputTokenDetails'->>'cacheWriteTokens')::bigint), 0)::bigint AS cache_write_tokens,
|
||||
COALESCE(SUM((m.usage->'outputTokenDetails'->>'reasoningTokens')::bigint), 0)::bigint AS reasoning_tokens
|
||||
FROM bot_history_messages m
|
||||
LEFT JOIN bot_sessions s ON s.id = m.session_id
|
||||
WHERE m.bot_id = sqlc.arg(bot_id)
|
||||
AND m.usage IS NOT NULL
|
||||
AND m.created_at >= sqlc.arg(from_time)
|
||||
AND m.created_at < sqlc.arg(to_time)
|
||||
AND (sqlc.narg(model_id)::uuid IS NULL OR m.model_id = sqlc.narg(model_id)::uuid)
|
||||
GROUP BY day
|
||||
ORDER BY day;
|
||||
GROUP BY session_type, day
|
||||
ORDER BY day, session_type;
|
||||
|
||||
-- name: GetHeartbeatTokenUsageByDay :many
|
||||
SELECT
|
||||
date_trunc('day', h.started_at)::date AS day,
|
||||
COALESCE(SUM((h.usage->>'inputTokens')::bigint), 0)::bigint AS input_tokens,
|
||||
COALESCE(SUM((h.usage->>'outputTokens')::bigint), 0)::bigint AS output_tokens,
|
||||
COALESCE(SUM((h.usage->'inputTokenDetails'->>'cacheReadTokens')::bigint), 0)::bigint AS cache_read_tokens,
|
||||
COALESCE(SUM((h.usage->'inputTokenDetails'->>'cacheWriteTokens')::bigint), 0)::bigint AS cache_write_tokens,
|
||||
COALESCE(SUM((h.usage->'outputTokenDetails'->>'reasoningTokens')::bigint), 0)::bigint AS reasoning_tokens
|
||||
FROM bot_heartbeat_logs h
|
||||
WHERE h.bot_id = sqlc.arg(bot_id)
|
||||
AND h.usage IS NOT NULL
|
||||
AND h.started_at >= sqlc.arg(from_time)
|
||||
AND h.started_at < sqlc.arg(to_time)
|
||||
AND (sqlc.narg(model_id)::uuid IS NULL OR h.model_id = sqlc.narg(model_id)::uuid)
|
||||
GROUP BY day
|
||||
ORDER BY day;
|
||||
|
||||
-- name: GetMessageTokenUsageByModel :many
|
||||
-- name: GetTokenUsageByModel :many
|
||||
SELECT
|
||||
m.model_id,
|
||||
COALESCE(mo.model_id, 'unknown') AS model_slug,
|
||||
@@ -49,21 +34,3 @@ WHERE m.bot_id = sqlc.arg(bot_id)
|
||||
AND m.created_at < sqlc.arg(to_time)
|
||||
GROUP BY m.model_id, mo.model_id, mo.name, lp.name
|
||||
ORDER BY input_tokens DESC;
|
||||
|
||||
-- name: GetHeartbeatTokenUsageByModel :many
|
||||
SELECT
|
||||
h.model_id,
|
||||
COALESCE(mo.model_id, 'unknown') AS model_slug,
|
||||
COALESCE(mo.name, 'Unknown') AS model_name,
|
||||
COALESCE(lp.name, 'Unknown') AS provider_name,
|
||||
COALESCE(SUM((h.usage->>'inputTokens')::bigint), 0)::bigint AS input_tokens,
|
||||
COALESCE(SUM((h.usage->>'outputTokens')::bigint), 0)::bigint AS output_tokens
|
||||
FROM bot_heartbeat_logs h
|
||||
LEFT JOIN models mo ON mo.id = h.model_id
|
||||
LEFT JOIN llm_providers lp ON lp.id = mo.llm_provider_id
|
||||
WHERE h.bot_id = sqlc.arg(bot_id)
|
||||
AND h.usage IS NOT NULL
|
||||
AND h.started_at >= sqlc.arg(from_time)
|
||||
AND h.started_at < sqlc.arg(to_time)
|
||||
GROUP BY h.model_id, mo.model_id, mo.name, lp.name
|
||||
ORDER BY input_tokens DESC;
|
||||
|
||||
+33
-46
@@ -28,46 +28,52 @@ func (h *Handler) buildUsageGroup() *CommandGroup {
|
||||
toTS := pgtype.Timestamptz{Time: now, Valid: true}
|
||||
nullModel := pgtype.UUID{Valid: false}
|
||||
|
||||
chatRows, err := h.queries.GetMessageTokenUsageByDay(cc.Ctx, dbsqlc.GetMessageTokenUsageByDayParams{
|
||||
rows, err := h.queries.GetTokenUsageByDayAndType(cc.Ctx, dbsqlc.GetTokenUsageByDayAndTypeParams{
|
||||
BotID: botUUID, FromTime: fromTS, ToTime: toTS, ModelID: nullModel,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hbRows, err := h.queries.GetHeartbeatTokenUsageByDay(cc.Ctx, dbsqlc.GetHeartbeatTokenUsageByDayParams{
|
||||
BotID: botUUID, FromTime: fromTS, ToTime: toTS, ModelID: nullModel,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(chatRows) == 0 && len(hbRows) == 0 {
|
||||
if len(rows) == 0 {
|
||||
return "No token usage in the last 7 days.", nil
|
||||
}
|
||||
|
||||
type bucket struct {
|
||||
label string
|
||||
rows []dbsqlc.GetTokenUsageByDayAndTypeRow
|
||||
}
|
||||
buckets := []bucket{
|
||||
{label: "Chat"},
|
||||
{label: "Heartbeat"},
|
||||
{label: "Schedule"},
|
||||
}
|
||||
for _, r := range rows {
|
||||
switch r.SessionType {
|
||||
case "heartbeat":
|
||||
buckets[1].rows = append(buckets[1].rows, r)
|
||||
case "schedule":
|
||||
buckets[2].rows = append(buckets[2].rows, r)
|
||||
default:
|
||||
buckets[0].rows = append(buckets[0].rows, r)
|
||||
}
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("Token usage (last 7 days):\n\n")
|
||||
|
||||
if len(chatRows) > 0 {
|
||||
b.WriteString("Chat:\n")
|
||||
var totalIn, totalOut int64
|
||||
for _, r := range chatRows {
|
||||
day := r.Day.Time.Format("01-02")
|
||||
fmt.Fprintf(&b, " %s: in=%d out=%d\n", day, r.InputTokens, r.OutputTokens)
|
||||
totalIn += r.InputTokens
|
||||
totalOut += r.OutputTokens
|
||||
first := true
|
||||
for _, bk := range buckets {
|
||||
if len(bk.rows) == 0 {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(&b, " Total: in=%d out=%d\n", totalIn, totalOut)
|
||||
}
|
||||
|
||||
if len(hbRows) > 0 {
|
||||
if len(chatRows) > 0 {
|
||||
if !first {
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
b.WriteString("Heartbeat:\n")
|
||||
first = false
|
||||
b.WriteString(bk.label + ":\n")
|
||||
var totalIn, totalOut int64
|
||||
for _, r := range hbRows {
|
||||
for _, r := range bk.rows {
|
||||
day := r.Day.Time.Format("01-02")
|
||||
fmt.Fprintf(&b, " %s: in=%d out=%d\n", day, r.InputTokens, r.OutputTokens)
|
||||
totalIn += r.InputTokens
|
||||
@@ -92,42 +98,23 @@ func (h *Handler) buildUsageGroup() *CommandGroup {
|
||||
fromTS := pgtype.Timestamptz{Time: from, Valid: true}
|
||||
toTS := pgtype.Timestamptz{Time: now, Valid: true}
|
||||
|
||||
chatRows, err := h.queries.GetMessageTokenUsageByModel(cc.Ctx, dbsqlc.GetMessageTokenUsageByModelParams{
|
||||
BotID: botUUID, FromTime: fromTS, ToTime: toTS,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
hbRows, err := h.queries.GetHeartbeatTokenUsageByModel(cc.Ctx, dbsqlc.GetHeartbeatTokenUsageByModelParams{
|
||||
rows, err := h.queries.GetTokenUsageByModel(cc.Ctx, dbsqlc.GetTokenUsageByModelParams{
|
||||
BotID: botUUID, FromTime: fromTS, ToTime: toTS,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(chatRows) == 0 && len(hbRows) == 0 {
|
||||
if len(rows) == 0 {
|
||||
return "No token usage in the last 7 days.", nil
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("Token usage by model (last 7 days):\n\n")
|
||||
|
||||
if len(chatRows) > 0 {
|
||||
b.WriteString("Chat:\n")
|
||||
for _, r := range chatRows {
|
||||
for _, r := range rows {
|
||||
fmt.Fprintf(&b, " %s (%s): in=%d out=%d\n", r.ModelName, r.ProviderName, r.InputTokens, r.OutputTokens)
|
||||
}
|
||||
}
|
||||
|
||||
if len(hbRows) > 0 {
|
||||
if len(chatRows) > 0 {
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
b.WriteString("Heartbeat:\n")
|
||||
for _, r := range hbRows {
|
||||
fmt.Fprintf(&b, " %s (%s): in=%d out=%d\n", r.ModelName, r.ProviderName, r.InputTokens, r.OutputTokens)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.TrimRight(b.String(), "\n"), nil
|
||||
},
|
||||
|
||||
@@ -11,135 +11,9 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const getHeartbeatTokenUsageByDay = `-- name: GetHeartbeatTokenUsageByDay :many
|
||||
SELECT
|
||||
date_trunc('day', h.started_at)::date AS day,
|
||||
COALESCE(SUM((h.usage->>'inputTokens')::bigint), 0)::bigint AS input_tokens,
|
||||
COALESCE(SUM((h.usage->>'outputTokens')::bigint), 0)::bigint AS output_tokens,
|
||||
COALESCE(SUM((h.usage->'inputTokenDetails'->>'cacheReadTokens')::bigint), 0)::bigint AS cache_read_tokens,
|
||||
COALESCE(SUM((h.usage->'inputTokenDetails'->>'cacheWriteTokens')::bigint), 0)::bigint AS cache_write_tokens,
|
||||
COALESCE(SUM((h.usage->'outputTokenDetails'->>'reasoningTokens')::bigint), 0)::bigint AS reasoning_tokens
|
||||
FROM bot_heartbeat_logs h
|
||||
WHERE h.bot_id = $1
|
||||
AND h.usage IS NOT NULL
|
||||
AND h.started_at >= $2
|
||||
AND h.started_at < $3
|
||||
AND ($4::uuid IS NULL OR h.model_id = $4::uuid)
|
||||
GROUP BY day
|
||||
ORDER BY day
|
||||
`
|
||||
|
||||
type GetHeartbeatTokenUsageByDayParams struct {
|
||||
BotID pgtype.UUID `json:"bot_id"`
|
||||
FromTime pgtype.Timestamptz `json:"from_time"`
|
||||
ToTime pgtype.Timestamptz `json:"to_time"`
|
||||
ModelID pgtype.UUID `json:"model_id"`
|
||||
}
|
||||
|
||||
type GetHeartbeatTokenUsageByDayRow struct {
|
||||
Day pgtype.Date `json:"day"`
|
||||
InputTokens int64 `json:"input_tokens"`
|
||||
OutputTokens int64 `json:"output_tokens"`
|
||||
CacheReadTokens int64 `json:"cache_read_tokens"`
|
||||
CacheWriteTokens int64 `json:"cache_write_tokens"`
|
||||
ReasoningTokens int64 `json:"reasoning_tokens"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetHeartbeatTokenUsageByDay(ctx context.Context, arg GetHeartbeatTokenUsageByDayParams) ([]GetHeartbeatTokenUsageByDayRow, error) {
|
||||
rows, err := q.db.Query(ctx, getHeartbeatTokenUsageByDay,
|
||||
arg.BotID,
|
||||
arg.FromTime,
|
||||
arg.ToTime,
|
||||
arg.ModelID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetHeartbeatTokenUsageByDayRow
|
||||
for rows.Next() {
|
||||
var i GetHeartbeatTokenUsageByDayRow
|
||||
if err := rows.Scan(
|
||||
&i.Day,
|
||||
&i.InputTokens,
|
||||
&i.OutputTokens,
|
||||
&i.CacheReadTokens,
|
||||
&i.CacheWriteTokens,
|
||||
&i.ReasoningTokens,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getHeartbeatTokenUsageByModel = `-- name: GetHeartbeatTokenUsageByModel :many
|
||||
SELECT
|
||||
h.model_id,
|
||||
COALESCE(mo.model_id, 'unknown') AS model_slug,
|
||||
COALESCE(mo.name, 'Unknown') AS model_name,
|
||||
COALESCE(lp.name, 'Unknown') AS provider_name,
|
||||
COALESCE(SUM((h.usage->>'inputTokens')::bigint), 0)::bigint AS input_tokens,
|
||||
COALESCE(SUM((h.usage->>'outputTokens')::bigint), 0)::bigint AS output_tokens
|
||||
FROM bot_heartbeat_logs h
|
||||
LEFT JOIN models mo ON mo.id = h.model_id
|
||||
LEFT JOIN llm_providers lp ON lp.id = mo.llm_provider_id
|
||||
WHERE h.bot_id = $1
|
||||
AND h.usage IS NOT NULL
|
||||
AND h.started_at >= $2
|
||||
AND h.started_at < $3
|
||||
GROUP BY h.model_id, mo.model_id, mo.name, lp.name
|
||||
ORDER BY input_tokens DESC
|
||||
`
|
||||
|
||||
type GetHeartbeatTokenUsageByModelParams struct {
|
||||
BotID pgtype.UUID `json:"bot_id"`
|
||||
FromTime pgtype.Timestamptz `json:"from_time"`
|
||||
ToTime pgtype.Timestamptz `json:"to_time"`
|
||||
}
|
||||
|
||||
type GetHeartbeatTokenUsageByModelRow struct {
|
||||
ModelID pgtype.UUID `json:"model_id"`
|
||||
ModelSlug string `json:"model_slug"`
|
||||
ModelName string `json:"model_name"`
|
||||
ProviderName string `json:"provider_name"`
|
||||
InputTokens int64 `json:"input_tokens"`
|
||||
OutputTokens int64 `json:"output_tokens"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetHeartbeatTokenUsageByModel(ctx context.Context, arg GetHeartbeatTokenUsageByModelParams) ([]GetHeartbeatTokenUsageByModelRow, error) {
|
||||
rows, err := q.db.Query(ctx, getHeartbeatTokenUsageByModel, arg.BotID, arg.FromTime, arg.ToTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetHeartbeatTokenUsageByModelRow
|
||||
for rows.Next() {
|
||||
var i GetHeartbeatTokenUsageByModelRow
|
||||
if err := rows.Scan(
|
||||
&i.ModelID,
|
||||
&i.ModelSlug,
|
||||
&i.ModelName,
|
||||
&i.ProviderName,
|
||||
&i.InputTokens,
|
||||
&i.OutputTokens,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getMessageTokenUsageByDay = `-- name: GetMessageTokenUsageByDay :many
|
||||
const getTokenUsageByDayAndType = `-- name: GetTokenUsageByDayAndType :many
|
||||
SELECT
|
||||
COALESCE(s.type, 'chat')::text AS session_type,
|
||||
date_trunc('day', m.created_at)::date AS day,
|
||||
COALESCE(SUM((m.usage->>'inputTokens')::bigint), 0)::bigint AS input_tokens,
|
||||
COALESCE(SUM((m.usage->>'outputTokens')::bigint), 0)::bigint AS output_tokens,
|
||||
@@ -147,23 +21,25 @@ SELECT
|
||||
COALESCE(SUM((m.usage->'inputTokenDetails'->>'cacheWriteTokens')::bigint), 0)::bigint AS cache_write_tokens,
|
||||
COALESCE(SUM((m.usage->'outputTokenDetails'->>'reasoningTokens')::bigint), 0)::bigint AS reasoning_tokens
|
||||
FROM bot_history_messages m
|
||||
LEFT JOIN bot_sessions s ON s.id = m.session_id
|
||||
WHERE m.bot_id = $1
|
||||
AND m.usage IS NOT NULL
|
||||
AND m.created_at >= $2
|
||||
AND m.created_at < $3
|
||||
AND ($4::uuid IS NULL OR m.model_id = $4::uuid)
|
||||
GROUP BY day
|
||||
ORDER BY day
|
||||
GROUP BY session_type, day
|
||||
ORDER BY day, session_type
|
||||
`
|
||||
|
||||
type GetMessageTokenUsageByDayParams struct {
|
||||
type GetTokenUsageByDayAndTypeParams struct {
|
||||
BotID pgtype.UUID `json:"bot_id"`
|
||||
FromTime pgtype.Timestamptz `json:"from_time"`
|
||||
ToTime pgtype.Timestamptz `json:"to_time"`
|
||||
ModelID pgtype.UUID `json:"model_id"`
|
||||
}
|
||||
|
||||
type GetMessageTokenUsageByDayRow struct {
|
||||
type GetTokenUsageByDayAndTypeRow struct {
|
||||
SessionType string `json:"session_type"`
|
||||
Day pgtype.Date `json:"day"`
|
||||
InputTokens int64 `json:"input_tokens"`
|
||||
OutputTokens int64 `json:"output_tokens"`
|
||||
@@ -172,8 +48,8 @@ type GetMessageTokenUsageByDayRow struct {
|
||||
ReasoningTokens int64 `json:"reasoning_tokens"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetMessageTokenUsageByDay(ctx context.Context, arg GetMessageTokenUsageByDayParams) ([]GetMessageTokenUsageByDayRow, error) {
|
||||
rows, err := q.db.Query(ctx, getMessageTokenUsageByDay,
|
||||
func (q *Queries) GetTokenUsageByDayAndType(ctx context.Context, arg GetTokenUsageByDayAndTypeParams) ([]GetTokenUsageByDayAndTypeRow, error) {
|
||||
rows, err := q.db.Query(ctx, getTokenUsageByDayAndType,
|
||||
arg.BotID,
|
||||
arg.FromTime,
|
||||
arg.ToTime,
|
||||
@@ -183,10 +59,11 @@ func (q *Queries) GetMessageTokenUsageByDay(ctx context.Context, arg GetMessageT
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetMessageTokenUsageByDayRow
|
||||
var items []GetTokenUsageByDayAndTypeRow
|
||||
for rows.Next() {
|
||||
var i GetMessageTokenUsageByDayRow
|
||||
var i GetTokenUsageByDayAndTypeRow
|
||||
if err := rows.Scan(
|
||||
&i.SessionType,
|
||||
&i.Day,
|
||||
&i.InputTokens,
|
||||
&i.OutputTokens,
|
||||
@@ -204,7 +81,7 @@ func (q *Queries) GetMessageTokenUsageByDay(ctx context.Context, arg GetMessageT
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getMessageTokenUsageByModel = `-- name: GetMessageTokenUsageByModel :many
|
||||
const getTokenUsageByModel = `-- name: GetTokenUsageByModel :many
|
||||
SELECT
|
||||
m.model_id,
|
||||
COALESCE(mo.model_id, 'unknown') AS model_slug,
|
||||
@@ -223,13 +100,13 @@ GROUP BY m.model_id, mo.model_id, mo.name, lp.name
|
||||
ORDER BY input_tokens DESC
|
||||
`
|
||||
|
||||
type GetMessageTokenUsageByModelParams struct {
|
||||
type GetTokenUsageByModelParams struct {
|
||||
BotID pgtype.UUID `json:"bot_id"`
|
||||
FromTime pgtype.Timestamptz `json:"from_time"`
|
||||
ToTime pgtype.Timestamptz `json:"to_time"`
|
||||
}
|
||||
|
||||
type GetMessageTokenUsageByModelRow struct {
|
||||
type GetTokenUsageByModelRow struct {
|
||||
ModelID pgtype.UUID `json:"model_id"`
|
||||
ModelSlug string `json:"model_slug"`
|
||||
ModelName string `json:"model_name"`
|
||||
@@ -238,15 +115,15 @@ type GetMessageTokenUsageByModelRow struct {
|
||||
OutputTokens int64 `json:"output_tokens"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetMessageTokenUsageByModel(ctx context.Context, arg GetMessageTokenUsageByModelParams) ([]GetMessageTokenUsageByModelRow, error) {
|
||||
rows, err := q.db.Query(ctx, getMessageTokenUsageByModel, arg.BotID, arg.FromTime, arg.ToTime)
|
||||
func (q *Queries) GetTokenUsageByModel(ctx context.Context, arg GetTokenUsageByModelParams) ([]GetTokenUsageByModelRow, error) {
|
||||
rows, err := q.db.Query(ctx, getTokenUsageByModel, arg.BotID, arg.FromTime, arg.ToTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetMessageTokenUsageByModelRow
|
||||
var items []GetTokenUsageByModelRow
|
||||
for rows.Next() {
|
||||
var i GetMessageTokenUsageByModelRow
|
||||
var i GetTokenUsageByModelRow
|
||||
if err := rows.Scan(
|
||||
&i.ModelID,
|
||||
&i.ModelSlug,
|
||||
|
||||
@@ -60,12 +60,13 @@ type ModelTokenUsage struct {
|
||||
type TokenUsageResponse struct {
|
||||
Chat []DailyTokenUsage `json:"chat"`
|
||||
Heartbeat []DailyTokenUsage `json:"heartbeat"`
|
||||
Schedule []DailyTokenUsage `json:"schedule"`
|
||||
ByModel []ModelTokenUsage `json:"by_model"`
|
||||
}
|
||||
|
||||
// GetTokenUsage godoc
|
||||
// @Summary Get token usage statistics
|
||||
// @Description Get daily aggregated token usage for a bot, split by chat and heartbeat, with optional model filter and per-model breakdown
|
||||
// @Description Get daily aggregated token usage for a bot, split by chat, heartbeat, and schedule session types, with optional model filter and per-model breakdown
|
||||
// @Tags token-usage
|
||||
// @Param bot_id path string true "Bot ID"
|
||||
// @Param from query string true "Start date (YYYY-MM-DD)"
|
||||
@@ -124,7 +125,7 @@ func (h *TokenUsageHandler) GetTokenUsage(c echo.Context) error {
|
||||
|
||||
ctx := c.Request().Context()
|
||||
|
||||
chatRows, hbRows, err := h.fetchUsage(ctx, pgBotID, fromTS, toTS, pgModelID)
|
||||
chat, heartbeat, schedule, err := h.fetchUsageByDay(ctx, pgBotID, fromTS, toTS, pgModelID)
|
||||
if err != nil {
|
||||
h.logger.Error("fetch token usage failed", slog.Any("error", err))
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "failed to fetch token usage")
|
||||
@@ -137,39 +138,48 @@ func (h *TokenUsageHandler) GetTokenUsage(c echo.Context) error {
|
||||
}
|
||||
|
||||
resp := TokenUsageResponse{
|
||||
Chat: convertMessageRows(chatRows),
|
||||
Heartbeat: convertHeartbeatRows(hbRows),
|
||||
Chat: chat,
|
||||
Heartbeat: heartbeat,
|
||||
Schedule: schedule,
|
||||
ByModel: byModel,
|
||||
}
|
||||
return c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (h *TokenUsageHandler) fetchUsage(ctx context.Context, botID pgtype.UUID, from, to pgtype.Timestamptz, modelID pgtype.UUID) ([]sqlc.GetMessageTokenUsageByDayRow, []sqlc.GetHeartbeatTokenUsageByDayRow, error) {
|
||||
chatRows, err := h.queries.GetMessageTokenUsageByDay(ctx, sqlc.GetMessageTokenUsageByDayParams{
|
||||
func (h *TokenUsageHandler) fetchUsageByDay(ctx context.Context, botID pgtype.UUID, from, to pgtype.Timestamptz, modelID pgtype.UUID) (chat, heartbeat, schedule []DailyTokenUsage, err error) {
|
||||
rows, err := h.queries.GetTokenUsageByDayAndType(ctx, sqlc.GetTokenUsageByDayAndTypeParams{
|
||||
BotID: botID,
|
||||
FromTime: from,
|
||||
ToTime: to,
|
||||
ModelID: modelID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
hbRows, err := h.queries.GetHeartbeatTokenUsageByDay(ctx, sqlc.GetHeartbeatTokenUsageByDayParams{
|
||||
BotID: botID,
|
||||
FromTime: from,
|
||||
ToTime: to,
|
||||
ModelID: modelID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
||||
for _, r := range rows {
|
||||
d := DailyTokenUsage{
|
||||
Day: formatPgDate(r.Day),
|
||||
InputTokens: r.InputTokens,
|
||||
OutputTokens: r.OutputTokens,
|
||||
CacheReadTokens: r.CacheReadTokens,
|
||||
CacheWriteTokens: r.CacheWriteTokens,
|
||||
ReasoningTokens: r.ReasoningTokens,
|
||||
}
|
||||
return chatRows, hbRows, nil
|
||||
switch r.SessionType {
|
||||
case "heartbeat":
|
||||
heartbeat = append(heartbeat, d)
|
||||
case "schedule":
|
||||
schedule = append(schedule, d)
|
||||
default:
|
||||
chat = append(chat, d)
|
||||
}
|
||||
}
|
||||
return chat, heartbeat, schedule, nil
|
||||
}
|
||||
|
||||
func (h *TokenUsageHandler) fetchUsageByModel(ctx context.Context, botID pgtype.UUID, from, to pgtype.Timestamptz) ([]ModelTokenUsage, error) {
|
||||
merged := map[string]*ModelTokenUsage{}
|
||||
|
||||
chatRows, err := h.queries.GetMessageTokenUsageByModel(ctx, sqlc.GetMessageTokenUsageByModelParams{
|
||||
rows, err := h.queries.GetTokenUsageByModel(ctx, sqlc.GetTokenUsageByModelParams{
|
||||
BotID: botID,
|
||||
FromTime: from,
|
||||
ToTime: to,
|
||||
@@ -177,85 +187,21 @@ func (h *TokenUsageHandler) fetchUsageByModel(ctx context.Context, botID pgtype.
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range chatRows {
|
||||
key := r.ModelID.String()
|
||||
if m, ok := merged[key]; ok {
|
||||
m.InputTokens += r.InputTokens
|
||||
m.OutputTokens += r.OutputTokens
|
||||
} else {
|
||||
merged[key] = &ModelTokenUsage{
|
||||
|
||||
result := make([]ModelTokenUsage, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
result = append(result, ModelTokenUsage{
|
||||
ModelID: formatOptionalUUID(r.ModelID),
|
||||
ModelSlug: r.ModelSlug,
|
||||
ModelName: r.ModelName,
|
||||
ProviderName: r.ProviderName,
|
||||
InputTokens: r.InputTokens,
|
||||
OutputTokens: r.OutputTokens,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hbRows, err := h.queries.GetHeartbeatTokenUsageByModel(ctx, sqlc.GetHeartbeatTokenUsageByModelParams{
|
||||
BotID: botID,
|
||||
FromTime: from,
|
||||
ToTime: to,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range hbRows {
|
||||
key := r.ModelID.String()
|
||||
if m, ok := merged[key]; ok {
|
||||
m.InputTokens += r.InputTokens
|
||||
m.OutputTokens += r.OutputTokens
|
||||
} else {
|
||||
merged[key] = &ModelTokenUsage{
|
||||
ModelID: formatOptionalUUID(r.ModelID),
|
||||
ModelSlug: r.ModelSlug,
|
||||
ModelName: r.ModelName,
|
||||
ProviderName: r.ProviderName,
|
||||
InputTokens: r.InputTokens,
|
||||
OutputTokens: r.OutputTokens,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]ModelTokenUsage, 0, len(merged))
|
||||
for _, m := range merged {
|
||||
result = append(result, *m)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func convertMessageRows(rows []sqlc.GetMessageTokenUsageByDayRow) []DailyTokenUsage {
|
||||
out := make([]DailyTokenUsage, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
out = append(out, DailyTokenUsage{
|
||||
Day: formatPgDate(r.Day),
|
||||
InputTokens: r.InputTokens,
|
||||
OutputTokens: r.OutputTokens,
|
||||
CacheReadTokens: r.CacheReadTokens,
|
||||
CacheWriteTokens: r.CacheWriteTokens,
|
||||
ReasoningTokens: r.ReasoningTokens,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func convertHeartbeatRows(rows []sqlc.GetHeartbeatTokenUsageByDayRow) []DailyTokenUsage {
|
||||
out := make([]DailyTokenUsage, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
out = append(out, DailyTokenUsage{
|
||||
Day: formatPgDate(r.Day),
|
||||
InputTokens: r.InputTokens,
|
||||
OutputTokens: r.OutputTokens,
|
||||
CacheReadTokens: r.CacheReadTokens,
|
||||
CacheWriteTokens: r.CacheWriteTokens,
|
||||
ReasoningTokens: r.ReasoningTokens,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func formatPgDate(d pgtype.Date) string {
|
||||
if !d.Valid {
|
||||
return ""
|
||||
|
||||
@@ -1698,7 +1698,7 @@ export const getBotsByBotIdTokenUsageQueryKey = (options: Options<GetBotsByBotId
|
||||
/**
|
||||
* Get token usage statistics
|
||||
*
|
||||
* Get daily aggregated token usage for a bot, split by chat and heartbeat, with optional model filter and per-model breakdown
|
||||
* Get daily aggregated token usage for a bot, split by chat, heartbeat, and schedule session types, with optional model filter and per-model breakdown
|
||||
*/
|
||||
export const getBotsByBotIdTokenUsageQuery = defineQueryOptions((options: Options<GetBotsByBotIdTokenUsageData>) => ({
|
||||
key: getBotsByBotIdTokenUsageQueryKey(options),
|
||||
|
||||
@@ -963,7 +963,7 @@ export const putBotsByBotIdSubagentsByIdSkills = <ThrowOnError extends boolean =
|
||||
/**
|
||||
* Get token usage statistics
|
||||
*
|
||||
* Get daily aggregated token usage for a bot, split by chat and heartbeat, with optional model filter and per-model breakdown
|
||||
* Get daily aggregated token usage for a bot, split by chat, heartbeat, and schedule session types, with optional model filter and per-model breakdown
|
||||
*/
|
||||
export const getBotsByBotIdTokenUsage = <ThrowOnError extends boolean = false>(options: Options<GetBotsByBotIdTokenUsageData, ThrowOnError>) => (options.client ?? client).get<GetBotsByBotIdTokenUsageResponses, GetBotsByBotIdTokenUsageErrors, ThrowOnError>({ url: '/bots/{bot_id}/token-usage', ...options });
|
||||
|
||||
|
||||
@@ -903,6 +903,7 @@ export type HandlersTokenUsageResponse = {
|
||||
by_model?: Array<HandlersModelTokenUsage>;
|
||||
chat?: Array<HandlersDailyTokenUsage>;
|
||||
heartbeat?: Array<HandlersDailyTokenUsage>;
|
||||
schedule?: Array<HandlersDailyTokenUsage>;
|
||||
};
|
||||
|
||||
export type HandlersCreateSessionRequest = {
|
||||
@@ -1194,7 +1195,7 @@ export type ModelsTestResponse = {
|
||||
status?: ModelsTestStatus;
|
||||
};
|
||||
|
||||
export type ModelsTestStatus = 'ok' | 'auth_error' | 'error';
|
||||
export type ModelsTestStatus = 'ok' | 'auth_error' | 'model_not_supported' | 'error';
|
||||
|
||||
export type ModelsUpdateRequest = {
|
||||
client_type?: ModelsClientType;
|
||||
|
||||
+9
-1
@@ -4685,7 +4685,7 @@ const docTemplate = `{
|
||||
},
|
||||
"/bots/{bot_id}/token-usage": {
|
||||
"get": {
|
||||
"description": "Get daily aggregated token usage for a bot, split by chat and heartbeat, with optional model filter and per-model breakdown",
|
||||
"description": "Get daily aggregated token usage for a bot, split by chat, heartbeat, and schedule session types, with optional model filter and per-model breakdown",
|
||||
"tags": [
|
||||
"token-usage"
|
||||
],
|
||||
@@ -11192,6 +11192,12 @@ const docTemplate = `{
|
||||
"items": {
|
||||
"$ref": "#/definitions/handlers.DailyTokenUsage"
|
||||
}
|
||||
},
|
||||
"schedule": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/handlers.DailyTokenUsage"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -11918,11 +11924,13 @@ const docTemplate = `{
|
||||
"enum": [
|
||||
"ok",
|
||||
"auth_error",
|
||||
"model_not_supported",
|
||||
"error"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"TestStatusOK",
|
||||
"TestStatusAuthError",
|
||||
"TestStatusModelNotSupported",
|
||||
"TestStatusError"
|
||||
]
|
||||
},
|
||||
|
||||
+9
-1
@@ -4676,7 +4676,7 @@
|
||||
},
|
||||
"/bots/{bot_id}/token-usage": {
|
||||
"get": {
|
||||
"description": "Get daily aggregated token usage for a bot, split by chat and heartbeat, with optional model filter and per-model breakdown",
|
||||
"description": "Get daily aggregated token usage for a bot, split by chat, heartbeat, and schedule session types, with optional model filter and per-model breakdown",
|
||||
"tags": [
|
||||
"token-usage"
|
||||
],
|
||||
@@ -11183,6 +11183,12 @@
|
||||
"items": {
|
||||
"$ref": "#/definitions/handlers.DailyTokenUsage"
|
||||
}
|
||||
},
|
||||
"schedule": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/handlers.DailyTokenUsage"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -11909,11 +11915,13 @@
|
||||
"enum": [
|
||||
"ok",
|
||||
"auth_error",
|
||||
"model_not_supported",
|
||||
"error"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"TestStatusOK",
|
||||
"TestStatusAuthError",
|
||||
"TestStatusModelNotSupported",
|
||||
"TestStatusError"
|
||||
]
|
||||
},
|
||||
|
||||
+8
-2
@@ -1488,6 +1488,10 @@ definitions:
|
||||
items:
|
||||
$ref: '#/definitions/handlers.DailyTokenUsage'
|
||||
type: array
|
||||
schedule:
|
||||
items:
|
||||
$ref: '#/definitions/handlers.DailyTokenUsage'
|
||||
type: array
|
||||
type: object
|
||||
handlers.createSessionRequest:
|
||||
properties:
|
||||
@@ -1966,11 +1970,13 @@ definitions:
|
||||
enum:
|
||||
- ok
|
||||
- auth_error
|
||||
- model_not_supported
|
||||
- error
|
||||
type: string
|
||||
x-enum-varnames:
|
||||
- TestStatusOK
|
||||
- TestStatusAuthError
|
||||
- TestStatusModelNotSupported
|
||||
- TestStatusError
|
||||
models.UpdateRequest:
|
||||
properties:
|
||||
@@ -5717,8 +5723,8 @@ paths:
|
||||
- subagent
|
||||
/bots/{bot_id}/token-usage:
|
||||
get:
|
||||
description: Get daily aggregated token usage for a bot, split by chat and heartbeat,
|
||||
with optional model filter and per-model breakdown
|
||||
description: Get daily aggregated token usage for a bot, split by chat, heartbeat,
|
||||
and schedule session types, with optional model filter and per-model breakdown
|
||||
parameters:
|
||||
- description: Bot ID
|
||||
in: path
|
||||
|
||||
Reference in New Issue
Block a user