How to Implement Custom LLM Providers Beyond OpenAI Compatibility in Reasonix

Implementing custom LLM providers in Reasonix requires creating a package that satisfies the Provider interface defined in internal/provider/provider.go, registering it via the Register function in an init() block, and exposing configuration keys in the TOML schema.

Reasonix uses a clean provider abstraction to support any language model backend, whether it follows the OpenAI chat completion protocol or a completely custom format. This architecture lives in the esengine/DeepSeek-Reasonix repository and enables plug-and-play integration of new LLM services without modifying core engine code.

The Provider Abstraction in Reasonix

All LLM drivers in Reasonix implement a common interface defined in internal/provider/provider.go. This file contains four essential components that every custom provider must interact with:

  • Config – a struct holding connection parameters including APIKey, Model, BaseURL, and provider-specific fields
  • Provider – the interface that abstracts chat generation, tool handling, and streaming capabilities
  • Factory – a function type func(cfg Config) (Provider, error) that constructs concrete provider instances
  • Register(kind string, f Factory) – a global registry function that maps a short identifier string to your factory

The registration mechanism uses a package-level map populated during init() execution. When Reasonix starts, it looks up the kind value from the configuration file and invokes the matching factory to instantiate the provider.

Built-In Provider Examples

Reasonix ships with two reference implementations that demonstrate the registration pattern:

Provider File Registration Key Compatibility
OpenAI internal/provider/openai/openai.go "openai" Native OpenAI API
Anthropic internal/provider/anthropic/anthropic.go "anthropic" Native Anthropic Messages API

Both providers register themselves using identical patterns. The OpenAI driver calls provider.Register("openai", New) in its init() function, while the Anthropic driver registers with provider.Register("anthropic", New). Despite speaking completely different protocols, they plug into the same engine interface.

Services with OpenAI-compatible endpoints—such as vLLM, Ollama, or Azure OpenAI—work immediately by setting kind = "openai" and pointing base_url at the service URL.

Step-by-Step: Creating a Non-OpenAI-Compatible Provider

Follow these seven steps to implement custom LLM providers beyond OpenAI compatibility in Reasonix:

1. Create a New Package

Create a directory under internal/provider for your implementation:

mkdir internal/provider/myprovider
touch internal/provider/myprovider/myprovider.go

2. Define the Provider Struct

Structure your provider to hold any client objects, authentication tokens, or service-specific configuration:

type client struct {
    endpoint string
    token    string
    httpClient *http.Client
}

3. Implement the Provider Interface

The exact method signatures are defined in internal/provider/provider.go. At minimum, implement:

  • Chat(ctx context.Context, msgs []Message, opts Options) (ChatResult, error) – the core generation method
  • Tools() []Tool – returns supported tool definitions (may return nil)
  • Optional methods like ModelInfo(), Close(), or streaming handlers

4. Write the Constructor Factory

Your New function must match the Factory signature and validate required configuration:

func New(cfg provider.Config) (provider.Provider, error) {
    if cfg.BaseURL == "" {
        return nil, errors.New("myprovider: base_url is required")
    }
    // Initialize and return your client
}

5. Register in init()

Add registration to ensure your provider is available at runtime:

func init() {
    provider.Register("myprovider", New)
}

6. Expose Configuration Keys

Add fields to the TOML schema so users can configure your provider:

[model]
kind = "myprovider"
model = "my-model/1.0"
base_url = "https://api.myprovider.com/v1"
api_key_env = "MYPROVIDER_API_KEY"

7. Add Tests and Documentation

Reference internal/provider/openai/openai_test.go for test patterns. Verify your provider handles edge cases in request translation, response parsing, and error propagation.

Complete Implementation Example

Here's a minimal skeleton for internal/provider/myprovider/myprovider.go:

package myprovider

import (
    "context"
    "errors"
    "net/http"

    "github.com/esengine/DeepSeek-Reasonix/main-v2/internal/provider"
)

type client struct {
    endpoint string
    token    string
    httpClient *http.Client
}

