Skip to content

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 Provider against their own upstream shape if they have one.
  • OpenAIProvider is 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.