Skip to content

desktop: custom provider save always throws "unavailable on this server" #50650

Open

Description

@throny

Summary

OpenCode Desktop exposes the "Custom OpenAI-compatible provider" form in Settings → Providers, but the save handler unconditionally throws provider.custom.unavailable, so the flow can never succeed on any server — including the app's own bundled local server.

Environment

  • opencode version: Desktop 2.0.14 (bundled opencode-cli v2.0.14, dpkg opencode 2.0.14)
  • OS: Ubuntu 26.04.1 LTS, Linux 7.0.0-31-generic, x86_64
  • Terminal: Unavailable: this is the Desktop (Electron) UI, no terminal involved
  • Shell: /bin/bash
  • Install/channel: release .deb downloaded from the Desktop section of https://opencode.ai/v2/docs
  • Active plugins: none found in config

Reproduction

  1. Install opencode-desktop-linux-amd64.deb (Desktop 2.0.14) and launch OpenCode Desktop.
  2. Open Settings → Providers.
  3. Select the custom provider entry ("Custom OpenAI-compatible provider").
  4. Fill in valid-looking values, e.g. provider ID local, name My Local Server, baseURL http://localhost:8080/v1, model ID my-model.
  5. Click Save/Connect.

Expected Behavior

The provider is written to the user config (providers.<id>) and shows up in the model picker — the same result as adding the block to opencode.json manually.

Actual Behavior

Save always fails with the toast: "Custom providers are unavailable on this server" (i18n key provider.custom.unavailable).

The shipped app.asar build shows the mutation is a stub with no server or protocol condition:

mutationFn: async e => {
  throw Error(a.t(`provider.custom.unavailable`))
}

It throws for every server, so the dialog is reachable but unusable.

Additional Context

  • Not a server/config problem: adding the provider manually to ~/.config/opencode/opencode.json loads it correctly on the very server the app spawns — GET /api/provider on the bundled server returns {"id":"local","name":"My Local Server","activation":"enabled", ...}. The failure is isolated to the dialog's save path.
  • Frequency: 100% reproducible (unconditional code path).
  • Current source appears to rework this: the same handler is gated with if ((await serverSDK().protocol) !== "v1") throw ... and the settings entry is wrapped in <Show when={protocol() === "v1"}>, so it looks like the shipped 2.0.14 build predates that change.
  • Suggestion: either ship the save implementation in the build, or hide the entry point until it works — the UI currently offers an action that can never succeed.

