Skip to content

Commit 78fa432

Browse files
authored
Merge pull request #6364 from remix-project-org/ollama_endpoint_config
Ollama endpoint config
2 parents 95206a9 + a1f0f55 commit 78fa432

File tree

9 files changed

+114
-40
lines changed

9 files changed

+114
-40
lines changed

.gitignore

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,8 @@ apps/remixdesktop/log_input_signals_new.txt
7979
logs
8080
apps/remix-ide-e2e/src/extensions/chrome/metamask
8181
apps/remix-ide-e2e/tmp/
82-
apps/remix-ide-e2e/tmp/
8382

8483
# IDE - Cursor
85-
<<<<<<< HEAD
86-
.cursor/
87-
=======
8884
.cursor/
8985
PR_MESSAGE.md
90-
>>>>>>> master
86+
apps/remix-ide-e2e/tmp/

apps/remix-ide/src/app/plugins/remixAIPlugin.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import * as packageJson from '../../../../../package.json'
22
import { Plugin } from '@remixproject/engine';
33
import { trackMatomoEvent } from '@remix-api'
4-
import { IModel, RemoteInferencer, IRemoteModel, IParams, GenerationParams, AssistantParams, CodeExplainAgent, SecurityAgent, CompletionParams, OllamaInferencer, isOllamaAvailable, getBestAvailableModel } from '@remix/remix-ai-core';
5-
//@ts-ignore
4+
import { IModel, RemoteInferencer, IRemoteModel, IParams, GenerationParams, AssistantParams, CodeExplainAgent, SecurityAgent, CompletionParams, OllamaInferencer, isOllamaAvailable, getBestAvailableModel, resetOllamaHostOnSettingsChange } from '@remix/remix-ai-core';
65
import { CodeCompletionAgent, ContractAgent, workspaceAgent, IContextType, mcpDefaultServersConfig } from '@remix/remix-ai-core';
76
import { MCPInferencer } from '@remix/remix-ai-core';
87
import { IMCPServer, IMCPConnectionStatus } from '@remix/remix-ai-core';
@@ -64,6 +63,8 @@ export class RemixAIPlugin extends Plugin {
6463
}
6564

