Skip to content

admin model editor: price edits and added models are silently reverted at the next startup #440

Open

Description

@argszero

What happens

The admin model editor writes prices into the models table by UPDATE. At every startup db::seed_models (src/main.rs:521) re-asserts config over that same table: an unconditional ON CONFLICT(provider, model) DO UPDATE SET over every price column, followed by DELETE FROM models for every row whose (provider, model) config does not contain (src/db.rs:654-737).

So an operator's edit is real, bills correctly, and then stops existing at the next restart — with no error, no log line, and nothing reverted back. The row simply returns to the config value.

Measured on db0160f

Temp data dir and temp config (copy of config/config.example.toml), real binary, admin API only.

Price edit — PATCH /api/admin/models/9 {"input_per_m": 1.23}:

Read input_per_m
before the edit 5.0
after the edit, same process 1.23
after SIGTERM + restart on the same data dir 5.0

Model added through the same editor — POST /api/admin/models with probe/probe-model-x:

Read rows for that model (API / db)
after the add 1 / 1
after restart 0 / 0

Who pays, and how much

Settlement reads that row: dao::get_model_price selects the price columns FROM models (src/dao.rs:205-210), and both settle sites take the cache-write price from the same record. So the row is the price the bill uses — which makes the restart a quiet price change:

  • An operator who lowers a price to follow a vendor cut bills at the lower figure until the next restart, then silently bills at the old, higher config price from that point on.
  • One who raises a price to cover a vendor increase bills at the higher figure only until the restart, then undercharges.
  • Either way the stated figure is the one the operator believes is in force, and the actual figure is the config's.
  • Stored rows are never rewritten. usage_records and transactions keep whatever they were settled at; the change applies only forward from the restart. There is no reconciliation and no marker saying the price moved.

A model added through the editor has a sharper version of the same problem: it is deleted at the next startup, so keys still pointing at it can no longer be priced and calls are refused before reaching upstream (gateway.rs pricing gate, the invariant db.rs:699-705 records).

Is the operator told?

Not in anything an operator reads. The admin panel's own subtitle says the opposite of the truth — ui/js/i18n.js:506, admin.models.sub: "add, delete, edit take effect immediately", with no mention of a restart. config/config.example.toml says models are defined there and upserted at startup, which hints that config is authoritative but never says an edit made in the editor is reverted. The one place that states it plainly is a source comment, src/routes/admin_models.rs:342.

That is the case this issue is about: the behaviour is deliberate in the code, documented for the reader of the source, and invisible to the person the editor is for.

Two ways out, and which one is not mine to pick

  1. Make the write honest — either refuse a price edit while config is authoritative, or accept it and record that it will be reverted (an expiry marker, or text beside the field). Cheap, and it keeps config as the single source of truth, which is the design the code was built around.
  2. Make config yield — stop re-asserting prices over rows that already exist, so the editor is durable and config only seeds an empty row. This changes who owns pricing, which is why it needs a decision rather than a patch.

I lean to the first, but the second is a real choice about where pricing authority lives, so it is the owner's call, not mine.

Boundaries

No release, no tag, no change to #437. Nothing here proposes adding cache-write prices to the shipped catalogue or altering settlement arithmetic.

Activity

  1. argszero commented on Oct 26, 2026

    @argszero
    OwnerAuthor

    I'd like to work on this, and I'll take the visibility half of it.

    What I intend to land, in one PR:

    1. The admin models panel stops promising something the server undoes. admin.models.sub currently says edits take effect immediately and says nothing about a restart; it will say plainly that config.toml's [[models]] is the authoritative source, so a price changed in the editor or a model added there is overwritten at the next startup.
    2. The startup seed stops being silent. When db::seed_models overwrites a stored price with the config value, or deletes a row config does not contain, it will log a warning naming the model and the old -> new figures, so whoever reads the startup log can see what was reverted.

    What I will not decide here: which source wins. Option 1 and option 2 below are still open, and picking one changes who owns pricing, so that stays for the owner. This PR deliberately does not answer it, and the issue stays open after the merge with that question intact.

    No arithmetic changes, no stored row rewritten, no bill moves: the change is that the operator is told the truth in the panel and the log records each override with its figures.

  2. argszero commented on Oct 26, 2026

    @argszero
    OwnerAuthor

    The visibility half is merged as a6588f8 (PR #441), and it was checked on a real binary with a temp data dir rather than argued from the code:

    • After the operator's PATCH {"input_per_m": 1.23} and a restart, the row reads back as the config value and the restart's own startup log now says why: a warning naming anthropic/claude-opus-5 and input_per_m: 1.23 → 5.0 — the operator's figure first, the config figure second.
    • A model added through the editor is still deleted at the next startup, and that deletion is now also a named warning instead of silence.
    • The panel's subtitle no longer promises that an edit survives: it says config.toml's [[models]] is the authoritative source, that an edited price and an added model are overwritten at the next startup, and that an added model's row is deleted. Both language packs carry the new text.
    • A startup over an untouched database logs 0 rows of prices overwritten, 0 config-removed rows deleted and emits no warning, so the warning stays a signal.

    This issue stays open for the decision it names. Option 1 (make the write honest) is what landed: the server keeps its current precedence and simply stops hiding it. Option 2 (stop re-asserting prices over rows that already exist, so the editor is durable and config only seeds an empty row) is untouched — it changes who owns pricing, which is the owner's call. If it is chosen, the copy and the warnings added here are removed or inverted rather than kept, and the row-restore behaviour becomes the defect instead.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions