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 "@opencode-ai/ui/toast" import { batch, For } from "solid-js" import { createStore, produce } from "solid-js/store" 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 { DialogSelectProvider } from "./dialog-select-provider" type Props = { back?: "providers" | "close" } export function DialogCustomProvider(props: Props) { const dialog = useDialog() const globalSync = useGlobalSync() const globalSDK = useGlobalSDK() const language = useLanguage() const [form, setForm] = createStore({ providerID: "", name: "", baseURL: "", apiKey: "", models: [modelRow()], headers: [headerRow()], err: {}, }) const goBack = () => { if (props.back === "close") { dialog.close() return } dialog.show(() => ) } 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: globalSync.data.config.disabled_providers ?? [], existingProviderIDs: new Set(globalSync.data.provider.all.map((p) => p.id)), }) 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>) => { const disabledProviders = globalSync.data.config.disabled_providers ?? [] const nextDisabled = disabledProviders.filter((id) => id !== result.providerID) if (result.key) { await globalSDK.client.auth.set({ providerID: result.providerID, auth: { type: "api", key: result.key, }, }) } await globalSync.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 ( } transition >
{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")} />
)}
) }