research phase 9 w13 multi model picker - sinrimin/harness-nexus GitHub Wiki
Status: research only โ verified at source level 2026-09-16, nothing implemented yet. Verified against:
agentclientprotocol/claude-agent-acpv0.76.0+ main (src/session-model.ts,src/acp-agent.ts),zed-industries/codex-acp0.16.0 (src/thread.rs) +openai/codexrust-v0.137.0codex-rs/models-manager, andsst/opencodemain (packages/opencode/src/acp/{config-option,service}.ts). dsh needs no new work โ W10 already shipped its multi-model list (design-phase-9-w10-llm-providers.mdยง6).
The W9 session-config model selector lists whatever the ADAPTER advertises.
Under a W3 gateway push, everything except our pushed default is the agent's
own built-in catalog (claude SDK model infos / codex remote presets / opencode
models.dev full table) โ model ids the gateway does not serve, i.e. dead
entries in the dropdown. W10's RuntimeConfigSpec.models extras exist in
storage and UI (the collapsible checkbox list) but currently land only in the
dsh writer. Question: per target, can the dropdown show ONLY the models we
configured?
Answer up front: yes, on all four targets, without a single new platform concept โ one writer key (claude-code), one daemon-side option rewrite (codex/opencode), one server hint on the open wire (codex). dsh is done.
@agentclientprotocol/claude-agent-acp builds its model configOption from
the SDK model list restricted by the availableModels settings allowlist
(applyAvailableModelsAllowlist, src/session-model.ts; applied in
acp-agent.ts at session start from settingsManager.getSettings()):
-
~/.claude/settings.jsontop-levelavailableModels: string[]. Documented Claude Code semantics (model-config: "restrict model selection"); the wrapper applies it itself, so it works even where the CLI's own UI never runs. - The
defaultrow always survives the allowlist (docs semantics; it resolves to the effective default โ with ourmodelpush, that IS our model). -
Unknown ids are synthesized verbatim: an allowlist entry that matches no
SDK model becomes
{ value: <raw id>, displayName: <raw id> }and is passed tosetModelas-is โ exactly what gateway model ids (doubao-seed-1.6, โฆ) need. No SDK match required. -
session/set_config_optionvalidates the value against the option list /currentValue/ fuzzy-resolve โ with the allowlist in place the list IS our models, so our values pass by construction. - Resume edge: a session pinned to an out-of-list model reports it as a
verbatim
currentValueentry and re-assertingcurrentValueis explicitly allowed (out-of-picker handling in the set path). - Side effect (intended):
availableModelsalso constrains the machine's terminal/modelpicker โ consistent with a platform-managed route. - Also present but not needed:
CLAUDE_MODEL_CONFIGenv (JSON{modelOverrides, availableModels}) andANTHROPIC_CUSTOM_MODEL_OPTION.
Fix: the W3 claude writer (applyClaudeConfig) already merges
~/.claude/settings.json; add
availableModels: unique([spec.model, ...(spec.models ?? [])]) beside
model. Caveat: synthesized entries carry no SDK capability flags, so the
effort/thought_level option stays hidden for non-claude ids โ same as today,
no regression.
codex-acp 0.16.0 (src/thread.rs):
- Option list =
models_manager.list_models()โ codex-rsmodels-managerpulls a REMOTE catalog (auth-gated endpoint,models_cache.jsoncache, TTL 300s), filtered by auth mode +show_in_picker. The current model gets a verbatim entry appended when no preset matches it (that is the one usable row users see today). -
config.tomlcan only override model METADATA (model_context_window,model_supports_reasoning_summaries, โฆ โwith_config_overrides); there is no allowlist setting. The picker list is not configurable. -
session/set_config_option {configId:'model'}does NOT validate the value against the list:handle_set_config_modellooks the id up in the presets and, on miss, uses the RAW STRING as the model (effort keeps the current config for raw strings). Arbitrary gateway ids are accepted.
Fix: rewrite the model option's list on OUR wire only (see ยง2) โ sets
still flow through unchanged as raw ids.
opencode main (packages/opencode/src/acp/):
-
configOptions= every provider in the snapshot ร its models (Object.values(snapshot.providers)), valueproviderID/modelID(optional/variant); the models.dev built-ins are all in there โ the huge noise. -
set_config_optionIS validated:parseSelectedModelrequires the(providerID, modelID)pair to exist in the registry, elseACPError.InvalidModelError. But our W12 writer'sprovider['harness-nexus'].modelsmap (default + extras) is part of the registry โ everyharness-nexus/*value is selectable and valid.
Fix: daemon-side filter keeping only harness-nexus/* entries in the
model option. Self-contained โ no server knowledge needed, and W10 extras
are already persisted into the models map by the W12 writer.
W10: the dsh writer emits unique([model, ...models]) into the provider's
models: list; dsh's session model picker reads that live catalog natively.
Nothing to do.
Three converging moves on existing W9/W10 surfaces โ zero schema, storage, or web changes (the selectors already render whatever options arrive):
-
claude writer (
packages/cli/src/daemon/runtime-config.ts,applyClaudeConfig): writeavailableModelsnext tomodelin the settings.json merge. Always write it when a spec exists (even a single model โ the built-ins are dead entries under a gateway); a baseUrl-less spec keeps the same semantics. -
daemon option rewrite (
packages/cli/src/daemon/chat.ts): both arms that admit adapter options โ the establishment capture (takeSessionConfig) and theconfig_option_updatepush (takeConfigOptionscall site) โ pass the result through a per-targetrewriteModelOptions(options, target, modelSet?):-
opencode: keep onlyharness-nexus/-prefixed values in themodeloption (keepcurrentValueverbatim if it is out-of-list, mirroring the adapters' own out-of-picker semantics; if the filter empties the list, leave the option untouched โ a hand-managed opencode install). -
codex: keep only the server-provided set (ยง2.3) pluscurrentValue. -
claude-code/deepseek: untouched (writer / native already converge). - Rewriting the PUSH path too is load-bearing: the adapter re-emits the
full option list after every set, which would reintroduce the noise
mid-session otherwise. The rewrite only shapes OUR wire; adapter state
is untouched, and
chat:config.setpasses through unchanged (values already valid per ยง1).
-
-
codex model-set hint (server โ daemon): the codex writer does not
persist extras anywhere (single root
model), so the daemon cannot discover them from config files.ChatService.openalready knowsagentInstance โ (machineId, target); look up the storedRuntimeConfigand attach an optionalmodelOptions: string[](unique([model, ...models])) to thechat:session.openpayload (shared/realtime.tsschema, optional field โ pre-W13 daemons ignore it). Rejected alternative: daemon reads backconfig.tomlโ extras are not in it.
Web: nothing. The extras picker already exists (W10 checkbox list on MachineDetail); the session selector renders the filtered list.
-
availableModelsconstrains the machine's local terminal picker too โ the documented Claude Code semantics; intended for a platform-managed machine, but worth a note in the W13 design (an operator hand-running claude on the machine loses the built-in rows while a spec is applied). - Synthesized claude entries carry no capability metadata โ effort selector hidden for non-claude ids (status quo for gateway models; no regression).
- The model ids themselves must be servable by the gateway โ unchanged W10
semantics: extras come from the endpoint's own
/modelsfetch (server-side discovery), the user picks them. - opencode filter depends on our provider id constant
harness-nexus(same id the W12 writer uses โ single source it in shared if W13 proceeds). - Adapter versions drift: codex-acp's "raw id accepted" and opencode's registry validation are current-source behavior; both were checked at the versions we actually run (0.16.0 / opencode 1.18.31-era main).
- claude writer + tests: ~0.5 day
- daemon rewrite (both arms, per-target, + tests): ~1 day
- server open-hint + shared schema + tests: ~0.5 day