Skip to content

feat(provider): add dynamic model discovery for custom providers - #42660

Closed
Gr33ndev wants to merge 2 commits into
anomalyco:devfrom
Gr33ndev:dev
Closed

Gr33ndev wants to merge 2 commits into
anomalyco:devfrom
Gr33ndev:dev

Conversation

@Gr33ndev

@Gr33ndev Gr33ndev commented Aug 14, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #13891
Closes #29308
Closes #28999
Closes #25624
Closes #23327
Closes #26863

Type of change

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

What does this PR do?

Right now, setting up custom OpenAI-compatible providers (like LiteLLM, LM Studio, etc.) is pretty tedious because users have to manually type in every single model ID. This PR adds dynamic model discovery so we can just pull the available models directly from the provider's /v1/models endpoint.

Backend (packages/opencode):

  • New discovery endpoint: Added a POST /provider/discover endpoint. It takes the base URL (plus optional API key and headers) and fetches {baseURL}/v1/models server-side, returning either the model IDs or a safely typed error.
  • Background auto-discovery: If an @ai-sdk/openai-compatible config provider has a base URL and key, we now fetch its models at provider-build time (following the same pattern as the gitlab discovery loader). Hardcoded config models still win, so discovery just gracefully fills in the gaps.
  • Daily sync: I set up a daily Effect.forkScoped schedule to re-run discovery in the background so new remote models show up without requiring an app restart. Errors are swallowed here so an unreachable endpoint won't tank the provider load.
  • Shared logic: Extracted the core fetch logic into src/provider/discover.ts (discoverOpenAICompatibleModels & DiscoverError), which is shared by both the UI endpoint and the background loop.

Implementation note on path resolution: I made sure to handle subpaths correctly. If a user's base URL already contains a path segment (e.g., https://host/api/v1), I append /models to it. Using new URL("/v1/models", base) would otherwise strip the /api/v1 prefix away.

Frontend (packages/app):

  • Hooked up a "Load dynamically" button in the Models section of the custom provider dialog.
  • It calls client.provider.discover (via the regenerated SDK), parses the response, and automatically populates the model rows. Any connection issues surface nicely as toast notifications.

How did you verify your code works?

  • Unit tests: Wrote 9 tests in discover.test.ts against a real Bun.serve mock. Covered auth headers, {env:} resolution, deduplication, 401s, malformed responses, timeouts, invalid URLs, and the path preservation logic.
  • UI tests: Added parser and row-population tests for dialog-custom-provider-models.
  • Manual E2E: Spun up the backend locally (serve --port 4096), mocked a /v1/models response, and tested the UI button. Verified that connection refusals return a safe {"ok":false,"kind":"failed"} instead of blowing up with a 500, and bad URLs return {"ok":false,"kind":"invalidUrl"}.
  • Build checks: Ran bun run typecheck across both packages. The i18n parity tests also pass clean (979 assertions across 61 locales).

Screenshots / recordings

Bildschirmfoto 2026-08-14 um 23 47 18

Checklist

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

@github-actions

Copy link
Copy Markdown
Contributor

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

Based on my search, I found several related PRs that address similar functionality:

