Skip to content

Writing Go Plugins

This guide walks you through creating a native Go plugin for DeepIntShield using our hello-world example as a reference. You’ll learn how to structure your plugin, implement required functions, build the shared object, and integrate it with DeepIntShield.

Before you start, ensure you have:

  • Go 1.26.1 installed (must match DeepIntShield’s Go version)
  • Linux or macOS (Go plugins are not supported on Windows)
  • DeepIntShield installed and configured
  • Basic understanding of Go programming

A minimal plugin project should have the following structure:

hello-world/
├── main.go # Plugin implementation
├── go.mod # Go module definition
├── go.sum # Dependency checksums
├── Makefile # Build automation
└── .gitignore # Git ignore patterns

Create a new directory and initialize a Go module:

Terminal window
mkdir my-plugin
cd my-plugin
go mod init github.com/yourusername/my-plugin

Add DeepIntShield as a dependency:

Terminal window
go get github.com/maximhq/deepintshield/core@latest

Your go.mod should look like this:

module github.com/yourusername/my-plugin
go 1.26.1
require github.com/maximhq/deepintshield/core v1.2.38

Create main.go with the required plugin functions. Here’s the complete hello-world example:

package main
import (
"fmt"
"github.com/maximhq/deepintshield/core/schemas"
)
// Init is called when the plugin is loaded
// config contains the plugin configuration from config.json
func Init(config any) error {
fmt.Println("Init called")
// Initialize your plugin here (database connections, API clients, etc.)
return nil
}
// GetName returns the plugin's unique identifier
func GetName() string {
return "Hello World Plugin"
}
// HTTPTransportPreHook intercepts requests BEFORE they enter DeepIntShield core
// Modify req in-place. Return (*HTTPResponse, nil) to short-circuit.
// Only called when using HTTP transport (deepintshield-http)
func HTTPTransportPreHook(ctx *schemas.DeepIntShieldContext, req *schemas.HTTPRequest) (*schemas.HTTPResponse, error) {
fmt.Println("HTTPTransportPreHook called")
// Read headers using case-insensitive helper (recommended)
contentType := req.CaseInsensitiveHeaderLookup("Content-Type")
fmt.Printf("Content-Type: %s\n", contentType)
// Modify request in-place
req.Headers["x-custom-header"] = "custom-value"
// Store values in context for use in other hooks
ctx.SetValue(schemas.DeepIntShieldContextKey("my-plugin-key"), "pre-hook-value")
// Return nil to continue, or return &schemas.HTTPResponse{} to short-circuit
return nil, nil
}
// HTTPTransportPostHook intercepts responses AFTER they exit DeepIntShield core
// Modify resp in-place. Called in reverse order of pre-hooks.
// Only called for NON-STREAMING responses when using HTTP transport (deepintshield-http)
func HTTPTransportPostHook(ctx *schemas.DeepIntShieldContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
fmt.Println("HTTPTransportPostHook called")
// Modify response headers
resp.Headers["x-processed-by"] = "my-plugin"
// Read context values set in pre-hook
if val := ctx.Value(schemas.DeepIntShieldContextKey("my-plugin-key")); val != nil {
fmt.Printf("Context value: %v\n", val)
}
// Return nil to continue, or return error to short-circuit
return nil
}
// HTTPTransportStreamChunkHook intercepts streaming chunks BEFORE they're sent to the client
// Modify chunk or return nil to skip. Called in reverse order of pre-hooks.
// Only called for STREAMING responses when using HTTP transport (deepintshield-http)
func HTTPTransportStreamChunkHook(ctx *schemas.DeepIntShieldContext, req *schemas.HTTPRequest, chunk *schemas.DeepIntShieldStreamChunk) (*schemas.DeepIntShieldStreamChunk, error) {
fmt.Println("HTTPTransportStreamChunkHook called")
// chunk is a typed struct containing one of:
// - DeepIntShieldTextCompletionResponse (text completion streaming)
// - DeepIntShieldChatResponse (chat completion streaming)
// - DeepIntShieldResponsesStreamResponse (responses API streaming)
// - DeepIntShieldSpeechStreamResponse (speech synthesis streaming)
// - DeepIntShieldTranscriptionStreamResponse (transcription streaming)
// - DeepIntShieldImageGenerationStreamResponse (image generation streaming)
// - DeepIntShieldError (error during streaming)
// Return chunk unchanged to pass through
return chunk, nil
// Or return nil to skip/filter this chunk:
// return nil, nil
// Or return modified chunk:
// modifiedChunk := &schemas.DeepIntShieldStreamChunk{DeepIntShieldChatResponse: ...}
// return modifiedChunk, nil
}
// PreLLMHook is called before the request is sent to the provider
// This is where you can modify requests or short-circuit the flow
func PreLLMHook(ctx *schemas.DeepIntShieldContext, req *schemas.DeepIntShieldRequest) (*schemas.DeepIntShieldRequest, *schemas.LLMPluginShortCircuit, error) {
fmt.Println("PreLLMHook called")
// Modify the request or return a short-circuit to skip provider call
return req, nil, nil
}
// PostLLMHook is called after receiving a response from the provider
// This is where you can modify responses or handle errors
func PostLLMHook(ctx *schemas.DeepIntShieldContext, resp *schemas.DeepIntShieldResponse, bifrostErr *schemas.DeepIntShieldError) (*schemas.DeepIntShieldResponse, *schemas.DeepIntShieldError, error) {
fmt.Println("PostLLMHook called")
// Modify the response or error before returning to caller
return resp, bifrostErr, nil
}
// Cleanup is called when DeepIntShield shuts down
func Cleanup() error {
fmt.Println("Cleanup called")
// Clean up resources (close connections, flush buffers, etc.)
return nil
}

