llmux

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 11, 2026 License: MIT Imports: 24 Imported by: 0

README

kelindar/llmux
Go Version PkgGoDev License Coverage

llmux is a small, embeddable Go HTTP handler for exposing application-owned agent logic through standard AI client protocols. It is a protocol server, not an LLM proxy: your Agent runs in-process and does not need an upstream HTTP API.

Quick start

go get github.com/kelindar/llmux
import (
	"context"
	"net/http"

	"github.com/kelindar/llmux"
	"github.com/kelindar/llmux/chat"
)

type agents struct {
	echo chat.Agent
}

func (a *agents) List(context.Context) (map[string]chat.Info, error) {
	return map[string]chat.Info{"echo": {}}, nil
}

func (a *agents) Load(_ context.Context, target string) (chat.Agent, chat.Info, error) {
	if target != "echo" {
		return nil, chat.Info{}, chat.NotFound()
	}
	return a.echo, chat.Info{}, nil
}

mux := http.NewServeMux()
mux.Handle("/v1/", http.StripPrefix("/v1", llmux.New(&agents{
	echo: chat.AgentFunc(func(ctx context.Context, req *chat.Request, emit chat.Emit) (chat.Outcome, error) {
		return chat.Outcome{}, emit.Text("hello")
	}),
})))
http.ListenAndServe(":8080", mux)

Mount llmux.New(catalog) under your existing net/http server so auth middleware and request context stay yours. The constructor does not open a listener.

Agent.Run runs once per accepted request. All output goes through the serial Emit callback (chat.ErrConcurrentEmit on concurrent calls). Stop when Emit returns an error. Use emit.Text, emit.Tool, emit.Delta, and the related helpers in github.com/kelindar/llmux/chat. Audio backends plug in with WithTranscriber / WithSpeaker from github.com/kelindar/llmux/audio.

See examples/basic.

Endpoints

Paths are exact. Mount under a prefix with http.StripPrefix (for example /v1).

Path Protocol Notes
POST /chat/completions OpenAI Chat Completions Text, media input, text/audio output, client tools, structured output, streaming
POST /responses OpenAI Responses compatibility profile Documented subset: text/media/tools/reasoning, generated images when requested
POST /messages Anthropic Messages Text/image/document input, tool use, streaming
GET /models OpenAI models list Catalog.List keys; empty when Catalog is nil
POST /audio/transcriptions OpenAI transcription Requires WithTranscriber
POST /audio/speech OpenAI speech Requires WithSpeaker
POST /mcp MCP Streamable HTTP Requires WithMCP

Unsupported recognized features return protocol errors instead of being ignored. Responses is a compatibility subset, not full OpenAI parity.

Catalog and Info

Catalog.List feeds /models and MCP discovery. Catalog.Load obtains the agent for each call. Listing visibility never replaces Load authorization. A nil Catalog projects empty listings and fails Load clearly.

chat.Info{
	InputModalities:  chat.ModalityText | chat.ModalityImage,
	OutputModalities: chat.ModalityText,
	Tools:            true,
	ClientTools:      true,
	Description:      "Draws charts from tabular input.",
	Tool:             "draw_chart", // nonempty exposes this agent on /mcp
	Created:          1735689600,
	OwnedBy:          "acme-agents",
}

Zero Info means text in/out with ordinary generation controls. Set ImageGeneration: true for Responses image output (request must include the image_generation tool). ClientTools is required before function calls are handed to a client. Internal agent tools never become client tool calls on their own.

Persistence and lifecycle

Without a Store, llmux assigns response IDs and timestamps. With WithStore, your app owns continuation (previous_response_id), identity, replay, and Finish. Retention defaults stay off until WithStoreDefault or an explicit store field says otherwise. Chat Completions and Anthropic have no continuation mapping here.

llmux.New(catalog,
	llmux.WithStore(store),
	llmux.WithStoreDefault(true),
)

Use ResponsesBody when you serve GET retrieval so create, replay, and retrieve share one encoder. For Accept / Finish / RunTimeout details, see examples/lifecycle and examples/lifecycle-full, plus the llmux.Store and chat.Acceptance docs.

MCP

WithMCP enables POST /mcp (stateless Streamable HTTP, revision 2026-07-28). Catalog entries with a nonempty Info.Tool become tools. Each tool takes {"message":"<string>"} and runs through the same Load, validation, limits, Store, and Finish path as the chat endpoints. Put your auth middleware in front of /mcp; llmux does not implement OAuth.

See examples/mcp.

Media and audio

Media carries inline bytes, an HTTP(S) URL, or an app-owned asset ref. URLs are not fetched during decode; use WithAssetResolver for authorized resolution. Protocol mappings differ: Chat Completions supports audio I/O, Responses supports generated images (not generic audio items), Anthropic has no audio mapping here.

See examples/multimodal.

Limits and errors

Zero Limits default to:

  • request JSON: 8 MiB
  • each inline media value: 16 MiB
  • resolved assets per request: 32
  • accumulated agent output: 8 MiB
  • one SSE event: 1 MiB
  • multipart request: 32 MiB

