feat(admin): profile visibility, default selection, and CRUD #25
Reference in New Issue
Block a user
Delete Branch "feat/admin-profile-management"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Named routing profiles shipped in #24 with no portal surface — they existed only as code defaults and an
auto:<name>string a client had to know to send. This gives them visibility, a default selector, and full CRUD.Part A — see what a profile actually admits
A profile's definition is not the useful fact; what it currently admits is, and the two diverge invisibly. On the live catalog:
bigboybritches(min_tier=3) matches 7 models but only 4 are reachable interactively, because three are-flexrows the defaultlatency_tolerancefilters out.localitymatches 1, and only for two categories, because per-modeleligible_categoriesANDs on top.GET /admin/api/profilesand a new Profiles page report each profile's definition, itsadmitted_count, itsinteractive_count, and azero_admitflag.Admission is computed by calling
routing.select_candidates, never by reimplementing the predicate —metrics.context_ceilingsis the precedent.test_implementation_uses_routing_helperspins that architecturally rather than leaving it to review.A profile admitting zero models is flagged. That is the shape of the 422 that cost ~19 hours in incident #3, and a profile can reach it silently through admin deprecations — which is exactly what happened to the vision-capable set on 2026-09-04.
Part B — operator-selectable default
routing.default_profilejoins the existing_CONFIG_ALLOWLISTalongsiderouting.default_flex_preference. Bareauto— whatopencode.jsonand every existing client already sends — now resolves to the operator's choice without touching client config.The control is a select populated from the live profile list, never free text: a typo there would silently redirect all default traffic. An unknown name fails at config load, consistent with
auto:nonsensereturning 422.auto:<name>on a request still overrides the default.Part C — create, edit and delete profiles
Full CRUD through the existing validated-write path only: comment-preserving ruamel round-trip,
RouterConfigvalidation before any byte reaches disk, backup first.routing.default_profileis refused — otherwise Part B's fail-at-load becomes an outage on next start. Editing it is allowed, since changing fields does not break the reference.allowed_model_idsis a catalog-populated multi-select, not free text.Verification
local_energy.enabledboth true and false.test_profile_create_preserves_comments_and_user_local_energy— this branch writesconfig/config.yamlprogrammatically, so preserving unrelated operator lines is asserted explicitly rather than assumed.🤖 Generated with Claude Code
https://claude.ai/code/session_01VRQXz5SYZYVWscxS1QqF6U
Generalize _persist_config_value into _persist_config_block: nested writes create intermediate CommentedMap blocks, delete mode prunes the leaf and an empty parent, and the whole-config RouterConfig validation, backup-first, .tmp+os.replace atomic swap discipline is preserved unchanged under the same _config_write_lock. _persist_config_value remains as a thin wrapper for scalar paths. Add POST /admin/api/profiles/ (create, 403 on builtin, 409 on existing), POST /admin/api/profiles/{name} (full replace, 404 unknown, 403 builtin; the current routing.default_profile may be edited), and DELETE /admin/api/profiles/{name} (404 unknown, 422 naming routing.default_profile when it targets the profile, no dangling empty profiles: block). Both save endpoints probe the CANDIDATE profile through the shared _profile_probe (extracted from GET) before writing and return zero_admit plus a warning when the profile admits zero models under the current catalog; every response notes a restart is required. GET /admin/api/profiles now enumerates config profiles from the persisted store so CRUD writes are visible without a reload. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>