Skip to content

feat(custom-model): mark the model a session is using in the endpoint model picker - #757

Merged
Ark0N merged 17 commits into
Ark0N:masterfrom
opticon454:feature/custom-endpoint-in-use
Nov 4, 2026
Merged

Ark0N merged 17 commits into
Ark0N:masterfrom
opticon454:feature/custom-endpoint-in-use

Conversation

@opticon454

Copy link
Copy Markdown
Contributor

Context

The model picker for a custom endpoint marks the model the endpoint reports as loaded (llama-swap) and the last one used. For an endpoint that cannot report which model it has loaded, nothing is marked, which reads like the question was ignored.

Split out of #752 at the maintainer's request (one change per PR). Stacked on #752: until that merges, this PR's diff also shows its commits; the change that is new here is the three commits at the end.

What changed

  • The picker marks the model a running session on that endpoint is using ("In use by a session"), found from the open sessions' saved custom-model selection.
  • When nothing is marked because the endpoint cannot report a loaded model and no session uses one, the picker says so ("Nothing is marked: ...") instead of leaving an unexplained alphabetical list. Strings are in the zh-CN table.
  • A remembered "last used" id that is no longer in the endpoint's model list no longer counts as a mark, so the "Nothing is marked" line shows when it should.
  • Docs section in docs/custom-model-endpoints.md and a changeset (mode 100644 like the others).

Verification

  • typecheck, lint and format:check are clean.
  • Full npm test: 666 files passed, 13,539 tests passed, 12 skipped, 0 failed.
  • Not tried in a real browser.

…d add model picker filter