Called once when the plugin is loaded. Use this to:

  • Parse plugin configuration
  • Initialize database connections
  • Set up API clients
  • Validate required environment variables
func Init(config any) error {
// Parse configuration
cfg, ok := config.(map[string]interface{})
if !ok {
return fmt.Errorf("invalid config format")
}
apiKey := cfg["api_key"].(string)
// Initialize your resources
return nil
}

Returns a unique identifier for your plugin. This name appears in logs and status reports.

HTTP transport only. Intercepts requests BEFORE they enter DeepIntShield core. Use this to:

  • Modify request headers, body, or query params in-place
  • Short-circuit with a custom response
  • Store values in DeepIntShieldContext for use in other hooks
  • Works with both native .so and WASM plugins

Key points:

  • Receives serializable *HTTPRequest (not raw fasthttp)
  • Modify req.Headers, req.Body, req.Query directly
  • Return (nil, nil) to continue to next plugin/handler
  • Return (*HTTPResponse, nil) to short-circuit with response
  • Return (nil, error) to short-circuit with error

HTTP transport only. Intercepts responses AFTER they exit DeepIntShield core. Use this to:

  • Modify response headers or body in-place
  • Log or monitor response data
  • Access context values set in pre-hook
  • Called in reverse order of pre-hooks

Key points:

  • Receives both *HTTPRequest and *HTTPResponse
  • Modify resp.Headers, resp.Body, resp.StatusCode directly
  • Return nil to continue to next plugin/handler
  • Return error to short-circuit with error and skip remaining post-hooks
  • NOT called for streaming responses - use HTTPTransportStreamChunkHook instead

HTTPTransportStreamChunkHook(ctx, req, chunk)

Section titled “HTTPTransportStreamChunkHook(ctx, req, chunk)”

HTTP transport only. Intercepts streaming response chunks BEFORE they’re written to the client. Use this to:

  • Modify streaming chunks in real-time
  • Filter/skip specific chunks
  • Log or monitor streaming data
  • Called in reverse order of pre-hooks