Potential Related PRs:

  1. feat(opencode): auto-discover models from OpenAI-compatible providers (feat(opencode): auto-discover models from OpenAI-compatible providers #32731)

    • Directly related - likely an earlier attempt or partial implementation of the same feature
  2. feat(opencode): local LAN provider discovery + auto-discover models (feat(opencode): local LAN provider discovery + auto-discover models #27554)

    • Related to provider discovery and auto-discovery of models from providers
  3. feat(provider): discover local model context limits (feat(provider): discover local model context limits #41104)

    • Related work on provider model discovery, specifically for context limits

These PRs seem to be addressing the same problem space of discovering models from providers. You may want to check if #32731 is a closed/incomplete predecessor or if these are complementary features addressing different provider types.

@Enough1122

Copy link
Copy Markdown

AI code review — automated review for reference; please use your judgment.

Scope: dynamic model discovery for custom OpenAI-compatible providers: new server-side POST /provider/discover that fetches {baseURL}/v1/models (with env-var key indirection, timeout, and typed error kinds), background auto-discovery on a 24h loop for configured compatible providers, and a dialog flow that fills model IDs from discovery. Closes six long-standing issues.

  • The URL normalization logic handles both bare hosts and versioned paths correctly (/v1 suffix → append models; otherwise prefix /v1/models), and discover.test.ts covers the matrix well including auth/env/timeout/bad-format paths.
  • SSRF consideration worth an explicit decision: /provider/discover makes the server fetch an arbitrary caller-supplied URL and reflects results back (model IDs, error kinds/messages). Fine for the local single-user case, but in shared/server-mode deployments any API client can use the daemon to probe internal network endpoints. At minimum document the trust assumption; ideally gate the endpoint behind the same auth tier as config writes or restrict schemes/hosts.
  • Background discovery blocks provider-layer init (yield* runDiscovery() before the service is returned): with the 10s timeout applied sequentially per provider, one slow endpoint delays startup by up to 10s×N. Consider forking the initial sweep too, or running providers concurrently.
  • Every discovery failure is swallowed by empty catch {} — both in the sweep and the gitlab loader. A single Effect.logDebug with kind+providerID would preserve sanity when "why aren't my models showing" bugs come in.
  • Discovered-model defaults are sensibly conservative (zero cost, no capabilities except toolcall); note limit: {context: 0, output: 0} — confirm downstream token math treats 0 as "unknown" rather than a hard zero budget.
  • SDK surfaces were regenerated rather than hand-edited, and translations were added across locales rather than leaving keys untranslated — unusually thorough for a feature PR.

@ssp97

ssp97 commented Aug 25, 2026

Copy link
Copy Markdown

Perfect, that's just what I needed!

@eliowang6

Copy link
Copy Markdown

+1

@TheSnowfield

TheSnowfield commented Aug 29, 2026 •

Copy link
Copy Markdown

really needed this, merge it asap

@SNMetamorph

Copy link
Copy Markdown

This is really what I'm lacking of

@ardvw

ardvw commented Sep 4, 2026

Copy link
Copy Markdown

Would be awesome if this is included in the new release!

@xcvrlv

xcvrlv commented Sep 14, 2026

Copy link
Copy Markdown

This is exactly where opencide still lacks behind. Absolutely needed!

@TannerSet

Copy link
Copy Markdown

We really need this!

hunterchristian added a commit to chipp-ai/opencode that referenced this pull request Sep 23, 2026
…viders

A configured provider can opt in (discover: true, or the v1 config's
discoverModels: true) to have opencode call GET {baseURL}/models and
add whatever the endpoint reports to the catalog, instead of
requiring every model to be hand-listed -- useful for LM Studio,
vLLM, llama.cpp/llama-swap, or any generic OpenAI-compatible
self-hosted endpoint.

Ported from upstream anomalyco#6231 (247 reactions/57
comments -- the second-highest reaction count found across a full
survey of this repo's open issues). Three substantial PRs sat open
and unmerged for 3+ months despite that reaction count. PR anomalyco#32731 (8
files, tightly scoped) is the primary source: this fork reuses its
discoverModels field name, which limit fields to read from the
response, the discovered-model shape, and the "configured/models.dev
wins" merge rule. Deliberately changed from anomalyco#32731: discovery is
opt-in per provider here, not automatic for every provider with a
baseURL -- always-on would fire a startup network request at every
existing custom provider. Unknown limits are left at 0 rather than
anomalyco#32731's guessed 128k default. PR anomalyco#42660's URL-with-existing-path
handling was reused, rewritten with this repo's own HttpClient/Schema
idioms instead of raw fetch in a try/catch; its UI and endpoint
weren't ported. PR anomalyco#27554 (100+ files) had nothing usable beyond
confirming scope overlap. New, since none of the three PRs target it:
a v2 Catalog.Service plugin (this repo didn't have a plugin/catalog
system when those PRs were written).

Runs in both of this repo's provider surfaces: the legacy provider
service (inline before the filter pass, what the TUI model picker
reads) and a new v2 Catalog plugin (packages/core/src/config/plugin/
provider-discovery.ts, once at startup then hourly, same schedule as
the models.dev refresh).

Known gaps: the legacy provider service has no periodic refresh (a
newly-loaded local model appears only after restart/reload); stale
models aren't hidden; discovered capabilities are guesses (no
vision/reasoning detection); only the standard OpenAI models response
shape is understood, not Ollama's native /api/tags; no settings UI or
HTTP endpoint; legacy auth only checks a stored credential or
options.apiKey, not plugin OAuth loaders.

New packages/core/test/provider-discovery.test.ts + config/
provider-discovery.test.ts (unit-level) and packages/opencode/test/
provider/discovery.test.ts (against a real local Bun.serve server,
not a mock). Verified against this fork's current dev, not just the
agent's own stale worktree base: bun typecheck clean across all 30
packages; packages/core 1187 pass (only the pre-existing pty flake);
packages/opencode 3617 pass, only the 4 pre-existing cf-ai-gateway
failures. bun run generate from packages/client correctly produced
no changes -- this is a config-only field, not a Protocol/HttpApi
schema change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Gr33ndev

Copy link
Copy Markdown
Author

@Enough1122 Sorry it took me so long to get to these changes, and thanks for the review! Addressed in the latest push:

  • SSRF: /provider/discover already uses the same Authorization middleware as config writes, and config writes can point a provider at any base URL anyway. I didn't restrict hosts, since loopback and LAN (Ollama, LM Studio) are the main use case. I documented the trust assumption in discover.ts and in the OpenAPI description.
  • Startup blocking: providers are now probed concurrently, so the worst case is one timeout instead of 10s × N. I kept the first sweep blocking because packages/opencode/AGENTS.md asks not to fork work inside the InstanceState closure. The 24h refresh still runs in the background.
  • Swallowed errors: discovery failures are now logged via Effect.logDebug with providerID, kind and message.
  • limit: 0: checked. isOverflow treats a zero context as unknown and skips compaction. I added a comment.

While in there I fixed two more things:

  • Discovery only used provider.key, so keyless providers (Ollama) and keys set via options.apiKey were never discovered. It now picks the key the same way requests do.
  • Models discovered during the 24h refresh skipped the config whitelist/blacklist and variants. They now go through the same setup as static models.

Added tests for concurrency, failure isolation, the whitelist and the config apiKey.

@Gr33ndev

Copy link
Copy Markdown
Author

Hi @Hona!
Whenever you find a moment, I'd really appreciate it if you could take a look at this. Quite a few people have been asking for it, and I'm happy to make any changes you'd like. Thanks!

Monarch505 added a commit to Monarch505/archrouter that referenced this pull request Oct 5, 2026
…/models

opencode builds its picker from models.dev plus the static `models` map in
opencode.json. It does not call a custom provider's /v1/models — discovery is
hardcoded to Ollama, LM Studio and vLLM at their default ports. A provider
written with only npm + baseURL shows up as "Provider not found" with an empty
list. Verified on 1.18.3, where `opencode models archrouter` listed nothing at
all; general discovery is still an open PR (anomalyco/opencode#42660), so there
is nothing to wait for.

So the static list is now the default and --variants becomes --no-models, which
only exists to clean up an older install. The key is resolved from
ARCHROUTER_KEY, then first-key.txt, then whatever /connect saved, and written
into options.apiKey: auth is required by default, so /v1/models answers 401
without it — including the probe this command runs to fetch that list, which is
why --variants died on every auth-on install with "router not answering".

Models opencode cannot reach are still listed, with the measurements kept in
modelCaps.js and opencodeConfig.js rather than hidden: muse-spark-*-contributor-
free and jev-1.13-free answer only on /v1/responses and /v1/messages and 500 on
the chat path, and ling-3.0-flash-fin-free (400) plus ling-3.1-flash-free (429)
are reproducibly dead upstream, 3/3 attempts each on 2026-10-04. The other 7
answer 200 through /v1/chat/completions.

206 tests pass: test-p0 52, test-auth-store 6, test-setup-sh 40,
test-uninstall 48, test-setup-flow 60. The suite carried a test whose name
asserted the false premise above; it is inverted now, and two more cover the
apiKey and the default shape.

Not tested: WARP registration end to end (it burns real Cloudflare slots), and
opencode's own picker — the catalog was confirmed through `opencode --pure
models`, because a local plugin's dependency install deadlocks the plain CLI
(anomalyco/opencode#47212, unrelated to this repo).
@thdxr

thdxr commented Oct 10, 2026

Copy link
Copy Markdown
Member

This branch has been upgraded to V2. We're no longer taking PRs for V1. If you think this is still relevant, please port it over and open it for V2.

— from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

@thdxr thdxr closed this Oct 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

10 participants