From e7a8b374b780ee477d2d42eb183411c2a8d82657 Mon Sep 17 00:00:00 2001 From: David Metcalfe Date: Fri, 17 Jul 2026 15:58:37 -0700 Subject: [PATCH] fix(desktop): expose Local / custom endpoint in Providers API-keys tab (#62818) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The onboarding overlay already contains a 'Local / custom endpoint' card that writes model.provider:custom + base_url + api_key, but no reachable Desktop GUI path opens it for a fresh add. The composer model pill falls back to the gateway menu panel (Edit Models…), and Settings → Providers → API keys is env-var-driven and never lists a custom endpoint — so users following their instincts cannot add an OpenAI-compatible endpoint (Zyphra, vLLM, Ollama, …) from the GUI. Add a 'Local / custom endpoint' row to the API-keys tab that calls startManualLocalEndpoint(), landing the overlay directly on the existing custom-endpoint form. Reuses the tested onboarding flow; no new UI surface. Regression test in providers-settings.test.tsx asserts the row renders and opens the custom-endpoint flow. Fixes #62817 Co-authored-by: David Metcalfe <80915+DavidMetcalfe@users.noreply.github.com> --- .../app/settings/providers-settings.test.tsx | 23 ++++++++++++- .../src/app/settings/providers-settings.tsx | 33 ++++++++++++++++++- apps/desktop/src/i18n/en.ts | 4 +++ apps/desktop/src/i18n/ja.ts | 4 +++ apps/desktop/src/i18n/types.ts | 4 +++ apps/desktop/src/i18n/zh-hant.ts | 4 +++ apps/desktop/src/i18n/zh.ts | 4 +++ 7 files changed, 74 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/app/settings/providers-settings.test.tsx b/apps/desktop/src/app/settings/providers-settings.test.tsx index ac63be962ae2..0a1da7c45449 100644 --- a/apps/desktop/src/app/settings/providers-settings.test.tsx +++ b/apps/desktop/src/app/settings/providers-settings.test.tsx @@ -8,6 +8,7 @@ const listOAuthProviders = vi.fn() const disconnectOAuthProvider = vi.fn() const getEnvVars = vi.fn() const startManualProviderOAuth = vi.fn() +const startManualLocalEndpoint = vi.fn() const onboarding = atom({ manual: false }) vi.mock('@/hermes', () => ({ @@ -18,7 +19,8 @@ vi.mock('@/hermes', () => ({ vi.mock('@/store/onboarding', () => ({ $desktopOnboarding: onboarding, - startManualProviderOAuth: (providerId: string) => startManualProviderOAuth(providerId) + startManualProviderOAuth: (providerId: string) => startManualProviderOAuth(providerId), + startManualLocalEndpoint: (reason: null | string) => startManualLocalEndpoint(reason) })) function provider(id: string, loggedIn: boolean, patch: Partial = {}): OAuthProvider { @@ -182,4 +184,23 @@ describe('ProvidersSettings', () => { }) expect(await screen.findByText('No providers match your search.')).toBeTruthy() }) + + it('offers a Local / custom endpoint entry in the API-keys tab that opens the custom-endpoint flow', async () => { + // Regression: the composer pill and the providers "have an API key" + // affordance both dead-end on the env-var-driven key catalog, which never + // lists a custom endpoint — so without this row there is no reachable + // Desktop GUI path to add one. See issue #62817. + getEnvVars.mockResolvedValue({}) + listOAuthProviders.mockResolvedValue({ providers: [] }) + + const { ProvidersSettings } = await import('./providers-settings') + render() + + const row = await screen.findByText('Local / custom endpoint') + expect(screen.getByText(/OpenAI-compatible endpoint/)).toBeTruthy() + + fireEvent.click(row) + + await waitFor(() => expect(startManualLocalEndpoint).toHaveBeenCalledWith(null)) + }) }) diff --git a/apps/desktop/src/app/settings/providers-settings.tsx b/apps/desktop/src/app/settings/providers-settings.tsx index c316cb0fc1e4..7b72c604fa12 100644 --- a/apps/desktop/src/app/settings/providers-settings.tsx +++ b/apps/desktop/src/app/settings/providers-settings.tsx @@ -21,7 +21,7 @@ import { Check, ChevronDown, ChevronRight, KeyRound, Loader2, Terminal, Trash2 } import { normalize } from '@/lib/text' import { cn } from '@/lib/utils' import { notify, notifyError } from '@/store/notifications' -import { $desktopOnboarding, startManualProviderOAuth } from '@/store/onboarding' +import { $desktopOnboarding, startManualLocalEndpoint, startManualProviderOAuth } from '@/store/onboarding' import type { EnvVarInfo, OAuthProvider } from '@/types/hermes' import { isKeyVar, ProviderKeyRows } from './credential-key-ui' @@ -300,6 +300,36 @@ function NoProviderKeys() { ) } +// Surfaces the "Local / custom endpoint" entry point directly in the API-keys +// tab so users can add any OpenAI-compatible endpoint (Zyphra, vLLM, Ollama…) +// from the GUI. The composer pill and the providers "have an API key" affordance +// both dead-end on the env-var-driven key catalog, which never lists a custom +// endpoint — so without this row there is no reachable Desktop path to it. +// The whole row is the button so the click target and a11y focus match the +// visible area (the chevron + gutter are inside the button, not beside it). +// Pass reason: null — the onboarding overlay renders an unmapped reason string +// verbatim as a banner (see ReasonNotice in onboarding/index.tsx), and we don't +// want a raw identifier like "providers-keys-tab" showing as literal text. +function LocalEndpointRow({ onOpen }: { onOpen: (reason: null | string) => void }) { + const { t } = useI18n() + const copy = t.settings.providers.localEndpoint + + return ( + onOpen(null)} + > +
+ {copy.title} + + {copy.description} + +
+ +
+ ) +} + export function ProvidersSettings({ onClose, onViewChange, view }: ProvidersSettingsProps) { const { t } = useI18n() const { rowProps, vars } = useEnvCredentials() @@ -417,6 +447,7 @@ export function ProvidersSettings({ onClose, onViewChange, view }: ProvidersSett return ( + {keyGroups.length > 0 ? (