Building a MIRASTACK Provider
A Provider is how the MIRASTACK Engine talks to an AI inference backend. The engine never calls any AI API directly — it always routes LLM requests through a registered Provider plugin over gRPC.
This means you can connect the engine to any LLM backend — OpenAI, Anthropic, a local Ollama instance, a vLLM server, LM Studio, or any custom inference server — by writing a Provider.
Who should build a Provider? If you want to connect MIRASTACK to an AI backend that is not already covered by an existing provider, or if you want to customise how an existing backend is called (retry logic, custom headers, prompt formatting), you need a Provider plugin.
Providers vs Agents — Key Differences
| Agent | Provider | |
|---|---|---|
| Purpose | Perform tasks on external systems | Bridge the engine to an AI inference backend |
| Called by | The engine’s workflow/agentic executor | The engine’s internal LLM Router |
| Has actions | Yes — every provider action is a distinct tool | No — providers do not contribute to the tool catalog |
| Intent patterns | Yes — can register intent patterns | No |
| Prompt templates | Yes | No |
| SDK | mirastack-agents-sdk-go / mirastack-agents-sdk-python |
mirastack-provider-sdk-go / mirastack-provider-sdk-python |
What a Provider Must Do
Every provider receives a completion request from the engine (containing a prompt, conversation history, tool definitions, and parameters) and returns a response. It must:
- Accept
Completerequests — single-turn prompt completion - Accept
Streamrequests — streaming token-by-token completion - Accept
Embedrequests — text embedding (optional but recommended) - Report its health status by actively checking the backing AI service
- Accept
ConfigUpdatedcalls to reload credentials and settings at runtime without restarting
The Provider SDK
The Provider SDK is a separate SDK from the Agent SDK. Install it directly:
Go
go get github.com/mirastacklabs-ai/mirastack-provider-sdk-go
Python
pip install mirastack-provider-sdk-python
The Provider SDK does not have
Action,IntentPattern, orPromptTemplatetypes. Those belong to the Agent SDK. Do not mix the two SDKs in the same plugin.
Building a Provider in Go
Directory structure
mirastack-provider-my-llm/
├── go.mod
├── main.go — reads env, calls mirastack.Serve()
├── provider.go — implements mirastack.Plugin interface
├── LICENSE
└── README.md
provider.go
package main
import (
"context"
"fmt"
"sync"
mirastack "github.com/mirastacklabs-ai/mirastack-provider-sdk-go"
)
type MyLLMProvider struct {
endpoint string
apiKey string
model string
mu sync.RWMutex
}
func (p *MyLLMProvider) Info() *mirastack.PluginInfo {
return &mirastack.PluginInfo{
Name: "my_llm",
Version: "1.0.0",
Description: "My custom LLM inference backend",
Permissions: []mirastack.Permission{mirastack.PermissionRead},
ConfigParams: []mirastack.ConfigParam{
{Key: "endpoint", Type: "string", Required: true,
Description: "Inference server base URL"},
{Key: "api_key", Type: "string", Required: false,
Description: "API key for authentication", IsSecret: true},
{Key: "model", Type: "string", Required: true,
Description: "Model name to use for completions"},
},
}
}
func (p *MyLLMProvider) Schema() *mirastack.PluginSchema {
return &mirastack.PluginSchema{
InputParams: []mirastack.ParamSchema{
{Name: "prompt", Type: "string", Required: false,
Description: "User prompt (single-turn mode)"},
{Name: "messages", Type: "string", Required: false,
Description: "JSON array of messages (multi-turn/agentic mode)"},
{Name: "tools", Type: "string", Required: false,
Description: "JSON array of tool definitions"},
{Name: "tool_choice", Type: "string", Required: false,
Description: "Tool selection strategy: auto, required, none"},
{Name: "max_tokens", Type: "number", Required: false,
Description: "Maximum tokens to generate"},
{Name: "temperature", Type: "string", Required: false,
Description: "Sampling temperature"},
},
OutputParams: []mirastack.ParamSchema{
{Name: "content", Type: "string", Required: true,
Description: "LLM response text"},
{Name: "model", Type: "string", Required: true,
Description: "Model that generated the response"},
{Name: "finish_reason", Type: "string", Required: true,
Description: "Why the model stopped: stop, tool_calls, length"},
{Name: "tool_calls", Type: "string", Required: false,
Description: "JSON array of tool calls requested by the model"},
},
}
}
func (p *MyLLMProvider) Execute(ctx context.Context, req *mirastack.ExecuteRequest) (*mirastack.ExecuteResponse, error) {
p.mu.RLock()
endpoint := p.endpoint
apiKey := p.apiKey
model := p.model
p.mu.RUnlock()
if endpoint == "" {
return nil, fmt.Errorf("provider not configured: endpoint is empty")
}
// Build your completion request to the backing AI service here.
// Return the response in the standard format.
content := "..." // call your backend
return &mirastack.ExecuteResponse{
Output: map[string]string{
"content": content,
"model": model,
"finish_reason": "stop",
},
}, nil
}
// HealthCheck must actively test the backing AI service.
// This is called by the engine on a regular interval.
// A failing health check marks the provider as unhealthy and
// prevents it from receiving new requests until it recovers.
func (p *MyLLMProvider) HealthCheck(ctx context.Context) error {
p.mu.RLock()
endpoint := p.endpoint
p.mu.RUnlock()
if endpoint == "" {
return fmt.Errorf("provider not configured")
}
// Make a lightweight real call to your AI service (e.g. /models or /health endpoint)
// Return nil if healthy, an error if not.
return nil
}
// ConfigUpdated is called by the engine whenever a config value changes in Kine.
// Update your in-memory config without restarting the process.
func (p *MyLLMProvider) ConfigUpdated(ctx context.Context, config map[string]string) error {
p.mu.Lock()
defer p.mu.Unlock()
if v, ok := config["endpoint"]; ok {
p.endpoint = v
}
if v, ok := config["api_key"]; ok {
p.apiKey = v
}
if v, ok := config["model"]; ok {
p.model = v
}
return nil
}
main.go
package main
import (
mirastack "github.com/mirastacklabs-ai/mirastack-provider-sdk-go"
"go.uber.org/zap"
)
func main() {
logger, _ := zap.NewProduction()
defer logger.Sync()
// All runtime config comes from Kine via ConfigUpdated.
// Set initial defaults here; `miractl provider config-set` will push
// the real values after the provider registers.
plugin := &MyLLMProvider{
endpoint: "http://localhost:11434",
model: "llama3",
}
mirastack.Serve(plugin)
}
Registering Your Provider with the Engine
Registration is automatic — no manual steps, no CRDs. When you call mirastack.Serve(), the SDK starts the provider’s gRPC server, connects to the engine at MIRASTACK_ENGINE_ADDR, and self-registers via the RegisterPlugin RPC. The engine calls back to verify metadata and adds the provider to its LLM Router. No engine restart is required.
The same two deployment models as agents:
Co-located (same host)
Put the provider binary in the engine’s plugins.dir directory:
/opt/mirastack/plugins/
my-llm/
mirastack-provider-my-llm ← your binary
Remote (different host)
Start the provider with MIRASTACK_ENGINE_ADDR pointing to the engine. Optionally set MIRASTACK_PLUGIN_ADVERTISE_ADDR for the address the engine can reach the provider on:
export MIRASTACK_ENGINE_ADDR=engine-host:9090
export MIRASTACK_PLUGIN_ADVERTISE_ADDR=10.0.1.55:50051
./mirastack-provider-my-llm
The provider self-registers on startup. No changes to the engine’s config.yaml are needed.
Backward compatibility: You can still list remote providers in
plugins.externalin the engine’sconfig.yaml. Self-registration takes precedence when both are configured.
Check it registered:
miractl provider list
# my_llm 1.0.0 READ healthy
Configuring the Provider at Runtime
Provider configuration (API keys, endpoint URLs, model names) is managed at runtime through the engine’s settings store — not through environment variables or files. Once the provider registers, initialize its config:
miractl provider config-set my_llm endpoint http://my-inference-server:8080
miractl provider config-set my_llm api_key sk-...
miractl provider config-set my_llm model my-model-name
The engine pushes these values to the provider over gRPC via ConfigUpdated. The provider applies them live. No restart is needed.
To view the current config:
miractl provider config my_llm
Making Your Provider the Default
When multiple providers are registered, tell the engine which one to use by default:
miractl settings set llm.default_provider my_llm
You can also configure the engine to use specific providers for specific workflows — see the workflow YAML documentation for LLM step provider routing.
Health Checking — Non-Negotiable
All providers must implement a real health check that calls the backing AI service. The engine uses health status to decide whether to send requests to a provider.
A health check that always returns nil is dangerous: it hides outages and causes the engine to send requests to a dead backend, resulting in confusing user-facing errors.
What a good health check should do:
- Make the lightest possible real call to the backend (e.g. list models, ping endpoint)
- Return an error with a clear message if something is wrong
- Complete within a few seconds (the engine calls this on a 30-second interval by default)
func (p *MyLLMProvider) HealthCheck(ctx context.Context) error {
// Example: ping the /models endpoint
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// Make a real call to your backend
resp, err := p.httpClient.Get(ctx, p.endpoint+"/models")
if err != nil {
return fmt.Errorf("AI backend unreachable: %w", err)
}
if resp.StatusCode != 200 {
return fmt.Errorf("AI backend returned HTTP %d", resp.StatusCode)
}
return nil
}
Checklist Before Publishing
Info()has clearConfigParamswithIsSecret: trueon API keysHealthCheck()makes a real network call to the AI serviceConfigUpdated()applies new values live (no restart required)- Both single-turn (
prompt) and multi-turn (messages) modes are handled - Tool calling (
tools,tool_choice,tool_callsoutput) is implemented - No hardcoded API keys, endpoints, or model names
LICENSEfile is present (AGPL v3 for OSS, Proprietary for enterprise)README.mdexplains supported models and required config keys