Skip to content

feat(opencode): local LAN provider discovery + auto-discover models - #27554

Open
androidand wants to merge 288 commits into
anomalyco:devfrom
androidand:dev
Open

feat(opencode): local LAN provider discovery + auto-discover models#27554
androidand wants to merge 288 commits into
anomalyco:devfrom
androidand:dev

Conversation

@androidand

@androidandandroidand commented May 14, 2026

Copy link
Copy Markdown

Issue for this PR

Closes#6231
Closes#27553

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

  • Adds Local (LAN) discovery in /connect for local OpenAI-compatible servers.

    • Combines mDNS, localhost probing, and bounded LAN probing to find reachable local instances.
    • Lets users add discovered providers in bulk and marks already configured entries.
    • Shows if local providers are online or offline
  • Auto-discovers models for OpenAI-compatible providers from /models, including limits like context window and max output tokens when available.

  • Compatible with and preserves manually configured/edited providers and models and respects discoverModels: true|false.

  • Adds context current / context_length when available (see llama-swap PR below) to Context in sidebar and footer.

  • Fixes unrelated bugs introduced by rebase, --agent parameter would crash opencode.

Related

Server-side companion: mostlygeek/llama-swap#755 adds context_length and max_output_tokens to the /v1/models response. When those fields are present, this PR reads them to configure each model's context window and output token limits automatically — no manual config needed.

How did you verify your code works?

  • bun run typecheck
  • bun test test/provider/provider.test.ts --timeout 30000
  • bun turbo typecheck
  • Manually tested /connect -> Local (LAN) against multiple local OpenAI-compatible servers on localhost and LAN.

Screenshots / recordings

imageimageimage

Provider config example

 "proxmox": {
"npm": "@ai-sdk/openai-compatible",
"name": "Proxmox",
"options": {
"baseURL": "http://192.168.1.123:8080/v1",
"apiKey": "ollama"
},
"discoverModels": true
},

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@github-actionsgithub-actionsBot added the needs:compliance This means the issue will auto-close after 2 hours. label May 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

Based on the search results, here are the potentially related PRs:

Most Related:

Related by Pattern:

Note: PR #27554 (the current PR) appears as the top result in all searches, which is expected. The most directly related duplicate candidate is #26756, which already implements discovery from /v1/models for LM Studio specifically. You may want to review if this current PR consolidates or extends that approach to work with all OpenAI-compatible providers.

@androidand
androidandforce-pushed the dev branch 2 times, most recently from c368353 to a56a8feCompareMay 14, 2026 14:53
@github-actionsgithub-actionsBot removed the needs:compliance This means the issue will auto-close after 2 hours. label May 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

@androidandandroidand changed the title feat(opencode): auto-discover models from openai-compatible providersfeat(opencode): local LAN provider discovery + auto-discover modelsMay 14, 2026
@github-actionsgithub-actionsBot added needs:compliance This means the issue will auto-close after 2 hours. and removed needs:compliance This means the issue will auto-close after 2 hours. labels May 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

@androidand

Copy link
Copy Markdown
Author

Please have a look, I think people will appreciate this, especially in combination with the updated llama-swap which adds feature parity (and more) with ollama.

@vlameiras

Copy link
Copy Markdown

It would be very much appreciated. Thanks!

