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 includingAPIKey,Model,BaseURL, and provider-specific fieldsProvider– the interface that abstracts chat generation, tool handling, and streaming capabilitiesFactory– a function typefunc(cfg Config) (Provider, error)that constructs concrete provider instancesRegister(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 methodTools() []Tool– returns supported tool definitions (may returnnil)- 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.goabstracts 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →