diff --git a/client/src/ee/pages/settings/platform/ai-providers/components/AiProviderForm.tsx b/client/src/ee/pages/settings/platform/ai-providers/components/AiProviderForm.tsx index 6e23130c40c..6f59ac7b001 100644 --- a/client/src/ee/pages/settings/platform/ai-providers/components/AiProviderForm.tsx +++ b/client/src/ee/pages/settings/platform/ai-providers/components/AiProviderForm.tsx @@ -1,6 +1,7 @@ import Button from '@/components/Button/Button'; import {Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage} from '@/components/ui/form'; import {Input} from '@/components/ui/input'; +import {AiProvider} from '@/ee/shared/middleware/platform/configuration'; import {useUpdateAiProviderMutation} from '@/ee/shared/mutations/platform/aiProvider.mutations'; import {AiProviderKeys} from '@/ee/shared/queries/platform/aiProviders.queries'; import {WorkflowNodeOptionKeys} from '@/shared/queries/platform/workflowNodeOptions.queries'; @@ -9,26 +10,28 @@ import {useQueryClient} from '@tanstack/react-query'; import {useForm} from 'react-hook-form'; import {z} from 'zod'; -const formSchema = z.object({ - apiKey: z.string().min(1, { - message: 'API Key is required.', - }), -}); - const AiProviderForm = ({ + aiProvider, environment, - id, onClose, showCancel = false, }: { + aiProvider: AiProvider; environment: number; - id: number; onClose: () => void; showCancel: boolean; }) => { + const isOllama = aiProvider.name?.toLowerCase() === 'ollama'; + + const formSchema = z.object({ + apiKey: isOllama ? z.string().optional() : z.string().min(1, {message: 'API Key is required.'}), + url: z.string().optional(), + }); + const form = useForm>({ defaultValues: { apiKey: '', + url: aiProvider.url ?? '', }, resolver: zodResolver(formSchema), }); @@ -51,33 +54,53 @@ const AiProviderForm = ({ function handleSubmit(values: z.infer) { updateAiProviderMutation.mutate({ environment, - id, - updateAiProviderRequest: { - apiKey: values.apiKey, - }, + id: aiProvider.id!, + updateAiProviderRequest: isOllama ? {url: values.url} : {apiKey: values.apiKey}, }); } return (
- ( - - API Key + {isOllama ? ( + ( + + Base URL - - - + + + - This is your AI provider'`s API key. + + The base URL of your Ollama server. Leave blank to use http://localhost:11434. + - - - )} - /> + + + )} + /> + ) : ( + ( + + API Key + + + + + + This is your AI provider's API key. + + + + )} + /> + )}
{showCancel && ( diff --git a/client/src/ee/pages/settings/platform/ai-providers/components/AiProviderList.test.tsx b/client/src/ee/pages/settings/platform/ai-providers/components/AiProviderList.test.tsx index 79efc20e1c7..34bc21208fe 100644 --- a/client/src/ee/pages/settings/platform/ai-providers/components/AiProviderList.test.tsx +++ b/client/src/ee/pages/settings/platform/ai-providers/components/AiProviderList.test.tsx @@ -1,6 +1,6 @@ import {AiProvider} from '@/ee/shared/middleware/platform/configuration'; import {createTestQueryClientWrapper} from '@/shared/util/test-utils'; -import {render, screen} from '@testing-library/react'; +import {fireEvent, render, screen} from '@testing-library/react'; import {ReactNode} from 'react'; import {describe, expect, it, vi} from 'vitest'; @@ -75,6 +75,45 @@ describe('AiProviderList', () => { expect(screen.getByText('Anthropic')).toBeInTheDocument(); }); + it('shows the Base URL row with the localhost default for Ollama when no URL is set', async () => { + const ollamaProviders: AiProvider[] = [ + { + enabled: false, + icon: '/icons/ollama.svg', + id: 3, + name: 'Ollama', + supportsEmbeddings: true, + }, + ]; + + renderWithProviders(); + + fireEvent.click(screen.getByText('Ollama')); + + expect(await screen.findByText('Base URL:')).toBeInTheDocument(); + expect(screen.getByText('http://localhost:11434')).toBeInTheDocument(); + }); + + it('shows the configured Base URL for Ollama', async () => { + const ollamaProviders: AiProvider[] = [ + { + enabled: true, + icon: '/icons/ollama.svg', + id: 3, + name: 'Ollama', + supportsEmbeddings: true, + url: 'http://remote-host:11434', + }, + ]; + + renderWithProviders(); + + fireEvent.click(screen.getByText('Ollama')); + + expect(await screen.findByText('Base URL:')).toBeInTheDocument(); + expect(screen.getByText('http://remote-host:11434')).toBeInTheDocument(); + }); + it('does not render the Embeddings badge when no provider supports embeddings', () => { const noEmbeddingProviders: AiProvider[] = [ { diff --git a/client/src/ee/pages/settings/platform/ai-providers/components/AiProviderList.tsx b/client/src/ee/pages/settings/platform/ai-providers/components/AiProviderList.tsx index edaf342bf48..e888c7f1100 100644 --- a/client/src/ee/pages/settings/platform/ai-providers/components/AiProviderList.tsx +++ b/client/src/ee/pages/settings/platform/ai-providers/components/AiProviderList.tsx @@ -13,6 +13,11 @@ import InlineSVG from 'react-inlinesvg'; import './AiProviderList.css'; +const isOllamaProvider = (aiProvider: AiProvider) => aiProvider.name?.toLowerCase() === 'ollama'; + +// Ollama runs locally and needs no API key, so it counts as configured; other providers require an API key. +const isConfigured = (aiProvider: AiProvider) => isOllamaProvider(aiProvider) || !!aiProvider.apiKey; + const AiProviderList = ({aiProviders, environment}: {aiProviders: AiProvider[]; environment: number}) => { const [enabledItems, setEnabledItems] = useState<{[key: number]: boolean}>({}); const [openItem, setOpenItem] = useState(); @@ -39,11 +44,13 @@ const AiProviderList = ({aiProviders, environment}: {aiProviders: AiProvider[]; setOpenItem(undefined); - if (!aiProvider.apiKey && value) { + const configured = isConfigured(aiProvider); + + if (!configured && value) { setOpenItem(`item-${aiProvider.id}`); } - if (aiProvider.apiKey) { + if (configured) { enableAiProviderMutation.mutate({ enable: value, environment, @@ -58,7 +65,7 @@ const AiProviderList = ({aiProviders, environment}: {aiProviders: AiProvider[]; aiProviders.forEach((aiProvider) => { enabledItems[aiProvider.id!] = !!aiProvider.enabled; - showForm[aiProvider.id!] = !aiProvider.apiKey; + showForm[aiProvider.id!] = !isConfigured(aiProvider); }); setEnabledItems(enabledItems); @@ -112,21 +119,27 @@ const AiProviderList = ({aiProviders, environment}: {aiProviders: AiProvider[]; {showForm[aiProvider.id!] ? ( setShowForm((prev) => ({ ...prev, [aiProvider.id!]: false, })) } - showCancel={!!aiProvider.apiKey} + showCancel={isConfigured(aiProvider)} /> ) : (
- API Key: + + {isOllamaProvider(aiProvider) ? 'Base URL: ' : 'API Key: '} + - {aiProvider.apiKey} + + {isOllamaProvider(aiProvider) + ? aiProvider.url || 'http://localhost:11434' + : aiProvider.apiKey} +