import { createMemo, createSignal, onMount, Show } from "solid-js" import { useSync } from "../context/sync" import { map, pipe, sortBy } from "remeda" import { DialogSelect } from "../ui/dialog-select" import { useDialog } from "../ui/dialog" import { useSDK } from "../context/sdk" import { DialogPrompt } from "../ui/dialog-prompt" import { Link } from "../ui/link" import { useTheme } from "../context/theme" import { TextAttributes } from "@opentui/core" import type { ProviderAuthAuthorization, ProviderAuthMethod } from "@opencode-ai/sdk/v2" import { DialogModel } from "./dialog-model" import { useToast } from "../ui/toast" import { isConsoleManagedProvider } from "../util/provider-origin" import { useConnected } from "./use-connected" import { useBindings } from "../keymap" import { useClipboard } from "../context/clipboard" const PROVIDER_PRIORITY: Record = { opencode: 0, "opencode-go": 1, openai: 2, "github-copilot": 3, anthropic: 4, google: 5, } const CUSTOM_PROVIDER_OPTION_VALUE = "__opencode_custom_provider__" const CUSTOM_PROVIDER_ID = /^[a-z0-9][a-z0-9-_]*$/ type ProviderOptionBase = { title: string value: string description?: string category: string } type ProviderOption = | (ProviderOptionBase & { type: "provider" providerID: string }) | (ProviderOptionBase & { type: "custom" }) export function providerOptions(list: { id: string; name: string }[]): ProviderOption[] { return [ ...pipe( list, sortBy( (x) => PROVIDER_PRIORITY[x.id] ?? 99, (x) => x.name.toLowerCase(), (x) => x.id, ), map((provider) => ({ type: "provider" as const, title: provider.name, value: provider.id, providerID: provider.id, description: { opencode: "(Recommended)", anthropic: "(API key)", openai: "(ChatGPT Plus/Pro or API key)", "opencode-go": "Low cost subscription for everyone", }[provider.id], category: provider.id in PROVIDER_PRIORITY ? "Popular" : "Providers", })), ), { type: "custom", title: "Other", value: CUSTOM_PROVIDER_OPTION_VALUE, description: "Custom provider", category: "Providers", }, ] } export function normalizeCustomProviderID(value: string) { const providerID = value.trim().replace(/^@ai-sdk\//, "") if (!CUSTOM_PROVIDER_ID.test(providerID)) return return providerID } export function createDialogProviderOptions() { const sync = useSync() const dialog = useDialog() const sdk = useSDK() const toast = useToast() const { theme } = useTheme() const onboarded = useConnected() async function promptCustomProviderID(): Promise { const value = await DialogPrompt.show(dialog, "Other", { placeholder: "Provider id", description: () => ( This only stores a credential. Configure the provider in opencode.json to use it. ), }) if (value === null) return const providerID = normalizeCustomProviderID(value) if (providerID) return providerID toast.show({ variant: "error", message: "Provider ids must start with a lowercase letter or number and only use lowercase letters, numbers, hyphens, and underscores", }) return promptCustomProviderID() } const options = createMemo(() => { return pipe( providerOptions(sync.data.provider_next.all), map((provider) => { if (provider.type === "custom") { return { title: provider.title, value: provider.value, description: provider.description, category: provider.category, async onSelect() { const providerID = await promptCustomProviderID() if (!providerID) return return dialog.replace(() => ) }, } } const providerID = provider.providerID const consoleManaged = isConsoleManagedProvider(sync.data.console_state.consoleManagedProviders, providerID) const connected = sync.data.provider_next.connected.includes(providerID) return { title: provider.title, value: provider.value, description: provider.description, footer: consoleManaged ? sync.data.console_state.activeOrgName : undefined, category: provider.category, gutter: connected && onboarded() ? () => : undefined, async onSelect() { if (consoleManaged) return const methods = sync.data.provider_auth[providerID] ?? [ { type: "api", label: "API key", }, ] let index: number | null = 0 if (methods.length > 1) { index = await new Promise((resolve) => { dialog.replace( () => ( ({ title: x.label, value: index, }))} onSelect={(option) => resolve(option.value)} /> ), () => resolve(null), ) }) } if (index == null) return const method = methods[index] if (method.type === "oauth") { let inputs: Record | undefined if (method.prompts?.length) { const value = await PromptsMethod({ dialog, prompts: method.prompts, }) if (!value) return inputs = value } const result = await sdk.client.provider.oauth.authorize({ providerID, method: index, inputs, }) if (result.error) { toast.show({ variant: "error", message: JSON.stringify(result.error), }) dialog.clear() return } if (result.data?.method === "code") { dialog.replace(() => ( )) } if (result.data?.method === "auto") { dialog.replace(() => ( )) } } if (method.type === "api") { let metadata: Record | undefined if (method.prompts?.length) { const value = await PromptsMethod({ dialog, prompts: method.prompts }) if (!value) return metadata = value } return dialog.replace(() => ( )) } }, } }), ) }) return options } export function DialogProvider() { const options = createDialogProviderOptions() return } interface AutoMethodProps { index: number providerID: string title: string authorization: ProviderAuthAuthorization } function AutoMethod(props: AutoMethodProps) { const { theme } = useTheme() const sdk = useSDK() const dialog = useDialog() const sync = useSync() const toast = useToast() const clipboard = useClipboard() useBindings(() => ({ bindings: [ { key: "c", desc: "Copy provider code", group: "Dialog", cmd: () => { const code = props.authorization.instructions.match(/[A-Z0-9]{4}-[A-Z0-9]{4,5}/)?.[0] ?? props.authorization.url clipboard .write?.(code) .then(() => toast.show({ message: "Copied to clipboard", variant: "info" })) .catch(toast.error) }, }, ], })) onMount(async () => { const result = await sdk.client.provider.oauth.callback({ providerID: props.providerID, method: props.index, }) if (result.error) { toast.show({ variant: "error", message: "name" in result.error && result.error.name === "ProviderAuthOauthCallbackFailed" ? "OAuth authorization failed. Try /connect again." : JSON.stringify(result.error), }) dialog.clear() return } await sdk.client.instance.dispose() await sync.bootstrap() dialog.replace(() => ) }) return ( {props.title} dialog.clear()}> esc {props.authorization.instructions} Waiting for authorization... c copy ) } interface CodeMethodProps { index: number title: string providerID: string authorization: ProviderAuthAuthorization } function CodeMethod(props: CodeMethodProps) { const { theme } = useTheme() const sdk = useSDK() const sync = useSync() const dialog = useDialog() const [error, setError] = createSignal(false) return ( { const { error } = await sdk.client.provider.oauth.callback({ providerID: props.providerID, method: props.index, code: value, }) if (!error) { await sdk.client.instance.dispose() await sync.bootstrap() dialog.replace(() => ) return } setError(true) }} description={() => ( {props.authorization.instructions} Invalid code )} /> ) } interface ApiMethodProps { providerID: string title: string metadata?: Record custom?: boolean } function ApiMethod(props: ApiMethodProps) { const dialog = useDialog() const sdk = useSDK() const sync = useSync() const toast = useToast() const { theme } = useTheme() return ( OpenCode Zen gives you access to all the best coding models at the cheapest prices with a single API key. Go to https://opencode.ai/zen to get a key ), "opencode-go": ( OpenCode Go is a $10 per month subscription that provides reliable access to popular open coding models with generous usage limits. Go to https://opencode.ai/go and enable OpenCode Go ), }[props.providerID] ?? undefined } onConfirm={async (value) => { if (!value) return await sdk.client.auth.set({ providerID: props.providerID, auth: { type: "api", key: value, ...(props.metadata ? { metadata: props.metadata } : {}), }, }) await sdk.client.instance.dispose() await sync.bootstrap() if (props.custom && !sync.data.provider_next.all.some((provider) => provider.id === props.providerID)) { toast.show({ variant: "info", message: `Saved credential for ${props.providerID}. Configure it in opencode.json to use it.`, }) dialog.clear() return } dialog.replace(() => ) }} /> ) } interface PromptsMethodProps { dialog: ReturnType prompts: NonNullable[number][] } async function PromptsMethod(props: PromptsMethodProps) { const inputs: Record = {} for (const prompt of props.prompts) { if (prompt.when) { const value = inputs[prompt.when.key] if (value === undefined) continue const matches = prompt.when.op === "eq" ? value === prompt.when.value : value !== prompt.when.value if (!matches) continue } if (prompt.type === "select") { const value = await new Promise((resolve) => { props.dialog.replace( () => ( ({ title: x.label, value: x.value, description: x.hint, }))} onSelect={(option) => resolve(option.value)} /> ), () => resolve(null), ) }) if (value === null) return null inputs[prompt.key] = value continue } const value = await new Promise((resolve) => { props.dialog.replace( () => ( resolve(value)} /> ), () => resolve(null), ) }) if (value === null) return null inputs[prompt.key] = value } return inputs }