Skip to content

Commit 60ef1e8

Browse files
committed
Add support for Ollama provider #951
1 parent dd89b94 commit 60ef1e8

17 files changed

+666
-188
lines changed

browser/data-browser/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"ai": "^4.3.16",
3636
"downshift": "^9.0.9",
3737
"emoji-mart": "^5.6.0",
38+
"ollama-ai-provider": "^1.2.0",
3839
"polished": "^4.3.1",
3940
"prismjs": "^1.29.0",
4041
"query-string": "^7.1.3",

browser/data-browser/src/components/AI/AISettings.tsx

Lines changed: 104 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { transition } from '../../helpers/transition';
88
import { useSettings } from '../../helpers/AppSettings';
99
import { useEffect, useState } from 'react';
1010
import { OpenRouterLoginButton } from './OpenRouterLoginButton';
11+
import { TabPanel, Tabs } from '../Tabs';
12+
import { effectFetch } from '../../helpers/effectFetch';
1113

1214
interface CreditUsage {
1315
total: number;
@@ -16,6 +18,17 @@ interface CreditUsage {
1618

1719
const CREDITS_ENDPOINT = 'https://openrouter.ai/api/v1/credits';
1820

21+
const PROVIDER_TABS = [
22+
{
23+
label: 'OpenRouter',
24+
value: 'openrouter',
25+
},
26+
{
27+
label: 'Ollama',
28+
value: 'ollama',
29+
},
30+
];
31+
1932
const AISettings: React.FC = () => {
2033
const {
2134
enableAI,
@@ -26,6 +39,8 @@ const AISettings: React.FC = () => {
2639
setMcpServers,
2740
showTokenUsage,
2841
setShowTokenUsage,
42+
ollamaUrl,
43+
setOllamaUrl,
2944
} = useSettings();
3045

3146
const [creditUsage, setCreditUsage] = useState<CreditUsage | undefined>();
@@ -37,18 +52,16 @@ const AISettings: React.FC = () => {
3752
return;
3853
}
3954

40-
fetch(CREDITS_ENDPOINT, {
55+
return effectFetch(CREDITS_ENDPOINT, {
4156
headers: {
4257
Authorization: `Bearer ${openRouterApiKey}`,
4358
},
44-
})
45-
.then(res => res.json())
46-
.then(data => {
47-
setCreditUsage({
48-
total: data.data.total_credits,
49-
used: data.data.total_usage,
50-
});
59+
})(data => {
60+
setCreditUsage({
61+
total: data.data.total_credits,
62+
used: data.data.total_usage,
5163
});
64+
});
5265
}, [openRouterApiKey]);
5366

5467
return (
@@ -59,45 +72,74 @@ const AISettings: React.FC = () => {
5972
Features
6073
</CheckboxLabel>
6174
<ConditionalSettings enabled={enableAI} inert={!enableAI}>
62-
<label htmlFor='openrouter-api-key'>
63-
<Column gap='0.5rem'>
64-
OpenRouter API Key
65-
<Row center>
66-
{!openRouterApiKey && (
67-
<>
68-
<OpenRouterLoginButton />
69-
or
70-
</>
71-
)}
72-
<InputWrapper>
73-
<InputStyled
74-
id='openrouter-api-key'
75-
type='password'
76-
value={openRouterApiKey || ''}
77-
onChange={e =>
78-
setOpenRouterApiKey(e.target.value || undefined)
79-
}
80-
placeholder='Enter your OpenRouter API key'
81-
/>
82-
</InputWrapper>
83-
</Row>
84-
{creditUsage && (
85-
<CreditUsage>
86-
Credits used: {creditUsage.used} / Total: {creditUsage.total}
87-
</CreditUsage>
88-
)}
89-
{!openRouterApiKey && (
90-
<CreditUsage>
91-
<p>
92-
OpenRouter provides a unified API that gives you access to
93-
hundreds of AI models from all major vendors, while
94-
automatically handling fallbacks and selecting the most
95-
cost-effective options.
96-
</p>
97-
</CreditUsage>
98-
)}
99-
</Column>
100-
</label>
75+
<Heading>AI Provider</Heading>
76+
<TabWrapper>
77+
<Tabs tabs={PROVIDER_TABS} label='AI Provider' rounded>
78+
<StyledTabPanel value='openrouter'>
79+
<Column gap='0.5rem'>
80+
<label htmlFor='openrouter-api-key'>OpenRouter API Key</label>
81+
<Row center>
82+
{!openRouterApiKey && (
83+
<>
84+
<OpenRouterLoginButton />
85+
or
86+
</>
87+
)}
88+
<InputWrapper>
89+
<InputStyled
90+
id='openrouter-api-key'
91+
type='password'
92+
value={openRouterApiKey || ''}
93+
onChange={e =>
94+
setOpenRouterApiKey(e.target.value || undefined)
95+
}
96+
placeholder='Enter your OpenRouter API key'
97+
/>
98+
</InputWrapper>
99+
</Row>
100+
{creditUsage && (
101+
<Subtle as='p'>
102+
Credits used: {creditUsage.used} / Total:{' '}
103+
{creditUsage.total}
104+
</Subtle>
105+
)}
106+
{!openRouterApiKey && (
107+
<Subtle as='p'>
108+
OpenRouter provides a unified API that gives you access to
109+
hundreds of AI models from all major vendors, while
110+
automatically handling fallbacks and selecting the most
111+
cost-effective options.
112+
</Subtle>
113+
)}
114+
</Column>
115+
</StyledTabPanel>
116+
<StyledTabPanel value='ollama'>
117+
<Column gap='0.5rem'>
118+
<label htmlFor='ollama-url'>Ollama API Url</label>
119+
<InputWrapper>
120+
<InputStyled
121+
id='ollama-url'
122+
value={ollamaUrl || ''}
123+
onChange={e => setOllamaUrl(e.target.value || undefined)}
124+
type='url'
125+
placeholder='http://localhost:11434/api'
126+
/>
127+
</InputWrapper>
128+
<Subtle as='p'>
129+
Host your own AI models locally using{' '}
130+
<a
131+
href='https://ollama.com/'
132+
target='_blank'
133+
rel='noreferrer'
134+
>
135+
Ollama
136+
</a>
137+
.
138+
</Subtle>
139+
</Column>
140+
</StyledTabPanel>
141+
</Tabs>
142+
</TabWrapper>
101143
<CheckboxLabel>
102144
<Checkbox checked={showTokenUsage} onChange={setShowTokenUsage} />
103145
Show token usage in chats
@@ -122,9 +164,23 @@ const ConditionalSettings = styled(Column)<{ enabled: boolean }>`
122164
${transition('opacity')}
123165
`;
124166

125-
const CreditUsage = styled.div`
167+
const Subtle = styled.div`
126168
font-size: 0.8rem;
127169
color: ${p => p.theme.colors.textLight};
128170
`;
129171

172+
const TabWrapper = styled.div`
173+
border: 1px solid ${p => p.theme.colors.bg2};
174+
border-radius: ${p => p.theme.radius};
175+
`;
176+
177+
const StyledTabPanel = styled(TabPanel)`
178+
padding: ${p => p.theme.size()};
179+
padding-top: 0;
180+
181+
${InputWrapper}:has(input:user-invalid) {
182+
border-color: ${p => p.theme.colors.alert};
183+
}
184+
`;
185+
130186
export default AISettings;

browser/data-browser/src/components/AI/AgentConfig.tsx

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { styled, useTheme } from 'styled-components';
33
import { Row, Column } from '../Row';
44
import { FaPencil, FaPlus, FaTrash, FaStar } from 'react-icons/fa6';
55
import { IconButton } from '../IconButton/IconButton';
6-
import { ModelSelect } from './ModelSelect';
7-
import type { AIAgent } from './types';
6+
import { ModelSelect } from './ModelSelect/ModelSelect';
7+
import { AIProvider, type AIAgent, type AIModelIdentifier } from './types';
88
import {
99
Dialog,
1010
DialogTitle,
@@ -43,7 +43,10 @@ const defaultNewAgent: Omit<AIAgent, 'id'> = {
4343
description: '',
4444
systemPrompt: '',
4545
availableTools: [],
46-
model: 'openai/gpt-4o-mini',
46+
model: {
47+
id: 'openai/gpt-4o-mini',
48+
provider: AIProvider.OpenRouter,
49+
},
4750
canReadAtomicData: false,
4851
canWriteAtomicData: false,
4952
temperature: 0.1,
@@ -66,7 +69,10 @@ Keep the following things in mind:
6669
- If you don't know the answer to the users question, try to figure it out by using the tools provided to you.
6770
`,
6871
availableTools: [],
69-
model: 'openai/gpt-4o-mini',
72+
model: {
73+
id: 'openai/gpt-4o-mini',
74+
provider: AIProvider.OpenRouter,
75+
},
7076
canReadAtomicData: true,
7177
canWriteAtomicData: true,
7278
temperature: 0.1,
@@ -77,7 +83,10 @@ Keep the following things in mind:
7783
description: "A basic agent that doesn't have any special purpose.",
7884
systemPrompt: ``,
7985
availableTools: [],
80-
model: 'openai/gpt-4.1-nano',
86+
model: {
87+
id: 'openai/gpt-4.1-mini',
88+
provider: AIProvider.OpenRouter,
89+
},
8190
canReadAtomicData: false,
8291
canWriteAtomicData: false,
8392
temperature: 0.1,
@@ -316,7 +325,7 @@ const AgentForm = ({ agent, onChange }: AgentFormProps) => {
316325

317326
const handleChange = (
318327
field: keyof AIAgent,
319-
value: string | boolean | number,
328+
value: string | boolean | number | AIModelIdentifier,
320329
) => {
321330
onChange({
322331
...agent,
@@ -423,13 +432,11 @@ const AgentForm = ({ agent, onChange }: AgentFormProps) => {
423432

424433
<FormGroup>
425434
<Label>Model</Label>
426-
<ModelDropdown>
427-
<ModelSelect
428-
defaultModel={agent.model}
429-
onSelect={model => handleChange('model', model)}
430-
enforceToolSupport={enforceToolSupport}
431-
/>
432-
</ModelDropdown>
435+
<ModelSelect
436+
defaultModel={agent.model}
437+
onSelect={model => handleChange('model', model)}
438+
enforceToolSupport={enforceToolSupport}
439+
/>
433440
</FormGroup>
434441

435442
<FormGroup>
@@ -558,14 +565,6 @@ const Textarea = styled.textarea`
558565
}
559566
`;
560567

561-
const ModelDropdown = styled.div`
562-
/* This container helps with styling the ModelSelect component */
563-
border: 1px solid ${p => p.theme.colors.bg2};
564-
border-radius: ${p => p.theme.radius};
565-
padding: ${p => p.theme.size(2)};
566-
background-color: ${p => p.theme.colors.bg};
567-
`;
568-
569568
const ToolList = styled.ul`
570569
list-style: none;
571570
padding: 0;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Row } from '../../Row';
2+
import { styled } from 'styled-components';
3+
4+
interface ModelInfoLayoutProps {
5+
Pricing?: React.ReactNode;
6+
About?: React.ReactNode;
7+
}
8+
9+
export const ModelInfoLayout = ({ Pricing, About }: ModelInfoLayoutProps) => {
10+
return (
11+
<>
12+
{Pricing && <Row wrapItems>{Pricing}</Row>}
13+
14+
{About && <AboutWrapper>{About}</AboutWrapper>}
15+
</>
16+
);
17+
};
18+
19+
ModelInfoLayout.Empty = styled.div`
20+
background-color: ${p => p.theme.colors.bg1};
21+
display: grid;
22+
place-items: center;
23+
color: ${p => p.theme.colors.textLight};
24+
padding: ${p => p.theme.size()};
25+
border-radius: ${p => p.theme.radius};
26+
`;
27+
28+
const AboutWrapper = styled.div`
29+
background-color: ${p => p.theme.colors.bg1};
30+
padding: ${p => p.theme.size()};
31+
border-radius: ${p => p.theme.radius};
32+
`;

0 commit comments

Comments
 (0)