Providers¶
Pluma talks to two kinds of upstream: chat backends (LLM) and image backends (SD). Each has a small Go interface; today's implementations register at init() and live in the binary.
Chat: the Provider interface¶
type Provider interface {
ID() string
Upstream() string
ListModels(ctx context.Context) ([]ModelInfo, error)
StreamChat(ctx context.Context, req ChatRequest, onLine func(string) error) error
}
| Implementation | Upstream | Notes |
|---|---|---|
OpenAIProvider |
Any OpenAI-compatible /v1/chat/completions |
The workhorse — Ollama, LM Studio, mlx_lm, llama.cpp, vLLM, text-generation-webui, OpenAI hosted all speak this. |
AnthropicProvider |
https://api.anthropic.com/v1/messages |
Translates the Messages API shape back to OpenAI's SSE format so the UI doesn't know the difference. |
Registering a new provider:
func init() {
RegisterProvider("mybackend", func(profile ConnectionProfile, client *http.Client) Provider {
return &MyProvider{ ... }
})
}
The connection profile's compat field picks the factory. UI populates the compat dropdown from the registered set.
Image: the ImageProvider interface¶
type ImageProvider interface {
Kind() string
Upstream() string
Ping(ctx context.Context) error
Generate(ctx context.Context, req ImageRequest) (ImageResult, error)
ListCheckpoints(ctx context.Context) ([]string, error)
}
| Implementation | Backend | Notes |
|---|---|---|
A1111Provider |
Stable Diffusion web UI | /sdapi/v1/txt2img |
ComfyProvider |
ComfyUI | /prompt with a workflow JSON; %placeholder% substitution |
Same factory-registration pattern. The image-connection profile's kind field picks the factory.
ConnectionManager¶
server/connections.go is the registry of LLM connection profiles. It tracks which one is active, plus the boot-time fallback (from -base-url / PLUMA_BASE_URL) that takes over when no profiles are configured yet.
type ConnectionManager struct {
profiles []ConnectionProfile
activeID string
fallback *ConnectionProfile // built from CLI flags
}
func (cm *ConnectionManager) Provider() Provider { ... }
func (cm *ConnectionManager) Active() (ConnectionProfile, bool) { ... }
Provider() returns a fresh Provider for the active profile (or the fallback) each call. Cheap — providers are stateless wrappers around the shared HTTP client.
ImageConnectionManager mirrors this for image backends.
Plugin axis (future)¶
Today everything's in-binary. The plugin axis story (smelt-7bq) keeps these same interfaces at the call site but adds:
- A per-axis protobuf contract under
proto/. - A Host adapter that fronts either an in-process built-in (current behaviour) or a subprocess plugin via hashicorp/go-plugin's gRPC bus.
- Discovery: scan
<datadir>/plugins/<axis>/at boot, launch each binary, register on handshake success.
Built-in providers stay in the binary; community providers drop in as separate binaries. The UI doesn't know the difference.
The same axis pattern extends to: transports (Tailscale Serve, Cloudflared, ngrok, Tor), card sources (chub.ai, JanitorAI), auth backends (OIDC, mTLS, hardware keys), secret stores (Vault, 1Password, YubiKey), storage backends (SQLite, Postgres), themes (data only, not code).
The native vs OpenAI-compat stance¶
OpenAIProvider is the catchall — anything that speaks the OAI shape plugs in unchanged. But Pluma's official position is "native first, OpenAI-compat is a fallback":
- New backends should implement
Provideragainst their own upstream shape if they have one. OpenAIProvideris for cases where the upstream's native API is itself OpenAI-compat (mlx_lm, Ollama, vLLM all are).- This keeps Anthropic-style features (system prompts as top-level, citations, tool use) accessible without forcing every backend through OAI's translation layer.