Last run: 2026-04-16 Index:
~/.costea/task-index.json, 2799 raw → 2769 usable tasks Split: 80 / 10 / 10 by timestamp → train 2215, val 277, test 277 Cost target: Sonnet 4.6 prices ($3 in / $15 out / $0.30 cache_read per 1M tokens) Features: 47-dim (18 original + 1 task_type + 12 keyword-group + 16 TF-IDF SVD) Models: GBDT (LightGBM, 1028 trees) | MLP (PyTorch, 128→64→1 + BatchNorm) | Linear (QuantileRegressor)
Reproduce locally:
cd fitting
node scripts/eval-baseline.mjs # current heuristic
node scripts/eval-knn.mjs # TF-IDF kNN + empirical quantile
node scripts/eval-gbdt.mjs # bundled boosted-tree heads
node scripts/compare.mjs # all three, side by side| Metric | baseline | kNN | GBDT | MLP | Linear | best |
|---|---|---|---|---|---|---|
| MAPE | 407.1% | 37.2% | 34.5% | 34.3% | 40.2% | MLP |
| median APE | 70.9% | 28.1% | 20.7% | 19.1% | 22.7% | MLP |
| log-RMSE | 1.261 | 0.543 | 0.492 | 0.524 | 0.604 | GBDT |
| within ±25% | 31.8% | 43.3% | 55.6% | 58.5% | 52.3% | MLP |
| within ±50% | 41.2% | 71.1% | 72.6% | 71.5% | 69.7% | GBDT |
Key finding: MLP wins on cost median APE (19.1%) and within-±25% (58.5%), while GBDT wins on log-RMSE (0.492) — the more robust global metric. The two models have complementary strengths: GBDT handles tail cases better, MLP is more accurate at the median.
Bold = best of three.
| Metric | baseline | knn | gbdt v2 |
|---|---|---|---|
| MAPE | 407.1% | 37.2% | 34.5% |
| median APE | 70.9% | 28.1% | 24.0% |
| log-RMSE | 1.261 | 0.543 | 0.514 |
| within ±25% | 31.8% | 43.3% | 51.6% |
| within ±50% | 41.2% | 71.1% | 71.5% |
| P10–P90 coverage | — | 59.2% | 59.6% |
| Metric | baseline | knn | gbdt v2 |
|---|---|---|---|
| MAPE | 11 024 156.1% | 159.0% | 101.7% |
| median APE | 833.3% | 88.0% | 60.1% |
| log-RMSE | 6.836 | 1.267 | 1.125 |
| within ±25% | 6.1% | 14.4% | 20.9% |
| within ±50% | 16.6% | 31.8% | 42.2% |
| P10–P90 coverage | — | 68.6% | 79.1% |
| Metric | baseline | knn | gbdt v2 |
|---|---|---|---|
| MAPE | 4074.8% | 1241.9% | 720.7% |
| median APE | 219.9% | 81.7% | 74.8% |
| log-RMSE | 2.280 | 1.700 | 1.527 |
| within ±25% | 11.2% | 11.9% | 13.4% |
| within ±50% | 18.8% | 27.8% | 29.6% |
| P10–P90 coverage | — | 82.3% | 80.5% |
| Metric | baseline | knn | gbdt v2 |
|---|---|---|---|
| MAPE | 706.0% | 132.2% | 95.2% |
| median APE | 89.7% | 75.7% | 68.3% |
| log-RMSE | 1.869 | 1.670 | 1.564 |
| within ±25% | 13.7% | 14.4% | 13.4% |
| within ±50% | 24.5% | 28.9% | 33.2% |
| P10–P90 coverage | — | 82.3% | 70.4% |
| Metric | baseline | knn | gbdt v2 |
|---|---|---|---|
| MAPE | 603.5% | 143.2% | 141.3% |
| median APE | 166.7% | 76.2% | 68.3% |
| log-RMSE | 1.587 | 1.081 | 0.990 |
| within ±25% | 10.5% | 17.3% | 17.0% |
| within ±50% | 22.4% | 30.0% | 32.9% |
| P10–P90 coverage | — | 72.6% | 76.9% |
prompt + ctx
│
├──► extractFeatures + encodeFeatures
│ 47-dim Float64 vector:
│ 5 prompt-shape + 5 session-position + 4 categorical-index
│ + 4 misc + 1 task_type + 12 keyword-group binary
│ + 16 TF-IDF SVD components
│
├──► fitting/models/<target>_<quantile>.txt
│ LightGBM Booster, quantile objective, learning_rate=0.05,
│ leaves=31, early stopping on val
│
└──► gbdt.mjs: parses the .txt format, walks every tree per query
(~10 µs / head), aggregates leaf values, applies expm1
and the same isotonic + conformal calibration as the kNN
path.
The 29 new features give the trees semantic signal about task intent:
- 12 keyword-group features: regex-detected binary indicators for test/refactor/fix/create/read/deploy/doc/config/perf/security/ui/data
- 1 task_type: coarse classification into 6 categories
- 16 SVD components: TruncatedSVD on the TF-IDF prompt matrix, projection matrix stored in manifest for JS inference
Pure-JS inference. No native bindings, no ONNX runtime, no Python
required at predict-time. The SVD projection at inference is a single
matrix multiply (~0.1 ms). The only Python dependency is at train-time
(pip install lightgbm scikit-learn + brew install libomp on macOS).
The kNN remains active even on the GBDT path — it provides the top-K historical evidence shown in receipts and feeds the confidence proxy.
| Target | Val coverage (post-cal) | Test coverage |
|---|---|---|
| input | 81.9% | 75.5% |
| output | 82.7% | 79.4% |
| cache_read | 79.4% | 70.4% |
| tools | 83.4% | 70.8% |
| cost | 78.0% | 57.8% |
cost coverage drops most on test — a conformal-on-shift symptom we also see on the kNN bundle. The intervals are well-calibrated on val but the test slice is the most recent 5 days, where prompt distribution drifts faster than the calibration set tracks. Retraining (see below) takes a fresh val slice and recovers calibration.
The bundle in fitting/models/ was trained at
2026-04-13T08:44:36+00:00 on whatever was in
~/.costea/task-index.json at that moment (2769 tasks). To refresh:
brew install libomp # macOS, once
pip install lightgbm numpy # once
cd fitting
python3 training/train.py # writes into fitting/models/
node scripts/eval-gbdt.mjs # measure
node scripts/compare.mjs # vs other methodsTunables that matter:
python3 training/train.py --num-trees 400 --leaves 63More trees + wider leaves widen the gap to kNN further at the cost of a larger bundle. The 919-tree default lands at 2.7 MB total.
After a successful retrain:
git add fitting/models/*.txt fitting/models/manifest.json
git commit -m "models: retrain on $(date +%Y-%m-%d)"| Stage | Latency (laptop, M-series) |
|---|---|
| Bundle load (15 .txt files, 919 trees parsed) | ~80 ms (one-shot, on import) |
| Encode one query | <0.1 ms |
| GBDT predict (15 heads) | ~150 µs |
| TF-IDF + kNN evidence path | ~3 ms |
| Calibration apply | <0.1 ms |
| End-to-end predict (warm) | ~3 ms |
End-to-end budget unchanged from the kNN-only path — both still run; the GBDT heads are the cheap part.
Bundle predictions are bit-deterministic given the model files:
sample task row, cost_p50.txt:
python lgb predict(): 0.0638968822208243
js gbdt.mjs predict(): 0.0638968822208243
The Python trainer is seeded on LightGBM defaults; bagging introduces
minor run-to-run variance unless you pin seed= and
feature_fraction_seed= in the params dict in
training/train.py.