Override with WithLimits. Client-facing errors are sanitized; WithErrorLog observes operational failures without leaking them into responses. Streaming failures before the first event are ordinary error bodies; after headers are committed, codecs emit their protocol stream error.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func IsDelivery

func IsDelivery(err error) bool

IsDelivery reports whether err is or wraps ErrDelivery.

func ResponsesBody

func ResponsesBody(resp chat.Response) (any, error)

ResponsesBody builds the same Responses JSON envelope used for creation, idempotent replay, and application-owned GET retrieval from one Response.

Types

type Catalog

type Catalog interface {
	List(context.Context) (map[string]chat.Info, error)
	Load(context.Context, string) (chat.Agent, chat.Info, error)
}

Catalog is the application integration for agent discovery and loading. Both methods receive the authenticated request context.

List returns caller-visible targets and their Info without constructing agents. Map keys are the targets accepted by Load. llmux reads the returned map and never mutates it or its values. List failures are operational errors and are never exposed to clients.

Load obtains one executable agent and independently checks execution authorization. It returns current Info for capability validation; execution never calls List merely to obtain capabilities. Listing visibility never replaces Load authorization.

A nil Catalog is allowed: List projects an empty catalog, and Load fails with a clear operational error. llmux never implements Load by scanning List and never caches catalogs across authenticated callers.

type Handler

type Handler struct {
	// contains filtered or unexported fields
}

Handler exposes configured agents through standard HTTP endpoints. It does not create a listener and is safe for concurrent requests.

func New

func New(catalog Catalog, options ...Option) *Handler

New builds a Handler with the given catalog and options. A nil catalog is allowed: GET /models and MCP discovery project empty catalogs, and Load fails with a clear operational error. Store is optional via WithStore.

func (*Handler) ServeHTTP

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP routes supported protocol and audio endpoints at exact paths. Mount under an application prefix with http.StripPrefix; authentication remains outside llmux.

type Option

type Option func(*Handler)

Option configures Handler. Options are applied once by New.

func WithAssetResolver

func WithAssetResolver(resolver chat.AssetResolver) Option

WithAssetResolver enables resolution of application-owned media references.

func WithErrorLog

func WithErrorLog(logf func(context.Context, error)) Option

WithErrorLog lets an application observe operational failures without exposing raw backend or prompt data to clients. The hook is not called for ordinary client validation errors.

func WithLimits

func WithLimits(limits chat.Limits) Option

WithLimits sets request, media, and output size limits for the handler.

func WithMCP

func WithMCP() Option

WithMCP enables the optional MCP endpoint at the exact path /mcp (over stateless Streamable HTTP). It takes no arguments: the exposed tools come from Catalog.List, filtered to entries whose Info.Tool is nonempty. A nil catalog yields an empty tool catalog, never implicit enumeration.

The transport is constructed after all options are applied, so option ordering does not matter.

func WithSpeaker

func WithSpeaker(speaker audio.Speaker) Option

WithSpeaker enables POST /audio/speech.

func WithStore

func WithStore(store Store) Option

WithStore enables continuation loading and response lifecycle acceptance. Merely supplying a Store does not change retention defaults; see WithStoreDefault. Applications without persistence omit this option.

func WithStoreDefault

func WithStoreDefault(retain bool) Option

WithStoreDefault sets the content-retention policy when the request omits store. The zero option default is false (current behavior). Explicit store:true or store:false always overrides this default. Effective retention (Retain) requires a configured Store.

func WithTranscriber

func WithTranscriber(transcriber audio.Transcriber) Option

WithTranscriber enables POST /audio/transcriptions.

type Store

type Store interface {
	Load(context.Context, string) ([]chat.Item, error)
	Accept(context.Context, *chat.TurnRequest) (chat.Acceptance, error)
}

Store is the optional application integration for continuation and response lifecycle. Configure it with WithStore.

Load authorizes access and returns the ordered history that should precede the current turn for previous_response_id. A Store that does not support continuation may return an appropriate error.

Accept reserves new work, rejects conflicts, or returns an existing response for replay. Acceptance.Finish remains the per-request completion callback: it captures reserved application state and persists the terminal result. Implementations may share underlying persistence; llmux does not require coordination maps between Load and Accept.

Ordering for a retained request:

  1. Validate request, resolve store policy, load continuation via Load.
  2. Accept — reserve identity, reject conflicts, or return Replay. Capture request-local resources on Acceptance.Finish.
  3. Agent.Run (skipped on Replay). RunTimeout > 0 detaches client cancel and bounds execution; llmux owns that context and cancels it on exit.
  4. Finish exactly once for accepted executions, with the execution context. That context may already be cancelled. Finish owns any detached, bounded cleanup work (for example context.WithTimeout(context.WithoutCancel(ctx), timeout)). Not called for Accept errors, completed Replays, or when Finish is nil. Treat the Response as read-only: nested data is shared with the response encoded after Finish returns. Call Response.Clone() before retaining or modifying it.
  5. Advertise success only after Finish succeeds.

Activity: set Acceptance.Activity and emit Activity(name, json). Keep application-specific fields outside standard envelopes.

Directories

Path Synopsis
examples
basic command
lifecycle command
lifecycle-full command
mcp command
multimodal command
internal
mcp

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL