Files
goclaw/internal/store/usage_event_store.go
Duc Nguyen 747b58d24c fix(usage): repair cost analytics and display precision (#1330)
fix(usage): repair cost analytics and display precision

- Automatic OpenRouter pricing sync with cost backfill for traces/snapshots/events
- Live usage data merging for current-hour dashboard accuracy
- 2-decimal API cost formatting across usage/overview pages
- Comprehensive test coverage (PG, SQLite, HTTP, UI)

Merged by github-maintain automation.
2026-07-03 06:24:21 +07:00

166 lines
8.0 KiB
Go

package store
import (
"context"
"encoding/json"
"time"
"github.com/google/uuid"
)
const (
UsageEventTypeToolCall = "tool_call"
UsageEventTypeSkillActivation = "skill_activation"
UsageEventTypeMCPToolCall = "mcp_tool_call"
UsageEventTypeRuntimeToolCall = "runtime_tool_call"
UsageResourceTypeTool = "tool"
UsageResourceTypeSkill = "skill"
UsageResourceTypeMCPTool = "mcp_tool"
UsageResourceTypeRuntimeTool = "runtime_tool"
UsageSourceToolCall = "tool_call"
UsageSourceUseSkill = "use_skill"
UsageSourceSlashCommand = "slash-command"
)
// UsageEvent is an append-only analytics row for resource usage.
// It intentionally excludes user IDs, raw prompts, tool args, outputs, and shell commands.
type UsageEvent struct {
ID uuid.UUID `json:"id" db:"id"`
TenantID uuid.UUID `json:"tenant_id" db:"tenant_id"`
EventTime time.Time `json:"event_time" db:"event_time"`
BucketHour time.Time `json:"bucket_hour" db:"bucket_hour"`
EventType string `json:"event_type" db:"event_type"`
ResourceType string `json:"resource_type" db:"resource_type"`
ResourceName string `json:"resource_name" db:"resource_name"`
ResourceID string `json:"resource_id" db:"resource_id"`
Source string `json:"source" db:"source"`
AgentID *uuid.UUID `json:"agent_id,omitempty" db:"agent_id"`
TeamID *uuid.UUID `json:"team_id,omitempty" db:"team_id"`
TraceID *uuid.UUID `json:"trace_id,omitempty" db:"trace_id"`
SpanID *uuid.UUID `json:"span_id,omitempty" db:"span_id"`
RunID string `json:"run_id" db:"run_id"`
SessionKey string `json:"session_key" db:"session_key"`
Channel string `json:"channel" db:"channel"`
Provider string `json:"provider" db:"provider"`
Model string `json:"model" db:"model"`
Status string `json:"status" db:"status"`
InputTokens int64 `json:"input_tokens" db:"input_tokens"`
OutputTokens int64 `json:"output_tokens" db:"output_tokens"`
TotalTokens int64 `json:"total_tokens" db:"total_tokens"`
CacheReadTokens int64 `json:"cache_read_tokens" db:"cache_read_tokens"`
CacheCreateTokens int64 `json:"cache_create_tokens" db:"cache_create_tokens"`
ThinkingTokens int64 `json:"thinking_tokens" db:"thinking_tokens"`
CostUSD float64 `json:"cost_usd" db:"cost_usd"`
DurationMS int `json:"duration_ms" db:"duration_ms"`
CallCount int `json:"call_count" db:"call_count"`
ErrorCount int `json:"error_count" db:"error_count"`
Metadata json.RawMessage `json:"metadata,omitempty" db:"metadata"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
}
// UsageEventQuery filters usage event analytics.
type UsageEventQuery struct {
From time.Time
To time.Time
AgentID *uuid.UUID
Channel string
EventType string
ResourceType string
ResourceName string
Provider string
Model string
Status string
Source string
GroupBy string
Limit int
}
type UsageEventSummary struct {
Calls int `json:"calls" db:"calls"`
Errors int `json:"errors" db:"errors"`
InputTokens int64 `json:"input_tokens" db:"input_tokens"`
OutputTokens int64 `json:"output_tokens" db:"output_tokens"`
TotalTokens int64 `json:"total_tokens" db:"total_tokens"`
CacheReadTokens int64 `json:"cache_read_tokens" db:"cache_read_tokens"`
CacheCreateTokens int64 `json:"cache_create_tokens" db:"cache_create_tokens"`
ThinkingTokens int64 `json:"thinking_tokens" db:"thinking_tokens"`
CostUSD float64 `json:"cost_usd" db:"cost_usd"`
AvgDurationMS int `json:"avg_duration_ms" db:"avg_duration_ms"`
}
type UsageEventTimeSeries struct {
BucketTime time.Time `json:"bucket_time" db:"bucket_time"`
Calls int `json:"calls" db:"calls"`
Errors int `json:"errors" db:"errors"`
InputTokens int64 `json:"input_tokens" db:"input_tokens"`
OutputTokens int64 `json:"output_tokens" db:"output_tokens"`
TotalTokens int64 `json:"total_tokens" db:"total_tokens"`
CacheReadTokens int64 `json:"cache_read_tokens" db:"cache_read_tokens"`
CacheCreateTokens int64 `json:"cache_create_tokens" db:"cache_create_tokens"`
ThinkingTokens int64 `json:"thinking_tokens" db:"thinking_tokens"`
CostUSD float64 `json:"cost_usd" db:"cost_usd"`
AvgDurationMS int `json:"avg_duration_ms" db:"avg_duration_ms"`
}
type UsageEventBreakdown struct {
Key string `json:"key" db:"key"`
EventType string `json:"event_type" db:"event_type"`
ResourceType string `json:"resource_type" db:"resource_type"`
ResourceName string `json:"resource_name" db:"resource_name"`
Source string `json:"source" db:"source"`
Calls int `json:"calls" db:"calls"`
Errors int `json:"errors" db:"errors"`
InputTokens int64 `json:"input_tokens" db:"input_tokens"`
OutputTokens int64 `json:"output_tokens" db:"output_tokens"`
TotalTokens int64 `json:"total_tokens" db:"total_tokens"`
CacheReadTokens int64 `json:"cache_read_tokens" db:"cache_read_tokens"`
CacheCreateTokens int64 `json:"cache_create_tokens" db:"cache_create_tokens"`
ThinkingTokens int64 `json:"thinking_tokens" db:"thinking_tokens"`
CostUSD float64 `json:"cost_usd" db:"cost_usd"`
AvgDurationMS int `json:"avg_duration_ms" db:"avg_duration_ms"`
}
type UsageEventRollup struct {
ID uuid.UUID `json:"id" db:"id"`
TenantID uuid.UUID `json:"tenant_id" db:"tenant_id"`
BucketHour time.Time `json:"bucket_hour" db:"bucket_hour"`
EventType string `json:"event_type" db:"event_type"`
ResourceType string `json:"resource_type" db:"resource_type"`
ResourceName string `json:"resource_name" db:"resource_name"`
Source string `json:"source" db:"source"`
AgentID *uuid.UUID `json:"agent_id,omitempty" db:"agent_id"`
Channel string `json:"channel" db:"channel"`
Provider string `json:"provider" db:"provider"`
Model string `json:"model" db:"model"`
Status string `json:"status" db:"status"`
InputTokens int64 `json:"input_tokens" db:"input_tokens"`
OutputTokens int64 `json:"output_tokens" db:"output_tokens"`
TotalTokens int64 `json:"total_tokens" db:"total_tokens"`
CacheReadTokens int64 `json:"cache_read_tokens" db:"cache_read_tokens"`
CacheCreateTokens int64 `json:"cache_create_tokens" db:"cache_create_tokens"`
ThinkingTokens int64 `json:"thinking_tokens" db:"thinking_tokens"`
CostUSD float64 `json:"cost_usd" db:"cost_usd"`
DurationMS int `json:"duration_ms" db:"duration_ms"`
CallCount int `json:"call_count" db:"call_count"`
ErrorCount int `json:"error_count" db:"error_count"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
type UsageEventCostBackfillStats struct {
EventRowsUpdated int64
RollupBuckets []time.Time
}
type UsageEventStore interface {
InsertEvent(ctx context.Context, event *UsageEvent) error
InsertEvents(ctx context.Context, events []UsageEvent) error
RefreshEventRollupHour(ctx context.Context, bucketHour time.Time) error
GetLatestEventRollupBucket(ctx context.Context) (*time.Time, error)
GetEventTimeSeries(ctx context.Context, q UsageEventQuery) ([]UsageEventTimeSeries, error)
GetEventBreakdown(ctx context.Context, q UsageEventQuery) ([]UsageEventBreakdown, error)
GetEventSummary(ctx context.Context, q UsageEventQuery) (*UsageEventSummary, error)
}