Key points:

  • Receives a *schemas.DeepIntShieldStreamChunk typed struct (not raw bytes)
  • The struct contains one non-nil field based on the response type (chat, text completion, responses, speech, transcription, image generation, or error)
  • Return (chunk, nil) to pass through unchanged
  • Return (nil, nil) to skip/filter the chunk entirely
  • Return (modifiedChunk, nil) to return a modified chunk
  • Return (nil, error) to send error to client and stop streaming

Called before each provider request. Use this to:

  • Modify request parameters
  • Add logging or monitoring
  • Implement caching (check cache, return cached response)
  • Apply governance rules (rate limiting, budget checks)
  • Short-circuit to skip provider calls

Short-Circuiting Example:

func PreLLMHook(ctx *schemas.DeepIntShieldContext, req *schemas.DeepIntShieldRequest) (*schemas.DeepIntShieldRequest, *schemas.LLMPluginShortCircuit, error) {
// Return cached response without calling provider
if cachedResponse := checkCache(req) {
return req, &schemas.LLMPluginShortCircuit{
Response: cachedResponse,
}, nil
}
return req, nil, nil
}

Called after provider responses (or short-circuits). Use this to:

  • Transform responses
  • Log response data
  • Store responses in cache
  • Handle errors or implement fallback logic
  • Add custom metadata

Response Transformation Example:

func PostLLMHook(ctx *schemas.DeepIntShieldContext, resp *schemas.DeepIntShieldResponse, bifrostErr *schemas.DeepIntShieldError) (*schemas.DeepIntShieldResponse, *schemas.DeepIntShieldError, error) {
if resp != nil && resp.ChatResponse != nil {
// Add custom metadata
resp.ChatResponse.ExtraFields.RawResponse = map[string]interface{}{
"plugin_processed": true,
"timestamp": time.Now().Unix(),
}
}
return resp, bifrostErr, nil
}

Called on DeepIntShield shutdown. Use this to:

  • Close database connections
  • Flush buffers
  • Save state
  • Release resources

Create a Makefile to automate building your plugin:

.PHONY: all build clean install help
PLUGIN_NAME = my-plugin
OUTPUT_DIR = build
# Platform detection
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Linux)
PLUGIN_EXT = .so
PLATFORM = linux
endif
ifeq ($(UNAME_S),Darwin)
PLUGIN_EXT = .so
PLATFORM = darwin
endif
# Architecture detection
UNAME_M := $(shell uname -m)
ifeq ($(UNAME_M),x86_64)
ARCH = amd64
endif
ifeq ($(UNAME_M),arm64)
ARCH = arm64
endif
OUTPUT = $(OUTPUT_DIR)/$(PLUGIN_NAME)$(PLUGIN_EXT)
build: ## Build the plugin for current platform
@echo "Building plugin for $(PLATFORM)/$(ARCH)..."
@mkdir -p $(OUTPUT_DIR)
go build -buildmode=plugin -o $(OUTPUT) main.go
@echo "Plugin built successfully: $(OUTPUT)"
clean: ## Remove build artifacts
@rm -rf $(OUTPUT_DIR)
install: build ## Build and install to DeepIntShield plugins directory
@mkdir -p ~/.deepintshield/plugins
@cp $(OUTPUT) ~/.deepintshield/plugins/
@echo "Plugin installed to ~/.deepintshield/plugins/"

Build the plugin using the Makefile:

Terminal window
make build

This creates build/my-plugin.so in your project directory.

For production, you may need to build for specific platforms:

Terminal window
# Build for Linux AMD64
GOOS=linux GOARCH=amd64 go build -buildmode=plugin -o my-plugin-linux-amd64.so main.go
# Build for Linux ARM64
GOOS=linux GOARCH=arm64 go build -buildmode=plugin -o my-plugin-linux-arm64.so main.go
# Build for macOS ARM64 (M1/M2)
GOOS=darwin GOARCH=arm64 go build -buildmode=plugin -o my-plugin-darwin-arm64.so main.go

