251 lines
15 KiB
Go
251 lines
15 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.
|
|
//
|
|
// ROUTING IS PROJECTED, AND IT ROUNDS UP (D39.150 п.1). A call whose usage is on file is priced by the
|
|
// model its stage resolves to TODAY — the current escalate_to for a hop call, the stage's own model
|
|
// otherwise — and never below what the model that ANSWERED would charge for the same tokens: the dearer of
|
|
// the two. The direction is the whole contract: a stage that moved to a dearer model must not be quoted at
|
|
// the retired model's table ($0.003640 for $0.036400 of work is the acceptance's measurement of exactly
|
|
// that), and quoting the dearer price when the stage moved to a CHEAPER model is the safe error, which is
|
|
// why the answering model stays a floor.
|
|
//
|
|
// What stays unknowable is the TOKEN COUNT a model that has never seen these chunks would spend, and no
|
|
// stored fact can answer it; a call with no usable usage on file cannot be re-priced at all and is carried
|
|
// at its billed figure (usd). So the figure is an ESTIMATE that errs upward where the tokens are known, not
|
|
// a re-pricing of known tokens: the consent text says so and names how many units each caveat holds for
|
|
// (projectionBasis), and nothing on this path promises accuracy.
|
|
|
|
// 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
|
|
// moved is true when the stage this call belongs to resolves today to a model other than the one this
|
|
// call was SENT to, so `now` is the dearer of two prices for a token count only one of them produced.
|
|
// The comparison is against the requested slug, not the answering one, because the re-run will request
|
|
// what the stage resolves to: a provider that canonicalises its slug in the response (`gpt-5-mini` →
|
|
// `gpt-5-mini-2025-08-07`) has not re-routed anything, and reading the answer here would report every
|
|
// call on such a provider as a routing move.
|
|
moved 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, moved := r.repriceCheckpoint(cu)
|
|
// The cell is the POSITION — chapter AND chunk. A chapter is several chunks whenever the cut says so,
|
|
// and a cell keyed by chapter alone pools their calls, so the newest-first walk in usd() eats the
|
|
// neighbour's money. Pinned by TestEveryChunkOfAChapterIsRePricedFromItsOwnCalls and catalogued
|
|
// (cmd/tmmutate, FC1-cell-key).
|
|
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, moved: moved})
|
|
}
|
|
return rp, nil
|
|
}
|
|
|
|
// currentModelFor is the model a stored call's stage would send the same request to TODAY: the stage's
|
|
// resolved escalate_to for a hop call, its resolved model otherwise. ok is false for a stage the current
|
|
// pipeline does not run — such a row is never re-bought (rebill.go), so it keeps the answering model's
|
|
// price and no routing question arises.
|
|
func (r *Runner) currentModelFor(stage string, escalation bool) (model string, ok bool) {
|
|
for _, st := range r.Pipeline.Stages {
|
|
if st.Name != stage {
|
|
continue
|
|
}
|
|
if escalation && st.ResolvedHop != "" {
|
|
return st.ResolvedHop, true
|
|
}
|
|
return st.ResolvedModel, st.ResolvedModel != ""
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// repriceCheckpoint prices one stored call at today's table, at the model its stage resolves to TODAY and
|
|
// never below the answering model's price (the file comment). `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. `moved` is true
|
|
// when the stage resolves to a slug other than the one this call REQUESTED (see repricedCall), so the
|
|
// caller can say how much of its figure is an upper estimate.
|
|
func (r *Runner) repriceCheckpoint(cu store.CheckpointUsage) (usd float64, priced, moved bool) {
|
|
var u llm.Usage
|
|
if err := json.Unmarshal([]byte(cu.UsageJSON), &u); err != nil {
|
|
return cu.CostUSD, false, false
|
|
}
|
|
if u == (llm.Usage{}) {
|
|
// No tokens to price. A $0 row with no usage is the derived checkpoint (a repair or export
|
|
// projection, which reaches no provider and re-derives free): re-pricing it to the same $0 is the
|
|
// answer, not a gap. The test is on the whole struct rather than on selected fields, so a new usage
|
|
// axis cannot make it stale.
|
|
//
|
|
// ⚠ UNLESS THE STAGE HAS MOVED. Then the row is not provably derived — a provider that returned no
|
|
// usage block leaves the same shape — and its re-run goes to a model whose price is not $0, while
|
|
// there are no tokens to price it with. Publishing a bare $0 there is the silent under-quote this
|
|
// file exists to remove, so the amount stays what is known and the row is reported as one that
|
|
// could NOT be re-priced (projectionBasis names it).
|
|
if cu.CostUSD == 0 {
|
|
today, ok := r.currentModelFor(cu.Stage, cu.Escalation)
|
|
return 0, !ok || today == cu.ModelRequested, false
|
|
}
|
|
return cu.CostUSD, false, false
|
|
}
|
|
// The floor: these tokens at the answering model's price on today's table — the same
|
|
// PriceForResponse ordering settle used, so a canonicalised slug still finds its price.
|
|
floorPrice, _ := r.Pricer.PriceForResponse(cu.ModelRequested, cu.ModelActual)
|
|
usd = ledger.CostUSD(floorPrice, u)
|
|
today, ok := r.currentModelFor(cu.Stage, cu.Escalation)
|
|
if !ok {
|
|
return usd, true, false
|
|
}
|
|
// The model that WILL answer sets the other bound, whether or not the stage moved: a provider that
|
|
// answered a request for X under a slug Y the table lists cheaper would otherwise price the re-run —
|
|
// which asks for X again — at Y's table. The dearer of the two is the answer: the token count belongs
|
|
// to the model that answered, the price to the one that will, and neither may pull the figure below
|
|
// the other (D39.150 п.1: round up when it cannot be known).
|
|
if at := ledger.CostUSD(r.Pricer.PriceFor(today), u); at > usd {
|
|
usd = at
|
|
}
|
|
return usd, true, today != cu.ModelRequested
|
|
}
|
|
|
|
// 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, `moved` that some of it is priced at a model
|
|
// other than the one that produced the tokens (the file comment: an upper estimate, not a re-pricing).
|
|
//
|
|
// 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, moved bool) {
|
|
calls := rp.cells[chunkKey{cs.Chapter, cs.ChunkIdx}][cs.Stage]
|
|
var accounted float64
|
|
// A CALL THAT COST NOTHING NEVER STOPS THE WALK, and this is the whole of how a $0 model is handled.
|
|
// Membership is read off money, and a free call adds none: it can neither complete the row's cost nor
|
|
// overshoot it, so stopping on one would drop a call the re-run WILL make — and with it the price that
|
|
// call now carries. That is a silent under-quote, one function below the pricing that answered
|
|
// correctly, and it bites in two shapes: a stage run entirely on a $0 model (the row itself is $0), and
|
|
// a $0 primary whose PAID escalation hop alone accounts for the row (a local model with a cloud
|
|
// fallback). Both quote what the re-run costs only if the free calls travel with the paid ones.
|
|
//
|
|
// So: a free call is taken whenever the walk reaches it, and the walk stops at the first PAID call once
|
|
// the row's money is accounted for.
|
|
//
|
|
// ⚠ WHAT THAT COSTS, said out loud: a free call is not provably of THIS generation either. A $0 stage
|
|
// leaves cs.CostUSD == 0 after every run, so N free generations at one position are indistinguishable
|
|
// and all N are counted; a free call sitting at the end of an OLDER generation is taken along with this
|
|
// one's. The quote is then larger than one re-pass buys — an OVER-estimate, which is the ratified
|
|
// direction (D39.150 п.1: round up when it cannot be known) and what the consent sentence already
|
|
// declares («an ESTIMATE that errs upward»). Making it exact needs a generation marker on
|
|
// `checkpoints`, the same schema question the docstring above defers.
|
|
taken := 0
|
|
for i := len(calls) - 1; i >= 0; i-- {
|
|
free := calls[i].then <= residueEpsilonUSD
|
|
if accounted >= cs.CostUSD-residueEpsilonUSD && !free {
|
|
break // the row's money is accounted for, and this call is somebody else's
|
|
}
|
|
accounted += calls[i].then
|
|
usd += calls[i].now
|
|
taken++
|
|
// The disclosure travels WITH the amount: a call carried at its billed figure makes the row's
|
|
// quote partly old money, and the text the operator consents to names that (projectionBasis).
|
|
// Pinned on the production path by TestABilledDecodeRowIsDisclosedAsOldMoneyEndToEnd and
|
|
// catalogued (cmd/tmmutate, FC1-unpriced-not-disclosed).
|
|
if !calls[i].priced {
|
|
fromHistory = true
|
|
}
|
|
if calls[i].moved {
|
|
moved = true
|
|
}
|
|
}
|
|
if accounted > cs.CostUSD+residueEpsilonUSD {
|
|
// The newest calls are not this row's — see the revert case above — so the row is quoted at what it
|
|
// was BILLED. That figure is not priced at any model's current table, which is exactly what
|
|
// ModelMovedRows counts, so the routing caveat does not apply to it and `moved` is dropped with the
|
|
// calls it came from. What the reader is owed here is the OTHER caveat, and fromHistory carries it.
|
|
return cs.CostUSD, true, false
|
|
}
|
|
if residue := cs.CostUSD - accounted; residue > residueEpsilonUSD {
|
|
usd += residue
|
|
fromHistory = true
|
|
}
|
|
// ⚠ A $0 ROW WHOSE NEWEST CALL IS SOMEBODY ELSE'S is quoted at nothing, and nothing is not an answer.
|
|
// It is the revert case in the one shape the overshoot test above cannot see: the row's money is $0, so
|
|
// no walk can overshoot it, and the calls on file all belong to older generations. Publishing a bare $0
|
|
// for it is the same silent under-quote in a quieter place, so the amount stays what is known and the
|
|
// row is reported as one whose generation could NOT be established.
|
|
//
|
|
// The two guards say what the branch is NOT about. A row with no calls at all is genuinely free and has
|
|
// nothing to disclose — there is no generation to fail to establish. A `skipped` row never reached a
|
|
// provider, and the guard for it is DEFENSIVE rather than load-bearing: projectRebill drops skipped rows
|
|
// before this function sees them (rebill.go) and projectBookUSD ignores the flag, so today it changes
|
|
// only what a hand-built row gets. It stays because a future caller passing dispositions through would
|
|
// otherwise start disclosing rows that cost nothing by construction.
|
|
if taken == 0 && len(calls) > 0 && cs.Disposition != string(DispSkipped) {
|
|
fromHistory = true
|
|
}
|
|
return usd, fromHistory, moved
|
|
}
|
|
|
|
// 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
|