{"record":{"id":"d5b3571c34938f62","repo":"JuliusBrussee/caveman","slug":"usage-export-contained-no-priced-rows","errorCode":null,"errorMessage":"usage export contained no priced rows","messagePattern":"usage export contained no priced rows","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"proxy/internal/store/learn_reconcile.go","lineNumber":177,"sourceCode":"\t\tmodelIndex := columns[\"model\"]\n\t\tif modelIndex >= len(record) {\n\t\t\tcontinue\n\t\t}\n\t\tmodel := strings.TrimSpace(record[modelIndex])\n\t\tif model == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\ttotal := reconcileCell(record, columns, \"input\") +\n\t\t\treconcileCell(record, columns, \"output\") +\n\t\t\treconcileCell(record, columns, \"cache_read\") +\n\t\t\treconcileCell(record, columns, \"cache_write\")\n\t\tif total <= 0 {\n\t\t\tcontinue\n\t\t}\n\t\tbilled[model] += total\n\t}\n\tif len(billed) == 0 {\n\t\treturn nil, fmt.Errorf(\"usage export contained no priced rows\")\n\t}\n\treturn billed, nil\n}\n\n// BuildLearnReconcile compares an export against the scanned window.\nfunc (s *Store) BuildLearnReconcile(cwd, exportPath string, sources []string, sinceExpr string) (LearnReconcile, error) {\n\tbilled, err := parseUsageExport(exportPath)\n\tif err != nil {\n\t\treturn LearnReconcile{}, err\n\t}\n\tplan, err := s.BuildLearnPlan(cwd, sources, sinceExpr)\n\tif err != nil {\n\t\treturn LearnReconcile{}, err\n\t}\n\treturn buildLearnReconcile(billed, plan.Spend, exportPath), nil\n}\n\nfunc buildLearnReconcile(billed map[string]int64, spend *LearnSpend, source string) LearnReconcile {","sourceCodeStart":159,"sourceCodeEnd":195,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/81536f57b3303b7de7f5bc5b564cc344f9112d68/proxy/internal/store/learn_reconcile.go#L159-L195","documentation":"parseUsageExport reads a provider usage-export CSV and accumulates billed token totals per model. After the header maps successfully, each row only counts when it has a non-empty model cell AND a positive sum of input+output+cache_read+cache_write tokens (reconcileCell silently returns 0 for blank, non-numeric, or negative cells). If the loop finishes and not a single row qualified, the map is empty and the function fails closed with 'usage export contained no priced rows' rather than reconciling against zero. This guards BuildLearnReconcile from producing a meaningless 100%-unattributed reconciliation.","triggerScenarios":"Calling Store.BuildLearnReconcile(cwd, exportPath, sources, sinceExpr) with a CSV whose header maps (it has recognizable token columns) but where every data row is skipped: model cell blank, model column index beyond the row length, all token cells empty, token cells containing non-numeric text (currency symbols, 'N/A'), or token values that parse to 0/negative. Also triggered by a header-only CSV with no data rows, or by pointing exportPath at a cost-only/dollars-only export that lacks populated token columns.","commonSituations":"Downloading the wrong export artifact (an invoice or spend summary instead of the token usage CSV); exporting from a provider console with a date filter that excludes all metered traffic; a schema change where the provider renames or reformats token columns so reconcileCell reads empty cells; hand-edited CSVs where commas were used as thousands separators inconsistently or values were replaced with dashes; passing a template/example CSV that has headers but no rows.","solutions":["Open the CSV and confirm it has data rows with a non-empty model column and at least one populated token column (input/output/cache_read/cache_write) per row","Check you exported the token usage report for the same time window as the reconcile scan — an empty window yields zero qualifying rows","If token cells contain formatted numbers (e.g. '1,234.0' works, but '$12' or '—' does not), clean them so they parse as plain floats, since non-numeric cells silently count as 0","If the provider renamed columns, update mapReconcileHeader to recognize the new header variants (see how TestReconcileAcceptsProviderHeaderVariants pins this)","As a last resort, regenerate the export from the provider and rerun BuildLearnReconcile"],"exampleFix":"// before: exporting a spend-only CSV\n// model,input,output,cost\n// claude-sonnet-4,,,12.50\n\n// after: export the token usage report\n// model,input,output,cache_read,cache_write\n// claude-sonnet-4,1024,2048,0,0","handlingStrategy":"validation","validationCode":"func hasPricedRows(path string) (bool, error) {\n\tf, err := os.Open(path)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tdefer f.Close()\n\tr := csv.NewReader(f)\n\tr.FieldsPerRecord = -1\n\theader, err := r.Read()\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"no header row: %w\", err)\n\t}\n\tcols, err := mapReconcileHeader(header) // same mapping the parser uses\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tmi := cols[\"model\"]\n\tfor {\n\t\trec, err := r.Read()\n\t\tif err == io.EOF {\n\t\t\tbreak\n\t\t}\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\tif mi < len(rec) && strings.TrimSpace(rec[mi]) != \"\" {\n\t\t\ttotal := reconcileCell(rec, cols, \"input\") + reconcileCell(rec, cols, \"output\") +\n\t\t\t\treconcileCell(rec, cols, \"cache_read\") + reconcileCell(rec, cols, \"cache_write\")\n\t\t\tif total > 0 {\n\t\t\t\treturn true, nil\n\t\t\t}\n\t\t}\n\t}\n\treturn false, nil\n}\n\n// before calling:\n// ok, err := hasPricedRows(exportPath)\n// if err != nil { return err }\n// if !ok { return errors.New(\"export has no priced rows; re-download the token usage report\") }","typeGuard":"//go:build go\n// not a type error — guard the error value instead:\nfunc isNoPricedRowsErr(err error) bool {\n\treturn err != nil && strings.Contains(err.Error(), \"usage export contained no priced rows\")\n}","tryCatchPattern":"reconcile, err := store.BuildLearnReconcile(cwd, exportPath, sources, sinceExpr)\nif err != nil {\n\tif isNoPricedRowsErr(err) {\n\t\t// treat as bad/incomplete export input: surface an actionable message,\n\t\t// do NOT fall back to a zero-billed reconciliation\n\t\tlog.Printf(\"%s: export %s has headers but no rows with a model and positive tokens; re-export the token usage report\", err, exportPath)\n\t\treturn err\n\t}\n\treturn fmt.Errorf(\"reconcile: %w\", err)\n}","preventionTips":["Always download the token-usage CSV (with input/output/cache columns), not a dollars-only invoice export","Verify the export's date range overlaps the sinceExpr window used for the scan before reconciling","Sanity-check the CSV in a spreadsheet: at least one row with a model name and non-zero token counts","Automate a pre-flight row count check (data rows > 0) in pipelines that feed BuildLearnReconcile","When a provider changes its export schema, extend mapReconcileHeader and add a header-variant test rather than editing the CSV by hand"],"tags":["go","csv","usage-export","data-validation","reconciliation"],"backgroundTag":"empty-csv-data","analyzedSha":"81536f57b3303b7de7f5bc5b564cc344f9112d68","analyzedAt":"2026-08-27T02:25:50.681Z","contentChangedAt":"2026-08-27T02:25:50.681Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}