mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-12 22:14:37 +00:00
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.
101 lines
2.9 KiB
Go
101 lines
2.9 KiB
Go
package pricing
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestFetchOpenRouterCatalogMapsPricingFields(t *testing.T) {
|
|
client := &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
|
if req.Method != http.MethodGet {
|
|
t.Fatalf("method = %s, want GET", req.Method)
|
|
}
|
|
if req.URL.String() != OpenRouterModelsURL {
|
|
t.Fatalf("url = %s, want %s", req.URL.String(), OpenRouterModelsURL)
|
|
}
|
|
body := `{
|
|
"data": [
|
|
{
|
|
"id": "openai/gpt-4o-mini",
|
|
"name": "GPT-4o mini",
|
|
"pricing": {
|
|
"prompt": "0.00000015",
|
|
"completion": "0.0000006",
|
|
"input_cache_read": "0.000000075",
|
|
"input_cache_write": "0.0000003",
|
|
"internal_reasoning": "0.0000002",
|
|
"request": "0",
|
|
"image": "0.001",
|
|
"web_search": "0.01"
|
|
}
|
|
},
|
|
{"name": "missing id"}
|
|
]
|
|
}`
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Body: io.NopCloser(strings.NewReader(body)),
|
|
Header: make(http.Header),
|
|
}, nil
|
|
})}
|
|
|
|
entries, err := FetchOpenRouterCatalog(context.Background(), client)
|
|
if err != nil {
|
|
t.Fatalf("FetchOpenRouterCatalog: %v", err)
|
|
}
|
|
if len(entries) != 1 {
|
|
t.Fatalf("entries = %d, want 1", len(entries))
|
|
}
|
|
got := entries[0]
|
|
if got.ModelID != "openai/gpt-4o-mini" || got.CanonicalModelID != got.ModelID {
|
|
t.Fatalf("model ids = %q/%q, want openai/gpt-4o-mini", got.ModelID, got.CanonicalModelID)
|
|
}
|
|
assertStringPtr(t, "input", got.Pricing.Input, "0.00000015")
|
|
assertStringPtr(t, "output", got.Pricing.Output, "0.0000006")
|
|
assertStringPtr(t, "cache_read", got.Pricing.CacheRead, "0.000000075")
|
|
assertStringPtr(t, "cache_write", got.Pricing.CacheWrite, "0.0000003")
|
|
assertStringPtr(t, "reasoning", got.Pricing.Reasoning, "0.0000002")
|
|
assertStringPtr(t, "request", got.Pricing.Request, "0")
|
|
assertStringPtr(t, "image", got.Pricing.Image, "0.001")
|
|
assertStringPtr(t, "web_search", got.Pricing.WebSearch, "0.01")
|
|
if len(got.RawPricing) == 0 || len(got.RawModel) == 0 {
|
|
t.Fatal("expected raw pricing and raw model payloads")
|
|
}
|
|
if got.SyncedAt.IsZero() {
|
|
t.Fatal("SyncedAt is zero")
|
|
}
|
|
}
|
|
|
|
func TestFetchOpenRouterCatalogReturnsStatusError(t *testing.T) {
|
|
client := &http.Client{Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
|
|
return &http.Response{
|
|
StatusCode: http.StatusBadGateway,
|
|
Body: io.NopCloser(strings.NewReader(`bad gateway`)),
|
|
Header: make(http.Header),
|
|
}, nil
|
|
})}
|
|
|
|
if _, err := FetchOpenRouterCatalog(context.Background(), client); err == nil {
|
|
t.Fatal("expected non-2xx status error")
|
|
}
|
|
}
|
|
|
|
func assertStringPtr(t *testing.T, name string, got *string, want string) {
|
|
t.Helper()
|
|
if got == nil {
|
|
t.Fatalf("%s = nil, want %q", name, want)
|
|
}
|
|
if *got != want {
|
|
t.Fatalf("%s = %q, want %q", name, *got, want)
|
|
}
|
|
}
|
|
|
|
type roundTripFunc func(*http.Request) (*http.Response, error)
|
|
|
|
func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
return f(req)
|
|
}
|