Plugin Development: Extending Ollama with Custom Functionality

Learn Ollama plugin development to extend functionality beyond core features. Build custom plugins with our step-by-step guide and code examples.

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.