Activity

  1. techysy commented on Sep 23, 2026

    @techysy

    Additional technical detail (root cause analysis)

    Confirming and extending #50650 from the opencode serve / web-UI side (not just Desktop).
    Environment: opencode 2.0.12, opencode serve + browser UI. Same unconditional throw.

    1. The stub is not protocol-independent in dev

    On the 2.0.12 release the handler is untouched:

    // packages/app/src/providers/credentials/dialog.tsx
    mutationFn: async (result) => {
      // TODO: Restore custom providers when V2 exposes config and arbitrary credential APIs.
      throw new Error(language.t("provider.custom.unavailable"))
    }

    On dev it has been partially implemented, but is gated behind a v1-only server check
    (packages/app/src/components/dialog-custom-provider.tsx:134):

    if ((await serverSDK().protocol) !== "v1") throw new Error(language.t("provider.custom.unavailable"))

    And packages/app/src/utils/server-protocol.ts probes for the legacy endpoint:

    const legacy = await probe(server, fetch, "/global/health").catch(() => undefined)
    if (legacy && "healthy" in legacy && legacy.healthy === true) return "v1"

    So the dialog can only ever work against a v1 server. That is consistent with the TODO, but it
    means the feature is not merely "not implemented yet" — it is actively gated off for v2 servers.

    2. The write APIs genuinely do not exist on a v2 server (verified)

    I enumerated every registered route in packages/server/src/handlers/ for 2.0.12:
    all of them are .get(), except one .put() on a session handler. There is no config write,
    and no auth.set.

    Probing a live opencode serve 2.0.12 instance:

    POST   /api/config    -> 404      PUT /api/config    -> 404
    POST   /api/auth      -> 404      PUT /api/auth      -> 404
    POST   /api/provider  -> 404      PUT /api/provider  -> 404
    

    So the TODO comment is accurate — there is no client-side workaround. This matches the
    reporter of #50650 observing that the app's own source references a serverSDK().client.auth.set(...)
    that has no counterpart on the server.

    3. There is also no opencode config write command as a fallback

    opencode config only exposes list / path. opencode config set … falls through to the default
    handler and just launches the TUI. So neither the UI nor the CLI can persist a custom provider.

    4. Workaround that does work

    Hand-editing the global config is unaffected — the engine loads it correctly on the same server
    where the dialog throws. Two substitutions are supported (packages/core/src/config/variable.ts):

    // ~/.config/opencode/opencode.json   (XDG_CONFIG_HOME/opencode/opencode.json)
    {
      "$schema": "https://opencode.ai/config.json",
      "provider": {
        "deepseek": {
          "npm": "@ai-sdk/openai-compatible",
          "name": "DeepSeek",
          "options": {
            "baseURL": "https://api.deepseek.com/v1",
            "apiKey": "{env:DEEPSEEK_API_KEY}"
          },
          "models": { "deepseek-chat": { "name": "DeepSeek Chat" } }
        }
      }
    }

    {env:VAR} reads from the process env, {file:path} from a file (supports ~/) — both are useful
    for keeping the key out of the config file.

    This is what the reporter meant by "works when added manually" — but it is worth stating explicitly
    that this is the only path today, because the protocol gate in dev means even a future v2
    implementation must add the missing server endpoints first.

    Suggested split

    • Server: add config write + credential write endpoints (/api/config, /api/auth).
    • App: then the existing dev implementation can drop the !== "v1" gate.

    Until the server side exists, the UI should probably hide the custom-provider entry rather than
    present a form that cannot be submitted — showing it implies the feature is available.

  2. jpike88 commented on Sep 23, 2026

    @jpike88

    welp. opencode is too unstable to use. moving on

  3. CengizKuscu commented on Sep 24, 2026

    @CengizKuscu

    Additional reproduction on v2.0.16 (macOS Desktop, Apple Silicon), plus a source-level note on the gated handler.

    1. Still broken in 2.0.16 — not just an old build

    The shipped desktop bundle still contains the unconditional stub. Extracted from the running server's own web assets (/_assets/dialog-CdLYz1aX.js):

    w=M(()=>({mutationFn:async e=>{throw Error(s.t(`provider.custom.unavailable`))}, ...}))

    No server or protocol condition. Every Submit fails with Custom providers are unavailable on this server, exactly as reported. So the "shipped build predates the fix" theory does not fully explain 2.0.16.

    2. The protocol === "v1" gate can never pass on the bundled server

    The current dev source gates the save on the detected server protocol:

    // packages/app/src/components/dialog-custom-provider.tsx
    if ((await serverSDK().protocol) !== "v1") throw new Error(language.t("provider.custom.unavailable"))

    detectServerProtocol (packages/app/src/utils/server-protocol.ts) probes /global/health and /api/health and requires a JSON body before returning "v1", otherwise it defaults to "v2".

    On the desktop app's own bundled server (OPENCODE_CLIENT=desktop, v2.0.16) those probes do not return JSON:

    Probe Result
    GET /global/health 200 but content-type: text/html → HTML catch-all page, so value is not an object
    GET /api/health 404 → rejected by !response.ok
    GET /api/info 200 application/json → works, but is not one of the probed paths

    detectServerProtocol therefore falls through to return "v2". Combined with the !== "v1" check, the custom-provider dialog can never succeed on the app's own bundled v2 server — even once the save implementation actually ships. The v1-only gate looks like a leftover from the compatibility layer rather than an intentional product decision, since the dialog's own copy points users at the OpenAI-compatible provider docs and the equivalent manual providers.<id> config works fine on that same server.

    3. Workaround (config file)

    Adding the block to ~/.config/opencode/opencode.json works correctly on the bundled server, including model limits and capabilities. Verified end-to-end with opencode run:

    {
      "$schema": "https://opencode.ai/config.json",
      "providers": {
        "myprovider": {
          "name": "My Provider",
          "env": ["MYPROVIDER_API_KEY"],
          "package": "@opencode/ai/providers/openai-compatible",
          "settings": {
            "baseURL": "http://localhost:20128/v1",
            "apiKey": "sk-..."
          },
          "models": {
            "my-model": {
              "modelID": "upstream/model-id",
              "name": "My Model",
              "limit": { "context": 200000, "output": 64000 },
              "capabilities": { "tools": true, "input": ["text", "image"], "output": ["text"] }
            }
          }
        }
      }
    }

    Suggest either shipping the save implementation without the v1 gate, or hiding the entry point (the dialog.provider.custom.label entry) on v2 servers so the UI does not offer an action that cannot succeed.

    Environment

    • opencode version: Desktop 2.0.16 (bundled CLI v2.0.16), macOS Apple Silicon (Darwin 27.0.0)
    • Install/channel: release, installed via the desktop app auto-updater
    • Terminal: Unavailable: Desktop (Electron) UI
    • Shell: /bin/zsh
    • Active plugins: none found in config
  4. AI-Agent-Tip-Line commented on Sep 27, 2026

    @AI-Agent-Tip-Line

    Experiencing the same issue. Very frustrating!

  5. alexisvalentino commented on Oct 3, 2026

    @alexisvalentino

    This issue still persists even after 2 weeks! Any updates?

  6. Mechful commented on Oct 4, 2026

    @Mechful

    Any updates about this problem?

    Image
  7. EverythingsComputer commented on Oct 8, 2026

    @EverythingsComputer

    yes this issue remains active in the latest version. I have rolled back to a previous month's version due to this - here's to hoping the devs give it some focus soon!

  8. davispuh commented on Oct 12, 2026

    @davispuh

    Same issue with opencode v2.0.25

  9. alexmocanu commented on Oct 15, 2026

    @alexmocanu

    Same issue with 2.0.29

  10. prigacv60 commented on Oct 17, 2026

    @prigacv60

    Still present on the latest release, 2.0.32 (Desktop on Windows 10.0.26100 x64; the bundled CLI and the local server both report 2.0.32).

    Filling the custom provider form in completely (provider ID, name, API base URL, model id) and submitting still fails with the same toast: "Request failed / Custom providers are unavailable on this server" (provider.custom.unavailable).

    Custom provider form fails with provider.custom.unavailable

    Two extra data points that may help whoever picks this up:

    • The 2.0.32 renderer bundle still ships the stub. In out/renderer/assets/dialog-DcarZ3_2.js the custom entry ($ = "_custom") renders form De, and its only mutationFn unconditionally does throw Error(r.t(`provider.custom.unavailable`)) — there is no capability or version check anywhere near it.
    • There is still nothing on the server to write to: GET /api/config is read-only, and PATCH /api/experimental/config accepts a single field, shell (Config.Patch schema). No endpoint for writing providers or credentials exists.

    Workaround for anyone else landing here — define the provider by hand in ~/.config/opencode/opencode.json, and the server picks it up at runtime, no restart needed:

    {
      "providers": {
        "my-provider": {
          "name": "My Provider",
          "package": "@opencode/ai/providers/openai-compatible",
          "settings": { "baseURL": "https://example.com/v1", "apiKey": "..." },
          "models": { "some-model-id": { "name": "Some Model" } }
        }
      }
    }
  11. AtmanActive commented on Oct 19, 2026

    @AtmanActive

    still broken on 2.0.32

  12. css521 commented on Oct 22, 2026

    @css521

    Proposed a fix in #57997, targeting dev. It replaces the unconditional error with a save flow that writes providers.<id> to the global JSONC config while preserving existing settings, stores literal API keys through the credential API, and refreshes the catalog so the new models become selectable. Success is reported after the models load; rejected writes keep the form available for retry.

    Local verification passed: repository lint/typechecks, the server suite (88 passed, 1 skipped), and the targeted browser regressions. Before/after screenshots and full validation details, including existing baseline failures, are in the PR. It is open for review.

  13. ahmadsbeh commented on Oct 23, 2026

    @ahmadsbeh

    Adding diagnostic details from Windows 11 (Desktop 2.1.4, latest channel) that may help reviewing the referenced PR #57997 — originally reported in #58533 (expected to close as duplicate):

    • GET /api/config is read-only, and in 2.1.4 PATCH /api/experimental/config accepts only { "shell": ... } (Config.Patch, additionalProperties: false) — so GUI clients have no way to persist a provider today. (Matches the "Public API impact" note in the PR description.)
    • The V2 server does load custom providers instantly once the config file has the V2 shape (providers.<id> with package: "@opencode/ai/providers/openai-compatible", settings.baseURL, models), verified on the same running server via GET /api/provider right after the write — no restart needed. File-watcher provider.updated events fire on save. So the polling approach in the PR should work for reload timing, though the bot review's concern about tying the form to reload timing is valid on Windows too.
    • One extra data point: after a failed save attempt from the dialog, selecting a catalog provider elsewhere works normally, so the failure is isolated to the custom-provider save path.

    Happy to help test a build with this fix if useful.

  14. opencodejr commented on Oct 28, 2026

    @opencodejr

    Thanks for the detailed report, @throny! I couldn't run the Desktop app here, but the source matches what you found.

    • In 2.0.14 (and still in 2.1.4), the custom provider form's save handler always throws provider.custom.unavailable, whatever server it is connected to. A TODO in the code says custom providers will come back once V2 has the config and credential APIs they need.
    • On the latest dev (fa1260d6c), and already in 2.2.0, that form has been removed from the desktop provider dialog (feat(app): match the TUI integration dialog in desktop (beta) #56877). The error should no longer appear there, but for now you can only add a custom OpenAI-compatible provider by editing opencode.json, as you did.
    • Open PR fix(app): save custom OpenAI-compatible providers #57997 would bring the form back with a working save.

    A maintainer with the Desktop app will need to confirm how it behaves in the current release.

  15. miaorunquan commented on Nov 2, 2026

    @miaorunquan

    Another reproduction on Desktop 2.2.5 (Windows)

    • OpenCode Desktop 2.2.5, bundled opencode-cli 2.2.5, Windows x64
    • Windows 10 Enterprise LTSC 21H2 (10.0.19044), 64-bit
    • Desktop (Electron) UI only, no terminal involved
    • Global config: ~/.config/opencode/opencode.json (legacy provider shape)

    Steps:

    1. Open Settings → Providers.
    2. There is no "Custom" / "Custom OpenAI-compatible provider" entry in the integration picker anymore. Providers already defined in the config file still load, and their models remain selectable, so the server side is fine.
    3. Those config-defined entries cannot be edited from the UI either — there is no action to change the base URL, API key, or model list.

    Expected: a way to add/edit an OpenAI-compatible provider from Settings. The 2.0.x dialog offered this entry (although its save always failed, see above); in the current release the entry is gone entirely.

    Notes:

    • Reproduced after upgrading 2.2.0 → 2.2.5.
    • The 2.2.5 renderer bundle still contains the settings.providers.tag.custom i18n key, but no provider.custom / custom-provider form code, which is consistent with the removal in feat(app): match the TUI integration dialog in desktop (beta) #56877.
    • Current workaround is hand-editing the global config; hot reload picks it up without a restart, but it is not discoverable from the app.

    Related: #57997 would restore the form with a working save, and #51527 tracks the missing credential path for custom provider IDs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions