Commit ·
cf4bfaf
0
Parent(s):
scaffold: cobra CLI skeleton, package layout, project thesis
Browse filesInitial 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 +9 -0
- LICENSE +21 -0
- README.md +29 -0
- cmd/llmtrace/main.go +165 -0
- examples/basic-config.toml +26 -0
- go.mod +10 -0
- go.sum +10 -0
- internal/config/config.go +70 -0
- internal/detect/anomaly.go +10 -0
- internal/pricing/rates.go +27 -0
- internal/providers/provider.go +16 -0
- internal/proxy/server.go +11 -0
- internal/ratelimit/limiter.go +4 -0
- internal/report/text.go +3 -0
- internal/storage/schema.go +71 -0
- internal/storage/sqlite.go +47 -0
.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 |
+
}
|