Step 5: Configure DeepIntShield to Load Your Plugin

Section titled “Step 5: Configure DeepIntShield to Load Your Plugin”

Add your plugin to DeepIntShield’s config.json:

{
"plugins": [
{
"enabled": true,
"name": "my-plugin",
"path": "/path/to/my-plugin.so",
"version": 1,
"config": {
"api_key": "your-api-key",
"custom_setting": "value"
}
}
]
}
  • enabled - Set to true to load the plugin
  • name - Plugin identifier (used in logs)
  • path - Absolute or relative path to the .so file
  • config - Plugin-specific configuration passed to Init()
  • version - (Optional) Plugin version number (default: 1). Increment this value to force a reload of the plugin and database update when DeepIntShield restarts. Useful when you want to ensure config changes take effect without manually clearing plugin state.

Start DeepIntShield and verify your plugin loads:

Terminal window
./deepintshield-http

You should see output like:

Init called
[INFO] Plugin loaded: Hello World Plugin

Make a test request:

Terminal window
curl -X POST http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "openai/gpt-4o-mini",
"messages": [{"role": "user", "content": "Hello!"}]
}'

Check the logs for plugin hook calls:

HTTPTransportPreHook called
PreLLMHook called
PostLLMHook called
HTTPTransportPostHook called

For plugins that need to maintain state across requests:

package main
import (
"sync"
"github.com/maximhq/deepintshield/core/schemas"
)
var (
requestCount int64
mu sync.Mutex
)
func PreLLMHook(ctx *schemas.DeepIntShieldContext, req *schemas.DeepIntShieldRequest) (*schemas.DeepIntShieldRequest, *schemas.LLMPluginShortCircuit, error) {
mu.Lock()
requestCount++
count := requestCount
mu.Unlock()
// Use count for rate limiting, metrics, etc.
return req, nil, nil
}

Control whether DeepIntShield should try fallback providers:

func PostLLMHook(ctx *schemas.DeepIntShieldContext, resp *schemas.DeepIntShieldResponse, bifrostErr *schemas.DeepIntShieldError) (*schemas.DeepIntShieldResponse, *schemas.DeepIntShieldError, error) {
if bifrostErr != nil {
// Allow fallbacks for rate limit errors
if bifrostErr.Error.Type != nil && *bifrostErr.Error.Type == "rate_limit" {
allowFallbacks := true
bifrostErr.AllowFallbacks = &allowFallbacks
} else {
// Don't try fallbacks for auth errors
allowFallbacks := false
bifrostErr.AllowFallbacks = &allowFallbacks
}
}
return resp, bifrostErr, nil
}
var cache sync.Map
func PreLLMHook(ctx *schemas.DeepIntShieldContext, req *schemas.DeepIntShieldRequest) (*schemas.DeepIntShieldRequest, *schemas.LLMPluginShortCircuit, error) {
// Generate cache key from request
key := generateCacheKey(req)
// Check cache
if cached, ok := cache.Load(key); ok {
return req, &schemas.LLMPluginShortCircuit{
Response: cached.(*schemas.DeepIntShieldResponse),
}, nil
}
return req, nil, nil
}
func PostLLMHook(ctx *schemas.DeepIntShieldContext, resp *schemas.DeepIntShieldResponse, bifrostErr *schemas.DeepIntShieldError) (*schemas.DeepIntShieldResponse, *schemas.DeepIntShieldError, error) {
if resp != nil && bifrostErr == nil {
// Store in cache
key := generateCacheKeyFromResponse(resp)
cache.Store(key, resp)
}
return resp, bifrostErr, nil
}

Error: plugin: not a plugin file

Solution: Ensure you built with -buildmode=plugin:

Terminal window
go build -buildmode=plugin -o plugin.so main.go

Error: plugin was built with a different version of package

Why this happens: Go’s plugin system requires exact version matching for:

  • The Go compiler version
  • All shared packages (especially github.com/maximhq/deepintshield/core)
  • Transitive dependencies (packages that your dependencies depend on)

