Skip to content

feat(standard): price runs from the user's own price list, not a table in the code - #39

Open
aitianshi wants to merge 5 commits into
mainfrom
feat/user-prices
Open

aitianshi wants to merge 5 commits into
mainfrom
feat/user-prices

Conversation

@aitianshi

@aitianshi aitianshi commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #38

What

A standard run's cost estimate came from a 13-model, OpenAI-only table written in run.py, matched by name prefix. Prices are now the user's data (app/nullius/prices.py): one prices.json in the configuration directory holding a public list fetched on request (LiteLLM's model_prices_and_context_window.json, keyed by the providers' own model names, kept with its address and day) and the user's own rows, which win over it. Nullius ships no price and a run never fetches one. A call is priced by the provider's own figure when its reply carries one, else by exactly its model name, else not at all. nullius prices (list, update, set, remove) and a Model prices section in Settings manage the list; --price MODEL=INPUT,OUTPUT[,CACHED] prices one run. A docker unit's relay is handed only its run's models' rows, a run on a host takes this machine's list with it (NULLIUS_PRICES), an empty one too, so a host's own list never prices a run started elsewhere, and a job's config.json records the prices of the models it calls, each with its source (list, own, or run for --price), and names the list and its day only when a price is the list's.

Touches

backend · routes · templates and static · docs and site · the standard

Before and after

Settings gains a Model prices section under Evaluation, with a dollar icon: a Price list card (how many models, the day fetched, Update with the refresh icon, turning while it updates) and Your prices (rows with Remove, and a form to add one). Before, there was no such section and no way to see or change a price.

Before After
Settings before: Evaluation has Model providers, Agent keys and Judges, no prices Settings after: Model prices with the price list, 3,946 models, Update, and Your prices

Verified

  • New app/tests/test_prices.py (16 tests): a new profile has no prices; the list is converted per million tokens with its source and day; a bad list or a failed fetch changes nothing; only https or a file is a source; the fetch sends nothing of the user; own rows win and survive an update; rows and --price are checked; NULLIUS_PRICES names another file; the CLI.
  • test_standard_relay.py: a model whose name only starts like a priced one has no cost. test_standard_run.py: a run given a price estimates its calls and records only its own models' rows in config.json, each with its source, the list named only for a row that is its; a run with none has no cost and no prices block. test_execution.py: the list is sent to a host and exported as NULLIUS_PRICES; with no list here an empty one is sent, so the host's own is never read.
  • Route test in app/ui_tests/test_settings_page.py: the three routes, their errors and their guards (token, origin, loopback); nothing lands in the settings file.
  • Backend suite python3 app/tests/parallel.py (1621 tests, after main was merged in) and route suite (356 tests), OK. The wheel built and app/packaging/verify.py passed, with prices among the installed commands. docs-drift script: 0 findings.
  • Browser (headless Chromium, a temporary profile), dark and light at 1280 px and dark at 390 px: Update against the real public list priced 3,943 models; add and keyboard Remove of an own price; no page error, no sideways scroll, no clipped text.
  • By hand, in a temporary profile, against a stand-in provider (an OpenAI-shaped endpoint on this machine reporting fixed usage: 1,000 input tokens, 400 of them cached, 200 output), GSM8K with a limit of 1 or 2:
    • Shell: nullius prices on a new profile, a real update (3,946 models), list, set, remove, each refusal (a price below zero, not a number, an output limit of 0, an unknown model, an http address, a missing file, a file that is not a list), NULLIUS_PRICES. nullius standard run priced each call from the list at 0.00024 USD, as computed by hand; with --price gpt-4o-mini=1,10,0.5 at 0.0028; a model whose name only starts like a priced one had no cost; an own price for that name priced it; a malformed --price was refused before anything ran.
    • Docker: the same run with --backend docker, from the list and with --price: the container's relay priced the calls the same.
    • Browser: Settings refused a price below zero in the form and on the server (400), and a request without the form token (403); add, replace and Update (own rows kept) worked, and the shell read back what the page saved. A run started from the builder's Review step (GSM8K, Original, 2 problems) priced its calls from the list, and the Runs page shows each run's cost, with — for the unpriced one.
    • Host: a container with an SSH server as a Settings host, the run started from the builder's Review step on it. The list landed in the run's directory on the host (3,946 models and the own rows) and priced each call at 0.00024 USD, while a list of the host's own, planted with a price of 999, was not used. From a profile with no list, the host received an empty list and the run had no cost.
  • Not verified: a run on a real remote machine; the host was a container on this machine. No real provider was called: the profiles were temporary and hold no key.

