Applies the profiles-page lesson before relearning it: the admin surface
must report what dispatch actually does, using the same functions dispatch
uses, never a second guess that can drift. GET /admin/api/classifier-config
calls routing.cheapest_classifier_candidate directly against the live
catalog for cloud_primary_auto's resolved_primary -- the same function
classify() calls -- so the card never shows a static "auto" label while the
real resolution is doing something else.
admin.py's _persist_to gains a sibling, _persist_many_to, generalizing its
existing load/validate/backup/write sequence from one mutation to a list
applied together before the single validate-then-write pass. Needed because
classifier.mode: cloud_llm and its companion (cloud_primary or
cloud_primary_auto) are only valid TOGETHER -- writing them as two separate
_persist_to calls would validate, and could fail, after the first one
alone, since RouterConfig's own cross-field validators reject exactly that
intermediate state. _persist_to becomes a one-line wrapper; every existing
caller (profile CRUD, the persisted-config-key endpoint) is unchanged.
A dedicated "Classifier" card in controls.html, not a row in the generic
Runtime Knobs list -- the same reasoning that gave gaming mode its own card
elsewhere: this config has cross-field structure a flat scalar/boolean
input can't safely represent.
Two real bugs found and fixed during a browser smoke test against a
throwaway 8081 instance (8080 is the live router), not caught by the
initial test suite:
- Toggling the "auto" checkbox called the same full-regenerate handler the
mode <select> uses, which rebuilds the fields from the ORIGINAL loaded
config and so discarded the checkbox state (and anything already typed
into the pinned fields) the instant it was ticked. Split into
onClassifierModeChange (full regenerate, for the mode switch) and
onClassifierCloudAutoToggle (visibility only, for the checkbox).
- A blank base_url/model was sent as {"base_url": "", "model": ""}, a
truthy object, so the backend's `if not body.cloud_primary` check missed
it and silently saved an unusable config. Fixed on both sides: the
frontend now sends null when either field is blank, and the backend
independently checks both fields are non-empty rather than trusting
presence alone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRQXz5SYZYVWscxS1QqF6U
10 KiB
10 KiB