androidandand others added 12 commits June 1, 2026 19:21
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- beads/sync.ts: replace removed Bus with EventV2Bridge, fix event.data shape
- beads.ts: remove Config.beads (gone upstream), fix Todo.Service usage, add --session arg
- stream.transport.ts: fix RunEvent vs DOM Event type confusion on buffered arrays
- prompt/index.tsx: remove dead useCommandPalette import (symbol removed upstream)
- provider.test.ts: include Provider.defaultLayer in runList helper so Provider.Service is satisfied
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On app init, runs scanLlamaSwap() (mDNS + LAN probe) as a background
fiber and upserts discovered providers into the global opencode.json.
Existing providers with stale IPs are updated in-place by slug match.
Offline providers are left untouched — they may just be temporarily down.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A machine with multiple LAN interfaces was being added as a new provider
for each non-primary interface (probeLAN only skips one IP per subnet).
Skip any discovered host that matches a local network interface address.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
androidandand others added 21 commits August 14, 2026 19:02
Second pass over the divergence backlog. Each marker is a string the fork's
edit introduces that upstream's version of the same file does not contain —
checked both ways per file, because a marker that also appears upstream would
report "intact" against a file the merge had already reverted, which is worse
than no marker at all.
Verified end to end: reverting one registered file to upstream drops
fork:verify to 76/77 and fails the run; restoring it goes green again.
patched 13 -> 77, accepted 84 -> 20.
Third pass. 18 more upstream files registered with a full added line as the
marker — verified absent from upstream's copy of each file, same rule as
before.
subagent-footer.tsx is now byte-identical to upstream: the only fork change
had been dropping the context percentage, which was restored earlier, and the
residual diff was those same two lines sitting in a different place.
That leaves one accepted entry, packages/core/src/effect/dfdf — a file
upstream committed by accident, containing an editor's "File to save in:"
prompt. Deleted here deliberately; it cannot carry a marker because there is
no file left, so it stays accepted with that explanation recorded.
Backlog: 225 -> 1. owned 150, patched 95.
fork-verify already detects every class of fork regression — a dropped owned
file, a reverted patch marker, unregistered divergence — and already exits 1.
Nothing ever ran it, so it only helped when someone thought to ask.
Two features were lost that way today. Self-healing compaction went missing in
a merge and its only witness was a test that had never been able to run. The
MCP exports went missing and surfaced as 35 type errors in unrelated files,
attributed to a migration nobody was doing. Both were invisible until
something happened to compile.
Now it runs on every push: 150 owned files present, 13 patch markers intact,
0 unregistered divergence.
Commit e0c09dc removed the `--auto` handling from run.ts without meaning
to. Verifying that a fork:verify marker actually fires involved
`git checkout <baseline> -- run.ts`, which does not just change the working
tree — it stages upstream's copy. The working tree was restored straight
after, but the index kept the upstream version, and the next `git commit`
took it along even though only fork/manifest.json had been `git add`-ed.
The result is exactly the failure this fork keeps hitting: a feature present
in the working tree, absent from the commit, and green on every check.
fork:verify did not catch it because it reads the working tree, not HEAD — so
the marker was "intact" in a checkout whose HEAD had lost the feature. Worth
closing separately; noting it here because the gate's blind spot is the
interesting part, not my mistake.
The gate read only the working tree, so a fork feature present in a checkout
but missing from the commit passed clean. That is not hypothetical — it is
what happened an hour ago: a stray `git checkout <baseline> -- run.ts` staged
upstream's copy, the next commit took it out of the index, and auto-mode left
HEAD while the worktree still had it. Every check stayed green.
With a pre-push hook now running fork:verify, that blind spot is the one that
matters most: the hook would have waved through a push whose HEAD had lost a
feature.
Owned files must exist in HEAD, and patch markers must appear in HEAD as well
as on disk. `--worktree-only` skips the HEAD half for mid-edit runs.
Verified by reproducing the exact bug — worktree correct, index holding
upstream's copy — and confirming the gate now fails on it.
similarity("", "") returned 1 via the na === nb short-circuit, and that one
line is why /loop finalized as "stalled" seconds after starting, every time.
Both no-progress signals come from the same array. When message.parts comes
back empty, toolCalls is 0 and output is "" — so noToolCalls holds, and
similarity scores the previous empty output as a 1.0 match, so nearIdentical
holds too. The streak then increments every iteration instead of resetting,
reaches the default limit of 3, and finalizes. Empty turns return fast, which
is why it took seconds.
One failure produced three agreeing signals. Absence of output is now scored
as absence: the empty check runs before the equality check, so an empty turn
resets the streak instead of confirming a stall.
This stops the false stall. It does not explain why message.parts is empty in
the first place — that is upstream in the prompt path and wants its own fix;
maxIterations remains the backstop until then.
Reconciles the 35/34 divergence between local dev and origin/dev. 53
conflict hunks: bun.lock regenerated rather than hand-merged,
fork/manifest.json reconciled last against what the source files actually
became, and 17 source hunks decided per hunk.
Most source hunks were ties — both sides made the same change (log.info ->
Effect.logInfo, LayerNode.make positional -> object form) and differed only
in wrapping. Resolved to whichever side matches prettier's 120-column
output.
The genuine divergences went to the fork, because upstream's half was the
same problem solved a different way and the fork's half is the completed
refactor:
- local/placement.ts: pick() returns a discriminated outcome and the Effect
caller logs (task.ts:254-268 logs all three outcomes). Upstream instead
logged in place via syncLog*.
- provider/provider.ts: discoverOpenAICompatibleModels returns
{ models, warnings }; the caller logs them at provider.ts:2114.
- The retired defaultLayer exports stay retired — the only remaining
references in the tree are two comments explaining the removal.
provider.ts's OPENAI_HEADER_TIMEOUT_DEFAULT keeps 300_000. That looks like
a fork value but is upstream's own (PR anomalyco#37770), already restored here by
30104df; origin/dev still carries the superseded 10_000.
Two defects came from git's *automatic* merges, not the conflicts — both
sides added the same lines at different offsets, so both copies survived:
duplicate fork script keys in package.json (bun warned) and a duplicate
ForkDistribution import in installation/index.ts (TS2300).
fork/manifest.json takes origin/dev's newer baseline (91df883, 2026-08-13)
and its _ownedRemoved_* notes; the owned/patched sets resolve to the local
side, which is a strict superset. util/sync-log.ts is registered as owned
but is now unused — see _ownedUnused_2026-08-14; it needs a decision, not a
silent deletion.
typecheck 30/30 clean, loop tests 149 pass / 0 fail.
accaf32 appended the tail of the install script's output to
UpgradeFailedError.stderr so a real cause (missing curl, 404 asset, disk
full) was diagnosable instead of every failure reading "exit code 1".
That output is arbitrary and untrusted, and the sanitization tests on
both sides of the merge pin down that it must not reach the error:
their fixtures return "token=secret" and assert stderr does not contain
it. The two assertions had been failing on origin/dev since that commit.
Keep both properties: the error is sanitized again, and the detail moves
to the log, where it was always the more useful half anyway. The curl
path logs its cause via tapError rather than folding it into the message.
Also drop util/sync-log.ts. It was added by 21c1ac9 to give non-Effect
call sites a TUI-safe logger, but the design that landed on dev has those
helpers return outcomes (PickOutcome, DiscoveryResult.warnings) with the
Effect caller logging, leaving it with zero call sites. Recorded in the
manifest as a deliberate removal so fork:verify does not read it as a
dropped fork feature.
installation tests 12 pass / 0 fail; typecheck 30/30.
…ndings
Lands the design.md resolving the proposal's three open questions, plus the
discoveries and task updates from working through them.
Three findings in discoveries.md are llama-skein bugs this client change
cannot work around, and they are worth reading before implementing:
- HOST_PACED_PENALTY never fires for pinned models, because llama-skein
returns perf_class=native-gpu unconditionally for ModeCustom. The guard
against routing subagents to CPU-bound models is disabled by exactly the
config that creates them.
- The read-modify-write cycle this change depends on is unsafe today: the
config GET returns --port resolved while config stores ${PORT}, and the
patch contract has no way to remove n_gpu_layers.
- Host B's models are invisible to subagent placement and have been — all 8
report fit_level=unknown, so placement skips them. A deployment fault, not
part of the -ngl story.
Renames one host to the label scheme this change's own proposal declares
("this repo is public, so fleet hosts are named by capability and shape").
discoveries.md had the real hostname; the mapping belongs in docs-skein.
Merges 20 upstream commits — model catalogue updates (GLM 5.3, Gemini 3.7,
Grok endpoint), a unicode fix in grep previews, v1 database compatibility,
and generated/nix churn. Merged without conflicts.
Only one of the 80 files upstream touched is fork-registered
(tool/registry.ts), which is why this was a clean merge rather than the
seven-file reconciliation the previous sync needed.
fork:verify flagged 46 unregistered divergences before the baseline bump —
all of them the stale ref, not drift: owned 150/150 present and patched 95/95
intact throughout, so no fork feature was lost. Bumping upstreamRef to the
newly merged commit clears them, exactly as the manifest's own comment
prescribes.
Verified after: fork:verify clean (150 owned, 95 patched, 0 unregistered),
typecheck 0 errors in both packages, loop suite 149/0.
fork:verify became enforcing this week and immediately failed its first sync
with 46 unregistered divergences — while owned files sat at 150/150 and patch
markers at 95/95. Nothing was lost; the baseline ref was stale. Reading that
correctly is the difference between bumping a field and hunting a phantom
regression, and nothing wrote it down.
The skill records that distinction, the sync procedure that produced a
conflict-free merge of 20 upstream commits, and three traps:
- measure the upstream delta from the merge-base. `git diff HEAD..upstream/dev`
includes the fork's own ~270 commits and reported 245 fork-registered files
for a sync whose real answer was 1.
- merge, do not rebase. A rebase replays one decision once per local commit.
- grep upstream/dev before calling a symbol fork-specific. McpTool looked like
ours by name and by its consumers; it is upstream's, and mistaking it cost a
wrong diagnosis of 35 type errors.
Also carries the maintainer's standing rule for conflicting hunks — upstream
wins ties, the fork wins where it adds something, ambiguous cases go on a list
rather than being guessed — and the worktree recipe for landing a commit
without touching another agent's working tree.
Verifying the loop-stall fix required actually running test/loop/*,
which uncovered that the fork's LayerNode/defaultLayer plumbing had
been broken for a while and this test suite had likely never passed:
- LayerNode.make(layer, [...]) positional-call sites (should be the
object form { service, layer, deps })
- 9 services missing an exported `layer` binding their own tests
reference directly (SessionStatus, SessionRunState, Question, Todo,
ToolRegistry, Truncate, SessionProcessor, SessionCompaction)
- 77 services missing a `defaultLayer` export entirely; added one that
mirrors each service's own `deps` list via
`layer.pipe(Layer.provide(dep.defaultLayer), ...)`, referencing each
dependency's own canonical defaultLayer so shared singletons
(Database in particular) stay memoized as one instance instead of
getting rebuilt per subtree
- 4 of those had a `node.layer` field that wasn't the bare `layer`
identifier (database.ts, runtime-flags.ts, llm.ts, core session.ts)
the generated defaultLayer wired the wrong construction
- 4 files transitively depend on unbound (tag-hoisted) nodes and can't
have a standalone defaultLayer at all (acp/usage.ts, acp/directory.ts,
project/instance-store.ts, worktree/index.ts) - left unexported
- tool/registry.ts's defaultLayer chained 21 separate Layer.provide()
calls in one .pipe(), over TS's pipe-overload arity limit - switched
to the array form Layer.provide([...])
- loop.test.ts and queue-mode.test.ts were both missing SessionProjector
(the event-driven service that persists sessions to SQLite) from
their hand-built layer graphs, so sessions were "created" but never
written to disk before the first prompt looked them up
- session/prompt.ts used `new Prompt(...)` where Prompt is a
Schema.Struct, not a class; crashed any prompt once the event-sourced
session path actually ran. Fixed to Prompt.make(...), matching the
convention used elsewhere (core/src/session.ts)
test/loop/ now passes clean (143 tests, 8 files).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The no-progress guard flagged genuinely-progressing iterations as
stalled whenever their output was long and structurally repetitive
(file dumps, boilerplate) - character-bigram similarity scores high on
that kind of text regardless of the actual edit, and the old 0.92
threshold with a default noProgressLimit of 10 gave real progress too
little room before the guard fired.
- Raise the near-identical similarity threshold 0.92 -> 0.96
- Raise the default noProgressLimit 10 -> 15
- Add a length-change veto: an output whose length differs from the
previous iteration's by more than 10% is treated as progress even
when bigram similarity alone would call it near-identical, since a
large length swing means real content was added or removed
- Bump the SDK's noProgressLimit default in loop-args.ts to match (3 -> 10)
fix-loop-stall tasks 1-5 done; task 6 (verify with real /loop usage)
left open pending a live TUI run.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ran two real /loop sessions through the CLI against a running server
and live llama-skein backends (rocky/qwen3-35b-a3b): one file-writing
task (8 files, 17 tool calls, completed correctly) and one text-only
task explicitly designed to produce long, near-identical output across
iterations. Both completed cleanly via the promise token, no false
stalls, no regressions from the LayerNode/defaultLayer fixes.
Neither run naturally produced multiple no-tool-call iterations - a
capable model just finishes an unconstrained task in one turn - so the
precise similarity/length-veto boundary condition stays covered by the
existing deterministic unit tests rather than a live run, which is the
more reliable way to pin an exact numeric threshold anyway.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
sync.tsx and local.tsx both call usePermission() from their own init(),
but nothing in app.tsx's provider tree ever wrapped it in
<PermissionProvider> — permission.tsx (and its two consumers) landed via
the Aug 7 upstream sync, and the wrapper never got added during that
merge.
Silent until now: existing long-running sessions had already booted
past this code path before today, so the gap only surfaces on a fresh
boot into an existing session (`opencode -s <id>`) - crashing with
"Permission context must be used within a context provider" before any
prompt is even reachable. Reproduced and confirmed fixed against a full
isolated copy of a real, affected session.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same issue as core/src/session.ts: LocationServiceMap.node is
LayerNode.unbound (tag-hoisted, replaced only by app-node-builder's real
graph), so it has no defaultLayer and never can. The earlier defaultLayer
sweep referenced LocationServiceMap.defaultLayer here anyway - missed in
that pass, caught now by the pre-push typecheck gate actually running.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…gration, provider capabilities, and TUI
Fixes the ~15 pre-existing typecheck error clusters the newly-enforced
pre-push fork:verify+typecheck gate exposed (never run before today, so this
debt accumulated silently). All 30 turbo packages now pass `bun typecheck`.
- mcp/index.ts: reshape MCP.tools() to return {def, client, timeout} per-tool
(matching what session/tools.ts, code-mode.ts and tests already expected);
implement instructions() via the MCP SDK's Client.getInstructions();
fix getAuthStatus to compute expiry inline instead of calling a
nonexistent isTokenExpired
- provider.ts: widen ProviderInterleaved.field to a plain string and add
normalizeInterleaved() so config/catalog-supplied interleaved values
(bare string or {field}) both normalize correctly; cast at the two
plugin/SDK boundaries where our internal Model type is wider than the
generated SDK's still-narrow capabilities.interleaved
- share-next.ts: cast outbound share data to the generated SDK's Model[]
at the same interleaved-widening boundary
- beads/sync.ts, effect/app-runtime.ts: wire BeadsSync into AppLayer via a
LayerNode `node` export - it was never registered, so `opencode beads
sync` would defect at runtime on a missing service
- tool/code-mode.ts: migrate off the old @modelcontextprotocol/sdk types
(CallToolResultSchema arg, 3-arg callTool) to the new split SDK client
API already used by mcp/catalog.ts
- tool/shell.ts, tool/shell/prompt.ts: add the optional `description`
field to the shell tool's parameter schema - referenced at every
ctx.ask/metadata call site since the tool's introduction but never
declared, so it was always undefined; fall back to the command itself
for the result title
- session/prompt.ts: drop a stray `messages` argument to
compaction.isOverflow that the interface never accepted
- cli/cmd/attach.ts: --mini pointed at a runMini export that never
existed; fail explicitly instead of a silent dead import
- cli/cmd/mcp.ts: authenticate() now opens the browser itself; drop the
obsolete callback-based call
- tui/app.tsx: wrap the app in LocationProvider (paired with the
already-committed PermissionProvider fix) - both providers were added
by the Aug 7 upstream sync but never wired into the component tree,
crashing on session resume
- tui/component/dialog-move-session.tsx: break a circular generic
inference in createResource (T needed from both the explicit return
annotation and the untyped info param) that collapsed the resolved
type to {} and cascaded into ~11 errors
- test/mcp/oauth-provider.test.ts, test/session/prompt.test.ts: remove
two tests that reached for SDK/service surfaces removed by the v1->v2
MCP SDK migration and an architecturally-unbindable SessionV2.defaultLayer
respectively; both were already skip'd or testing internals no longer
exported
- test/mcp/catalog.test.ts, test/tool/code-mode*.test.ts,
test/tool/registry.test.ts: migrate remaining old-SDK imports
(Server/Client/InMemoryTransport/schemas) to the new split packages
Verified: bun run typecheck clean across all 30 packages, bun run
fork:verify clean, and a stash-based control test (same 44-file suite
with/without this commit) shows 972/1047 pass vs 953/1040 on the
pre-fix baseline - net improvement, no regressions introduced.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…typecheck fixes
Re-baselines fork/manifest.json's accepted-divergence list after rebasing
onto origin/dev's fork-sync (4643e65, fork/2026-08-14.1). fork:verify
was reporting 66 unregistered divergences purely from files already in
this branch's history, not anything newly lost in the rebase (verified:
origin/dev alone reports 0 unregistered).
Sampled across the flagged files to classify before accepting, per the
fork-sync skill's rule against blind --accept-divergence:
- ~55 files (core services + opencode services): the mechanical
defaultLayer-export sweep from an earlier commit in this branch —
`const layer` -> `export const layer` plus a `defaultLayer` composing
each service's own deps' defaultLayers. Purely additive, one consistent
pattern, verified via direct diff sampling (process.ts, session/store.ts,
question/index.ts, session/compaction.ts, tool/truncate.ts,
tool/registry.ts) — no upstream behavior altered or removed.
- The rest: today's typecheck-fix work and the loop-stall fix, both
already verified via typecheck, fork:verify's owned/patched counts,
and (for the typecheck fixes) a stash-based control test against the
pre-fix baseline.
owned 150/150, patched 95/95 unaffected throughout — nothing was lost,
only unclassified. baseline.upstreamRef/syncedAt/forkTag unchanged
(still accurate). Individual markers for the defaultLayer sweep are left
as a followup, not blocking here — it is one coherent, understood
pattern repeated across files, not 55 separate decisions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… defaultLayers
Rebasing onto origin/dev surfaced real breakage that merged silently (no
conflict markers, since both sides touched different lines of the same
files):
- beads/sync.ts, effect/app-runtime.ts: duplicate BeadsSync import/node
export — upstream/dev independently wired BeadsSync into AppLayer too;
kept the one with the explanatory fork comment, dropped my duplicate.
- mcp/index.ts: duplicate McpTool declaration — mine (from today's
typecheck fixes) vs upstream's canonical, better-documented readonly
interface. Dropped mine.
- test/loop/loop.test.ts, queue-mode.test.ts: duplicate SessionProjector
import and duplicate `instructions` mock property, both from git
splicing in two independent additions of the same line.
- test/session/snapshot-tool-race.test.ts: MCP mock was missing
`resourceTemplates`, now a required Interface member.
Also: provider.ts, mcp/index.ts, session/prompt.ts, auto-mode/service.ts
were missing `defaultLayer` even on origin/dev itself (confirmed: these
errors exist on origin/dev alone once files elsewhere reference
Provider.defaultLayer/MCP.defaultLayer/SessionPrompt.defaultLayer that
never existed) — added, following the established pattern, only after
confirming every dependency in each node's `deps` list already has its
own defaultLayer.
bun typecheck: 0 errors across all 30 packages. fork:verify: 150/150
owned, 95/95 patched, 0 unregistered. test/loop/: 149/149, matching the
pre-rebase dev baseline exactly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…eter
The shell tool's description parameter (added to fix a pre-existing
typecheck gap where the field was referenced at every ctx.ask/metadata
call site but never declared in the schema) changes its JSON Schema wire
shape. Snapshot updated to match.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
llama-skein's max_fit_ctx has vramSafetyFrac/promptMarginFrac headroom
built in, so it's conservative by design — but this dialog was also
using it as the hard block in apply(), which meant the recommended
value and the refusal threshold could disagree: pick the recommended
ctx, get told it doesn't fit.
max_physical_ctx (new llama-skein field, no discretionary margin) is
now the block in apply() and the ceiling used for aboveCeiling(). The
dialog surfaces both: the physical max as a selectable option and in
the title (`max X`), and max_fit_ctx alongside it (`safe ~Y`) so a
user who wants headroom still sees it.
- packages/*/gen/types.gen.ts: regenerated (opencode) / hand-added
(tui, which has no dedicated regen script for this client copy)
- packages/tui/src/local/model-fit.ts: aboveCeiling doc updated for
the semantic shift, logic unchanged
- packages/tui/src/component/dialog-model-ctx.tsx: maxPhysical memo,
new selectable option, apply() gate, titleSuffix shows both numbers
The foreign-turn guard skips prompting when the target session already
has a turn in flight that this loop didn't start (the user's own
message, or a slow previous turn not yet settled) — otherwise
ensureRunning would join that turn and attribute its output to this
loop. But the skip result still advanced info.iteration and counted
against maxIterations. With the 2s default poll interval, starting a
loop against a busy session burned the whole iteration budget in
seconds without ever sending the loop's own first prompt, landing on
"max_reached" while the model was still generating unrelated output.
Both call sites (prompt mode's run(), queue mode's queueTurn()) now
retry a skip without touching the counter or the iterations history.
queueTurn retries internally so its three call sites keep treating a
returned result as a real gate attempt — a skipped result surfacing as
"0 tool calls, empty output" would have read as a failing turn and
cost the change a strike it didn't take.
Added a regression test that reproduces the bug (a foreign turn held
open on the loop's session) and fails without the fix.
ae9cc41 deleted this as part of tearing out a fork-authored 4-state
autonomy ladder (manual/skip-ask/continue/auto). Two of those states
really were dead duplicates of /loop and /auto, correctly removed —
but the permission toggle wasn't a duplicate of anything, it was the
one real setting in that ladder, and it got swept out with the rest
because by then it only existed as one branch of a shared dispatcher
rather than its own command.
This is upstream's own permission.mode command (context/permission.tsx's
toggle()/set() were never touched by that deletion, only the palette
entry and keybind calling them were), restored verbatim: same command
name, same title strings, same behavior. <leader>p is the same binding
it had before the deletion.
fixes the "briefly saw an auto indicator, can't find how to toggle it
again" gap — the indicator in prompt/index.tsx was always live, this
was the only thing missing.
/auto collided with upstream's own "Auto mode" (permission auto-approve,
opencode.ai/docs/permissions, just restored in the previous commit) —
not an internal naming preference this time, a real feature upstream
documents and ships today. One word, two unrelated meanings.
/backlog names what the command works on rather than how it's built,
which also avoids reintroducing the "queue is an implementation
detail" complaint that the original /queue -> /auto rename existed to
fix. /queue remains as an alias, unchanged.
Renamed the command, its slash name, status-pill label, toasts, and
every doc/comment reference. Left mode: "queue", QueueAuthority,
experimental.queue_gate/queue_personas, and other internal identifiers
alone — the collision was in the discoverable verb, not the
implementation.
Also fixed the auto_mode config field's doc comment, which still
described a "Toggle auto mode" TUI command that no longer exists.
loop.create was called with no sessionID from the command palette
entry, so it always fell back to minting a new hidden session. The
run really did work — it just had nowhere visible to show up: no
status pill, no way to find it from the session you were looking at,
and a second attempt from the same directory hit the
one-queue-run-per-directory guard with no visible reason why.
Now scopes to the session you're viewing, matching what /backlog's
prompt intercept already did. Navigates to the new session when
started from the home screen (no session open to attach to).
180s covered a cold model load, which is what it was calibrated
against. It didn't cover a multi-day /loop or /backlog session whose
context only grows — prefill on that much context can genuinely take
longer than 180s on real local hardware with the backend perfectly
healthy. Observed killing an otherwise-fine generation mid-run.
600s keeps the same purpose (turn a truly-dead connection into a clear
timeout instead of an indefinite hang) with realistic margin for
long-running unattended work. Per-provider options.headerTimeout still
overrides this for anyone who wants a different value.
<leader>p was defined in keybind.ts and the command existed, but
appBindingCommands — the list app.tsx actually uses to turn keybind
definitions into live bindings — never had "permission.mode" added to
it. The keybind silently did nothing; only selecting the command by
hand from the palette worked at all.
Sign up for freeto 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.

[FEATURE]: Auto-discover models from OpenAI-compatible providers Auto-discover models from OpenAI-compatible provider endpoints

3 participants

@androidand@vlameiras@bandwith