Files
2026-08-23 22:52:13 +02:00

101 lines
3.1 KiB
Go

// Command backfill-ai-usage recovers AI cost history for products that were
// processed before prompt/usage capture existed.
//
// It reads the provider usage blocks still stored in processed_products.gpt_response
// and rolls them into ai_usage_daily with source='backfill', so /admin/ai-costs
// shows spend from before the feature shipped. See internal/aiaudit/backfill.go for
// what is and is not recoverable.
//
// go run ./cmd/backfill-ai-usage # dry run, reports what it would write
// go run ./cmd/backfill-ai-usage -apply
// go run ./cmd/backfill-ai-usage -apply -before 2026-08-23
//
// Safe to re-run: each pass replaces its own rows and never touches live capture.
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"log"
"os"
"strings"
"time"
"github.com/descrybe/descrybe-v2/apps/api/internal/aiaudit"
"github.com/descrybe/descrybe-v2/apps/api/internal/config"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
apply := flag.Bool("apply", false, "write rows (default is a dry run)")
before := flag.String("before", "", "only products last updated before this date (YYYY-MM-DD); default: the day live capture started")
dsn := flag.String("postgres", os.Getenv("DATABASE_URL"), "Postgres URL (defaults to DATABASE_URL / repo .env)")
flag.Parse()
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel()
url := strings.TrimSpace(*dsn)
if url == "" {
cfg, err := config.Load()
if err != nil {
log.Fatalf("config: %v", err)
}
url = cfg.DatabaseURL
}
pool, err := pgxpool.New(ctx, url)
if err != nil {
log.Fatalf("db: %v", err)
}
defer pool.Close()
cutoff, err := resolveCutoff(ctx, pool, *before)
if err != nil {
log.Fatalf("cutoff: %v", err)
}
res, err := aiaudit.BackfillFromProcessedProducts(ctx, pool, cutoff, !*apply)
if err != nil {
log.Fatalf("backfill: %v", err)
}
b, _ := json.MarshalIndent(res, "", " ")
fmt.Println(string(b))
fmt.Printf("\nrecovered %s across %d product(s); %d product(s) had no stored usage block\n",
usd(res.CostUSD()), res.ProductsUsed, res.ProductsNoUsage)
if !*apply {
fmt.Println("dry run — re-run with -apply to write these rows")
}
}
// resolveCutoff keeps the backfill from double-counting calls the live recorder
// already logged: by default it stops at the first day live capture wrote a row.
func resolveCutoff(ctx context.Context, pool *pgxpool.Pool, before string) (time.Time, error) {
if s := strings.TrimSpace(before); s != "" {
t, err := time.Parse("2006-01-02", s)
if err != nil {
return time.Time{}, fmt.Errorf("invalid -before date (YYYY-MM-DD): %w", err)
}
return t.UTC(), nil
}
start, err := aiaudit.LiveCaptureStart(ctx, pool)
if err != nil {
return time.Time{}, err
}
if start.IsZero() {
log.Println("no live capture recorded yet — scanning all history")
return time.Time{}, nil
}
log.Printf("live capture starts %s — backfilling products updated before that", start.Format("2006-01-02"))
return start, nil
}
func usd(v float64) string {
if v != 0 && v < 0.01 {
return fmt.Sprintf("$%.6f", v)
}
return fmt.Sprintf("$%.2f", v)
}