- opencode: write limit.context/output from the discovered context length (Ark0N#749)
- codex: override the native launch model when a custom endpoint is selected (Ark0N#750), and set model_context_window
- model picker: add a search/filter box with a no-matches line
… injection

- share one context-length resolution (discovered figure, endpoint default fallback) across the routes and boot recovery, with a recovery test
- record listed per-slot context figures with a slot count of 1 and force the slot-count fallback once per rediscovery
- open up the codex model launch token for qwen3-style ids (model-plain-colon), pinned by a spawn-golden case
- pass the full discovered window to opencode as limit.context
- translate the picker search strings and document the discovery precedence
…rite

Round-four review item 3: the swap route wrote the new config.toml into the very dir a live codex session uses as CODEX_HOME before checking the pattern, and its refusal then deleted that dir. buildCustomModelInjection is pure, so the check runs on its result and a refused id never touches the filesystem. Pinned by a two-apply codex route test.
Round-four review item 4: focusing the search input on every picker open raises the OS keyboard over the list on phones; guard it the way readmymind-ui.js does.
…r routes and schema

Round-four review smaller item: the discovery comment claimed a shared ceiling while schemas.ts restated the literal twice; the constant now lives in schemas.ts (routes already import from it, no cycle) and both bounds reference it.
…ed figure wins

Round-four review smaller items: api-reference now states the opposite-of-apiKey behaviour for an omitted defaultContextLength, and the wiki sentence follows the code, modelContextLengths[id] ?? defaultContextLength: any stored figure beats the default.
Round-four review smaller item: one getCustomModelForPersist().launchModel expectation, like the pi/grok/omp tests beside it, pins Ark0N#750 on the one-shot path.
@Ark0N

Ark0N commented Nov 2, 2026

Copy link
Copy Markdown
Owner

Thanks for splitting this out of #752. This PR marks the model one of your live sessions is using on an endpoint that cannot report its own loaded model, and explains an unmarked list when nothing is known. The tests are thorough and the displayModel fence is exactly right. A few things before it goes in:

  1. The "Nothing is marked" line shows for an endpoint that is down (src/web/public/session-ui.js:1139). The gate reads isLlamaSwap === false as "the endpoint answered that it cannot report" and null as "no answer", but the server does not make that distinction: getLlamaSwapStatus() (src/web/routes/custom-model-routes.ts:496-518) turns every failure into { isLlamaSwap: false }. I ran it against a closed port (answered in 19 ms) and an unresolvable hostname (11 ms), both well inside the 800 ms client bound, so a llama-swap box whose service is stopped, a mistyped host, or a 401 from a wrong API key all get "this endpoint cannot report which model it has loaded". The picker only sees null when Codeman's own request fails or the upstream hangs past 800 ms. The test at test/custom-model-run-menu-ui.test.ts:834 returns null from _apiJson, which the route never produces for those cases, so it passes while the real path does the opposite. The changeset, docs/custom-model-endpoints.md:249-256 and the comment at session-ui.js:1130-1138 all promise the silence. Please pick one:

    • Make the distinction real: have getLlamaSwapStatus() record whether an HTTP response came back at all (false when the fetch threw or timed out), return it from running-status as an additive field (documented at docs/api-reference.md:1250), gate nothingKnown on it, and add a route test against a closed port.
    • Drop the promise: remove the reachability claim from the changeset, the docs and the comment, and word the line so it is also true for an endpoint that is down (the zh-CN key changes with it).
  2. A session whose terminal process has exited still counts as "in use" (src/web/public/session-ui.js:1076). The skip checks status === 'stopped' and paneExit, but not pid. Session clears pid when its PTY exits (src/session.ts:3311) and the PTY-exit breaker sets status: 'error' (src/session.ts:3347); neither records a paneExit when the tmux session itself is gone. Keep-warm tests a live pane as session.pid !== null && !session.paneExit (src/prompt-cache-keepwarm.ts:365). Please use the same test here (keeping the stopped check) and add a pid: null row to the "mark nothing" table, giving the live() fixture a pid so the other rows still mean what they say.

  3. docs/architecture-invariants.md:641 still describes two tiers. It lists "Currently loaded" and "Last used" only. Please add the "In use by a session" tier with its two fences (reads customModel, never displayModel; a stopped or exited session does not count) and the #customModelPickUnknown line with its [hidden] rule, since that file is where the next person looks for these rules.

  4. Small one: in multi-user mode the browser's session list holds only the user's own sessions, while endpoints are server-wide, so "a session of this very instance" in docs/custom-model-endpoints.md:217 reads broader than the code. "One of your own sessions" says it exactly.

On the stack: #752 is now at 884e67d, eight commits past the 1b8ac40 this branch carries. The three commits that are this PR's own change apply cleanly on top of it (checked with git merge-tree), so once #752 has merged, a rebase onto master leaves only this PR's change. With the points above in that rebase, this is ready to merge after #752.

…nt cannot report it

Only llama-swap answers what is loaded, so behind a gateway such as Bifrost
nothing was marked and the picker fell back to discovery order apart from the
one CLI that had launched there before. A live session whose own injected
endpoint is that host now promotes its model as "In use by a session", under
its own tag and below llama-swap's "Currently loaded" answer, reading only
what Codeman injected rather than terminal text, and never a stopped session.
When genuinely nothing is known the picker says so instead of leaving an
unmarked wall, and stays quiet for an endpoint that merely failed to answer.
…r lists

Round-four review, split fix 1: a stale 'last used' id kept the promotion truthy, so no row was tagged and the 'Nothing is marked' line stayed hidden. Derive it from promoted && models.includes(promoted).
… and exited panes

Review round on PR Ark0N#757. The route turns EVERY upstream failure into
isLlamaSwap: false, so the old line silently promised an unreachable
endpoint was merely mute, and the live-session check missed a null pid
(the PTY exited, the same test keep-warm applies).

- Require pid !== null in _getCustomModelInUseBySession, pinned by a
  'process exited' case in the marks-nothing table
- Reword #customModelPickUnknown to claim only that nothing was
  reported, true for a down endpoint too; drop the 'stays quiet for a
  failed probe' promise from changeset, docs and comments
- architecture-invariants: add the In-use tier, both pin fences
  (customModel not displayModel; stopped/null-pid/paneExit never
  count) and the unknown-line rule
- Mirror the wording through i18n (key + zh-CN), index.html fixture,
  styles.css comment, and the run-menu tests
@opticon454

Copy link
Copy Markdown
Contributor Author

Pushed 4aeab0d, rebased onto #752's 884e67d so the diff is this PR's four commits only. Thanks for running it against a closed port and a dead hostname.

  1. I took the "drop the promise" option, so the server is unchanged. The line now reads "Nothing is marked: this endpoint did not report which model it has loaded, and none of your running sessions uses one", which is true for an endpoint that is down as well as one that cannot report. The reachability claims are out of the changeset, docs/custom-model-endpoints.md and the session-ui.js comment; the zh-CN entry and the tests that pin the wording changed with it. The test no longer returns null; it models what the route does (isLlamaSwap: false for every failure).
  2. A session counts as live only when it is not stopped, has a pid and has no paneExit (the keep-warm test). The live() fixture carries a pid, and the "mark nothing" table has a pid: null row.
  3. docs/architecture-invariants.md now lists the "In use by a session" tier with its two fences (it reads customModel, never displayModel; a stopped or exited session does not count) and the #customModelPickUnknown line with its [hidden] rule.
  4. docs/custom-model-endpoints.md says "one of your own sessions".

Full npm test passes (one failure in my local copy was a missing gitignored spreadsheet asset bundle, not the change). The changeset is mode 100644.

@opticon454
opticon454 force-pushed the feature/custom-endpoint-in-use branch from 86e3e0f to 4aeab0d Compare November 2, 2026 11:28
@Ark0N

Ark0N commented Nov 2, 2026

Copy link
Copy Markdown
Owner

Thank you, Devvyn, for the quick round-two push. This PR marks the model one of your running sessions is using on an endpoint that cannot report its own loaded model, and says so plainly when nothing is known.

All four round-one points are in at this head: the line now claims only that nothing was reported (true for a down endpoint as well), the in-use lookup skips a session with a null pid and the table has that row, docs/architecture-invariants.md:641 carries the new tier with both fences and the [hidden] rule, and the docs say "one of your own sessions". Typecheck, lint, format, the frontend syntax check, the touched test files and the full npm test (666 files, 13,543 tests) are green here, and so is CI.

Three small things I will tidy at merge time unless you get to them first:

  1. docs/wiki/Custom-Model-Endpoints.md:61-67: the user manual still says that when nothing is loaded the last-used model is moved up. With this PR a running session's model wins there, and the "Nothing is marked" line is new. One or two sentences covering "In use by a session" (it beats "Last used", never "Currently loaded") and the line would bring it in step.
  2. src/web/public/index.html:1044-1046: the comment above the new line still says it shows when "the endpoint answered that it is not llama-swap". The route answers isLlamaSwap: false for a down endpoint too, so "the running-status probe answered isLlamaSwap: false" matches what the JS comment and the docs now say.
  3. .changeset/custom-model-picker-in-use.md:5: this text becomes the CHANGELOG entry, so the closing "Pinned by test/custom-model-run-menu-ui.test.ts." and the isLlamaSwap: false rationale can go; the user-facing sentences before them say everything a reader of the release notes needs.

This merges after #752. If #752 lands as a squash merge, this branch needs a rebase onto master first so only its own four commits remain.

@Ark0N
Ark0N merged commit f7dfe09 into Ark0N:master Nov 4, 2026
2 checks passed
Ark0N pushed a commit that referenced this pull request Nov 4, 2026
The launch-id check is pure, so it now runs before the context warning and the
swap confirmation: nobody agrees to evict a model only to get the 400. Also the
search-box and in-use comments, the model-plain-colon wording, and the wiki's
order of the picker's marks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@opticon454
opticon454 deleted the feature/custom-endpoint-in-use branch November 4, 2026 23:16
@Ark0N

Ark0N commented Nov 4, 2026

Copy link
Copy Markdown
Owner

Thank you, this is in the 1.48.0 release, merged after #752 with the wiki and comment tidies on top.

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.

2 participants