fix(pricing): deterministic price tiebreak; admit a zero-cost reserve with a zero hold (#98, #104)#115
Merged
Merged
Conversation
… with a zero hold (#98, #104) #98: resolve_price ordered only by published_at DESC, but published_at defaults to the transaction timestamp, so a bulk seed/migration publishes several versions at the same instant and the "current" pick was arbitrary and could vary run to run. The resolved version is stamped on the reservation and drives commit reconciliation, so an ambiguous pick silently shifts the cost basis. Add version DESC as a deterministic tiebreak so "latest published" is a total order. #104: reserve raised NonPositiveEstimate (422) when the worst-case estimate rounded to 0 — which, because ceil_micro keeps any positive worst case at >= 1 micro, only happens for a zero-token request or a genuinely free (zero-priced) model. A free model could therefore never transit the gate. Admit a zero estimate with a zero-micro hold instead, so free and paid models route through the same guard; the reserve walks every node at amount 0 (always succeeds on a node with any non-negative headroom) and commit reconciles to 0. NonPositiveEstimate was raised only by reserve, so it is now dead — an error the server can never emit, the same vestige pattern flagged in #96/#100. Removed the type and its api/sdk mappings (the SDK's 422 fallback already resolves to InvalidRequest, so client behavior is unchanged). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BjJ4QScUKbrMzF1jujL5Bg
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BjJ4QScUKbrMzF1jujL5Bg
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#98 — non-deterministic current-price resolution
resolve_priceordered bypublished_at DESC LIMIT 1with no tiebreak.price_book.published_atdefaults tofunc.now()(the transaction timestamp, constant within a transaction), so a bulk seed/migration publishes several versions at the same instant and the "current" pick is arbitrary — and can differ run to run if physical order shifts. The resolvedversionis stamped on the reservation and drives commit reconciliation, so an ambiguous pick silently shifts the cost basis.Fix:
ORDER BY published_at DESC, version DESC, making "latest published" a total order.#104 — a genuinely free model can't transit the gate (decision: admit with a zero hold)
reserveraisedNonPositiveEstimate(422) whenever the worst-case estimate rounded to 0. Becauseceil_microkeeps any positive worst case at ≥ 1 micro,estimate == 0happens only for a zero-token request or a zero-priced (free) model — so a free model always 4xx'd on reserve.Fix (per the resolved question): admit a zero estimate with a zero-micro hold so free and paid models route through the same guard. The reserve walks every node at amount 0 — which succeeds on any node with non-negative headroom (
ensure_periodseeds the row, the guard isheadroom >= 0) — and commit later reconciles to 0.estimate_micronever returns negative, soestimate >= 0always holds.Dead-error cleanup
NonPositiveEstimatewas raised only byreserve, so admitting zero makes it an error the server can never emit — the same vestige pattern the review flags in #96 (unused status column) and #100 (dead 402). Removed the domain type and itsapi/errors+ SDK mappings. The SDK'serror_foralready falls a 422-with-unmapped-code back toInvalidRequest(whatnon_positive_estimatemapped to), so client behavior is unchanged.Tests
published_atresolve deterministically to the higher version; the existing latest-published test unchanged.estimated_micro=0, caches the response) instead of raising;NonPositiveEstimatecases dropped from the error-mapping tests.Local gate: ruff, mypy --strict, import-linter, full unit suite (489), price-book + reserve integration — all green.
Closes #98, #104.