osrforge.providers.ledger
The cost ledger: a file that records what a model service has cost, with a stop.
A ledger file is JSON: {"stop_usd": ..., "limit_usd": ..., "spent_usd": ...,
"entries": [...]}. Two ledgers exist, and their entries differ. The Claude
ledger's entries are {"t", "script", "label", "model", "cost_usd"}, one per
request. The Clef ledger's entries are {"date", "what", "requests", "usd"},
one per run. The ledger code doesn't know either schema: the caller gives it
the entry and names the cost key (and, for a per-run entry, the request-count
key), and the ledger keeps every entry it finds in the file.
Spend is reserved before a request at its worst case and settled after it at the cost the answer reports, so a request that fails after it was sent, or a process that dies between the two, leaves its worst case on the ledger. A request's retries share its one reservation.
The file is written under a lock that excludes threads and processes alike,
and any script that writes the same file follows the same protocol: take an
exclusive fcntl.flock on the sidecar file <ledger path>.lock, read the
ledger, change it, write it to a temporary file in the same directory,
os.replace the temporary file over the ledger, and release the lock.
Amounts are kept in whole micro-dollars and written rounded to six decimal
places, as the files already are, so the stop comparison is exact and
spent_usd never shows float noise.
Ledger
One ledger file.
Examples:
from pathlib import Path
from osrforge.providers.ledger import Ledger
ledger = Ledger(Path("claude-ledger.json"), cost_key="cost_usd")
entry = {"t": "2026-10-15T10:00:00", "script": "osrforge", "label": "items p38", "model": "m"}
reservation = ledger.open_reserved(entry, 0.08)
# ... the request runs ...
ledger.settle(reservation, 0.0312)
Bind to a ledger file without touching it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
Path
|
The ledger file. It must exist, with the four top-level keys, when the first method that reads it runs. |
required |
cost_key
|
str
|
The entry key that holds an entry's cost. |
required |
stop_usd
|
float | None
|
The stop. When omitted, the file's |
None
|
count_key
|
str | None
|
The entry key that counts the requests settled against
the entry, for a per-run entry. |
None
|
read
Return the ledger file's content.
Returns:
| Type | Description |
|---|---|
dict[str, object]
|
The parsed file. |
Raises:
| Type | Description |
|---|---|
ProviderError
|
If the file is missing, isn't JSON, or lacks
|
spent
spent() -> float
Return the file's spent_usd, which includes every open reservation at its worst case.
Returns:
| Type | Description |
|---|---|
float
|
The amount spent. |
open
Append an entry with a cost of zero and return its index.
The entry gets cost_key set to 0.0 and, when the ledger has a
count_key, that key set to 0. The caller supplies every other
field in the file's own schema.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entry
|
Mapping[str, object]
|
The entry's fields. |
required |
Returns:
| Type | Description |
|---|---|
int
|
The entry's index in |
open_reserved
open_reserved(entry: Mapping[str, object], usd: float) -> Reservation
Append an entry and reserve against it in one locked step, writing nothing on a refusal.
The stop is checked before the entry is written, so a refused request
leaves the file byte for byte as it was. Otherwise this is open
followed by reserve under one lock.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entry
|
Mapping[str, object]
|
The entry's fields. |
required |
usd
|
float
|
The worst-case cost. |
required |
Returns:
| Type | Description |
|---|---|
Reservation
|
The reservation, to settle. |
Raises:
| Type | Description |
|---|---|
LedgerStopError
|
If the reservation would take the spend past the stop. |
reserve
reserve(index: int, usd: float) -> Reservation
Reserve a request's worst-case cost against an entry.
Under the lock: if the file's spent_usd plus usd would pass the
stop, nothing is written and LedgerStopError is raised. Otherwise
the entry's cost and the file's spent_usd grow by usd, and the
entry's pending count grows by one. The comparison is made in whole
micro-dollars, so a reservation that reaches the stop exactly is
allowed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index
|
int
|
The entry's index. |
required |
usd
|
float
|
The worst-case cost. |
required |
Returns:
| Type | Description |
|---|---|
Reservation
|
The reservation, to settle. |
Raises:
| Type | Description |
|---|---|
LedgerStopError
|
If the reservation would take the spend past the stop. The message names the spend, the reservation, and the stop. |
settle
settle(reservation: Reservation, usd: float) -> None
Replace a reservation with the cost the answer reports.
Under the lock: the entry's cost and the file's spent_usd change by
usd minus the reserved amount, the entry's pending count falls by
one (and the key is removed at zero), and, when the ledger has a
count_key, the entry's count grows by one. Settling may take the
spend past the stop; the stop is checked only when reserving.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
reservation
|
Reservation
|
The reservation. |
required |
usd
|
float
|
The actual cost. |
required |