From 15d4e6fd87c091d27a934fcf99ca2242b23677b3 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Sun, 25 Jan 2026 15:59:56 -0600 Subject: [PATCH 1/7] wip(app): provider settings --- .../app/src/components/dialog-settings.tsx | 68 ++++---- .../app/src/components/settings-providers.tsx | 149 +++++++++++++++++- packages/app/src/i18n/en.ts | 10 ++ packages/opencode/src/server/server.ts | 30 ++++ packages/sdk/js/src/v2/gen/sdk.gen.ts | 32 ++++ packages/sdk/js/src/v2/gen/types.gen.ts | 29 ++++ packages/sdk/openapi.json | 50 ++++++ 7 files changed, 322 insertions(+), 46 deletions(-) diff --git a/packages/app/src/components/dialog-settings.tsx b/packages/app/src/components/dialog-settings.tsx index dbbc8fa7adc..0b8d108d0ac 100644 --- a/packages/app/src/components/dialog-settings.tsx +++ b/packages/app/src/components/dialog-settings.tsx @@ -39,16 +39,30 @@ export const DialogSettings: Component = () => { "padding-top": "12px", }} > - {language.t("settings.section.desktop")} -
- - - {language.t("settings.tab.general")} - - - - {language.t("settings.tab.shortcuts")} - +
+
+ {language.t("settings.section.desktop")} +
+ + + {language.t("settings.tab.general")} + + + + {language.t("settings.tab.shortcuts")} + +
+
+ +
+ {language.t("settings.section.server")} +
+ + + {language.t("settings.providers.title")} + +
+
@@ -56,31 +70,6 @@ export const DialogSettings: Component = () => { v{platform.version}
- {/* Server */} - {/* */} - {/* */} - {/* Permissions */} - {/* */} - {/* */} - {/* */} - {/* Providers */} - {/* */} - {/* */} - {/* */} - {/* Models */} - {/* */} - {/* */} - {/* */} - {/* Agents */} - {/* */} - {/* */} - {/* */} - {/* Commands */} - {/* */} - {/* */} - {/* */} - {/* MCP */} - {/* */} @@ -88,12 +77,9 @@ export const DialogSettings: Component = () => { - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} - {/* */} + + + {/* */} {/* */} {/* */} diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index 7b6ca193924..b175a570b0e 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -1,14 +1,153 @@ -import { Component } from "solid-js" +import { Button } from "@opencode-ai/ui/button" +import { useDialog } from "@opencode-ai/ui/context/dialog" +import { ProviderIcon } from "@opencode-ai/ui/provider-icon" +import { Tag } from "@opencode-ai/ui/tag" +import { showToast } from "@opencode-ai/ui/toast" +import type { IconName } from "@opencode-ai/ui/icons/provider" +import { popularProviders, useProviders } from "@/hooks/use-providers" +import { createMemo, type Component, For, Show } from "solid-js" import { useLanguage } from "@/context/language" +import { useGlobalSDK } from "@/context/global-sdk" +import { DialogConnectProvider } from "./dialog-connect-provider" +import { DialogSelectProvider } from "./dialog-select-provider" + +type ProviderSource = "env" | "api" | "config" | "custom" +type ProviderMeta = { source?: ProviderSource } export const SettingsProviders: Component = () => { + const dialog = useDialog() const language = useLanguage() + const globalSDK = useGlobalSDK() + const providers = useProviders() + + const connected = createMemo(() => providers.connected()) + const popular = createMemo(() => { + const items = providers.popular().slice() + items.sort((a, b) => popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)) + return items + }) + + const source = (item: unknown) => (item as ProviderMeta).source + + const disconnect = async (providerID: string, name: string) => { + await globalSDK.client.auth + .remove({ providerID }) + .then(async () => { + await globalSDK.client.global.dispose() + showToast({ + variant: "success", + icon: "circle-check", + title: language.t("provider.disconnect.toast.disconnected.title", { provider: name }), + description: language.t("provider.disconnect.toast.disconnected.description", { provider: name }), + }) + }) + .catch((err: unknown) => { + const message = err instanceof Error ? err.message : String(err) + showToast({ title: language.t("common.requestFailed"), description: message }) + }) + } return ( -
-
-

{language.t("settings.providers.title")}

-

{language.t("settings.providers.description")}

+
+
+
+
+

{language.t("settings.providers.title")}

+
+
+
+ +
+
+

{language.t("settings.providers.section.connected")}

+
+ 0} + fallback={ +
+ {language.t("settings.providers.connected.empty")} +
+ } + > + + {(item) => ( +
+
+ + {item.name} + + {language.t("settings.providers.tag.environment")} + + + {language.t("provider.connect.method.apiKey")} + +
+ + + +
+ )} +
+
+
+
+ +
+

{language.t("settings.providers.section.popular")}

+
+ + {(item) => ( +
+
+ + {item.name} + + {language.t("dialog.provider.tag.recommended")} + + +
{language.t("dialog.provider.anthropic.note")}
+
+ +
{language.t("dialog.provider.openai.note")}
+
+ +
{language.t("dialog.provider.copilot.note")}
+
+
+ +
+ )} +
+
+ + +
) diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index b32f0348551..a34c8ef2184 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -137,6 +137,9 @@ export const dict = { "provider.connect.toast.connected.title": "{{provider}} connected", "provider.connect.toast.connected.description": "{{provider}} models are now available to use.", + "provider.disconnect.toast.disconnected.title": "{{provider}} disconnected", + "provider.disconnect.toast.disconnected.description": "{{provider}} models are no longer available.", + "model.tag.free": "Free", "model.tag.latest": "Latest", "model.provider.anthropic": "Anthropic", @@ -159,6 +162,8 @@ export const dict = { "common.loading": "Loading", "common.loading.ellipsis": "...", "common.cancel": "Cancel", + "common.connect": "Connect", + "common.disconnect": "Disconnect", "common.submit": "Submit", "common.save": "Save", "common.saving": "Saving...", @@ -491,6 +496,7 @@ export const dict = { "sidebar.project.viewAllSessions": "View all sessions", "settings.section.desktop": "Desktop", + "settings.section.server": "Server", "settings.tab.general": "General", "settings.tab.shortcuts": "Shortcuts", @@ -599,6 +605,10 @@ export const dict = { "settings.providers.title": "Providers", "settings.providers.description": "Provider settings will be configurable here.", + "settings.providers.section.connected": "Connected providers", + "settings.providers.connected.empty": "No connected providers", + "settings.providers.section.popular": "Popular providers", + "settings.providers.tag.environment": "Environment", "settings.models.title": "Models", "settings.models.description": "Model settings will be configurable here.", "settings.agents.title": "Agents", diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index fa646f21ea8..302c5376d29 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -441,6 +441,36 @@ export namespace Server { return c.json(true) }, ) + .delete( + "/auth/:providerID", + describeRoute({ + summary: "Remove auth credentials", + description: "Remove authentication credentials", + operationId: "auth.remove", + responses: { + 200: { + description: "Successfully removed authentication credentials", + content: { + "application/json": { + schema: resolver(z.boolean()), + }, + }, + }, + ...errors(400), + }, + }), + validator( + "param", + z.object({ + providerID: z.string(), + }), + ), + async (c) => { + const providerID = c.req.valid("param").providerID + await Auth.remove(providerID) + return c.json(true) + }, + ) .get( "/event", describeRoute({ diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 67e7ac80cb9..d39dd2b3485 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -9,6 +9,8 @@ import type { AppLogResponses, AppSkillsResponses, Auth as Auth3, + AuthRemoveErrors, + AuthRemoveResponses, AuthSetErrors, AuthSetResponses, CommandListResponses, @@ -3054,6 +3056,36 @@ export class Formatter extends HeyApiClient { } export class Auth2 extends HeyApiClient { + /** + * Remove auth credentials + * + * Remove authentication credentials + */ + public remove( + parameters: { + providerID: string + directory?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "providerID" }, + { in: "query", key: "directory" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete({ + url: "/auth/{providerID}", + ...options, + ...params, + }) + } + /** * Set auth credentials * diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 38a52b325ad..9258bc0cde6 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -4867,6 +4867,35 @@ export type FormatterStatusResponses = { export type FormatterStatusResponse = FormatterStatusResponses[keyof FormatterStatusResponses] +export type AuthRemoveData = { + body?: never + path: { + providerID: string + } + query?: { + directory?: string + } + url: "/auth/{providerID}" +} + +export type AuthRemoveErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type AuthRemoveError = AuthRemoveErrors[keyof AuthRemoveErrors] + +export type AuthRemoveResponses = { + /** + * Successfully removed authentication credentials + */ + 200: boolean +} + +export type AuthRemoveResponse = AuthRemoveResponses[keyof AuthRemoveResponses] + export type AuthSetData = { body?: Auth path: { diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index a029d0ef0ed..8808bcf7d8c 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -5709,6 +5709,56 @@ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.auth.set({\n ...\n})" } ] + }, + "delete": { + "operationId": "auth.remove", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "providerID", + "schema": { + "type": "string" + }, + "required": true + } + ], + "summary": "Remove auth credentials", + "description": "Remove authentication credentials", + "responses": { + "200": { + "description": "Successfully removed authentication credentials", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.auth.remove({\n ...\n})" + } + ] } }, "/event": { From 2d12e9372fd0c68c6f26561eb7f1f70d6ab45240 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Sun, 25 Jan 2026 17:05:24 -0600 Subject: [PATCH 2/7] wip(app): provider settings --- .../app/src/components/settings-providers.tsx | 36 ++++++++++--------- packages/app/src/context/global-sync.tsx | 1 + packages/app/src/i18n/en.ts | 3 ++ 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index b175a570b0e..4d842037fa6 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -22,13 +22,28 @@ export const SettingsProviders: Component = () => { const connected = createMemo(() => providers.connected()) const popular = createMemo(() => { - const items = providers.popular().slice() + const connectedIDs = new Set(connected().map((p) => p.id)) + const items = providers + .popular() + .filter((p) => !connectedIDs.has(p.id)) + .slice() items.sort((a, b) => popularProviders.indexOf(a.id) - popularProviders.indexOf(b.id)) return items }) const source = (item: unknown) => (item as ProviderMeta).source + const type = (item: unknown) => { + const current = source(item) + if (current === "env") return language.t("settings.providers.tag.environment") + if (current === "api") return language.t("provider.connect.method.apiKey") + if (current === "config") return language.t("settings.providers.tag.config") + if (current === "custom") return language.t("settings.providers.tag.custom") + return language.t("settings.providers.tag.other") + } + + const canDisconnect = (item: unknown) => source(item) !== "env" + const disconnect = async (providerID: string, name: string) => { await globalSDK.client.auth .remove({ providerID }) @@ -48,14 +63,8 @@ export const SettingsProviders: Component = () => { } return ( -
-
+
+

{language.t("settings.providers.title")}

@@ -81,14 +90,9 @@ export const SettingsProviders: Component = () => {
{item.name} - - {language.t("settings.providers.tag.environment")} - - - {language.t("provider.connect.method.apiKey")} - + {type(item)}
- + diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 37d4d68912b..0da7cafa5f0 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -392,6 +392,7 @@ function createGlobalSync() { project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)), provider: () => sdk.provider.list().then((x) => { + console.log("provider", x) setStore("provider", normalizeProviderList(x.data!)) }), agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])), diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index a34c8ef2184..44bd40d0956 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -609,6 +609,9 @@ export const dict = { "settings.providers.connected.empty": "No connected providers", "settings.providers.section.popular": "Popular providers", "settings.providers.tag.environment": "Environment", + "settings.providers.tag.config": "Config", + "settings.providers.tag.custom": "Custom", + "settings.providers.tag.other": "Other", "settings.models.title": "Models", "settings.models.description": "Model settings will be configurable here.", "settings.agents.title": "Agents", From 70b2688a66bddc53ad038e9ebc667b937b2fa0af Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:40:28 -0600 Subject: [PATCH 3/7] fix(core): don't override source in custom provider loaders --- packages/opencode/src/provider/provider.ts | 23 +++++++++---------- .../opencode/test/provider/provider.test.ts | 7 +++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index fdd4ccdfb61..f898d3be430 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -854,10 +854,9 @@ export namespace Provider { // Load for the main provider if auth exists if (auth) { const options = await plugin.auth.loader(() => Auth.get(providerID) as any, database[plugin.auth.provider]) - mergeProvider(plugin.auth.provider, { - source: "custom", - options: options, - }) + const opts = options ?? {} + const patch: Partial = providers[providerID] ? { options: opts } : { source: "custom", options: opts } + mergeProvider(providerID, patch) } // If this is github-copilot plugin, also register for github-copilot-enterprise if auth exists @@ -870,10 +869,11 @@ export namespace Provider { () => Auth.get(enterpriseProviderID) as any, database[enterpriseProviderID], ) - mergeProvider(enterpriseProviderID, { - source: "custom", - options: enterpriseOptions, - }) + const opts = enterpriseOptions ?? {} + const patch: Partial = providers[enterpriseProviderID] + ? { options: opts } + : { source: "custom", options: opts } + mergeProvider(enterpriseProviderID, patch) } } } @@ -889,10 +889,9 @@ export namespace Provider { const result = await fn(data) if (result && (result.autoload || providers[providerID])) { if (result.getModel) modelLoaders[providerID] = result.getModel - mergeProvider(providerID, { - source: "custom", - options: result.options, - }) + const opts = result.options ?? {} + const patch: Partial = providers[providerID] ? { options: opts } : { source: "custom", options: opts } + mergeProvider(providerID, patch) } } diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 8a2009646e0..482587d8ac5 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -46,9 +46,10 @@ test("provider loaded from env variable", async () => { fn: async () => { const providers = await Provider.list() expect(providers["anthropic"]).toBeDefined() - // Note: source becomes "custom" because CUSTOM_LOADERS run after env loading - // and anthropic has a custom loader that merges additional options - expect(providers["anthropic"].source).toBe("custom") + // Provider should retain its connection source even if custom loaders + // merge additional options. + expect(providers["anthropic"].source).toBe("env") + expect(providers["anthropic"].options.headers["anthropic-beta"]).toBeDefined() }, }) }) From b9ea4d100871431c3630a7a3190cf42788505e07 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Sun, 25 Jan 2026 20:17:53 -0600 Subject: [PATCH 4/7] chore: cleanup --- packages/app/src/components/settings-providers.tsx | 13 +++++-------- packages/app/src/i18n/ar.ts | 2 +- packages/app/src/i18n/br.ts | 2 +- packages/app/src/i18n/da.ts | 2 +- packages/app/src/i18n/de.ts | 2 +- packages/app/src/i18n/en.ts | 2 +- packages/app/src/i18n/es.ts | 2 +- packages/app/src/i18n/fr.ts | 2 +- packages/app/src/i18n/ja.ts | 2 +- packages/app/src/i18n/ko.ts | 2 +- packages/app/src/i18n/no.ts | 2 +- packages/app/src/i18n/pl.ts | 2 +- packages/app/src/i18n/ru.ts | 2 +- packages/app/src/i18n/zh.ts | 2 +- packages/app/src/i18n/zht.ts | 2 +- 15 files changed, 19 insertions(+), 22 deletions(-) diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index 4d842037fa6..b10ac5d3905 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -63,12 +63,10 @@ export const SettingsProviders: Component = () => { } return ( -
+
-
-
-

{language.t("settings.providers.title")}

-
+
+

{language.t("settings.providers.title")}

@@ -127,7 +125,7 @@ export const SettingsProviders: Component = () => {