Skip to content

fix(app): save custom OpenAI-compatible providers - #57997

Open
css521 wants to merge 3 commits into
anomalyco:devfrom
css521:custom-provider-save
Open

css521 wants to merge 3 commits into
anomalyco:devfrom
css521:custom-provider-save

Conversation

@css521

@css521 css521 commented Oct 22, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #50650

Type of change

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

What does this PR do?

Restore custom OpenAI-compatible provider setup in the current integration picker and fix the failed submission reported in #50650. Save the provider definition in global JSONC while preserving other settings. Store literal API keys through the credential API and clean them up if the config write fails.

Wait for the configured models to load before reporting a connection. Rejected writes keep the form available to retry; saved configurations whose models do not load get a separate message.

How did you verify your code works?

  • bun run check: lint and all 35 package checks passed. App and client bun run typecheck:tsc also passed.
  • App form/save tests: 7 passed. Server config tests: 3 passed, including JSONC preservation, credential binding and reloading an already-open directory.
  • Provider browser cases: 15 passed. The 3 custom-provider cases passed 30/30 repetitions on a fresh build with 5 workers and no retries.
  • bun run check:generated: regeneration produces no diff.

The fully parallel settings suite passed 46/53. All seven shortcut/activity-layout failures also reproduced on unmodified dev at 86ce19d96e.

Visual

<svg xmlns="http://www.w3.org/2000/svg" width="240" height="72" viewBox="0 0 240 72" fill="none" role="img" aria-label="Save a custom provider and load its models">
  <style>
    .flow { animation: flow 1.8s linear infinite; }
    .ready { animation: ready 3.6s ease-in-out infinite; }
    @keyframes flow { to { stroke-dashoffset: -18; } }
    @keyframes ready { 0%, 100% { opacity: 1; } 50% { opacity: .45; } }
    @media (prefers-reduced-motion: reduce) { .flow, .ready { animation: none; } }
  </style>
  <g stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
    <rect x="8" y="10" width="74" height="52" rx="7" opacity=".45"/>
    <path d="M19 23h31M19 33h51M19 43h26" opacity=".55"/>
    <rect x="55" y="43" width="15" height="9" rx="3" opacity=".75"/>
    <path class="flow" d="M94 36h36" stroke-dasharray="4 5" opacity=".7"/>
    <path d="m124 30 6 6-6 6" opacity=".7"/>
    <rect x="146" y="16" width="86" height="40" rx="7" opacity=".45"/>
    <path d="M182 30h37M182 40h27" opacity=".6"/>
    <g class="ready">
      <circle cx="163" cy="36" r="8" opacity=".6"/>
      <path d="m159 36 3 3 6-6"/>
    </g>
  </g>
</svg>

Screenshots / recordings

Captured with local test fixtures. The before image shows the reported failure; the after image shows the saved model in the current picker.

Reported failure Current model picker
Submission fails Saved provider model is available

Checklist

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

Added by the opencode bot (AI-generated).

Related: #51527, #57365, #51031, #57325

Public API: makes shell optional and adds providers to the PATCH /api/experimental/config patch (and config.update in @opencode/client); backwards compatible

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The server side of the fix works. Run from source, the base rejects a providers-only config patch with 400 (Missing key ["shell"]), and the form always throws provider.custom.unavailable. With this change the same credential + config patch writes the provider to the global JSONC (comments and other keys kept, no key in the file), and the provider and model show up as enabled in /api/provider and /api/model within a few seconds. The credentials tests and the server config test pass, and app, core, schema and client typecheck. I didn't drive the desktop/web form itself, so the UI part relies on your screenshots and the unit tests.

Public API impact, all backwards compatible:

  • Config.Patch / PATCH /api/experimental/config: shell goes from required to optional. A new optional providers record (ConfigProvider.Info by ID) replaces each named entry in the global document.
  • @opencode/client: config.update(input) becomes config.update(input?), and ConfigUpdateInput gains providers. The effect client now exports ConfigProvider types.
  • Existing callers that send { shell } keep working.

Concerns are inline: the 10s polling loop in the form, duplicate credentials when retrying after a 'saved but not loaded' result, and the size and timing dependence of the new server test. For maintainers: #52653 took the same approach (native provider config plus a separate credential) and was closed without a comment, so whether this design is wanted is worth confirming.

Related: #51527, #57365, #51031, #57325

return output.result
}

const refresh = async (providerID: string, modelIDs: string[]) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The form re-reads the catalog every 250 ms for up to 10s, waiting for the server's debounced reload after the write. This ties the UI to server reload timing: a slow or large config reload shows 'saved, but its models could not be loaded' even though nothing is wrong. Could Config.update wait for the reload it requests (or the endpoint answer only once the new config is applied)? Then the form could refresh once instead of polling.

*/
export async function saveCustomProvider(api: ServerApi, provider: CustomProvider) {
const credential = provider.key
? await api.credential.create({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the write succeeds but the models never load, the dialog closes and the provider is not in data.location.provider.list(). If the user opens the form again with the same ID, the duplicate check passes, a second credential is created for the same integration and the config entry is overwritten. Could the save reuse or replace an existing credential for providerID, or the duplicate check also look at the config/integration?

)

it.live(
"saves a custom provider that becomes a selectable provider and model",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is about 160 lines, runs the real file watcher and polls for up to 15s across several endpoints, so it is slow and timing-dependent. The core behaviour is "a providers patch keeps unrelated JSONC and replaces just the named entry", which could be a short test like the existing shell one. The catalog/integration round-trip could be dropped or cut down to one check.

This branch has not been deployed

No deployments
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.

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

1 participant