This is more strict than typical Go builds. Even if only one transitive dependency differs by a patch version, the plugin will fail to load.

Solution: Ensure your plugin is built with the exact same versions as DeepIntShield.

Step 1: Diagnose the mismatch

Use go version -m to inspect the build info of both your plugin and the DeepIntShield binary:

Terminal window
# Check what versions your plugin was built with:
$ go version -m my-plugin.so
my-plugin.so: go1.26.1
dep github.com/maximhq/deepintshield/core v1.3.50
dep github.com/valyala/fasthttp v1.51.0
# Check what versions DeepIntShield was built with:
$ go version -m deepintshield-http
deepintshield-http: go1.26.1
dep github.com/maximhq/deepintshield/core v1.3.54 # <-- MISMATCH!
dep github.com/valyala/fasthttp v1.55.0 # <-- MISMATCH!

Notice that even though the Go version matches (go1.26.1), the package versions are different — this causes the error.

Step 2: Update your plugin dependencies

Terminal window
# Update to match DeepIntShield's core version
go get github.com/maximhq/deepintshield/core@v1.3.54
go mod tidy
# Rebuild the plugin
go build -buildmode=plugin -o my-plugin.so main.go

Step 3: Verify the fix

Terminal window
# Confirm versions now match
$ go version -m my-plugin.so | grep deepintshield
dep github.com/maximhq/deepintshield/core v1.3.54 # Now matches!

Error: cannot load plugin built for GOOS=linux on darwin

Solution: Build on the target platform or use the correct GOOS/GOARCH for your system.

Error: plugin: symbol Init not found

Solution: Ensure all required functions are exported (start with capital letter) and have the correct signature.

The complete hello-world example is available in the DeepIntShield repository:

Explore production-ready plugins in the DeepIntShield repository:

Do I need to rebuild my plugin when upgrading DeepIntShield?

Section titled “Do I need to rebuild my plugin when upgrading DeepIntShield?”

Yes, absolutely. Plugins must be compiled against the exact same version of github.com/maximhq/deepintshield/core that DeepIntShield is using. This is a fundamental requirement of Go’s plugin system.

When you upgrade DeepIntShield, you must:

  1. Update your plugin’s go.mod to use the matching core version
  2. Rebuild the plugin with the same Go version
  3. Redeploy the plugin alongside the new DeepIntShield version

Example:

If upgrading from DeepIntShield v1.2.17 to v1.3.0:

Terminal window
# Update your plugin dependency
go get github.com/maximhq/deepintshield/core@v1.3.0
go mod tidy
# Rebuild the plugin
go build -buildmode=plugin -o my-plugin.so main.go

Should plugin builds be part of my deployment pipeline?

Section titled “Should plugin builds be part of my deployment pipeline?”

Yes, strongly recommended. Your plugin build and deployment should be tightly coupled with your DeepIntShield deployment.

Recommended CI/CD Workflow:

# Example GitHub Actions workflow
name: Deploy DeepIntShield with Plugins
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
# 1. Checkout code
- uses: actions/checkout@v3
# 2. Setup Go
- uses: actions/setup-go@v4
with:
go-version: '1.26.1'
# 3. Build DeepIntShield
- name: Build DeepIntShield
run: |
cd transports/deepintshield-http
go build -o deepintshield-http
# 4. Build ALL plugins with matching version
- name: Build Plugins
run: |
cd plugins/my-plugin
# Ensure plugin uses same core version as DeepIntShield
go get github.com/maximhq/deepintshield/core@${{ env.DEEPINTSHIELD_VERSION }}
go mod tidy
go build -buildmode=plugin -o my-plugin.so main.go
# 5. Bundle everything together
- name: Create deployment bundle
run: |
mkdir -p deploy/plugins
cp transports/deepintshield-http/deepintshield-http deploy/
cp plugins/my-plugin/my-plugin.so deploy/plugins/
cp config.json deploy/
# 6. Deploy bundle to your infrastructure
- name: Deploy to Production
run: |
# Upload to S3, copy to servers, deploy to K8s, etc.
./deploy.sh

