feat(settings): edit omp model configuration from Settings → Models #17

Merged
lucas merged 2 commits from implement-full-model-configuration-parity-with-omp-sdk into main 2026-10-16 11:57:06 +02:00
Collaborator

This adds a Settings → Models section. It edits omp's own model configuration in the agent dir (~/.omp/agent by default), in the same files the omp CLI reads.

Model settings live in config.yml:

  • every setting on the SDK's Model tab except the TUI-only thinking-display toggles
  • thinkingBudgets.*, modelRoles, modelTags, cycleOrder
  • enabledModels, enabledProviders/disabledProviders, modelProviderOrder
  • compaction.modelThresholds, edit.modelVariants, task.agentModelOverrides

Each setting has a structured editor, with no raw JSON or YAML anywhere: a Roles table, reorderable lists, model selector fields, and row editors for fallback chains, subagent models, compaction thresholds and edit variants. Edits collect in one Save bar that writes config.yml as a single validated batch.

Per-model overrides live in models.yml under providers.<p>.modelOverrides.<id>. You pick a provider and a model, then set context window, max tokens, extended thinking, input types, pricing, the compat flags and compat.reasoningEffortMap. The file is edited as a YAML document, so comments elsewhere are kept. The SDK's ModelsConfigFile loader validates it before an atomic replace, and the model catalog reloads afterwards.

Deck sessions start with the model settings from config.yml, with path-scoped lists resolved for each session's own cwd. Saved changes are pushed into sessions that are already running.

