// 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) }