Risk

  • A fresh install, and an upgraded one, shows no cost estimate for runs sent directly to OpenAI until the list is updated once; before, the 13 built-in models were priced out of the box.
  • Exact names: a model the list does not name exactly (an alias, a gateway's own name) has no estimate until the user adds a price, where a prefix used to match.
  • The list is a third party's and community-maintained: a wrong price there is a wrong estimate here. Rows stay editable and a cost stays an estimate. If its format or address changes, Update fails with a message and the list held stays.
  • A host run sends the whole list (a few hundred kilobytes, or an empty one) before each part; if that fails the part does not start.

Undone by a revert; prices.json is then ignored.

A standard run's cost estimate came from 13 OpenAI models written in
run.py, matched by name prefix. The list is now the user's: fetched on
request from a public list, with the user's own rows over it, and a
model is priced by exactly its name. A run never fetches a price.
@aitianshi aitianshi mentioned this pull request Nov 10, 2026
1 of 9 tasks
@github-actions

This comment has been minimized.

# Conflicts:
#	app/nullius/standard/run.py
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@aitianshi

Copy link
Copy Markdown
Collaborator Author

/judge

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

A job's config.json credited the public list (its address and day) with
every recorded price, including a row of the user's own and a run's
--price. Each row now carries its source (list, own, run), and the list
is named only when a row is its.

A refused update repeated its address or file in the message.
@github-actions

Copy link
Copy Markdown

🟡 PR judge · 8 of 8 yes · #39 ↔ #38

Merge risk 🔴 MAJOR the judge's reading of the diff; app/nullius/execution.py is in the path of every run
Priority 🔴 EXTREME the deepest path is app/nullius/execution.py
Question Verdict
Changes do what the issue asks · 7 of 7 criteria ✅ yes
Issue and pull request describe the same change ✅ yes
Description matches the diff, no more, no less ✅ yes
Diff stays in scope ✅ yes
Tests cover the change, or Verified says how ✅ yes
Docs follow the change ✅ yes
Risk named in the description ✅ yes
Before and after shown · 2 images · the diff touches what is visible ✅ yes
Acceptance criteria, 7 of 7 met
# Criterion Verdict
1 No price is written in the code: run.PRICES is gone and a new profile has no prices. ✅ met (cut)
2 nullius prices update fills the user's price list from the public list, with its source and date, and Settings has th… ✅ met (cut)
3 A user's own price for a model (nullius prices set, --price on a run) wins over the list, and can be removed. ✅ met (cut)
4 A call is priced by its exact model name or not at all: no prefix match. ✅ met (cut)
5 A run in docker or on a host is priced from the list of the machine that started it. ✅ met (cut)
6 A job's config.json records the prices it was given and where the list came from. ✅ met (cut)
7 The field guide, the Settings guide and the CLI pages say where an estimate's price comes from. ✅ met (cut)
Checks the script runs itself, 4 of 4 pass
No machine attribution trailer in the commits or the description ✓
New code under app/, docs/, .github/ or .claude/ ✓
No Flask import under the backend package ✓
No key-shaped string in the diff ✓

some of the diff was cut before the judge read it

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Model prices are a table in the code: OpenAI only, and fixed between releases

1 participant