Decisions

  • The T-48 spec was out of date, so this follows SDK 18.10.1.
    • There is no ~/.pi/agent/settings.json or models.json; the real files are config.yml and models.yml.
    • defaultProvider and showCacheMissNotices don't exist and were left out.
    • thinkingLevelMap became compat.reasoningEffortMap.
    • cost.tiers isn't in the override schema and was left out.
    • Per-model fields sit under providers.<p>.modelOverrides.<id>, not a top-level .models[].
  • Exposed settings = the SDK registry's model tab plus the model keys the SDK has no panel for:
    • thinkingBudgets.*, modelRoles, modelTags, cycleOrder, enabledModels, enabledProviders, disabledProviders, modelProviderOrder
    • compaction.modelThresholds, edit.modelVariants, task.agentModelOverrides
    • Removed entirely (not listed, not writable, not seeded): hideThinkingBlock, expandThinkingBlocks and proseOnlyThinking. Only the TUI's interactive mode reads them.
    • Not added: the memory models (sharpshooter.model, mnemopi.*).
  • modelPresets is not editable. The Roles card says to manage presets with /modelpreset in the omp CLI and shows how many exist. Writes reject the key, so an unrelated save leaves the presets block untouched.
  • modelTags is per-role display metadata (name, theme color, hidden), not tags on models. It is edited together with modelRoles in one Roles table, which also accepts custom roles. cycleOrder is a separate reorderable list of role ids.
  • Model selector field:
    • a text input with catalog suggestions that accepts any pattern, glob or role alias
    • a separate thinking-level select that builds the :level suffix (max/auto are only split off when the whole value isn't a catalog id)
    • a pattern badge for values outside the catalog
    • Role suggestions are the authenticated models the role accepts, the same pool the SDK's role picker uses. Unsaved custom roles use the default role's list; a role with no candidates falls back to the full catalog.
  • Path-scoped lists (enabledModels, enabledProviders, disabledProviders) are edited as a Global list plus one list per registered deck workspace, and written as { path: <workspace>, models|providers: [...] }.
    • Entries scoped to any other folder are kept untouched and are not shown.
    • Plain entries that were interleaved with scoped ones move to the front on save.
    • These rows have no reset button, because removing the key would also drop the hidden entries.
    • This also fixes a data-loss bug in the first commit. Values were resolved against the agent dir, and saving wrote the flattened list back, which deleted folder-scoped entries. The server now reads and writes the raw global value.
  • Saving:
    • One page-level Save bar for config.yml edits, sent as a single PATCH. It shows the unsaved-change count and the note that saving rewrites config.yml without its comments, and offers Discard all. On failure the error is shown and the drafts are kept.
    • The per-model override form keeps its own Save.
    • While edits are unsaved, leaving the page asks first: useBlocker covers in-app navigation, including switching Settings sections, and beforeunload covers closing the tab.
  • Shape checks before writing for the structured records and lists the SDK only types as record/array: role selectors, tag fields and theme colors, non-empty fallback chains, known edit modes, subagent model lists, and registered workspace keys. The SDK's own validation (for example compaction thresholds) still runs afterwards.
  • Incomplete rows stay on screen, flagged and unsaved. This covers a chain with no fallback yet, a duplicate key, or a threshold out of range. In a number field, text that isn't a number is kept as typed and flagged; Save then reports the server's rejection instead of quietly saving an older value.
  • How deck sessions see the settings:
    • They stay on Settings.isolated, seeded with every model setting that config.yml sets explicitly (raw values; a value the SDK would reject is skipped with a warning), then re-scoped with reloadForCwd(cwd).
    • Running sessions are updated through Setting.override/clearOverride, using a new optional AgentBridge.applyModelSettings.
    • The rest of config.yml (tools, memory, approvals) still doesn't reach deck sessions. Repo-level omp config layers are ignored too; that is tracked as a deck inbox follow-up.
    • Visible effect: modelRoles.default from config.yml now picks the model a deck session uses when none is chosen.
    • If config.yml can't be read, sessions still start without the seeded values, and a warning is logged.
  • Settings are read with a deck-owned SDK instance, Settings.loadIsolated({ agentDir, cwd: agentDir }), not the process-global Settings.init(). The global one would also apply process-wide side effects (theme, request limits) from the user's config.
  • API shape:
    • GET /api/settings/models also returns roles, registered workspaces, bundled agent names, the model catalog and the preset count.
    • Model ids can contain /, so PUT /api/settings/model-overrides takes provider and modelId in the body rather than in the path.
  • Override form behaviour (unchanged in the second commit): keys the form doesn't edit round-trip unchanged, and overrides for models the catalog no longer lists can still be opened, edited and removed.

Verification

  • bun run typecheck (protocol, server, web): pass.
  • bun test in apps/server: 735 pass, 0 fail. model-config.test.ts now has 14 tests. Added in the second commit:
    • path-scoped split/merge round trip: global plus workspace lists, a foreign pathPrefix entry kept in place, a new workspace appended
    • emptying a path-scoped list removes the key
    • an unregistered workspace is rejected and nothing is written
    • deck sessions resolve enabledModels for their cwd: inside a workspace subfolder vs outside it, with todo.reminders: false kept. This also confirms isolated overrides accept object entries.
    • hideThinkingBlock and modelPresets are refused and not listed; task.agentModelOverrides is listed
    • a modelPresets block survives an unrelated write byte-for-byte and is counted
    • shape checks: invalid tag color, unknown tag field, unknown edit mode, empty fallback chain, empty subagent list, empty role selector; valid tags, variants and subagent lists are written
  • Kept from the first commit: set/read/unset, an all-or-nothing batch, out-of-domain keys refused, the session seed holds only configured keys, and the four models.yml override tests.
  • bun run --filter @omp-deck/web build: pass.
  • HTTP smoke, first commit, against a scratch agent dir through the real routes:
    • The model-settings PATCH returned 400 for invalid values, and a valid batch left unrelated keys alone.
    • Override PUT/remove round-tripped through models.yml with comments kept.
  • Bridge smoke, first commit: a session started with the configured values, and applyModelSettings updated a live session.
  • HTTP smoke, second commit: only partly run, then skipped at the reviewer's request. A scratch GET /api/settings/models, after registering one workspace, returned 15 roles starting default, smol, slow, vision; 5697 catalog entries; agents scout, reviewer, security-reviewer, task, sonic; presetCount 0; the workspace; and entries starting with the path-scoped lists and no hideThinkingBlock. The PATCH-writes-config.yml check and the manual browser check were not run. The new UI is covered only by typecheck and the production build.
This adds a **Settings → Models** section. It edits omp's own model configuration in the agent dir (`~/.omp/agent` by default), in the same files the omp CLI reads. **Model settings** live in `config.yml`: - every setting on the SDK's Model tab except the TUI-only thinking-display toggles - `thinkingBudgets.*`, `modelRoles`, `modelTags`, `cycleOrder` - `enabledModels`, `enabledProviders`/`disabledProviders`, `modelProviderOrder` - `compaction.modelThresholds`, `edit.modelVariants`, `task.agentModelOverrides` Each setting has a structured editor, with no raw JSON or YAML anywhere: a Roles table, reorderable lists, model selector fields, and row editors for fallback chains, subagent models, compaction thresholds and edit variants. Edits collect in one Save bar that writes `config.yml` as a single validated batch. **Per-model overrides** live in `models.yml` under `providers.<p>.modelOverrides.<id>`. You pick a provider and a model, then set context window, max tokens, extended thinking, input types, pricing, the `compat` flags and `compat.reasoningEffortMap`. The file is edited as a YAML document, so comments elsewhere are kept. The SDK's `ModelsConfigFile` loader validates it before an atomic replace, and the model catalog reloads afterwards. Deck sessions start with the model settings from `config.yml`, with path-scoped lists resolved for each session's own cwd. Saved changes are pushed into sessions that are already running. ## Decisions - **The T-48 spec was out of date, so this follows SDK 18.10.1.** - There is no `~/.pi/agent/settings.json` or `models.json`; the real files are `config.yml` and `models.yml`. - `defaultProvider` and `showCacheMissNotices` don't exist and were left out. - `thinkingLevelMap` became `compat.reasoningEffortMap`. - `cost.tiers` isn't in the override schema and was left out. - Per-model fields sit under `providers.<p>.modelOverrides.<id>`, not a top-level `.models[]`. - **Exposed settings** = the SDK registry's `model` tab plus the model keys the SDK has no panel for: - `thinkingBudgets.*`, `modelRoles`, `modelTags`, `cycleOrder`, `enabledModels`, `enabledProviders`, `disabledProviders`, `modelProviderOrder` - `compaction.modelThresholds`, `edit.modelVariants`, `task.agentModelOverrides` - Removed entirely (not listed, not writable, not seeded): `hideThinkingBlock`, `expandThinkingBlocks` and `proseOnlyThinking`. Only the TUI's interactive mode reads them. - Not added: the memory models (`sharpshooter.model`, `mnemopi.*`). - **`modelPresets` is not editable.** The Roles card says to manage presets with `/modelpreset` in the omp CLI and shows how many exist. Writes reject the key, so an unrelated save leaves the presets block untouched. - **`modelTags` is per-role display metadata** (name, theme color, hidden), not tags on models. It is edited together with `modelRoles` in one Roles table, which also accepts custom roles. `cycleOrder` is a separate reorderable list of role ids. - **Model selector field:** - a text input with catalog suggestions that accepts any pattern, glob or role alias - a separate thinking-level select that builds the `:level` suffix (`max`/`auto` are only split off when the whole value isn't a catalog id) - a `pattern` badge for values outside the catalog - Role suggestions are the authenticated models the role accepts, the same pool the SDK's role picker uses. Unsaved custom roles use the default role's list; a role with no candidates falls back to the full catalog. - **Path-scoped lists** (`enabledModels`, `enabledProviders`, `disabledProviders`) are edited as a Global list plus one list per registered deck workspace, and written as `{ path: <workspace>, models|providers: [...] }`. - Entries scoped to any other folder are kept untouched and are not shown. - Plain entries that were interleaved with scoped ones move to the front on save. - These rows have no reset button, because removing the key would also drop the hidden entries. - This also fixes a data-loss bug in the first commit. Values were resolved against the agent dir, and saving wrote the flattened list back, which deleted folder-scoped entries. The server now reads and writes the raw global value. - **Saving:** - One page-level Save bar for `config.yml` edits, sent as a single PATCH. It shows the unsaved-change count and the note that saving rewrites `config.yml` without its comments, and offers Discard all. On failure the error is shown and the drafts are kept. - The per-model override form keeps its own Save. - While edits are unsaved, leaving the page asks first: `useBlocker` covers in-app navigation, including switching Settings sections, and `beforeunload` covers closing the tab. - **Shape checks before writing** for the structured records and lists the SDK only types as `record`/`array`: role selectors, tag fields and theme colors, non-empty fallback chains, known edit modes, subagent model lists, and registered workspace keys. The SDK's own validation (for example compaction thresholds) still runs afterwards. - **Incomplete rows stay on screen, flagged and unsaved.** This covers a chain with no fallback yet, a duplicate key, or a threshold out of range. In a number field, text that isn't a number is kept as typed and flagged; Save then reports the server's rejection instead of quietly saving an older value. - **How deck sessions see the settings:** - They stay on `Settings.isolated`, seeded with every model setting that `config.yml` sets explicitly (raw values; a value the SDK would reject is skipped with a warning), then re-scoped with `reloadForCwd(cwd)`. - Running sessions are updated through `Setting.override`/`clearOverride`, using a new optional `AgentBridge.applyModelSettings`. - The rest of `config.yml` (tools, memory, approvals) still doesn't reach deck sessions. Repo-level omp config layers are ignored too; that is tracked as a deck inbox follow-up. - Visible effect: `modelRoles.default` from `config.yml` now picks the model a deck session uses when none is chosen. - If `config.yml` can't be read, sessions still start without the seeded values, and a warning is logged. - **Settings are read with a deck-owned SDK instance,** `Settings.loadIsolated({ agentDir, cwd: agentDir })`, not the process-global `Settings.init()`. The global one would also apply process-wide side effects (theme, request limits) from the user's config. - **API shape:** - `GET /api/settings/models` also returns roles, registered workspaces, bundled agent names, the model catalog and the preset count. - Model ids can contain `/`, so `PUT /api/settings/model-overrides` takes `provider` and `modelId` in the body rather than in the path. - **Override form behaviour (unchanged in the second commit):** keys the form doesn't edit round-trip unchanged, and overrides for models the catalog no longer lists can still be opened, edited and removed. ## Verification - `bun run typecheck` (protocol, server, web): pass. - `bun test` in `apps/server`: 735 pass, 0 fail. `model-config.test.ts` now has 14 tests. Added in the second commit: - path-scoped split/merge round trip: global plus workspace lists, a foreign `pathPrefix` entry kept in place, a new workspace appended - emptying a path-scoped list removes the key - an unregistered workspace is rejected and nothing is written - deck sessions resolve `enabledModels` for their cwd: inside a workspace subfolder vs outside it, with `todo.reminders: false` kept. This also confirms isolated overrides accept object entries. - `hideThinkingBlock` and `modelPresets` are refused and not listed; `task.agentModelOverrides` is listed - a `modelPresets` block survives an unrelated write byte-for-byte and is counted - shape checks: invalid tag color, unknown tag field, unknown edit mode, empty fallback chain, empty subagent list, empty role selector; valid tags, variants and subagent lists are written - Kept from the first commit: set/read/unset, an all-or-nothing batch, out-of-domain keys refused, the session seed holds only configured keys, and the four `models.yml` override tests. - `bun run --filter @omp-deck/web build`: pass. - HTTP smoke, first commit, against a scratch agent dir through the real routes: - The model-settings PATCH returned 400 for invalid values, and a valid batch left unrelated keys alone. - Override PUT/remove round-tripped through `models.yml` with comments kept. - Bridge smoke, first commit: a session started with the configured values, and `applyModelSettings` updated a live session. - HTTP smoke, second commit: only partly run, then skipped at the reviewer's request. A scratch `GET /api/settings/models`, after registering one workspace, returned 15 roles starting `default, smol, slow, vision`; 5697 catalog entries; agents `scout, reviewer, security-reviewer, task, sonic`; `presetCount` 0; the workspace; and entries starting with the path-scoped lists and no `hideThinkingBlock`. The PATCH-writes-`config.yml` check and the manual browser check were not run. The new UI is covered only by typecheck and the production build.
Add a Models settings section backed by the omp SDK's own files in the
agent dir, shared with the omp CLI:

- config.yml: every setting on the SDK's Model tab plus thinkingBudgets.*,
  modelRoles, enabledModels, enabled/disabledProviders,
  modelProviderOrder, cycleOrder, modelPresets and modelTags, read and
  written through a persisted SDK Settings instance (validated before
  anything is written).
- models.yml: per-model providers.<p>.modelOverrides.<id> (limits,
  capabilities, pricing, compat flags, reasoningEffortMap), edited as a
  YAML document and validated with the SDK's ModelsConfigFile loader
  before an atomic replace; the model catalog reloads afterwards.

Deck sessions stay isolated from the rest of config.yml but are now
seeded with the configured model settings, and saved changes are pushed
into live sessions.

Co-Authored-By: Claude Opus 5.5 via omp-deck
Replace the JSON text areas in Settings → Models with a dedicated editor
for every list and map setting (Roles table, reorderable lists, model
selector fields, fallback-chain/subagent/threshold/variant rows) and a
page-level Save bar that writes config.yml in one batch.

Path-scoped lists are now read and written raw: a global list plus one
list per registered workspace, with entries scoped elsewhere preserved.
Deck sessions resolve them for their own cwd. Previously they were
flattened against the agent dir, and saving dropped folder entries.

TUI-only thinking display toggles and modelPresets are no longer
exposed; compaction.modelThresholds, edit.modelVariants and
task.agentModelOverrides are added.

Co-Authored-By: Claude Opus 5.5 via omp-deck
lucas merged commit 20133b7a54 into main 2026-10-16 11:57:06 +02:00
lucas deleted branch implement-full-model-configuration-parity-with-omp-sdk 2026-10-16 11:57:06 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
lucas/omp-deck!17
No description provided.