6665
onActivation(): void {
66+
// Expose Ollama reset function globally for settings integration
67+
resetOllamaHostOnSettingsChange();
6768

6869
if (this.isOnDesktop) {
6970
this.useRemoteInferencer = true

apps/remix-ide/src/app/tabs/locales/en/settings.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,8 @@
6969
"settings.mcpServerConfigurationDescription": "Connect to Model Context Protocol servers for enhanced AI context",
7070
"settings.enableMCPEnhancement": "Enable MCP Integration",
7171
"settings.enableMCPEnhancementDescription": "Manage your MCP server connections",
72-
"settings.aiPrivacyPolicyDescription": "Understand how RemixAI processes your data."
72+
"settings.aiPrivacyPolicyDescription": "Understand how RemixAI processes your data.",
73+
"settings.ollamaConfig": "Ollama URL Configuration",
74+
"settings.ollamaConfigDescription": "Configure Ollama endpoint for local AI model integration",
75+
"settings.ollama-endpoint": "ENDPOINT URL"
7376
}

libs/remix-ai-core/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { RemoteInferencer } from './inferencers/remote/remoteInference'
99
import { OllamaInferencer } from './inferencers/local/ollamaInferencer'
1010
import { MCPInferencer } from './inferencers/mcp/mcpInferencer'
1111
import { RemixMCPServer, createRemixMCPServer } from './remix-mcp-server'
12-
import { isOllamaAvailable, getBestAvailableModel, listModels, discoverOllamaHost } from './inferencers/local/ollama'
12+
import { isOllamaAvailable, getBestAvailableModel, listModels, discoverOllamaHost, resetOllamaHostOnSettingsChange } from './inferencers/local/ollama'
1313
import { FIMModelManager, FIMModelConfig, FIM_MODEL_CONFIGS } from './inferencers/local/fimModelConfig'
1414
import { ChatHistory } from './prompts/chat'
1515
import { downloadLatestReleaseExecutable } from './helpers/inferenceServerReleases'
@@ -18,7 +18,7 @@ import { mcpDefaultServersConfig } from './config/mcpDefaultServers'
1818
export {
1919
IModel, IModelResponse, ChatCommandParser,
2020
ModelType, DefaultModels, ICompletions, IParams, IRemoteModel, buildChatPrompt,
21-
RemoteInferencer, OllamaInferencer, MCPInferencer, RemixMCPServer, isOllamaAvailable, getBestAvailableModel, listModels, discoverOllamaHost,
21+
RemoteInferencer, OllamaInferencer, MCPInferencer, RemixMCPServer, isOllamaAvailable, getBestAvailableModel, listModels, discoverOllamaHost, resetOllamaHostOnSettingsChange,
2222
FIMModelManager, FIMModelConfig, FIM_MODEL_CONFIGS, createRemixMCPServer,
2323
InsertionParams, CompletionParams, GenerationParams, AssistantParams,
2424
ChatEntry, AIRequestType, ChatHistory, downloadLatestReleaseExecutable,

libs/remix-ai-core/src/inferencers/local/ollama.ts

Lines changed: 66 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,89 @@
11
import axios from 'axios';
2-
3-
// Helper function to track events using MatomoManager instance
4-
function trackMatomoEvent(category: string, action: string, name?: string) {
5-
try {
6-
if (typeof window !== 'undefined' && (window as any)._matomoManagerInstance) {
7-
(window as any)._matomoManagerInstance.trackEvent(category, action, name)
8-
}
9-
} catch (error) {
10-
// Silent fail for tracking
11-
}
12-
}
2+
import { Registry } from '@remix-project/remix-lib';
3+
import { trackMatomoEvent } from '@remix-api'
134

145
// default Ollama ports to check (11434 is the legacy/standard port)
156
const OLLAMA_PORTS = [11434, 11435, 11436];
167
const OLLAMA_BASE_HOST = 'http://localhost';
8+
const DEFAULT_OLLAMA_HOST = 'http://localhost:11434';
179

1810
let discoveredOllamaHost: string | null = null;
1911

12+
function getConfiguredOllamaEndpoint(): string | null {
13+
const filemanager = Registry.getInstance().get('filemanager').api;
14+
try {
15+
const config = Registry.getInstance().get('config').api
16+
const configuredEndpoint = config.get('settings/ollama-endpoint');
17+
if (configuredEndpoint && configuredEndpoint !== DEFAULT_OLLAMA_HOST) {
18+
trackMatomoEvent(filemanager, { category: 'ai', action: 'remixAI', name: 'ollama_using_configured_endpoint', value: configuredEndpoint });
19+
return configuredEndpoint;
20+
}
21+
} catch (error) {
22+
trackMatomoEvent(filemanager, { category: 'ai', action: 'remixAI', name: 'ollama_config_access_failed', value: error.message || 'unknown' });
23+
}
24+
return null;
25+
}
26+
2027
export async function discoverOllamaHost(): Promise<string | null> {
28+
const filemanager = Registry.getInstance().get('filemanager').api;
2129
if (discoveredOllamaHost) {
22-
trackMatomoEvent('ai', 'remixAI', `ollama_host_cache_hit:${discoveredOllamaHost}`);
30+
trackMatomoEvent(filemanager, { category: 'ai', action: 'remixAI', name: `ollama_host_cache_hit:${discoveredOllamaHost}` })
2331
return discoveredOllamaHost;
2432
}
2533

34+
// First, try to use the configured endpoint from settings
35+
const configuredEndpoint = getConfiguredOllamaEndpoint();
36+
if (configuredEndpoint) {
37+
try {
38+
const res = await axios.get(`${configuredEndpoint}/api/tags`, { timeout: 2000 });
39+
if (res.status === 200) {
40+
discoveredOllamaHost = configuredEndpoint;
41+
trackMatomoEvent(filemanager, { category: 'ai', action: 'remixAI', name: 'ollama_configured_endpoint_success', value: configuredEndpoint });
42+
return configuredEndpoint;
43+
}
44+
return null;
45+
} catch (error) {
46+
trackMatomoEvent(filemanager, { category: 'ai', action: 'remixAI', name: 'ollama_configured_endpoint_failed', value: `${configuredEndpoint}:${error.message || 'unknown'}` });
47+
// Fall back to discovery if configured endpoint fails
48+
return null;
49+
}
50+
}
51+
52+
// Fall back to port discovery if no configured endpoint
2653
for (const port of OLLAMA_PORTS) {
2754
const host = `${OLLAMA_BASE_HOST}:${port}`;
28-
trackMatomoEvent('ai', 'remixAI', `ollama_port_check:${port}`);
55+
trackMatomoEvent(filemanager, { category: 'ai', action: 'remixAI', name: `ollama_port_check:${port}` });
2956
try {
3057
const res = await axios.get(`${host}/api/tags`, { timeout: 2000 });
3158
if (res.status === 200) {
3259
discoveredOllamaHost = host;
33-
trackMatomoEvent('ai', 'remixAI', `ollama_host_discovered_success:${host}`);
60+
trackMatomoEvent(filemanager, { category: 'ai', action: 'remixAI', name: `ollama_host_discovered_success:${host}` });
3461
return host;
3562
}
3663
} catch (error) {
37-
trackMatomoEvent('ai', 'remixAI', `ollama_port_connection_failed:${port}:${error.message || 'unknown'}`);
64+
trackMatomoEvent(filemanager, { category: 'ai', action: 'remixAI', name: `ollama_port_connection_failed:${port}:${error.message || 'unknown'}` });
3865
continue; // next port
3966
}
4067
}
41-
trackMatomoEvent('ai', 'remixAI', 'ollama_host_discovery_failed:no_ports_available');
68+
trackMatomoEvent(filemanager, { category: 'ai', action: 'remixAI', name: 'ollama_host_discovery_failed:no_ports_available' });
4269
return null;
4370
}
4471

4572
export async function isOllamaAvailable(): Promise<boolean> {
46-
trackMatomoEvent('ai', 'remixAI', 'ollama_availability_check:checking');
73+
const filemanager = Registry.getInstance().get('filemanager').api;
74+
trackMatomoEvent(filemanager, { category: 'ai', action: 'remixAI', name: 'ollama_availability_check:checking' });
4775
const host = await discoverOllamaHost();
4876
const isAvailable = host !== null;
49-
trackMatomoEvent('ai', 'remixAI', `ollama_availability_result:available:${isAvailable}`);
77+
trackMatomoEvent(filemanager, { category: 'ai', action: 'remixAI', name: `ollama_availability_result:available:${isAvailable}` });
5078
return isAvailable;
5179
}
5280

5381
export async function listModels(): Promise<string[]> {
54-
trackMatomoEvent('ai', 'remixAI', 'ollama_list_models_start:fetching');
82+
const filemanager = Registry.getInstance().get('filemanager').api;
83+
trackMatomoEvent(filemanager, { category: 'ai', action: 'remixAI', name: 'ollama_list_models_start:fetching' });
5584
const host = await discoverOllamaHost();
5685
if (!host) {
57-
trackMatomoEvent('ai', 'remixAI', 'ollama_list_models_failed:no_host');
86+
trackMatomoEvent(filemanager, { category: 'ai', action: 'remixAI', name: 'ollama_list_models_failed:no_host' });
5887
throw new Error('Ollama is not available');
5988
}
6089

@@ -71,26 +100,35 @@ export function getOllamaHost(): string | null {
71100
}
72101

73102
export function resetOllamaHost(): void {
74-
trackMatomoEvent('ai', 'remixAI', `ollama_reset_host:${discoveredOllamaHost || 'null'}`);
103+
const fileManager = Registry.getInstance().get('filemanager').api;
104+
trackMatomoEvent(fileManager, { category: 'ai', action: 'remixAI', name: `ollama_reset_host:${discoveredOllamaHost || 'null'}` });
75105
discoveredOllamaHost = null;
76106
}
77107

108+
export function resetOllamaHostOnSettingsChange(): void {
109+
const fileManager = Registry.getInstance().get('filemanager').api;
110+
// This function should be called when Ollama settings are updated
111+
resetOllamaHost();
112+
trackMatomoEvent(fileManager, { category: 'ai', action: 'remixAI', name: 'ollama_reset_on_settings_change' });
113+
}
114+
78115
export async function pullModel(modelName: string): Promise<void> {
116+
const filemanager = Registry.getInstance().get('filemanager').api;
79117
// in case the user wants to pull a model from registry
80-
trackMatomoEvent('ai', 'remixAI', `ollama_pull_model_start:${modelName}`);
118+
trackMatomoEvent(filemanager, { category: 'ai', action: 'remixAI', name: `ollama_pull_model_start:${modelName}` });
81119
const host = await discoverOllamaHost();
82120
if (!host) {
83-
trackMatomoEvent('ai', 'remixAI', `ollama_pull_model_failed:${modelName}|no_host`);
121+
trackMatomoEvent(filemanager, { category: 'ai', action: 'remixAI', name: `ollama_pull_model_failed:${modelName}|no_host` });
84122
throw new Error('Ollama is not available');
85123
}
86124

87125
try {
88126
const startTime = Date.now();
89127
await axios.post(`${host}/api/pull`, { name: modelName });
90128
const duration = Date.now() - startTime;
91-
trackMatomoEvent('ai', 'remixAI', `ollama_pull_model_success:${modelName}|duration:${duration}ms`);
129+
trackMatomoEvent(filemanager, { category: 'ai', action: 'remixAI', name: `ollama_pull_model_success:${modelName}|duration:${duration}ms` });
92130
} catch (error) {
93-
trackMatomoEvent('ai', 'remixAI', `ollama_pull_model_error:${modelName}|${error.message || 'unknown'}`);
131+
trackMatomoEvent(filemanager, { category: 'ai', action: 'remixAI', name: `ollama_pull_model_error:${modelName}|${error.message || 'unknown'}` });
94132
console.error('Error pulling model:', error);
95133
throw new Error(`Failed to pull model: ${modelName}`);
96134
}
@@ -106,7 +144,8 @@ export async function validateModel(modelName: string): Promise<boolean> {
106144
}
107145

108146
export async function getBestAvailableModel(): Promise<string | null> {
109-
trackMatomoEvent('ai', 'remixAI', 'ollama_get_best');
147+
const filemanager = Registry.getInstance().get('filemanager').api;
148+
trackMatomoEvent(filemanager, { category: 'ai', action: 'remixAI', name: 'ollama_get_best' });
110149
try {
111150
const models = await listModels();
112151
if (models.length === 0) return null;
@@ -125,7 +164,7 @@ export async function getBestAvailableModel(): Promise<string | null> {
125164
// TODO get model stats and get best model
126165
return models[0];
127166
} catch (error) {
128-
trackMatomoEvent('ai', 'remixAI', `ollama_get_best_model_error:${error.message || 'unknown'}`);
167+
trackMatomoEvent(filemanager, { category: 'ai', action: 'remixAI', name: `ollama_get_best_model_error:${error.message || 'unknown'}` });
129168
console.error('Error getting best available model:', error);
130169
return null;
131170
}

libs/remix-ui/remix-ai-assistant/src/components/remix-ui-remix-ai-assistant.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -651,7 +651,7 @@ export const RemixUiRemixAiAssistant = React.forwardRef<
651651
setMessages(prev => [...prev, {
652652
id: crypto.randomUUID(),
653653
role: 'assistant',
654-
content: '**Ollama is not available.**\n\nTo use Ollama with Remix IDE:\n\n1. **Install Ollama**: Visit [ollama.ai](https://ollama.ai) to download\n2. **Start Ollama**: Run `ollama serve` in your terminal\n3. **Install a model**: Run `ollama pull codestral:latest`\n4. **Configure CORS**: Set `OLLAMA_ORIGINS=https://remix.ethereum.org`\n\nSee the [Ollama Setup Guide](https://github.com/ethereum/remix-project/blob/master/OLLAMA_SETUP.md) for detailed instructions.\n\n*Switching back to previous model for now.*',
654+
content: '**Ollama is not available.**\n\nTo use Ollama with Remix IDE:\n\n1. **Install Ollama**: Visit [ollama.ai](https://ollama.ai) to download\n2. **Start Ollama**: Run `ollama serve` in your terminal\n3. **Install a model**: Run `ollama pull codestral:latest`\n4. **Configure CORS**: e.g `OLLAMA_ORIGINS=https://remix.ethereum.org ollama serve`\n\nSee the [Ollama Setup Guide](https://github.com/ethereum/remix-project/blob/master/OLLAMA_SETUP.md) for detailed instructions.\n\n*Switching back to previous model for now.*',
655655
timestamp: Date.now(),
656656
sentiment: 'none'
657657
}])
@@ -668,7 +668,7 @@ export const RemixUiRemixAiAssistant = React.forwardRef<
668668
setMessages(prev => [...prev, {
669669
id: crypto.randomUUID(),
670670
role: 'assistant',
671-
content: `**Failed to connect to Ollama.**\n\nError: ${error.message || 'Unknown error'}\n\nPlease ensure:\n- Ollama is running (\`ollama serve\`)\n- CORS is configured for Remix IDE\n- At least one model is installed\n\nSee the [Ollama Setup Guide](https://github.com/ethereum/remix-project/blob/master/OLLAMA_SETUP.md) for help.\n\n*Switching back to previous model.*`,
671+
content: `**Failed to connect to Ollama.**\n\nError: ${error.message || 'Unknown error'}\n\nPlease ensure:\n- Ollama is running (\`ollama serve\`)\n- The ollama CORS setting is configured for Remix IDE. e.g \`OLLAMA_ORIGINS=https://remix.ethereum.org ollama serve\` Please see [Ollama Setup Guide](https://github.com/ethereum/remix-project/blob/master/OLLAMA_SETUP.md) for detailed instructions.\n- At least one model is installed\n\nSee the [Ollama Setup Guide](https://github.com/ethereum/remix-project/blob/master/OLLAMA_SETUP.md) for help.\n\n*Switching back to previous model.*`,
672672
timestamp: Date.now(),
673673
sentiment: 'none'
674674
}])

libs/remix-ui/settings/src/lib/remix-ui-settings.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,16 @@ const settingsSections: SettingsSection[] = [
128128
action: 'link',
129129
link: 'https://remix-ide.readthedocs.io/en/latest/ai.html'
130130
}
131+
},
132+
{
133+
name: 'ollama-config',
134+
label: 'settings.ollamaConfig',
135+
description: 'settings.ollamaConfigDescription',
136+
type: 'toggle',
137+
toggleUIOptions: [{
138+
name: 'ollama-endpoint',
139+
type: 'text'
140+
}]
131141
}]
132142
},
133143
...(mcpEnabled ? [{

libs/remix-ui/settings/src/lib/settingsReducer.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Registry } from '@remix-project/remix-lib'
22
import { SettingsActions, SettingsState } from '../types'
3-
3+
import { resetOllamaHostOnSettingsChange } from '@remix/remix-ai-core';
44
const config = Registry.getInstance().get('config').api
55
const settingsConfig = Registry.getInstance().get('settingsConfig').api
66
const defaultTheme = config.get('settings/theme') ? settingsConfig.themes.find((theme) => theme.name.toLowerCase() === config.get('settings/theme').toLowerCase()) : settingsConfig.themes[0]
@@ -18,12 +18,14 @@ const sindriAccessToken = config.get('settings/sindri-access-token') || ''
1818
const etherscanAccessToken = config.get('settings/etherscan-access-token') || ''
1919
const mcpServersEnable = config.get('settings/mcp/servers/enable') || false
2020
const mcpServerManagement = config.get('settings/mcp-server-management') || false
21+
const ollamaEndpoint = config.get('settings/ollama-endpoint') || 'http://localhost:11434'
2122

2223
let githubConfig = config.get('settings/github-config') || false
2324
let ipfsConfig = config.get('settings/ipfs-config') || false
2425
let swarmConfig = config.get('settings/swarm-config') || false
2526
let sindriConfig = config.get('settings/sindri-config') || false
2627
let etherscanConfig = config.get('settings/etherscan-config') || false
28+
let ollamaConfig = config.get('settings/ollama-config') || false
2729
let generateContractMetadata = config.get('settings/generate-contract-metadata')
2830
let autoCompletion = config.get('settings/auto-completion')
2931
let showGas = config.get('settings/show-gas')
@@ -50,6 +52,10 @@ if (!etherscanConfig && etherscanAccessToken) {
5052
config.set('settings/etherscan-config', true)
5153
etherscanConfig = true
5254
}
55+
if (!ollamaConfig && ollamaEndpoint !== 'http://localhost:11434') {
56+
config.set('settings/ollama-config', true)
57+
ollamaConfig = true
58+
}
5359
if (typeof generateContractMetadata !== 'boolean') {
5460
config.set('settings/generate-contract-metadata', true)
5561
generateContractMetadata = true
@@ -196,6 +202,14 @@ export const initialState: SettingsState = {
196202
value: mcpServerManagement,
197203
isLoading: false
198204
},
205+
'ollama-config': {
206+
value: ollamaConfig,
207+
isLoading: false
208+
},
209+
'ollama-endpoint': {
210+
value: ollamaEndpoint,
211+
isLoading: false
212+
},
199213
toaster: {
200214
value: '',
201215
isLoading: false
@@ -206,6 +220,15 @@ export const settingReducer = (state: SettingsState, action: SettingsActions): S
206220
switch (action.type) {
207221
case 'SET_VALUE':
208222
config.set('settings/' + action.payload.name, action.payload.value)
223+
// Reset Ollama host cache when endpoint is changed
224+
if (action.payload.name === 'ollama-endpoint') {
225+
try {
226+
resetOllamaHostOnSettingsChange();
227+
} catch (error) {
228+
// Ignore errors - Ollama functionality is optional
229+
}
230+
}
231+
209232
return { ...state, [action.payload.name]: { ...state[action.payload.name], value: action.payload.value, isLoading: false } }
210233
case 'SET_LOADING':
211234
return { ...state, [action.payload.name]: { ...state[action.payload.name], isLoading: true } }

libs/remix-ui/settings/src/types/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ export interface SettingsState {
115115
'ai-privacy-policy': ConfigState,
116116
'mcp/servers/enable': ConfigState,
117117
'mcp-server-management': ConfigState,
118+
'ollama-config': ConfigState,
119+
'ollama-endpoint': ConfigState,
118120
toaster: ConfigState
119121
}
120122
export interface SettingsActionPayloadTypes {

0 commit comments

Comments
 (0)