Skip to content

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

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

androidand wants to merge 350 commits into
anomalyco:devfrom
androidand:dev

Conversation

@androidand

@androidand androidand 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

image image image

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-actions github-actions Bot 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
androidand force-pushed the dev branch 2 times, most recently from c368353 to a56a8fe Compare May 14, 2026 14:53
@github-actions github-actions Bot 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. 👍

@androidand
androidand requested a review from adamdotdevin as a code owner May 14, 2026 15:34
@androidand androidand changed the title feat(opencode): auto-discover models from openai-compatible providers feat(opencode): local LAN provider discovery + auto-discover models May 14, 2026
@github-actions github-actions Bot 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!

@bandwith

Copy link
Copy Markdown

This PR contains too much irrelevant data. You're adding local skills and theme content, along with debugging data from your local system.

llama-skein streams model-load "loading theme" flavor as reasoning_content
SSE deltas tagged with a top-level `skein_loading: true`. opencode persisted
them as reasoning, ballooning the session DB to ~16 GB and filling the disk
(SqlError → TUI crash).

The Vercel ai-sdk discards unknown top-level fields, so the marker is only
visible on the RAW chunk. stripSkeinLoading() is a line-buffered SSE transform
on the llama-skein local provider's stream that drops skein_loading events
before the ai-sdk — they never enter the reasoning/message/persistence path,
so they can never be stored, by construction. Their text is forwarded to the
SkeinLoading transient channel for live display (a no-op until a display
subscriber is wired — that's the remaining half of the task).

Unit-tested incl. an event split across stream chunks. Fixes the persistence
half of skein-duey (P1); live themed-display is the follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
androidand and others added 30 commits September 18, 2026 01:39
The turn-level loop-detection streak in prompt.ts resets on every run,
so a session that breaks out of a stuck loop, auto-recovers via
compaction, and breaks again can cycle forever (build -> compact ->
build -> ...). Track consecutive breaks per session across runs in
SessionCompaction; after 3 consecutive breaks the session is marked
loop-dead and auto-compaction refuses to run, surfacing the terminal
error instead of another silent recovery cycle. Manual /compact is
unaffected, and any genuine turn clears the dead state.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…peated text

The loop-detection block in runLoop only ever compared turns when neither
had a tool call, so any turn with a tool call unconditionally reset the
streak — a model stuck retrying the exact same tool call with the exact
same arguments (e.g. re-sending an identical peer message after a
failure) was invisible to it, and this is the more expensive failure
mode since each repeat fires a real side effect.

Extract the comparison into session/loop-detect.ts (pure, tested): a turn
with tool calls is compared by exact call signature (tool + sorted-key
arguments) against the previous turn's; a turn with no tool calls is
compared by bigram text similarity as before. The two are never
cross-compared, so switching between tool calls and plain text resets
the streak rather than counting as a repeat.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…de Code

SessionStatus and Permission (the only two signals statusFrom reads
besides loop state) are both per-process in-memory state. A session
another opencode process is actively driving was therefore
indistinguishable from one this process knows to be genuinely idle: both
are simply absent from `statuses`. resolvePeers (the `peers` tool's
awareness roster) then dropped every real opencode sibling, leaving only
the Claude Code peer — which has an actual cross-process registry
(peer/claude/registry.ts) — visible, even though send_peer_message could
already reach those siblings directly by session id.

Fall back to treating a session with unresolved idle status as busy when
it was updated within the last 45s (well above realistic turn/tool-call
cadence). Scoped to resolvePeers only: resolveMessageTargets already
treats idle as its intended target, and send_peer_message reads `status`
back out to refuse delivery into a session it believes is genuinely
mid-turn, so guessing "busy" there would make delivery to a real
cross-process peer unreliable instead of fixing anything.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
task_id is meant to be an opaque id this same tool previously returned,
but a model — especially a weaker one — will sometimes invent its own
human-readable label instead (e.g. "review-changes-1"), believing it is
naming the task rather than resuming one. SessionID.make() throws a raw
schema validation error ("Expected a string starting with 'ses', got
...") for anything not shaped like a real session id, and that throw
happened while constructing sessions.get()'s argument — before the
existing catchCause around sessions.get could apply — so it escaped as a
confusing tool error instead of falling through to "create a fresh task"
like an unresolvable-but-well-formed task_id already does.

Guard with Schema.is(SessionID) first, and reword the parameter
description to make clear it must be reused verbatim from a prior
result, not invented.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…es clickable

permission.mode (the auto-approve toggle, currently only reachable via
<leader>p or the command palette) was missing the slashName that
/auto was explicitly freed up for when the queue/backlog command
renamed away from that name — "Auto mode" is upstream's name for
permission auto-approve, not the queue driver, but nobody ever finished
wiring it. Add slashName: "auto" so it behaves like every other
slash-visible command.

Also make the footer's agent-name label and "● auto" badge clickable
(mouse-up, matching the existing loop/backlog badge's ClickText idiom)
instead of plain, non-interactive text: clicking the agent name opens
the same picker /agents does, and clicking the auto badge dispatches
the same permission.mode command /auto and <leader>p already run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
With only a couple of built-in agents, agent_cycle_reverse (shift+tab)
was barely distinguishable from agent_cycle (tab) — cycling backward or
forward through 2-3 items lands you in nearly the same place. Free
shift+tab from agent cycling (still reachable via /agents or the
command palette) and give it to permission_mode (auto-approve toggle)
instead, alongside the existing <leader>p. Matches the muscle memory of
similar tools' mode-cycling without overloading Tab's meaning with
state-dependent behavior.

Also add a rotating tip for /auto (permission.mode's new slashName),
matching the existing tip for agent.cycle — the toggle was otherwise
undocumented anywhere a new user would look.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fork:verify flagged four files as unregistered divergence after
today's commits: session/loop-detect.ts and its test are new fork-only
files (owned); tips-view.tsx and test/tool/task.test.ts are the first
fork edits to previously-untouched upstream files (patched, with a
marker unique to each edit).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
model-gallery-ui task 6.1: adds the section shell design.md decision 7
describes without changing existing behavior. SettingsModelsV2 becomes a
container with three sub-tabs:

- Installed: the pre-existing provider-grouped model list, moved verbatim
  into models-installed.tsx (SettingsModelsInstalledV2) — zero behavior
  change, same search/filter/visibility-toggle logic.
- Discover: real but deliberately narrow (models-discover.tsx) — lists
  llama-skein hosts via the already-shipped GET /gallery/hosts endpoint
  (model-gallery-ui task 5.7), online/offline status, installed-model count.
  Not the full catalog search/filter/candidate-card experience (task 6.2) —
  proof the gallery backend is reachable from this panel, nothing more.
- Operations: an honest empty state (models-operations.tsx). Real
  operation-list data needs the app-side operations client task 7.2 builds;
  no fabricated content in the meantime.

Added settings.models.{installed,discover,operations} tab labels and the
Discover panel's strings to en.ts, plus a new plural key
(settings.models.discover.hostInstalledCount) registered in
context/language.tsx's PluralKey union.

tsgo --noEmit -p packages/app: 0 errors (unchanged from baseline, this
package already typechecked clean). oxlint: 1 pre-existing-pattern warning
(narrowing assertion on the tab-value onChange, same shape as elsewhere in
this file tree), 0 errors.

Not yet verified in a running browser — no visual/E2E check performed this
pass; say so explicitly rather than claiming a look that didn't happen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… owner's sidecar; owners publish busy/idle to the registry; sidecars run without Claude Code
…, confirmed install, /downloads shows llama-skein operations; SDK regenerated
…irmed install) and Operations (progress, cancel, refresh on success)
…ed-store detection, load/unload, hide vs delete, copy/move between hosts; TUI /manage
…lete with shared-store warning, copy/move between hosts
…decar registry — Session.list is project-scoped
…rce_peer; provenance re-download is the fallback
- graphify-out/ (analysis-tool cache, ~32MB) was never gitignored and
  leaked into git; removed and ignored.
- Three stray debug .log files (openapi-ts-error-*.log,
  debug-storybook.log) leaked local absolute paths; removed, *.log
  ignored.
- artifacts/glm52-rise-video/ (personal AI-video experiment output,
  unrelated to opencode-skein) and HANDOFF*.md (personal home-lab
  GPU/VRAM tuning notes) removed — no product value to a public
  reader, out of scope for this repo.
- .opencode/opencode.jsonc: skein MCP command now resolves via PATH
  instead of a hardcoded personal home directory.
- family.ts: provenance comment now says ~/dev/skein instead of the
  full personal path.

Follow-up not done here: several archived openspec/changes/** planning
docs still reference real home-lab hostnames (rocky/z4/m3/m5) tied to
real incident narratives. Left alone — needs careful per-doc editing
to genericize without garbling the technical content, not a blind
find-replace.
…scan

scanLlamaSwap() opened a Bonjour browser, waited a fixed 1s window, tore
it down, and returned whatever arrived — every /local request paid that
latency, and any host that answered a beat late was silently missing
from that response with no way to get it back short of another request.

Adds startPeerRegistry()/getPeerRegistrySnapshot()/stopPeerRegistry() to
local/mdns.ts: one Bonjour browser opened at server startup and kept
running for the process lifetime, updated by "up"/"down" events as they
happen, with LAN/localhost probing and per-peer model refresh moved to
background intervals instead of per-request work. Reading a snapshot is
synchronous and never waits on network I/O; a peer that misses one
refresh cycle is marked `stale`, not dropped.

Wired in via a new peer-registry-node.ts (mirrors LocalProviderSync's
node shape) added to the app-runtime layer graph. The /local HTTP
handler now reads the snapshot instead of calling scanLlamaSwap.
scanLlamaSwap itself is untouched and still available for one-shot CLI
use (sync.ts's local-provider-sync job).

Verified against the real network (localhost + LAN probes found 5 real
peers within one refresh cycle); tsgo --noEmit clean.
Replace real hostnames (rocky, z4, m3, m5, proxmox) with generic
role-based labels (gpu-host-1..5) across ~14 archived planning docs,
preserving the technical narrative (VRAM sizing, GPU model, incident
details) — only the hostname changes.
The README was unmodified upstream content: install instructions pointed
at opencode.ai/anomalyco packages, and nothing explained the fork's
purpose or its pairing with llama-skein. Add a "Why this fork exists" /
"Getting started" section up front with the fork's actual positioning
and a working curl install command, and flag that the untouched sections
below (brew/npm/nix/etc.) install upstream opencode, not this fork.
A peer review found the earlier cleanup was incomplete: /Users/andreas
still appeared in 7 files, m5-llamaswap in 1. Fixes:
- ECOSYSTEM.md: ~/dev/... instead of the full personal path.
- Register packages/opencode/src/local/peer-registry-node.ts in
  fork/manifest.json (bun run script/fork-verify.ts was failing on it
  as an unregistered divergence).
- 4 archived openspec docs: /Users/andreas/ -> ~/.
- Test fixtures (presence-claude, peers, sync): personal paths and a
  real hostname (m5-llamaswap) swapped for generic equivalents.

Not done here, flagged separately: several 192.168.1.x IPs remain in
tracked test fixtures, at least one of which (192.168.1.126) matches
a real, currently-live host on the owner's network, not a coincidental
example address. Left alone pending a decision on scope, same as the
larger history-rewrite question this repo now also has (see report).
…nsistent

The earlier scrub renamed one m5 occurrence to gpu-host-1 but not the
rest of this file's rocky/m3/m5 slugs, and canonicalName strips the
-llamaswap suffix, so the input/assertion pair no longer matched
(caught by a peer review before this shipped further). Reverted the
premature partial rename here and kept the file using the real
hostnames consistently — the upcoming history rewrite's replace-text
pass handles renaming all occurrences everywhere in one atomic step,
which is the right place to do it, not piecemeal per-file edits.
Anonymized this file's IPs to the 192.0.2.0/24 documentation range
(RFC 5737) while here.
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.

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

3 participants