Your local AI model works great, but you need features that don't exist yet. Sound familiar? You're staring at Ollama's impressive capabilities while wishing it could integrate with your custom database, process specialized file formats, or connect to your company's API endpoints.
Ollama plugin development transforms your local AI setup from a standard tool into a powerhouse tailored for your specific needs. This guide shows you how to build custom plugins that extend Ollama's functionality without breaking existing workflows.
You'll learn to create plugins from scratch, integrate external APIs, and deploy custom extensions that solve real-world problems. By the end, you'll have working plugins and the knowledge to build whatever functionality your projects demand.
Understanding Ollama's Plugin Architecture
Ollama's plugin system operates through HTTP endpoints and WebSocket connections. The core architecture separates model inference from custom functionality, allowing plugins to intercept requests, modify responses, and add new capabilities.
Core Plugin Components
Every Ollama plugin contains three essential elements:
- API Handler: Processes incoming requests and routes them appropriately
- Model Interface: Communicates with Ollama's core inference engine
- Response Processor: Modifies or enhances model outputs before returning results
// Basic plugin structure
class OllamaPlugin {
constructor(config) {
this.name = config.name;
this.version = config.version;
this.endpoints = config.endpoints || [];
}
// Register plugin with Ollama
async register() {
return {
name: this.name,
endpoints: this.endpoints,
handlers: this.getHandlers()
};
}
// Define custom endpoints
getHandlers() {
return {
'/custom-endpoint': this.handleCustomRequest.bind(this),
'/process-data': this.processData.bind(this)
};
}
}
Setting Up Your Plugin Development Environment
Prerequisites and Dependencies
Before building Ollama plugins, install these required tools:
# Install Node.js (version 18 or higher)
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs
# Install Ollama CLI
curl -fsSL https://ollama.ai/install.sh | sh
# Create plugin development directory
mkdir ollama-plugins && cd ollama-plugins
npm init -y
Install essential dependencies for plugin development:
npm install express axios ws dotenv
npm install --save-dev nodemon jest supertest
Development Environment Configuration
Create your development configuration file:
// config/development.js
module.exports = {
ollama: {
host: 'localhost',
port: 11434,
timeout: 30000
},
plugin: {
port: 3001,
logLevel: 'debug',
enableCors: true
},
database: {
url: process.env.DATABASE_URL || 'sqlite:./dev.db'
}
};
Building Your First Custom Plugin
Creating a Text Processing Plugin
This example demonstrates building a plugin that adds custom text processing capabilities to Ollama responses.
// plugins/textProcessor.js
const express = require('express');
const axios = require('axios');
class TextProcessorPlugin {
constructor() {
this.app = express();
this.ollamaUrl = 'http://localhost:11434';
this.setupRoutes();
}
setupRoutes() {
// Middleware for JSON parsing
this.app.use(express.json());
// Custom text processing endpoint
this.app.post('/api/process-text', async (req, res) => {
try {
const { prompt, model, processType } = req.body;
// Send request to Ollama
const ollamaResponse = await this.callOllama(prompt, model);
// Apply custom processing
const processedText = this.applyProcessing(
ollamaResponse.response,
processType
);
res.json({
original: ollamaResponse.response,
processed: processedText,
metadata: {
model: model,
processType: processType,
timestamp: new Date().toISOString()
}
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
}
async callOllama(prompt, model = 'llama2') {
const response = await axios.post(`${this.ollamaUrl}/api/generate`, {
model: model,
prompt: prompt,
stream: false
});
return response.data;
}
applyProcessing(text, processType) {
switch (processType) {
case 'summarize':
return this.extractKeyPoints(text);
case 'format':
return this.formatAsMarkdown(text);
case 'extract':
return this.extractEntities(text);
default:
return text;
}
}
extractKeyPoints(text) {
// Simple key point extraction logic
const sentences = text.split('. ');
return sentences
.filter(sentence => sentence.length > 20)
.slice(0, 3)
.map(point => `• ${point.trim()}`)
.join('\n');
}
formatAsMarkdown(text) {
// Convert plain text to structured markdown
return text
.split('\n\n')
.map(paragraph => paragraph.trim())
.filter(p => p.length > 0)
.map((paragraph, index) => {
if (index === 0) return `# ${paragraph}`;
return `\n${paragraph}`;
})
.join('\n');
}
extractEntities(text) {
// Basic entity extraction (can be enhanced with NLP libraries)
const entities = {
emails: text.match(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g) || [],
urls: text.match(/https?:\/\/[^\s]+/g) || [],
dates: text.match(/\b\d{1,2}\/\d{1,2}\/\d{4}\b/g) || []
};
return entities;
}
start(port = 3001) {
this.app.listen(port, () => {
console.log(`Text Processor Plugin running on port ${port}`);
});
}
}
module.exports = TextProcessorPlugin;
Testing Your Plugin
Create comprehensive tests for your plugin functionality:
// tests/textProcessor.test.js
const request = require('supertest');
const TextProcessorPlugin = require('../plugins/textProcessor');
describe('Text Processor Plugin', () => {
let plugin;
let app;
beforeAll(() => {
plugin = new TextProcessorPlugin();
app = plugin.app;
});
test('should process text with summarize option', async () => {
const testData = {
prompt: 'Explain artificial intelligence',
model: 'llama2',
processType: 'summarize'
};
const response = await request(app)
.post('/api/process-text')
.send(testData)
.expect(200);
expect(response.body).toHaveProperty('original');
expect(response.body).toHaveProperty('processed');
expect(response.body.metadata.processType).toBe('summarize');
});
test('should extract entities from text', async () => {
const testData = {
prompt: 'Contact john@example.com or visit https://example.com',
model: 'llama2',
processType: 'extract'
};
const response = await request(app)
.post('/api/process-text')
.send(testData)
.expect(200);
const entities = response.body.processed;
expect(entities.emails).toContain('john@example.com');
expect(entities.urls).toContain('https://example.com');
});
});
Advanced Plugin Features
Database Integration for Persistent Storage
Many plugins require persistent storage for user preferences, cache data, or custom model configurations.
// plugins/databasePlugin.js
const sqlite3 = require('sqlite3').verbose();
const { promisify } = require('util');
class DatabasePlugin {
constructor(dbPath = './plugin.db') {
this.db = new sqlite3.Database(dbPath);
this.initializeDatabase();
}
async initializeDatabase() {
const run = promisify(this.db.run.bind(this.db));
// Create tables for plugin data
await run(`
CREATE TABLE IF NOT EXISTS conversations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
model TEXT NOT NULL,
prompt TEXT NOT NULL,
response TEXT NOT NULL,
metadata JSON,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
await run(`
CREATE TABLE IF NOT EXISTS user_preferences (
user_id TEXT PRIMARY KEY,
default_model TEXT,
processing_options JSON,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
}
async saveConversation(userId, model, prompt, response, metadata = {}) {
const run = promisify(this.db.run.bind(this.db));
return await run(
`INSERT INTO conversations (user_id, model, prompt, response, metadata)
VALUES (?, ?, ?, ?, ?)`,
[userId, model, prompt, response, JSON.stringify(metadata)]
);
}
async getUserHistory(userId, limit = 10) {
const all = promisify(this.db.all.bind(this.db));
return await all(
`SELECT * FROM conversations
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT ?`,
[userId, limit]
);
}
async updateUserPreferences(userId, preferences) {
const run = promisify(this.db.run.bind(this.db));
return await run(
`INSERT OR REPLACE INTO user_preferences (user_id, default_model, processing_options)
VALUES (?, ?, ?)`,
[userId, preferences.defaultModel, JSON.stringify(preferences.options)]
);
}
}
module.exports = DatabasePlugin;
Real-time WebSocket Integration
Enable real-time communication between your plugin and client applications:
// plugins/websocketPlugin.js
const WebSocket = require('ws');
const axios = require('axios');
class WebSocketPlugin {
constructor(port = 8080) {
this.wss = new WebSocket.Server({ port });
this.clients = new Map();
this.setupWebSocketHandlers();
}
setupWebSocketHandlers() {
this.wss.on('connection', (ws, req) => {
const clientId = this.generateClientId();
this.clients.set(clientId, ws);
console.log(`Client ${clientId} connected`);
ws.on('message', async (message) => {
try {
const data = JSON.parse(message);
await this.handleMessage(clientId, ws, data);
} catch (error) {
ws.send(JSON.stringify({ error: 'Invalid message format' }));
}
});
ws.on('close', () => {
this.clients.delete(clientId);
console.log(`Client ${clientId} disconnected`);
});
// Send welcome message
ws.send(JSON.stringify({
type: 'connection',
clientId: clientId,
message: 'Connected to Ollama Plugin WebSocket'
}));
});
}
async handleMessage(clientId, ws, data) {
switch (data.type) {
case 'generate':
await this.handleGeneration(clientId, ws, data);
break;
case 'stream':
await this.handleStreamGeneration(clientId, ws, data);
break;
default:
ws.send(JSON.stringify({ error: 'Unknown message type' }));
}
}
async handleStreamGeneration(clientId, ws, data) {
try {
// Send streaming request to Ollama
const response = await axios.post('http://localhost:11434/api/generate', {
model: data.model || 'llama2',
prompt: data.prompt,
stream: true
}, {
responseType: 'stream'
});
response.data.on('data', (chunk) => {
const lines = chunk.toString().split('\n').filter(line => line.trim());
lines.forEach(line => {
try {
const parsed = JSON.parse(line);
ws.send(JSON.stringify({
type: 'stream_chunk',
data: parsed,
clientId: clientId
}));
} catch (e) {
// Ignore invalid JSON lines
}
});
});
response.data.on('end', () => {
ws.send(JSON.stringify({
type: 'stream_complete',
clientId: clientId
}));
});
} catch (error) {
ws.send(JSON.stringify({
type: 'error',
message: error.message
}));
}
}
generateClientId() {
return 'client_' + Math.random().toString(36).substr(2, 9);
}
broadcast(message) {
this.clients.forEach((ws) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(message));
}
});
}
}
module.exports = WebSocketPlugin;
API Integration and External Services
Connecting to External APIs
Extend Ollama's capabilities by integrating external services and APIs:
// plugins/externalIntegration.js
const axios = require('axios');
class ExternalIntegrationPlugin {
constructor(config) {
this.weatherApiKey = config.weatherApiKey;
this.newsApiKey = config.newsApiKey;
this.searchApiKey = config.searchApiKey;
}
async enhancePromptWithContext(prompt, contextType) {
let enhancedPrompt = prompt;
switch (contextType) {
case 'weather':
const weather = await this.getCurrentWeather();
enhancedPrompt = `Current weather: ${weather}\n\nUser question: ${prompt}`;
break;
case 'news':
const news = await this.getLatestNews();
enhancedPrompt = `Recent news: ${news}\n\nUser question: ${prompt}`;
break;
case 'search':
const searchResults = await this.searchWeb(prompt);
enhancedPrompt = `Search context: ${searchResults}\n\nUser question: ${prompt}`;
break;
}
return enhancedPrompt;
}
async getCurrentWeather(city = 'New York') {
try {
const response = await axios.get(
`https://api.openweathermap.org/data/2.5/weather`,
{
params: {
q: city,
appid: this.weatherApiKey,
units: 'metric'
}
}
);
const weather = response.data;
return `${weather.main.temp}°C, ${weather.weather[0].description} in ${city}`;
} catch (error) {
return 'Weather data unavailable';
}
}
async getLatestNews(category = 'technology') {
try {
const response = await axios.get(
'https://newsapi.org/v2/top-headlines',
{
params: {
category: category,
country: 'us',
apiKey: this.newsApiKey,
pageSize: 3
}
}
);
const articles = response.data.articles;
return articles.map(article =>
`${article.title}: ${article.description}`
).join('\n');
} catch (error) {
return 'News data unavailable';
}
}
async searchWeb(query) {
try {
const response = await axios.get(
'https://api.serpapi.com/search',
{
params: {
q: query,
api_key: this.searchApiKey,
num: 3
}
}
);
const results = response.data.organic_results || [];
return results.map(result =>
`${result.title}: ${result.snippet}`
).join('\n');
} catch (error) {
return 'Search results unavailable';
}
}
}
module.exports = ExternalIntegrationPlugin;
Plugin Deployment and Distribution
Creating a Plugin Package
Structure your plugin for easy distribution and installation:
{
"name": "ollama-text-processor-plugin",
"version": "1.0.0",
"description": "Advanced text processing plugin for Ollama",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest",
"build": "npm run test && npm pack"
},
"keywords": ["ollama", "plugin", "text-processing", "ai"],
"author": "Your Name",
"license": "MIT",
"dependencies": {
"express": "^4.18.2",
"axios": "^1.4.0",
"ws": "^8.13.0",
"sqlite3": "^5.1.6"
},
"devDependencies": {
"jest": "^29.5.0",
"nodemon": "^2.0.22",
"supertest": "^6.3.3"
},
"engines": {
"node": ">=18.0.0"
},
"ollama": {
"pluginVersion": "1.0",
"compatibleVersions": [">=0.1.0"],
"endpoints": [
"/api/process-text",
"/api/websocket"
]
}
}
Installation and Configuration Scripts
Create automated installation scripts for your plugins:
#!/bin/bash
# install.sh - Plugin installation script
echo "Installing Ollama Text Processor Plugin..."
# Check if Ollama is installed
if ! command -v ollama &> /dev/null; then
echo "Error: Ollama is not installed. Please install Ollama first."
exit 1
fi
# Check if Node.js is installed
if ! command -v node &> /dev/null; then
echo "Error: Node.js is not installed. Please install Node.js 18 or higher."
exit 1
fi
# Create plugin directory
PLUGIN_DIR="$HOME/.ollama/plugins/text-processor"
mkdir -p "$PLUGIN_DIR"
# Copy plugin files
cp -r ./src/* "$PLUGIN_DIR/"
cp package.json "$PLUGIN_DIR/"
cp config.json "$PLUGIN_DIR/"
# Install dependencies
cd "$PLUGIN_DIR"
npm install --production
# Create systemd service (Linux)
if command -v systemctl &> /dev/null; then
sudo tee /etc/systemd/system/ollama-text-processor.service > /dev/null <<EOF
[Unit]
Description=Ollama Text Processor Plugin
After=network.target
[Service]
Type=simple
User=ollama
WorkingDirectory=$PLUGIN_DIR
ExecStart=/usr/bin/node index.js
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable ollama-text-processor
sudo systemctl start ollama-text-processor
fi
echo "Plugin installed successfully!"
echo "Configuration file: $PLUGIN_DIR/config.json"
echo "Start the plugin: cd $PLUGIN_DIR && npm start"
Performance Optimization and Best Practices
Memory Management and Caching
Implement efficient caching strategies to improve plugin performance:
// utils/cache.js
class PluginCache {
constructor(maxSize = 100, ttl = 300000) { // 5 minutes TTL
this.cache = new Map();
this.maxSize = maxSize;
this.ttl = ttl;
}
set(key, value) {
// Remove oldest entries if cache is full
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, {
value: value,
timestamp: Date.now()
});
}
get(key) {
const item = this.cache.get(key);
if (!item) return null;
// Check if item has expired
if (Date.now() - item.timestamp > this.ttl) {
this.cache.delete(key);
return null;
}
return item.value;
}
clear() {
this.cache.clear();
}
size() {
return this.cache.size;
}
}
module.exports = PluginCache;
Error Handling and Logging
Implement comprehensive error handling and logging:
// utils/logger.js
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'ollama-plugin' },
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
new winston.transports.Console({
format: winston.format.simple()
})
]
});
// Error handling middleware
const errorHandler = (err, req, res, next) => {
logger.error({
message: err.message,
stack: err.stack,
url: req.url,
method: req.method,
ip: req.ip
});
res.status(err.status || 500).json({
error: {
message: err.message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
}
});
};
module.exports = { logger, errorHandler };
Troubleshooting Common Plugin Issues
Connection Problems
Most plugin connection issues stem from incorrect configuration or port conflicts:
// utils/diagnostics.js
const axios = require('axios');
const net = require('net');
class PluginDiagnostics {
static async checkOllamaConnection(host = 'localhost', port = 11434) {
try {
const response = await axios.get(`http://${host}:${port}/api/tags`);
return { success: true, models: response.data.models };
} catch (error) {
return {
success: false,
error: `Cannot connect to Ollama at ${host}:${port}`
};
}
}
static async checkPortAvailability(port) {
return new Promise((resolve) => {
const server = net.createServer();
server.listen(port, () => {
server.once('close', () => resolve(true));
server.close();
});
server.on('error', () => resolve(false));
});
}
static async runDiagnostics() {
const results = [];
// Check Ollama connection
const ollamaCheck = await this.checkOllamaConnection();
results.push({
test: 'Ollama Connection',
status: ollamaCheck.success ? 'PASS' : 'FAIL',
details: ollamaCheck
});
// Check plugin port availability
const portAvailable = await this.checkPortAvailability(3001);
results.push({
test: 'Plugin Port (3001)',
status: portAvailable ? 'PASS' : 'FAIL',
details: portAvailable ? 'Port available' : 'Port in use'
});
return results;
}
}
module.exports = PluginDiagnostics;
Performance Monitoring
Monitor plugin performance and resource usage:
// utils/monitor.js
const os = require('os');
const process = require('process');
class PerformanceMonitor {
constructor() {
this.startTime = Date.now();
this.requestCount = 0;
this.errorCount = 0;
}
trackRequest() {
this.requestCount++;
}
trackError() {
this.errorCount++;
}
getStats() {
const uptime = Date.now() - this.startTime;
const memUsage = process.memoryUsage();
return {
uptime: Math.floor(uptime / 1000), // seconds
requests: this.requestCount,
errors: this.errorCount,
errorRate: (this.errorCount / Math.max(this.requestCount, 1)) * 100,
memory: {
rss: Math.round(memUsage.rss / 1024 / 1024), // MB
heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024), // MB
heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024) // MB
},
system: {
platform: os.platform(),
arch: os.arch(),
cpus: os.cpus().length,
loadAvg: os.loadavg(),
freeMemory: Math.round(os.freemem() / 1024 / 1024), // MB
totalMemory: Math.round(os.totalmem() / 1024 / 1024) // MB
}
};
}
// Health check endpoint
healthCheck() {
const stats = this.getStats();
const isHealthy = stats.memory.rss < 500 && stats.errorRate < 10;
return {
status: isHealthy ? 'healthy' : 'unhealthy',
timestamp: new Date().toISOString(),
...stats
};
}
}
module.exports = PerformanceMonitor;
Conclusion
Ollama plugin development unlocks unlimited customization possibilities for your local AI setup. You've learned to build plugins from basic text processors to complex integrations with external APIs and real-time WebSocket connections.
The plugins you create solve specific problems that generic AI tools cannot address. Your custom database integrations, specialized processing functions, and external service connections transform Ollama from a standard inference engine into a tailored solution for your unique requirements.
Start with the text processing plugin example, then expand into database integration and API connections as your needs grow. The plugin architecture supports everything from simple utilities to enterprise-grade extensions that scale with your projects.
Ready to extend your Ollama functionality beyond its core capabilities? Download the complete plugin starter kit and begin building custom solutions that integrate seamlessly with your existing workflows.