Files
hobokenchicken 5e0c10db01
CI / Lint (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Build (push) Has been cancelled
fix: goimports — strip unused imports from all server files
2026-04-26 15:00:04 -04:00

373 lines
9.5 KiB
Go

package server
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
)
type UsagePeriodFilter struct {
Period string `form:"period"`
From string `form:"from"`
To string `form:"to"`
}
func (f *UsagePeriodFilter) ToSQL() (string, []interface{}) {
period := f.Period
if period == "" {
period = "all"
}
if period == "custom" {
var clauses []string
var binds []interface{}
if f.From != "" {
clauses = append(clauses, "timestamp >= ?")
binds = append(binds, f.From)
}
if f.To != "" {
clauses = append(clauses, "timestamp <= ?")
binds = append(binds, f.To)
}
if len(clauses) > 0 {
return " AND " + strings.Join(clauses, " AND "), binds
}
return "", nil
}
now := time.Now().UTC()
var cutoff time.Time
switch period {
case "today":
cutoff = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
case "24h":
cutoff = now.Add(-24 * time.Hour)
case "7d":
cutoff = now.Add(-7 * 24 * time.Hour)
case "30d":
cutoff = now.Add(-30 * 24 * time.Hour)
default:
return "", nil
}
return " AND timestamp >= ?", []interface{}{cutoff.Format(time.RFC3339)}
}
func (s *Server) handleUsageSummary(c *gin.Context) {
var filter UsagePeriodFilter
if err := c.ShouldBindQuery(&filter); err != nil {
// ignore
}
clause, binds := filter.ToSQL()
// Total stats
var totalStats struct {
TotalRequests int `db:"total_requests"`
TotalTokens int `db:"total_tokens"`
CacheReadTokens int `db:"total_cache_read_tokens"`
CacheWriteTokens int `db:"total_cache_write_tokens"`
TotalCost float64 `db:"total_cost"`
ActiveClients int `db:"active_clients"`
}
err := s.database.Get(&totalStats, fmt.Sprintf(`
SELECT
COUNT(*) as total_requests,
COALESCE(SUM(total_tokens), 0) as total_tokens,
COALESCE(SUM(cache_read_tokens), 0) as total_cache_read_tokens,
COALESCE(SUM(cache_write_tokens), 0) as total_cache_write_tokens,
COALESCE(SUM(cost), 0.0) as total_cost,
COUNT(DISTINCT client_id) as active_clients
FROM llm_requests
WHERE 1=1 %s
`, clause), binds...)
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
return
}
// Today stats
var todayStats struct {
TodayRequests int `db:"today_requests"`
TodayCost float64 `db:"today_cost"`
}
today := time.Now().UTC().Format("2006-01-02")
err = s.database.Get(&todayStats, `
SELECT
COUNT(*) as today_requests,
COALESCE(SUM(cost), 0.0) as today_cost
FROM llm_requests
WHERE timestamp LIKE ?
`, today+"%")
if err != nil {
todayStats.TodayRequests = 0
todayStats.TodayCost = 0.0
}
// Error rate & Avg response time
var miscStats struct {
ErrorRate float64 `db:"error_rate"`
AvgResponseTime float64 `db:"avg_response_time"`
}
err = s.database.Get(&miscStats, `
SELECT
CASE WHEN COUNT(*) = 0 THEN 0.0 ELSE (CAST(SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) AS FLOAT) / COUNT(*)) * 100.0 END as error_rate,
COALESCE(AVG(duration_ms), 0.0) as avg_response_time
FROM llm_requests
`)
if err != nil {
miscStats.ErrorRate = 0.0
miscStats.AvgResponseTime = 0.0
}
c.JSON(http.StatusOK, SuccessResponse(gin.H{
"total_requests": totalStats.TotalRequests,
"total_tokens": totalStats.TotalTokens,
"total_cache_read_tokens": totalStats.CacheReadTokens,
"total_cache_write_tokens": totalStats.CacheWriteTokens,
"total_cost": totalStats.TotalCost,
"active_clients": totalStats.ActiveClients,
"today_requests": todayStats.TodayRequests,
"today_cost": todayStats.TodayCost,
"error_rate": miscStats.ErrorRate,
"avg_response_time": miscStats.AvgResponseTime,
}))
}
func (s *Server) handleTimeSeries(c *gin.Context) {
var filter UsagePeriodFilter
if err := c.ShouldBindQuery(&filter); err != nil {
// ignore
}
clause, binds := filter.ToSQL()
if clause == "" {
cutoff := time.Now().UTC().Add(-30 * 24 * time.Hour)
clause = " AND timestamp >= ?"
binds = []interface{}{cutoff.Format(time.RFC3339)}
}
query := fmt.Sprintf(`
SELECT
COALESCE(SUBSTR(timestamp, 1, 10), 'unknown') as bucket,
COUNT(*) as requests,
COALESCE(SUM(total_tokens), 0) as tokens,
COALESCE(SUM(cost), 0.0) as cost
FROM llm_requests
WHERE 1=1 %s
GROUP BY bucket
ORDER BY bucket
`, clause)
rows, err := s.database.Queryx(query, binds...)
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
return
}
defer rows.Close()
var series []gin.H
for rows.Next() {
var bucket string
var requests int
var tokens int
var cost float64
if err := rows.Scan(&bucket, &requests, &tokens, &cost); err != nil {
continue
}
series = append(series, gin.H{
"time": bucket,
"requests": requests,
"tokens": tokens,
"cost": cost,
})
}
granularity := "day"
c.JSON(http.StatusOK, SuccessResponse(gin.H{
"series": series,
"granularity": granularity,
}))
}
func (s *Server) handleProvidersUsage(c *gin.Context) {
var filter UsagePeriodFilter
if err := c.ShouldBindQuery(&filter); err != nil {
// ignore
}
clause, binds := filter.ToSQL()
rows, err := s.database.Queryx(fmt.Sprintf(`
SELECT
COALESCE(provider, 'unknown') as provider,
COUNT(*) as requests,
COALESCE(SUM(cost), 0.0) as cost
FROM llm_requests
WHERE 1=1 %s
GROUP BY provider
`, clause), binds...)
if err != nil {
c.JSON(http.StatusOK, SuccessResponse([]interface{}{}))
return
}
defer rows.Close()
var results []gin.H
for rows.Next() {
var provider string
var requests int
var cost float64
if err := rows.Scan(&provider, &requests, &cost); err == nil {
results = append(results, gin.H{"provider": provider, "requests": requests, "cost": cost})
}
}
c.JSON(http.StatusOK, SuccessResponse(results))
}
func (s *Server) handleClientsUsage(c *gin.Context) {
var filter UsagePeriodFilter
if err := c.ShouldBindQuery(&filter); err != nil {
// ignore
}
clause, binds := filter.ToSQL()
rows, err := s.database.Queryx(fmt.Sprintf(`
SELECT COALESCE(client_id, 'unknown') as client_id, COUNT(*) as requests
FROM llm_requests
WHERE 1=1 %s
GROUP BY client_id
`, clause), binds...)
if err != nil {
c.JSON(http.StatusOK, SuccessResponse([]interface{}{}))
return
}
defer rows.Close()
var results []gin.H
for rows.Next() {
var clientID string
var requests int
if err := rows.Scan(&clientID, &requests); err == nil {
results = append(results, gin.H{"client_id": clientID, "requests": requests})
}
}
c.JSON(http.StatusOK, SuccessResponse(results))
}
func (s *Server) handleAnalyticsBreakdown(c *gin.Context) {
var filter UsagePeriodFilter
if err := c.ShouldBindQuery(&filter); err != nil {
// ignore
}
clause, binds := filter.ToSQL()
// Models breakdown
var models []struct {
Label string `json:"label"`
Value int `json:"value"`
}
mRows, err := s.database.Queryx(fmt.Sprintf("SELECT COALESCE(model, 'unknown') as label, COUNT(*) as value FROM llm_requests WHERE 1=1 %s GROUP BY model ORDER BY value DESC", clause), binds...)
if err == nil {
for mRows.Next() {
var label string
var value int
if err := mRows.Scan(&label, &value); err == nil {
models = append(models, struct {
Label string `json:"label"`
Value int `json:"value"`
}{label, value})
}
}
mRows.Close()
}
// Clients breakdown
var clients []struct {
Label string `json:"label"`
Value int `json:"value"`
}
cRows, err := s.database.Queryx(fmt.Sprintf("SELECT COALESCE(client_id, 'unknown') as label, COUNT(*) as value FROM llm_requests WHERE 1=1 %s GROUP BY client_id ORDER BY value DESC", clause), binds...)
if err == nil {
for cRows.Next() {
var label string
var value int
if err := cRows.Scan(&label, &value); err == nil {
clients = append(clients, struct {
Label string `json:"label"`
Value int `json:"value"`
}{label, value})
}
}
cRows.Close()
}
c.JSON(http.StatusOK, SuccessResponse(gin.H{
"models": models,
"clients": clients,
}))
}
func (s *Server) handleDetailedUsage(c *gin.Context) {
var filter UsagePeriodFilter
if err := c.ShouldBindQuery(&filter); err != nil {
// ignore
}
clause, binds := filter.ToSQL()
query := fmt.Sprintf(`
SELECT
COALESCE(SUBSTR(timestamp, 1, 10), 'unknown') as date,
COALESCE(client_id, 'unknown') as client,
COALESCE(provider, 'unknown') as provider,
COALESCE(model, 'unknown') as model,
COUNT(*) as requests,
COALESCE(SUM(total_tokens), 0) as tokens,
COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens,
COALESCE(SUM(cache_write_tokens), 0) as cache_write_tokens,
COALESCE(SUM(cost), 0.0) as cost
FROM llm_requests
WHERE 1=1 %s
GROUP BY date, client, provider, model
ORDER BY date DESC, cost DESC
`, clause)
rows, err := s.database.Queryx(query, binds...)
if err != nil {
c.JSON(http.StatusOK, SuccessResponse([]interface{}{}))
return
}
defer rows.Close()
var results []gin.H
for rows.Next() {
var date, client, provider, model string
var requests, tokens, cacheRead, cacheWrite int
var cost float64
if err := rows.Scan(&date, &client, &provider, &model, &requests, &tokens, &cacheRead, &cacheWrite, &cost); err == nil {
results = append(results, gin.H{
"date": date,
"client": client,
"provider": provider,
"model": model,
"requests": requests,
"tokens": tokens,
"cache_read_tokens": cacheRead,
"cache_write_tokens": cacheWrite,
"cost": cost,
})
}
}
c.JSON(http.StatusOK, SuccessResponse(results))
}