Key Principles:

  1. Version Lock - Pin your plugin dependencies to specific DeepIntShield versions
  2. Atomic Deployment - Deploy DeepIntShield and plugins together as a single unit
  3. Build Verification - Test plugin loading as part of CI
  4. Rollback Strategy - Keep previous plugin versions for rollbacks

How do I handle plugin versioning in production?

Section titled “How do I handle plugin versioning in production?”

Organize your plugin deployments by version:

/opt/deepintshield/
├── v1.3.0/
│ ├── deepintshield-http
│ └── plugins/
│ ├── my-plugin.so
│ └── cache-plugin.so
├── v1.2.17/
│ ├── deepintshield-http
│ └── plugins/
│ ├── my-plugin.so
│ └── cache-plugin.so
└── current -> v1.3.0/ # Symlink to active version

This allows easy rollbacks:

Terminal window
# Rollback to previous version
ln -sfn /opt/deepintshield/v1.2.17 /opt/deepintshield/current
systemctl restart deepintshield

Can I use different plugin versions for different DeepIntShield instances?

Section titled “Can I use different plugin versions for different DeepIntShield instances?”

No. Each plugin must match the exact core version of the DeepIntShield instance loading it. If you’re running multiple DeepIntShield versions (e.g., staging vs production), you need separate plugin builds for each version.

staging/
deepintshield-http (v1.3.0)
plugins/
my-plugin-v1.3.0.so
production/
deepintshield-http (v1.2.17)
plugins/
my-plugin-v1.2.17.so

What happens if I forget to rebuild a plugin?

Section titled “What happens if I forget to rebuild a plugin?”

You’ll see errors like:

plugin: symbol Init not found in plugin github.com/you/plugin
plugin was built with a different version of package github.com/maximhq/deepintshield/core

Solution: Rebuild the plugin with the correct core version. See the Version Mismatch Errors troubleshooting section for detailed diagnosis steps using go version -m.

How do I test plugins before production deployment?

Section titled “How do I test plugins before production deployment?”

Multi-stage testing approach:

  1. Unit Tests - Test plugin logic in isolation

    func TestPreHook(t *testing.T) {
    req := &schemas.DeepIntShieldRequest{...}
    modifiedReq, shortCircuit, err := PreLLMHook(&ctx, req)
    assert.NoError(t, err)
    assert.Nil(t, shortCircuit)
    }
  2. Integration Tests - Load plugin in test DeepIntShield instance

    Terminal window
    # Start test DeepIntShield with plugin
    ./deepintshield-http --config test-config.json
    # Run test requests
    curl -X POST http://localhost:8080/v1/chat/completions ...
  3. Staging Environment - Deploy to staging with production-like load

  4. Canary Deployment - Gradually roll out to production

Can I hot-reload plugins without restarting DeepIntShield?

Section titled “Can I hot-reload plugins without restarting DeepIntShield?”

Yes! DeepIntShield supports hot-reloading plugins at runtime. You can update plugin configurations or reload plugin code without restarting the entire DeepIntShield instance.

Enable verbose logging:

{
"log_level": "debug",
"plugins": [
{
"enabled": true,
"name": "my-plugin",
"path": "./plugins/my-plugin.so",
"config": {}
}
]
}

Check plugin symbols:

Terminal window
# List symbols exported by plugin
go tool nm my-plugin.so | grep -E 'Init|GetName|PreLLMHook'

Verify Go version:

Terminal window
# Check Go version used to build plugin
go version -m my-plugin.so

Common debugging steps:

  1. Verify file exists and has correct permissions
  2. Check Go version matches DeepIntShield
  3. Confirm core package version matches
  4. Ensure all required symbols are exported
  5. Review DeepIntShield logs for detailed error messages