135 lines
6.9 KiB
Go
135 lines
6.9 KiB
Go
package pipeline
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"textmachine/backend/internal/ledger"
|
|
"textmachine/backend/internal/llm"
|
|
"textmachine/backend/internal/store"
|
|
)
|
|
|
|
// reprice.go: what already-billed work would cost IF BOUGHT AGAIN TODAY (row 181). A projection of
|
|
// FUTURE spend that adds up past `cost_usd` quotes a price list that no longer exists — DeepSeek's table
|
|
// moved 16.08.2026 (D39.137) — while reserve and settle price with the current one (stagerun.go).
|
|
//
|
|
// Scope: only spend that has NOT happened — the re-payment amount, its consent threshold and
|
|
// projected_book_usd. Committed/reserved and the per-chapter passport costs are historical fact.
|
|
//
|
|
// The recorded TOKENS are re-priced through settle's own seam (ledger.CostUSD + Pricer.PriceForResponse)
|
|
// rather than re-estimated with ledger.EstimateUSD: the estimate reserves the whole max_tokens budget and
|
|
// would overshoot by multiples, it needs rendered messages the read path deliberately does not have, and
|
|
// going through the settle seam is what keeps the quote and the booking from drifting apart.
|
|
//
|
|
// NOT projected (like the content axis in rebill.go): ROUTING. A row is priced by the model that
|
|
// ANSWERED, not by the one its stage resolves to now — predicting the token mix of a model that has never
|
|
// seen these chunks is a different question, and it is why an escalation hop is priced at its own model.
|
|
|
|
// repricedCall is one stored provider call: what it was billed, and what the same tokens cost today.
|
|
type repricedCall struct {
|
|
then float64
|
|
now float64
|
|
// priced is false when the checkpoint carries no usable token count, and `now` is the billed amount.
|
|
priced bool
|
|
}
|
|
|
|
// repricer answers "what would this stored row cost today" for every disposition row of a book. A cell
|
|
// holds its calls OLDEST FIRST, because which of them still back the row is decided from the newest end
|
|
// (see usd).
|
|
type repricer struct {
|
|
cells map[chunkKey]map[string][]repricedCall
|
|
}
|
|
|
|
// newRepricer reads the book's checkpoint usage once. It is EAGER rather than lazy on purpose: both
|
|
// callers (the consent gate and the status read-model) need it for the amounts they publish, and a
|
|
// lazily-loading variant would have to smuggle a store error out of an arithmetic helper.
|
|
func (r *Runner) newRepricer() (*repricer, error) {
|
|
rows, err := r.Store.CheckpointUsageForBook(r.Book.BookID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("pipeline: read checkpoint usage for the re-pricing of already-billed work: %w", err)
|
|
}
|
|
rp := &repricer{cells: make(map[chunkKey]map[string][]repricedCall)}
|
|
for _, cu := range rows {
|
|
usd, priced := r.repriceCheckpoint(cu)
|
|
key := chunkKey{cu.Chapter, cu.ChunkIdx}
|
|
byStage := rp.cells[key]
|
|
if byStage == nil {
|
|
byStage = map[string][]repricedCall{}
|
|
rp.cells[key] = byStage
|
|
}
|
|
byStage[cu.Stage] = append(byStage[cu.Stage], repricedCall{then: cu.CostUSD, now: usd, priced: priced})
|
|
}
|
|
return rp, nil
|
|
}
|
|
|
|
// repriceCheckpoint prices one stored call at today's table. `priced` is false when the checkpoint
|
|
// carries no usable token count and the historical amount is returned instead — the real case being the
|
|
// billed-decode-failure checkpoint, which settles the RESERVATION ESTIMATE against a `{}` usage
|
|
// (stagerun.go): re-pricing that to $0 would quietly delete money from the consent number.
|
|
func (r *Runner) repriceCheckpoint(cu store.CheckpointUsage) (usd float64, priced bool) {
|
|
var u llm.Usage
|
|
if err := json.Unmarshal([]byte(cu.UsageJSON), &u); err != nil {
|
|
return cu.CostUSD, false
|
|
}
|
|
if u == (llm.Usage{}) {
|
|
// No tokens to price. A genuinely $0 row (a local model, a $0 derived export checkpoint) is
|
|
// re-priced to the same $0 and is not a gap; only a row that COST something is. The test is on
|
|
// the whole struct rather than on selected fields, so a new usage axis cannot make it stale.
|
|
if cu.CostUSD == 0 {
|
|
return 0, true
|
|
}
|
|
return cu.CostUSD, false
|
|
}
|
|
return ledger.CostUSD(r.Pricer.PriceForResponse(cu.ModelRequested, cu.ModelActual), u), true
|
|
}
|
|
|
|
// usd is what re-buying this disposition row would cost at the current price table; `fromHistory` says
|
|
// the answer is partly the amount it was billed at instead.
|
|
//
|
|
// GENERATION MEMBERSHIP, and why it has to be DERIVED (this is an inference, not an identity — the one
|
|
// place in here worth distrusting). Checkpoints are append-only for the life of the book and carry no
|
|
// snapshot: re-buying a unit, or editing the source under it, leaves the old call on file beside the new
|
|
// one. chunk_status.cost_usd is the opposite — OVERWRITTEN each run with the CURRENT generation's cost.
|
|
// So pricing every call of a position answers "what has this position ever cost" in place of the question
|
|
// the operator is consenting to. The row's own money is the only per-generation authority in the store,
|
|
// so membership is taken from it: walk the calls NEWEST FIRST, keeping them while they fit inside
|
|
// cs.CostUSD.
|
|
//
|
|
// TWO PRECONDITIONS, and what happens when each fails:
|
|
// - the generation's calls SUM to the row's cost. Short of it means calls were lost (a restore, a
|
|
// legacy row) — the remainder is carried at its billed value, the conservative direction and the
|
|
// answer the projection gave before re-pricing existed. Not a routine branch: the one path that
|
|
// deletes checkpoints, ResetChunkStages, deletes the disposition row with them in one transaction.
|
|
// - they are the NEWEST calls of the cell. A config REVERT breaks this: the re-run addresses an old
|
|
// request_hash, the settle is a no-op (ON CONFLICT DO NOTHING, store/ledger.go) and the row is
|
|
// rewritten with an OLDER call's cost while newer, superseded ones stay on file. The walk then
|
|
// overshoots — which is detectable, so it is detected, and the row falls back to its billed amount
|
|
// marked NOT re-priced. That quotes stale money, but it quotes it out loud, which is the whole
|
|
// point of the row this file closes.
|
|
//
|
|
// Recovering the revert case EXACTLY needs a generation marker on `checkpoints` (they carry neither
|
|
// snapshot_id nor run id) — a money-table schema change, and a question for ratification rather than a
|
|
// third patch here.
|
|
func (rp *repricer) usd(cs store.ChunkStatus) (usd float64, fromHistory bool) {
|
|
calls := rp.cells[chunkKey{cs.Chapter, cs.ChunkIdx}][cs.Stage]
|
|
var accounted float64
|
|
for i := len(calls) - 1; i >= 0 && accounted < cs.CostUSD-residueEpsilonUSD; i-- {
|
|
accounted += calls[i].then
|
|
usd += calls[i].now
|
|
if !calls[i].priced {
|
|
fromHistory = true
|
|
}
|
|
}
|
|
if accounted > cs.CostUSD+residueEpsilonUSD {
|
|
return cs.CostUSD, true // the newest calls are not this row's — see the revert case above
|
|
}
|
|
if residue := cs.CostUSD - accounted; residue > residueEpsilonUSD {
|
|
usd += residue
|
|
fromHistory = true
|
|
}
|
|
return usd, fromHistory
|
|
}
|
|
|
|
// residueEpsilonUSD is far below any amount this engine prints (%.6f): it separates money a row really
|
|
// lost from the float noise of adding the same terms in a different order.
|
|
const residueEpsilonUSD = 1e-9
|