mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-22 16:25:58 +00:00
fix(desktop): expose Local / custom endpoint in Providers API-keys tab (#62818)
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>
This commit is contained in:
parent
b9267b50ea
commit
e7a8b374b7
7 changed files with 74 additions and 2 deletions
|
|
@ -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> = {}): 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(<ProvidersSettings onClose={vi.fn()} onViewChange={vi.fn()} view="keys" />)
|
||||
|
||||
const row = await screen.findByText('Local / custom endpoint')
|
||||
expect(screen.getByText(/OpenAI-compatible endpoint/)).toBeTruthy()
|
||||
|
||||
fireEvent.click(row)
|
||||
|
||||
await waitFor(() => expect(startManualLocalEndpoint).toHaveBeenCalledWith(null))
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<RowButton
|
||||
className="group grid grid-cols-[minmax(0,1fr)_auto] items-center gap-1 rounded-[6px] px-3 py-2.5 text-left transition-colors hover:bg-(--ui-control-hover-background)"
|
||||
onClick={() => onOpen(null)}
|
||||
>
|
||||
<div className="flex min-w-0 flex-col gap-0.5">
|
||||
<span className="truncate text-[length:var(--conversation-text-font-size)] font-semibold">{copy.title}</span>
|
||||
<span className="truncate text-[length:var(--conversation-caption-font-size)] leading-5 text-muted-foreground">
|
||||
{copy.description}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronRight className="size-4 text-muted-foreground transition group-hover:text-foreground" />
|
||||
</RowButton>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<SettingsContent>
|
||||
<LocalEndpointRow onOpen={startManualLocalEndpoint} />
|
||||
{keyGroups.length > 0 ? (
|
||||
<div className="grid gap-3">
|
||||
<SearchField
|
||||
|
|
|
|||
|
|
@ -766,6 +766,10 @@ export const en: Translations = {
|
|||
noProviderKeys: 'No provider API keys available.',
|
||||
searchKeys: 'Search providers…',
|
||||
noKeysMatch: 'No providers match your search.',
|
||||
localEndpoint: {
|
||||
title: 'Local / custom endpoint',
|
||||
description: 'Point Hermes at any OpenAI-compatible endpoint (Zyphra, vLLM, llama.cpp, Ollama, etc).'
|
||||
},
|
||||
loading: 'Loading providers...'
|
||||
},
|
||||
sessions: {
|
||||
|
|
|
|||
|
|
@ -799,6 +799,10 @@ export const ja = defineLocale({
|
|||
noProviderKeys: '利用可能なプロバイダー API キーがありません。',
|
||||
searchKeys: 'プロバイダーを検索…',
|
||||
noKeysMatch: '一致するプロバイダーがありません。',
|
||||
localEndpoint: {
|
||||
title: 'ローカル / カスタムエンドポイント',
|
||||
description: 'OpenAI 互換のエンドポイント(Zyphra、vLLM、llama.cpp、Ollama など)を指定します。'
|
||||
},
|
||||
loading: 'プロバイダーを読み込み中...'
|
||||
},
|
||||
sessions: {
|
||||
|
|
|
|||
|
|
@ -657,6 +657,10 @@ export interface Translations {
|
|||
noProviderKeys: string
|
||||
searchKeys: string
|
||||
noKeysMatch: string
|
||||
localEndpoint: {
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
loading: string
|
||||
}
|
||||
sessions: {
|
||||
|
|
|
|||
|
|
@ -775,6 +775,10 @@ export const zhHant = defineLocale({
|
|||
noProviderKeys: '沒有可用的提供方 API 金鑰。',
|
||||
searchKeys: '搜尋提供方…',
|
||||
noKeysMatch: '沒有符合的提供方。',
|
||||
localEndpoint: {
|
||||
title: '本地 / 自訂端點',
|
||||
description: '將 Hermes 指向任意 OpenAI 相容端點(Zyphra、vLLM、llama.cpp、Ollama 等)。'
|
||||
},
|
||||
loading: '正在載入提供方...'
|
||||
},
|
||||
sessions: {
|
||||
|
|
|
|||
|
|
@ -953,6 +953,10 @@ export const zh: Translations = {
|
|||
noProviderKeys: '没有可用的提供方 API 密钥。',
|
||||
searchKeys: '搜索提供方…',
|
||||
noKeysMatch: '没有匹配的提供方。',
|
||||
localEndpoint: {
|
||||
title: '本地 / 自定义端点',
|
||||
description: '将 Hermes 指向任意 OpenAI 兼容端点(Zyphra、vLLM、llama.cpp、Ollama 等)。'
|
||||
},
|
||||
loading: '正在加载提供方...'
|
||||
},
|
||||
sessions: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue