Yatsuiii commited on
Commit
cf4bfaf
·
0 Parent(s):

scaffold: cobra CLI skeleton, package layout, project thesis

Browse files

Initial scaffolding for llmtrace — self-hosted LLM proxy with cost ledger
and deploy-to-spend causal attribution. Same architectural pattern as
costtrace (anomaly detection + GitHub Actions deploy correlation +
lineage scoring) applied to LLM provider APIs instead of AWS spend.

CLI commands stubbed: serve, keys, stats, tail, anomalies, analyze,
explain, report. All return notImplemented errors — real work begins
week 1 (proxy + Anthropic provider + ledger).

See CLAUDE.md (gitignored) for the 3-week MVP scope and differentiation
vs Helicone/Portkey/LiteLLM/Langfuse.

.gitignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ /llmtrace
2
+ *.db
3
+ *.db-journal
4
+ *.db-wal
5
+ *.db-shm
6
+ config.toml
7
+ .env
8
+ .envrc
9
+ CLAUDE.md
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Raghav
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # llmtrace
2
+
3
+ Deploy-to-LLM-spend causal attribution.
4
+
5
+ > Which prompt change caused this $4k/week spike?
6
+
7
+ `llmtrace` is a self-hosted reverse proxy for LLM provider APIs (Anthropic, OpenAI, Bedrock) with a built-in cost ledger, latency tracking, and anomaly detection. It joins LLM spend anomalies to the deploy events that caused them — using the same architectural pattern as [costtrace](https://github.com/Yatsuiii/costtrace) does for AWS spend.
8
+
9
+ **Status: scaffolding.** See `CLAUDE.md` (gitignored) for the project thesis. Tracking issues against the 3-week MVP scope.
10
+
11
+ ## How it works (planned)
12
+
13
+ 1. **Proxy** — your code points at `llmtrace serve` instead of `api.anthropic.com`. It forwards requests upstream and records token usage, cost, latency, model, and prompt fingerprint per call.
14
+ 2. **Detect** — flags per-key spend/latency anomalies using a rolling baseline + sigma threshold.
15
+ 3. **Correlate** — matches anomalies to GitHub Actions deploys, scores confidence based on model-change / prompt-change evidence.
16
+
17
+ ## How it differs from other tools
18
+
19
+ | Tool | What it does | What it misses |
20
+ |---|---|---|
21
+ | Helicone | Hosted observability + caching | Hosted-only, no deploy correlation |
22
+ | Portkey | AI gateway with routing/caching | Feature-stew, no anomaly attribution |
23
+ | LiteLLM | Open-source proxy | No anomaly detection, no deploy join |
24
+ | Langfuse | LLM observability platform | Trace-focused, not cost-focused |
25
+ | **llmtrace** | **Deploy → LLM-spend causal chain** | MVP: Anthropic only, single tenant |
26
+
27
+ ## License
28
+
29
+ MIT
cmd/llmtrace/main.go ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package main
2
+
3
+ import (
4
+ "fmt"
5
+ "os"
6
+
7
+ "github.com/spf13/cobra"
8
+ )
9
+
10
+ func main() {
11
+ root := &cobra.Command{
12
+ Use: "llmtrace",
13
+ Short: "LLM call tracing with cost & latency anomaly detection",
14
+ }
15
+ root.AddCommand(
16
+ cmdInit(),
17
+ cmdServe(),
18
+ cmdKeys(),
19
+ cmdStats(),
20
+ cmdTail(),
21
+ cmdAnomalies(),
22
+ cmdAnalyze(),
23
+ cmdExplain(),
24
+ cmdReport(),
25
+ )
26
+ if err := root.Execute(); err != nil {
27
+ os.Exit(1)
28
+ }
29
+ }
30
+
31
+ func cmdInit() *cobra.Command {
32
+ return &cobra.Command{
33
+ Use: "init",
34
+ Short: "Interactive setup → config.toml",
35
+ RunE: func(cmd *cobra.Command, args []string) error {
36
+ return notImplemented("init")
37
+ },
38
+ }
39
+ }
40
+
41
+ func cmdServe() *cobra.Command {
42
+ var port int
43
+ cmd := &cobra.Command{
44
+ Use: "serve",
45
+ Short: "Run the proxy server",
46
+ RunE: func(cmd *cobra.Command, args []string) error {
47
+ return notImplemented("serve")
48
+ },
49
+ }
50
+ cmd.Flags().IntVar(&port, "port", 8080, "listen port")
51
+ return cmd
52
+ }
53
+
54
+ func cmdKeys() *cobra.Command {
55
+ cmd := &cobra.Command{
56
+ Use: "keys",
57
+ Short: "Manage inbound API keys",
58
+ }
59
+ cmd.AddCommand(
60
+ &cobra.Command{
61
+ Use: "add",
62
+ Short: "Mint a new API key",
63
+ RunE: func(cmd *cobra.Command, args []string) error {
64
+ return notImplemented("keys add")
65
+ },
66
+ },
67
+ &cobra.Command{
68
+ Use: "list",
69
+ Short: "List API keys",
70
+ RunE: func(cmd *cobra.Command, args []string) error {
71
+ return notImplemented("keys list")
72
+ },
73
+ },
74
+ &cobra.Command{
75
+ Use: "revoke <key-id>",
76
+ Short: "Revoke an API key",
77
+ Args: cobra.ExactArgs(1),
78
+ RunE: func(cmd *cobra.Command, args []string) error {
79
+ return notImplemented("keys revoke")
80
+ },
81
+ },
82
+ )
83
+ return cmd
84
+ }
85
+
86
+ func cmdStats() *cobra.Command {
87
+ var days int
88
+ cmd := &cobra.Command{
89
+ Use: "stats",
90
+ Short: "Ledger summary by key/model",
91
+ RunE: func(cmd *cobra.Command, args []string) error {
92
+ return notImplemented("stats")
93
+ },
94
+ }
95
+ cmd.Flags().IntVar(&days, "days", 7, "window in days")
96
+ return cmd
97
+ }
98
+
99
+ func cmdTail() *cobra.Command {
100
+ return &cobra.Command{
101
+ Use: "tail",
102
+ Short: "Live request stream",
103
+ RunE: func(cmd *cobra.Command, args []string) error {
104
+ return notImplemented("tail")
105
+ },
106
+ }
107
+ }
108
+
109
+ func cmdAnomalies() *cobra.Command {
110
+ var days int
111
+ var format string
112
+ cmd := &cobra.Command{
113
+ Use: "anomalies",
114
+ Short: "List spend/latency anomalies",
115
+ RunE: func(cmd *cobra.Command, args []string) error {
116
+ return notImplemented("anomalies")
117
+ },
118
+ }
119
+ cmd.Flags().IntVar(&days, "days", 30, "window to scan for anomalies")
120
+ cmd.Flags().StringVar(&format, "format", "text", "output format: text | markdown")
121
+ return cmd
122
+ }
123
+
124
+ func cmdAnalyze() *cobra.Command {
125
+ var days int
126
+ cmd := &cobra.Command{
127
+ Use: "analyze",
128
+ Short: "Anomalies + deploy correlations",
129
+ RunE: func(cmd *cobra.Command, args []string) error {
130
+ return notImplemented("analyze")
131
+ },
132
+ }
133
+ cmd.Flags().IntVar(&days, "days", 30, "window to analyze")
134
+ return cmd
135
+ }
136
+
137
+ func cmdExplain() *cobra.Command {
138
+ return &cobra.Command{
139
+ Use: "explain <anomaly-id>",
140
+ Short: "Deep-dive a single anomaly",
141
+ Args: cobra.ExactArgs(1),
142
+ RunE: func(cmd *cobra.Command, args []string) error {
143
+ return notImplemented("explain")
144
+ },
145
+ }
146
+ }
147
+
148
+ func cmdReport() *cobra.Command {
149
+ var days int
150
+ var format string
151
+ cmd := &cobra.Command{
152
+ Use: "report",
153
+ Short: "Polished report",
154
+ RunE: func(cmd *cobra.Command, args []string) error {
155
+ return notImplemented("report")
156
+ },
157
+ }
158
+ cmd.Flags().IntVar(&days, "days", 30, "window to report on")
159
+ cmd.Flags().StringVar(&format, "format", "text", "output format: text | markdown")
160
+ return cmd
161
+ }
162
+
163
+ func notImplemented(name string) error {
164
+ return fmt.Errorf("%s: not implemented yet — see CLAUDE.md for the 3-week scope", name)
165
+ }
examples/basic-config.toml ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [proxy]
2
+ listen = "0.0.0.0:8080"
3
+ tls_cert = ""
4
+ tls_key = ""
5
+
6
+ [providers.anthropic]
7
+ upstream_url = "https://api.anthropic.com"
8
+ upstream_key_env = "ANTHROPIC_API_KEY"
9
+
10
+ [providers.openai]
11
+ upstream_url = "https://api.openai.com"
12
+ upstream_key_env = "OPENAI_API_KEY"
13
+
14
+ [github]
15
+ repo = "org/name"
16
+ token_env = "GITHUB_TOKEN"
17
+ deploy_workflow_pattern = "deploy.*"
18
+
19
+ [detection]
20
+ baseline_days = 7
21
+ sigma_threshold = 2.5
22
+ min_delta_usd = 5
23
+ correlation_window_hours = 4
24
+
25
+ [output]
26
+ format = "text"
go.mod ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ module github.com/Yatsuiii/llmtrace
2
+
3
+ go 1.26.2
4
+
5
+ require github.com/spf13/cobra v1.10.2
6
+
7
+ require (
8
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
9
+ github.com/spf13/pflag v1.0.9 // indirect
10
+ )
go.sum ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
2
+ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
3
+ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
4
+ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
5
+ github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
6
+ github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
7
+ github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
8
+ github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
9
+ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
10
+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
internal/config/config.go ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package config
2
+
3
+ type Config struct {
4
+ Proxy ProxyConfig
5
+ Providers ProvidersConfig
6
+ GitHub GitHubConfig
7
+ Detection DetectionConfig
8
+ Output OutputConfig
9
+ }
10
+
11
+ type ProxyConfig struct {
12
+ Listen string
13
+ TLSCert string
14
+ TLSKey string
15
+ }
16
+
17
+ type ProvidersConfig struct {
18
+ Anthropic ProviderConfig
19
+ OpenAI ProviderConfig
20
+ Bedrock ProviderConfig
21
+ }
22
+
23
+ type ProviderConfig struct {
24
+ UpstreamURL string
25
+ UpstreamKeyEnv string
26
+ }
27
+
28
+ type GitHubConfig struct {
29
+ Repo string
30
+ TokenEnv string
31
+ DeployWorkflowPattern string
32
+ }
33
+
34
+ type DetectionConfig struct {
35
+ BaselineDays int
36
+ SigmaThreshold float64
37
+ MinDeltaUSD float64
38
+ CorrelationWindowHours int
39
+ }
40
+
41
+ type OutputConfig struct {
42
+ Format string
43
+ }
44
+
45
+ func Defaults() *Config {
46
+ return &Config{
47
+ Proxy: ProxyConfig{Listen: "0.0.0.0:8080"},
48
+ Providers: ProvidersConfig{
49
+ Anthropic: ProviderConfig{
50
+ UpstreamURL: "https://api.anthropic.com",
51
+ UpstreamKeyEnv: "ANTHROPIC_API_KEY",
52
+ },
53
+ OpenAI: ProviderConfig{
54
+ UpstreamURL: "https://api.openai.com",
55
+ UpstreamKeyEnv: "OPENAI_API_KEY",
56
+ },
57
+ },
58
+ GitHub: GitHubConfig{
59
+ TokenEnv: "GITHUB_TOKEN",
60
+ DeployWorkflowPattern: "deploy.*",
61
+ },
62
+ Detection: DetectionConfig{
63
+ BaselineDays: 7,
64
+ SigmaThreshold: 2.5,
65
+ MinDeltaUSD: 5,
66
+ CorrelationWindowHours: 4,
67
+ },
68
+ Output: OutputConfig{Format: "text"},
69
+ }
70
+ }
internal/detect/anomaly.go ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ package detect
2
+
3
+ // Detect logic lives here. Implemented week 2.
4
+ // Placeholder so the package compiles.
5
+
6
+ type Config struct {
7
+ BaselineDays int
8
+ SigmaThreshold float64
9
+ MinDeltaUSD float64
10
+ }
internal/pricing/rates.go ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package pricing
2
+
3
+ type ModelRate struct {
4
+ InputPerMillion float64
5
+ OutputPerMillion float64
6
+ }
7
+
8
+ var rates = map[string]ModelRate{
9
+ // Anthropic — placeholder, refresh against official docs before shipping
10
+ "claude-opus-4-7": {InputPerMillion: 15.00, OutputPerMillion: 75.00},
11
+ "claude-sonnet-4-6": {InputPerMillion: 3.00, OutputPerMillion: 15.00},
12
+ "claude-haiku-4-5-20251001": {InputPerMillion: 0.80, OutputPerMillion: 4.00},
13
+ }
14
+
15
+ func Lookup(model string) (ModelRate, bool) {
16
+ r, ok := rates[model]
17
+ return r, ok
18
+ }
19
+
20
+ func Cost(model string, inputTokens, outputTokens int) float64 {
21
+ r, ok := Lookup(model)
22
+ if !ok {
23
+ return 0
24
+ }
25
+ return float64(inputTokens)*r.InputPerMillion/1_000_000 +
26
+ float64(outputTokens)*r.OutputPerMillion/1_000_000
27
+ }
internal/providers/provider.go ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package providers
2
+
3
+ import "net/http"
4
+
5
+ type Provider interface {
6
+ Name() string
7
+ UpstreamURL() string
8
+ RoutePrefixes() []string
9
+ ParseUsage(req *http.Request, respBody []byte) (Usage, error)
10
+ }
11
+
12
+ type Usage struct {
13
+ Model string
14
+ InputTokens int
15
+ OutputTokens int
16
+ }
internal/proxy/server.go ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package proxy
2
+
3
+ import "github.com/Yatsuiii/llmtrace/internal/config"
4
+
5
+ type Server struct {
6
+ cfg *config.Config
7
+ }
8
+
9
+ func New(cfg *config.Config) *Server {
10
+ return &Server{cfg: cfg}
11
+ }
internal/ratelimit/limiter.go ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ package ratelimit
2
+
3
+ // Token-bucket per API key. Implemented week 2.
4
+ type Limiter struct{}
internal/report/text.go ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ package report
2
+
3
+ // CLI text rendering. Implemented week 1.
internal/storage/schema.go ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package storage
2
+
3
+ const schema = `
4
+ CREATE TABLE IF NOT EXISTS calls (
5
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6
+ timestamp INTEGER NOT NULL,
7
+ api_key_id TEXT NOT NULL,
8
+ provider TEXT NOT NULL,
9
+ model TEXT NOT NULL,
10
+ endpoint TEXT NOT NULL,
11
+ input_tokens INTEGER NOT NULL,
12
+ output_tokens INTEGER NOT NULL,
13
+ cost_usd REAL NOT NULL,
14
+ latency_ms INTEGER NOT NULL,
15
+ status INTEGER NOT NULL,
16
+ error_class TEXT NOT NULL DEFAULT '',
17
+ prompt_hash TEXT NOT NULL DEFAULT '',
18
+ session_id TEXT NOT NULL DEFAULT ''
19
+ );
20
+ CREATE INDEX IF NOT EXISTS calls_ts_idx ON calls(timestamp);
21
+ CREATE INDEX IF NOT EXISTS calls_key_ts_idx ON calls(api_key_id, timestamp);
22
+
23
+ CREATE TABLE IF NOT EXISTS api_keys (
24
+ id TEXT PRIMARY KEY,
25
+ hashed_key TEXT NOT NULL,
26
+ label TEXT NOT NULL DEFAULT '',
27
+ budget_usd REAL NOT NULL DEFAULT 0,
28
+ rate_limit_rpm INTEGER NOT NULL DEFAULT 0,
29
+ active INTEGER NOT NULL DEFAULT 1,
30
+ created_at INTEGER NOT NULL
31
+ );
32
+
33
+ CREATE TABLE IF NOT EXISTS anomalies (
34
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
35
+ detected_at INTEGER NOT NULL,
36
+ api_key_id TEXT NOT NULL,
37
+ date TEXT NOT NULL,
38
+ metric TEXT NOT NULL,
39
+ baseline_value REAL NOT NULL,
40
+ actual_value REAL NOT NULL,
41
+ delta REAL NOT NULL,
42
+ sigma REAL NOT NULL,
43
+ sample_size INTEGER NOT NULL,
44
+ UNIQUE(api_key_id, date, metric)
45
+ );
46
+
47
+ CREATE TABLE IF NOT EXISTS deploys (
48
+ id TEXT PRIMARY KEY,
49
+ repo TEXT NOT NULL,
50
+ branch TEXT NOT NULL,
51
+ commit_sha TEXT NOT NULL,
52
+ pr_number INTEGER,
53
+ title TEXT NOT NULL,
54
+ started_at INTEGER NOT NULL,
55
+ completed_at INTEGER NOT NULL,
56
+ status TEXT NOT NULL
57
+ );
58
+
59
+ CREATE TABLE IF NOT EXISTS correlations (
60
+ anomaly_id INTEGER NOT NULL,
61
+ deploy_id TEXT NOT NULL,
62
+ confidence REAL NOT NULL,
63
+ evidence TEXT NOT NULL,
64
+ PRIMARY KEY (anomaly_id, deploy_id)
65
+ );
66
+
67
+ CREATE TABLE IF NOT EXISTS sync_cursors (
68
+ name TEXT PRIMARY KEY,
69
+ value TEXT NOT NULL
70
+ );
71
+ `
internal/storage/sqlite.go ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package storage
2
+
3
+ import "time"
4
+
5
+ type DB struct {
6
+ // to be implemented in week 1
7
+ }
8
+
9
+ type CallRow struct {
10
+ ID int64
11
+ Timestamp time.Time
12
+ APIKeyID string
13
+ Provider string
14
+ Model string
15
+ Endpoint string
16
+ InputTokens int
17
+ OutputTokens int
18
+ CostUSD float64
19
+ LatencyMs int64
20
+ Status int
21
+ ErrorClass string
22
+ PromptHash string
23
+ SessionID string
24
+ }
25
+
26
+ type APIKeyRow struct {
27
+ ID string
28
+ HashedKey string
29
+ Label string
30
+ BudgetUSD float64
31
+ RateLimitRPM int
32
+ Active bool
33
+ CreatedAt time.Time
34
+ }
35
+
36
+ type AnomalyRow struct {
37
+ ID int64
38
+ DetectedAt time.Time
39
+ APIKeyID string
40
+ Date string
41
+ Metric string
42
+ BaselineValue float64
43
+ ActualValue float64
44
+ Delta float64
45
+ Sigma float64
46
+ SampleSize int64
47
+ }