101 lines
3.1 KiB
Go
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)
|
|
}
|