import { Button } from "@opencode-ai/ui/button" import { useDialog } from "@opencode-ai/ui/context/dialog" import { Dialog } from "@opencode-ai/ui/dialog" import { IconButton } from "@opencode-ai/ui/icon-button" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { useMutation } from "@tanstack/solid-query" import { TextField } from "@opencode-ai/ui/text-field" import { showToast } from "@/utils/toast" import { batch, For } from "solid-js" import { createStore, produce } from "solid-js/store" import { ExternalLink } from "@/components/external-link" import { useServerSDK } from "@/context/server-sdk" import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" import { type FormState, headerRow, modelRow, validateCustomProvider } from "./dialog-custom-provider-form" type Props = { onBack: () => void } export function DialogCustomProvider(props: Props) { const language = useLanguage() return ( } transition > ) } export function CustomProviderForm(props: { autofocus?: boolean } = {}) { const dialog = useDialog() const serverSync = useServerSync() const serverSDK = useServerSDK() const language = useLanguage() const [form, setForm] = createStore({ providerID: "", name: "", baseURL: "", apiKey: "", models: [modelRow()], headers: [headerRow()], err: {}, }) const addModel = () => { setForm( "models", produce((rows) => { rows.push(modelRow()) }), ) } const removeModel = (index: number) => { if (form.models.length <= 1) return setForm( "models", produce((rows) => { rows.splice(index, 1) }), ) } const addHeader = () => { setForm( "headers", produce((rows) => { rows.push(headerRow()) }), ) } const removeHeader = (index: number) => { if (form.headers.length <= 1) return setForm( "headers", produce((rows) => { rows.splice(index, 1) }), ) } const setField = (key: "providerID" | "name" | "baseURL" | "apiKey", value: string) => { setForm(key, value) if (key === "apiKey") return setForm("err", key, undefined) } const setModel = (index: number, key: "id" | "name", value: string) => { batch(() => { setForm("models", index, key, value) setForm("models", index, "err", key, undefined) }) } const setHeader = (index: number, key: "key" | "value", value: string) => { batch(() => { setForm("headers", index, key, value) setForm("headers", index, "err", key, undefined) }) } const validate = () => { const output = validateCustomProvider({ form, t: language.t, disabledProviders: serverSync().data.config.disabled_providers ?? [], existingProviderIDs: new Set(serverSync().data.provider.all.keys()), }) batch(() => { setForm("err", output.err) output.models.forEach((err, index) => setForm("models", index, "err", err)) output.headers.forEach((err, index) => setForm("headers", index, "err", err)) }) return output.result } const saveMutation = useMutation(() => ({ mutationFn: async (result: NonNullable>) => { if ((await serverSDK().protocol) !== "v1") throw new Error("Custom providers are unavailable on this server") const disabledProviders = serverSync().data.config.disabled_providers ?? [] const nextDisabled = disabledProviders.filter((id) => id !== result.providerID) if (result.key) { await serverSDK().client.auth.set({ providerID: result.providerID, auth: { type: "api", key: result.key, }, }) } await serverSync().updateConfig({ provider: { [result.providerID]: result.config }, disabled_providers: nextDisabled, }) return result }, onSuccess: (result) => { dialog.close() showToast({ variant: "success", icon: "circle-check", title: language.t("provider.connect.toast.connected.title", { provider: result.name }), description: language.t("provider.connect.toast.connected.description", { provider: result.name }), }) }, onError: (err) => { const message = err instanceof Error ? err.message : String(err) showToast({ title: language.t("common.requestFailed"), description: message }) }, })) const save = (e: SubmitEvent) => { e.preventDefault() if (saveMutation.isPending) return const result = validate() if (!result) return saveMutation.mutate(result) } return (
{language.t("provider.custom.title")}

{language.t("provider.custom.description.prefix")} {language.t("provider.custom.description.link")} {language.t("provider.custom.description.suffix")}

setField("providerID", v)} validationState={form.err.providerID ? "invalid" : undefined} error={form.err.providerID} /> setField("name", v)} validationState={form.err.name ? "invalid" : undefined} error={form.err.name} /> setField("baseURL", v)} validationState={form.err.baseURL ? "invalid" : undefined} error={form.err.baseURL} /> setField("apiKey", v)} />
{(m, i) => (
setModel(i(), "id", v)} validationState={m.err.id ? "invalid" : undefined} error={m.err.id} />
setModel(i(), "name", v)} validationState={m.err.name ? "invalid" : undefined} error={m.err.name} />
removeModel(i())} disabled={form.models.length <= 1} aria-label={language.t("provider.custom.models.remove")} />
)}
{(h, i) => (
setHeader(i(), "key", v)} validationState={h.err.key ? "invalid" : undefined} error={h.err.key} />
setHeader(i(), "value", v)} validationState={h.err.value ? "invalid" : undefined} error={h.err.value} />
removeHeader(i())} disabled={form.headers.length <= 1} aria-label={language.t("provider.custom.headers.remove")} />
)}
) }