#18220 · @neversaywanan · opened Mar 19, 2026 at 10:06 AM UTC · last updated Mar 21, 2026 at 8:27 AM UTC

feat: allow editing custom providers from model settings

appfeat
48
+153124 files

Score breakdown

Impact

8.0

Clarity

8.0

Urgency

4.0

Ease Of Review

4.0

Guidelines

6.0

Readiness

4.0

Size

4.0

Trust

5.0

Traction

2.0

Summary

This PR implements an edit flow for custom providers, allowing users to modify existing configurations directly from model settings. The feature addresses a clear user pain point but is incomplete without UI screenshots for review.

Open in GitHub

Description

Issue for this PR

Closes #18224

Type of change

  • [ ] Bug fix
  • [x] New feature
  • [ ] Refactor / code improvement
  • [ ] Documentation

What does this PR do?

Adds an edit flow for custom providers from the model settings page.

Custom providers now show an Edit action in Settings > Models, which opens the existing custom provider dialog in edit mode. The form is prefilled from the current provider config, keeps the provider ID locked while editing, and updates the saved config without failing duplicate ID validation for the provider being edited.

When models are removed during editing, they are moved into the provider blacklist so hidden models stay hidden instead of reappearing when the provider is saved again.

How did you verify your code works?

  • Ran bun test --preload ./happydom.ts ./src/components/dialog-custom-provider.test.ts from packages/app
  • Verified editing an existing provider does not fail duplicate ID validation
  • Verified removed models are added to the blacklist during edit saves
  • Verified blacklisted models stay hidden when the edit form is seeded
  • Manually tested the custom provider flow locally from the model settings page

Screenshots / recordings

TODO: add before/after screenshots or a short recording of editing a custom provider from Settings > Models

https://github.com/user-attachments/assets/3c1d9bc6-067a-498e-8cf6-f91e6fad3279

Checklist

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

Linked Issues

#18224 [FEATURE]:allow editing custom providers from model settings

View issue

Comments

No comments.

Changed Files

packages/app/src/components/dialog-custom-provider-form.ts

+373
@@ -47,6 +47,18 @@ type ValidateArgs = {
t: Translator
disabledProviders: string[]
existingProviderIDs: Set<string>
editProviderID?: string
}
type BlacklistArgs = {
prevModels: string[]
prevBlacklist: string[]
nextModels: string[]
}
type VisibleArgs = {
models: Record<string, { name?: string } | undefined>
blacklist: string[]
}
export function validateCustomProvider(input: ValidateArgs) {
@@ -74,7 +86,7 @@ export function validateCustomProvider(input: ValidateArgs) {
const disabled = input.disabledProviders.includes(providerID)
const existsError = idError
? undefined
: input.existingProviderIDs.has(providerID) && !disabled
: input.existingProviderIDs.has(providerID) && !disabled && input.editProviderID !== providerID
? input.t("provider.custom.error.providerID.exists")
: undefined
@@ -151,9 +163,31 @@ export function validateCustomProvider(input: ValidateArgs) {
}
}
export function nextBlacklist(input: BlacklistArgs) {
const next = new Set(input.nextModels)
const removed = input.prevModels.filter((id) => !next.has(id))
const kept = input.prevBlacklist.filter((id) => !next.has(id))
return

packages/app/src/components/dialog-custom-provider.test.ts

+451
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import { validateCustomProvider } from "./dialog-custom-provider-form"
import { nextBlacklist, validateCustomProvider, visibleModels } from "./dialog-custom-provider-form"
const t = (key: string) => key
@@ -79,4 +79,48 @@ describe("validateCustomProvider", () => {
value: undefined,
})
})
test("allows editing existing provider id", () => {
const result = validateCustomProvider({
form: {
providerID: "custom-provider",
name: "Provider",
baseURL: "https://api.example.com",
apiKey: "",
models: [{ row: "m0", id: "model-a", name: "Model A", err: {} }],
headers: [{ row: "h0", key: "", value: "", err: {} }],
saving: false,
err: {},
},
t,
disabledProviders: [],
existingProviderIDs: new Set(["custom-provider"]),
editProviderID: "custom-provider",
})
expect(result.result?.providerID).toBe("custom-provider")
expect(result.err.providerID).toBeUndefined()
})
test("adds removed models to blacklist in edit mode", () => {
const out = nextBlacklist({
prev

packages/app/src/components/dialog-custom-provider.tsx

+528
@@ -11,26 +11,53 @@ import { Link } from "@/components/link"
import { useGlobalSDK } from "@/context/global-sdk"
import { useGlobalSync } from "@/context/global-sync"
import { useLanguage } from "@/context/language"
import { type FormState, headerRow, modelRow, validateCustomProvider } from "./dialog-custom-provider-form"
import {
type FormState,
headerRow,
modelRow,
nextBlacklist,
validateCustomProvider,
visibleModels,
} from "./dialog-custom-provider-form"
import { DialogSelectProvider } from "./dialog-select-provider"
type Props = {
back?: "providers" | "close"
providerID?: string
}
export function DialogCustomProvider(props: Props) {
const dialog = useDialog()
const globalSync = useGlobalSync()
const globalSDK = useGlobalSDK()
const language = useLanguage()
const edit = () => (props.providerID ? globalSync.data.config.provider?.[props.providerID] : undefined)
const seed = () => {
const id = props.providerID ?? ""
const cfg = edit()
const provider = props.providerID ? globalSync.data.provider.all.find((x) => x.id === props.providerID) : undefined
const models = visibleModels({ models: cfg?.models ?? {},

packages/app/src/components/settings-models.tsx

+190
@@ -3,12 +3,16 @@ import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { Switch } from "@opencode-ai/ui/switch"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Button } from "@opencode-ai/ui/button"
import { TextField } from "@opencode-ai/ui/text-field"
import { type Component, For, Show } from "solid-js"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { useLanguage } from "@/context/language"
import { useGlobalSync } from "@/context/global-sync"
import { useModels } from "@/context/models"
import { popularProviders } from "@/hooks/use-providers"
import { SettingsList } from "./settings-list"
import { DialogCustomProvider } from "./dialog-custom-provider"
type ModelItem = ReturnType<ReturnType<typeof useModels>["list"]>[number]
@@ -33,7 +37,10 @@ const ListEmptyState: Component<{ message: string; filter: string }> = (props) =
export const SettingsModels: Component = () => {
const language = useLanguage()
const dialog = useDialog()
const globalSync = useGlobalSync()
const models = useModels()
const custom = (id: string) => globalSync.data.config.provider