Skip to content

feat(admin): accept the cache-write price in the model editor - #434

Merged
argszero merged 1 commit into
mainfrom
feat/admin-cache-write-price
Oct 24, 2026
Merged

argszero merged 1 commit into
mainfrom
feat/admin-cache-write-price

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

#433 added a fifth model price — models.cache_write_input_per_m — and made billing charge cache-write (cache-creation) tokens against it. The admin model editor was left out of that change: it never wrote the column, so a model created or edited through POST /api/admin/models or PATCH /api/admin/models/:id always carried cache_write_input_per_m = 0, and an operator who bills cache writes had no supported way to set that price through the API.

This closes that gap. The field now behaves exactly like its sibling prices, and nothing beyond that is changed.

Related Issue

None open. #432 was already closed by #433, and this PR closes the deliberate boundary #433 left in its own scope ("the admin model editor does not accept cache_write_input_per_m"), so there is no issue to close here.

Changes

  • POST /api/admin/models and PATCH /api/admin/models/:id accept cache_write_input_per_m — optional, default 0.
  • validate_common rejects a negative value with the same 400 the other six prices get. The admin endpoint keeps its own request struct on purpose, so Config::validate is not reused — importing it would drag config-only fields into an API body.
  • Both readback paths return it: the single-row ROW_SELECT shared by the create and patch responses, and dao::list_all_models behind GET /api/admin/models.
  • The module header's field list names it.
  • The column sits immediately after cache_hit_input_per_m in every column list, matching the ordering feat(billing): bill cache-write (cache-creation) tokens #433 established in db.rs and dao::ModelPrice.

Tests

  • cargo test — 583 passed, 0 failed (baseline 582, plus the one new test).
  • cargo fmt --check — clean.
  • cargo clippy --all-targets -- -D warnings — clean (this is the CI invocation).
  • New test admin_models_cache_write_price_round_trip: create with a value and read it back through both paths; create without the field and get 0; PATCH sets it and the response reflects it while the sibling prices stay put; a negative value on either entry point is a 400 and the rejected write never reaches the database.

The test gives all seven prices a distinct value, so a SELECT whose column order drifts from its r.get(i) indices shows up as the wrong value instead of passing quietly. Checked by mutation: pointing the readback at the neighbouring column yields left: 1.5, right: 0.25 and fails the test.

Deliberate boundaries

  • The default stays 0 and does not fall back to input_per_m — a fallback would silently re-price every deployment that never opted in.
  • seed_models still treats config.toml [[models]] as the source of truth, so an admin-side edit of this price, like every other admin-side edit, is overwritten at the next startup. That is documented behaviour, not a regression introduced here.
  • The model form in the UI does not expose the field yet; this change is the API surface only.
  • Tracked separately and deliberately not touched here: streaming Anthropic responses undercount uncached input, because UsageCapture::finish applies the OpenAI-shaped input_tokens - cached_tokens correction to a protocol whose input_tokens already excludes cache reads. It is pinned by a test today and will land as its own PR, since correcting it raises existing bills.

Checklist

  • Branch name follows the convention (feat/)
  • Commit message uses Conventional Commits
  • Single responsibility, minimal diff

#433 added `models.cache_write_input_per_m` and made billing charge it, but
the admin model editor never wrote the column: a model created or edited at
`POST /api/admin/models` (or `PATCH /api/admin/models/:id`) always carried
`cache_write_input_per_m = 0`, so an operator billing cache-write tokens had
no supported way to set the fifth price through the API.

The field now behaves exactly like its sibling prices: present in the create
and patch request bodies (default 0, which does not fall back to
`input_per_m`), rejected when negative by the same `validate_common` check
that guards the other six, written by both the INSERT and the UPDATE, and
returned by both readback paths — the single-row SELECT shared by create and
patch, and `dao::list_all_models` behind `GET /api/admin/models`.

The test creates a model with seven distinct prices so any divergence between
a SELECT's column order and its `r.get(i)` indices reddens on the wrong value
rather than passing silently.
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.

1 participant