// New satisfies the provider.Factory signature.
func New(cfg provider.Config) (provider.Provider, error) {
    if cfg.BaseURL == "" {
        return nil, errors.New("myprovider: base_url is required")
    }
    if cfg.APIKey == "" {
        return nil, errors.New("myprovider: api_key is required")
    }
    return &client{
        endpoint:   cfg.BaseURL,
        token:      cfg.APIKey,
        httpClient: &http.Client{Timeout: 30 * time.Second},
    }, nil
}

// Chat implements the core generation call for your custom LLM.
func (c *client) Chat(ctx context.Context, msgs []provider.Message, opts provider.Options) (provider.ChatResult, error) {
    // 1. Translate msgs to your provider's native request format
    // 2. Execute HTTP request using c.httpClient
    // 3. Parse response and populate provider.ChatResult
    // 4. Map usage statistics, finish reasons, and content
    
    return provider.ChatResult{
        Content: "response from custom provider",
        Usage: provider.Usage{
            PromptTokens:     0,
            CompletionTokens: 0,
            TotalTokens:      0,
        },
    }, nil
}

// Tools reports tool definitions supported by this backend.
func (c *client) Tools() []provider.Tool {
    // Return nil or populate with provider.Tool structs
    return nil
}

// ModelInfo exposes metadata about the configured model.
func (c *client) ModelInfo() provider.ModelInfo {
    return provider.ModelInfo{
        Name:   "my-model/1.0",
        Vendor: "MyProvider Inc.",
    }
}

func init() {
    // Register under the short name used in TOML configuration.
    provider.Register("myprovider", New)
}

Configuration Usage

Once compiled, your provider works identically to built-in options:

[model]
kind = "myprovider"
model = "my-model/1.0"
base_url = "https://api.myprovider.com/v1"
api_key_env = "MYPROVIDER_API_KEY"

The engine instantiates your driver through provider.New, which looks up "myprovider" in the global registry and invokes your factory with the parsed Config. All Reasonix features—tool calling, post-LLM hooks, effort tracking, and streaming—function automatically because they operate on the abstract Provider interface.

Key Source Files

File Purpose
internal/provider/provider.go Core abstraction: Config, Provider interface, Register function
internal/provider/openai/openai.go Reference for OpenAI-compatible implementations
internal/provider/anthropic/anthropic.go Reference for non-OpenAI protocol implementations
internal/provider/responses/responses.go Pattern for response-only or mock providers

Summary

  • Provider interface in internal/provider/provider.go abstracts all LLM backends behind unified method signatures
  • Factory pattern with Register(kind, factory) enables runtime discovery without code changes
  • OpenAI-compatible services reuse the existing driver by overriding base_url
  • Custom protocols require a new package implementing Chat(), Tools(), and registration
  • Zero core modifications needed—add your package, compile, and configure via TOML

Frequently Asked Questions

What is the minimum interface my custom provider must implement?

Your provider must implement Chat(context.Context, []Message, Options) (ChatResult, error) and Tools() []Tool as defined in internal/provider/provider.go. Optional methods like ModelInfo(), Close(), or streaming handlers extend functionality but aren't strictly required for basic operation.

Can I use a custom provider with existing OpenAI-compatible tools?

Yes. Your provider's Tools() method returns provider.Tool definitions that Reasonix serializes according to your driver's native format. The engine handles tool invocation and result integration; you only translate between Reasonix's tool schema and your LLM's expected format.

How does Reasonix handle provider errors and retries?

Providers return standard Go errors from Chat() and other methods. Reasonix's execution layer applies retry policies, circuit breakers, and fallback logic at the engine level without provider involvement. Your driver should propagate transient errors (timeouts, 5xx responses) and permanent errors (authentication failures, invalid requests) appropriately.

Where should I add configuration validation for my custom provider?

Validate required fields in your factory's New function before returning the provider instance. This ensures fast failure during startup rather than mid-execution. Reference the OpenAI driver's validation of APIKey and Model in internal/provider/openai/openai.go for patterns.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →