Documentation
¶
Overview ¶
Package lmchatkit is a self-contained chat UI + backend protocol that mounts into any HTTP server. The host implements Host to provide an LLM completion stream and (optionally) MCP tools, prompts and resources; lmchatkit owns the frontend bundle, the chat session protocol, persona loading and slash-command loading.
Routes are mounted under a configurable prefix (typically /chat) via Server.Mount. Auth is the host's responsibility — pass an [AuthMiddleware] in Config and it wraps every lmchatkit handler.
Index ¶
- Constants
- Variables
- func OpenAIChatRequest(req CompleteRequest) map[string]interface{}
- func TranslateOpenAIStream(ctx context.Context, body io.Reader, events chan<- Event) error
- type CommandSource
- type CompleteRequest
- type Config
- type ConversationSummary
- type Event
- type EventBroadcaster
- type EventType
- type FinishReason
- type HistoryStore
- type Host
- type Message
- type Model
- type Persona
- type PersonaSource
- type Prompt
- type PromptArgument
- type PromptMessage
- type PromptResult
- type Resource
- type ResourceResult
- type Role
- type Server
- type ServerEvent
- type SlashCommand
- type StandardHost
- func (h *StandardHost) AugmentSystemPrompt(ctx context.Context, current string) string
- func (h *StandardHost) CallTool(ctx context.Context, name string, arguments json.RawMessage) (ToolResult, error)
- func (h *StandardHost) Complete(ctx context.Context, req CompleteRequest, events chan<- Event) error
- func (h *StandardHost) GetPrompt(ctx context.Context, name string, args map[string]string) (PromptResult, error)
- func (h *StandardHost) ListPrompts(ctx context.Context) ([]Prompt, error)
- func (h *StandardHost) ListResources(ctx context.Context) ([]Resource, error)
- func (h *StandardHost) ListTools(ctx context.Context) ([]Tool, error)
- func (h *StandardHost) Models(ctx context.Context) ([]Model, error)
- func (h *StandardHost) ReadResource(ctx context.Context, uri string) (ResourceResult, error)
- type StaticCommands
- type StaticPersonas
- type StoredConversation
- type SystemPromptAugmenter
- type Tool
- type ToolCall
- type ToolResult
- type Usage
Constants ¶
const SkillToolName = "lmchatkit__get_skill"
SkillToolName is the name of the virtual skill-retrieval tool. The lmchatkit__ prefix prevents collisions with local scriptling tools (no prefix) and remote MCP tools (namespace__ prefix).
Variables ¶
var AssetFS = assetsFS
AssetFS exposes the bundled chat.js so hosts can serve it from their own asset pipeline if they prefer. Server.Mount already wires it up at {prefix}/assets/chat.js.
var ErrMissingHost = errString("lmchatkit: Config.Host is required")
ErrMissingHost is returned by New when Config.Host is nil.
var SkillTool = Tool{ Name: SkillToolName, Description: "Retrieve a skill's detailed instructions by URI. Pass the skill URI (e.g. 'skill://golang' or '@skill://golang'). Returns the skill content as text.", InputSchema: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "name": map[string]interface{}{ "type": "string", "description": "The skill URI to retrieve (e.g. skill://golang)", }, }, "required": []string{"name"}, }, }
SkillTool is the virtual tool definition appended to the tool list when the host has skill:// resources. It lets the LLM pull in skill instructions on demand — the model sees skill descriptions in the system prompt, then calls this tool to retrieve the full content.
Functions ¶
func OpenAIChatRequest ¶
func OpenAIChatRequest(req CompleteRequest) map[string]interface{}
use this instead of hand-rolling the conversion. The returned map is ready to json.Marshal and POST.
func TranslateOpenAIStream ¶
TranslateOpenAIStream reads an OpenAI-compatible SSE stream (the response body from POST /v1/chat/completions with stream:true) and emits lmchatkit events onto the events channel. Handles:
- delta.content → EventDelta
- delta.reasoning_content / delta.reasoning → EventReasoning
- delta.tool_calls (fragmented by index) → accumulated and flushed as EventToolCall before the terminal event
- finish_reason → mapped to FinishStop / FinishToolCalls / FinishLength
- data: [DONE] → stream end
Hosts that loopback to their own OpenAI endpoint can call this directly instead of reimplementing the SSE parsing. The events channel must be buffered (lmchatkit's chat handler uses a 32-slot buffer).
Types ¶
type CommandSource ¶
type CommandSource interface {
Commands(ctx context.Context) ([]SlashCommand, error)
}
CommandSource is the backend behind /api/commands. Same contract as PersonaSource: a file-watching default exists, hosts with a database (e.g. per-user commands in knot) implement their own.
type CompleteRequest ¶
type CompleteRequest struct {
Model string `json:"model"`
Messages []Message `json:"messages"`
Tools []Tool `json:"tools,omitempty"`
Params map[string]interface{} `json:"params,omitempty"`
}
CompleteRequest is the host-facing request to stream a chat completion. Messages is the full conversation including any prior tool results. Tools is the subset of tools the user has enabled for this chat (may be empty). Params is model parameters merged from persona + per-request overrides (temperature, max_tokens, etc.); the host passes it through to the underlying LLM API as it sees fit.
type Config ¶
type Config struct {
// Prefix is the URL prefix lmchatkit mounts under, e.g. "/chat". Must be
// non-empty. Routes are registered as Prefix+"/api/..." and
// Prefix+"/assets/...". The host owns the Prefix page route itself —
// see lmchatkit/examples/chat.html.
Prefix string
// PersonasDir is the directory scanned for persona TOML files. Empty
// means no persona files — only the built-in Default persona is offered.
//
// Ignored when PersonaSource is set; use whichever fits the host. File
// watching only applies to this dir, not to a custom source.
PersonasDir string
// CommandsDir is the directory scanned for slash-command markdown files.
// Empty disables slash commands entirely. Ignored when CommandSource is
// set.
CommandsDir string
// PersonaSource overrides PersonasDir. Use this when personas come from
// somewhere other than the filesystem — typically a database, or a single
// system-defined persona via [StaticPersonas].
PersonaSource PersonaSource
// CommandSource overrides CommandsDir. Same contract as PersonaSource.
CommandSource CommandSource
// Host is the contract between lmchatkit and the embedding application.
// Must be non-nil.
Host Host
// AuthMiddleware wraps every lmchatkit HTTP handler. It is the host's
// responsibility to enforce authentication, sessions, rate limiting, etc.
// nil means no auth (rare; only appropriate for fully internal hosts).
AuthMiddleware func(http.Handler) http.Handler
// History persists chat conversations server-side. nil = browser
// sessionStorage (no persistence across browser restarts, no
// cross-tab sync). When non-nil, conversation CRUD endpoints are
// mounted and the browser switches to server-side mode automatically.
History HistoryStore
// Events broadcasts changes to connected SSE clients for cross-tab
// sync and push notifications (tools/prompts/resources changed).
// nil = no SSE (browser falls back to polling on chat completion).
Events *EventBroadcaster
}
Config configures a Server at mount time.
type ConversationSummary ¶
type ConversationSummary struct {
ID string `json:"id"`
Title string `json:"title"`
PersonaID string `json:"persona_id"`
Model string `json:"model"`
Params map[string]interface{} `json:"params,omitempty"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
ConversationSummary is the lightweight sidebar entry — no messages.
type Event ¶
type Event struct {
Type EventType `json:"type"`
Delta string `json:"delta,omitempty"`
Reasoning string `json:"reasoning,omitempty"` // carries EventReasoning fragments
ToolCall *ToolCall `json:"tool_call,omitempty"`
FinishReason FinishReason `json:"finish_reason,omitempty"`
Usage *Usage `json:"usage,omitempty"`
Error string `json:"error,omitempty"`
}
Event is one streamed server-sent event in the chat protocol. Type determines which fields are meaningful.
type EventBroadcaster ¶
type EventBroadcaster struct {
// contains filtered or unexported fields
}
EventBroadcaster fans out events to all connected SSE subscribers. Used for cross-tab sync (conversation saved/deleted from another tab) and push notifications (tools/prompts/resources changed by the scriptling watcher).
func NewEventBroadcaster ¶
func NewEventBroadcaster() *EventBroadcaster
NewEventBroadcaster creates a ready-to-use broadcaster.
func (*EventBroadcaster) Broadcast ¶
func (b *EventBroadcaster) Broadcast(event ServerEvent)
Broadcast sends an event to all subscribers. Non-blocking — if a subscriber's buffer is full, the event is dropped.
func (*EventBroadcaster) Subscribe ¶
func (b *EventBroadcaster) Subscribe() chan ServerEvent
Subscribe returns a channel that receives events. The caller must Unsubscribe when done (e.g. when the SSE connection closes).
func (*EventBroadcaster) Unsubscribe ¶
func (b *EventBroadcaster) Unsubscribe(ch chan ServerEvent)
Unsubscribe removes a subscriber and closes its channel.
type EventType ¶
type EventType string
EventType identifies one SSE event in the chat stream protocol.
const ( EventDelta EventType = "delta" // partial assistant text EventReasoning EventType = "reasoning" // partial reasoning/thinking text (separate from visible content) EventToolCall EventType = "tool_call" // model requested a tool call; frontend must confirm + execute then resubmit EventDone EventType = "done" // stream complete; carry usage/finish_reason EventError EventType = "error" // stream failed )
type FinishReason ¶
type FinishReason string
FinishReason explains why the stream ended.
const ( FinishStop FinishReason = "stop" FinishToolCalls FinishReason = "tool_calls" FinishLength FinishReason = "length" )
type HistoryStore ¶
type HistoryStore interface {
List(ctx context.Context) ([]ConversationSummary, error)
Get(ctx context.Context, id string) (*StoredConversation, error)
Save(ctx context.Context, conv *StoredConversation) error
Delete(ctx context.Context, id string) error
}
HistoryStore persists chat conversations server-side. If nil on Config, the browser uses sessionStorage (no server-side persistence, no cross-tab sync). Hosts implement this with KV stores, databases, files — whatever fits. For per-user stores (knot), the implementation extracts the user from the request context; lmchatkit passes ctx through unchanged, so the host owns both the identity context key (set in its auth middleware) and its interpretation here.
type Host ¶
type Host interface {
// Models returns the models the chat user may select from. The list may
// be empty if the host has no concept of model picker (rare).
Models(ctx context.Context) ([]Model, error)
// Complete streams a chat completion for the given request. Implementations
// push events onto events (never block on a full channel — lmchatkit's
// channel is buffered) and return when the stream is finished. If the
// model emitted tool calls, emit one [EventToolCall] per call and return
// with FinishReason == FinishToolCalls — the frontend will execute the
// tools via CallTool and resubmit the conversation.
Complete(ctx context.Context, req CompleteRequest, events chan<- Event) error
// ListTools returns the MCP tools available to chat. May return nil/empty
// if no tools are configured.
ListTools(ctx context.Context) ([]Tool, error)
// CallTool invokes a tool by name with raw-JSON arguments. The arguments
// are exactly what the model produced (after the user confirmed), so the
// host is responsible for any validation.
CallTool(ctx context.Context, name string, arguments json.RawMessage) (ToolResult, error)
// ListPrompts / GetPrompt expose MCP prompts. GetPrompt renders the prompt
// with the given arguments.
ListPrompts(ctx context.Context) ([]Prompt, error)
GetPrompt(ctx context.Context, name string, args map[string]string) (PromptResult, error)
// ListResources / ReadResource expose MCP resources. ReadResource takes a
// concrete URI (the caller is responsible for expanding templates).
ListResources(ctx context.Context) ([]Resource, error)
ReadResource(ctx context.Context, uri string) (ResourceResult, error)
}
Host is the contract between lmchatkit and its embedding application. Every method takes a context so hosts can enforce timeouts / cancellation. Any method may return an error; lmchatkit surfaces it to the user.
All methods must be safe for concurrent use: lmchatkit is stateless and a single Server may serve many simultaneous chat sessions across many users.
type Message ¶
type Message struct {
ID string `json:"id,omitempty"`
Role Role `json:"role"`
Content any `json:"content"`
Thinking string `json:"thinking,omitempty"`
Info map[string]interface{} `json:"info,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
ToolCallID string `json:"tool_call_id,omitempty"`
ToolName string `json:"tool_name,omitempty"`
}
Message is one turn in a conversation. Content is normally a string, but when a message carries resource attachments the frontend may send it as an OpenAI-compatible content array. The Go type uses interface{} to accept both shapes and pass them through to the host's Complete implementation verbatim.
ID, Thinking, and Info are UI-only fields that the frontend sets on messages for display purposes (Alpine x-for keys, reasoning disclosure, info cards). They are persisted in conversation history so the UI can reconstruct the exact display on reload, but the chat handler strips them before passing messages to Host.Complete — the LLM never sees them.
Content does NOT use omitempty — empty string content must be preserved when saving/loading conversations from the history store. With omitempty, an empty assistant message would lose its "content" key entirely, and on reload the browser would see `undefined` instead of `""`.
type Model ¶
type Model struct {
ID string `json:"id"`
Label string `json:"label,omitempty"` // human-friendly label; falls back to ID
Provider string `json:"provider,omitempty"` // optional source tag for the UI
}
Model describes one model the chat user can pick from.
type Persona ¶
type Persona struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
SystemPrompt string `json:"system_prompt,omitempty"`
DefaultModel string `json:"default_model,omitempty"`
Params map[string]interface{} `json:"params,omitempty"`
// SourceFile is absolute path the persona was loaded from. Empty for the
// built-in "Default" persona.
SourceFile string `json:"-"`
}
Persona is one chat persona loaded from a TOML file in PersonasDir. SystemPrompt, DefaultModel and Params are all optional; an empty persona is valid and reduces to a no-op preset.
type PersonaSource ¶
PersonaSource is the backend behind /api/personas. The default implementation reads TOML files from a watched directory; hosts with a database (or a single system-defined persona) supply their own.
Personas is called on every /api/personas request so a DB-backed source always reflects current state without needing a watcher.
type Prompt ¶
type Prompt struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Arguments []PromptArgument `json:"arguments,omitempty"`
}
Prompt is one MCP prompt exposed to the chat.
type PromptArgument ¶
type PromptArgument struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Required bool `json:"required,omitempty"`
}
PromptArgument is one named argument a prompt accepts.
type PromptMessage ¶
PromptMessage is one message produced by rendering a prompt.
type PromptResult ¶
type PromptResult struct {
Description string `json:"description,omitempty"`
Messages []PromptMessage `json:"messages"`
}
PromptResult is the rendered output of GetPrompt.
type Resource ¶
type Resource struct {
URI string `json:"uri"`
Template bool `json:"template,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
MimeType string `json:"mime_type,omitempty"`
}
Resource is one MCP resource (static or templated) exposed to the chat. When Template is true, URI contains {var} placeholders the user must fill.
type ResourceResult ¶
type ResourceResult struct {
URI string `json:"uri"`
Text string `json:"text,omitempty"`
Blob string `json:"blob,omitempty"`
MimeType string `json:"mime_type,omitempty"`
}
ResourceResult is the content of a read resource. Text is used for textual content; Blob carries base64-encoded binary content.
type Role ¶
type Role string
Role identifies the speaker of a chat message. Mirrors OpenAI's role names so hosts that proxy to OpenAI-compatible APIs can pass messages through verbatim.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server is a self-contained chat UI + backend. Build one with New and mount it into any *http.ServeMux via Server.Mount.
func New ¶
New builds a Server, eagerly loading personas and slash commands from the configured sources so the first request is fast.
func (*Server) Close ¶
func (s *Server) Close()
Close releases watchers and goroutines owned by this Server. Sources the host supplied via Config.PersonaSource / Config.CommandSource are NOT closed — the host owns their lifecycle. Safe to call multiple times.
func (*Server) Mount ¶
Mount registers lmchatkit's API and asset routes on mux under the configured prefix. It deliberately does NOT register a page route — the host owns the chat page template (so it lives in the host's source tree where Tailwind can scan it). Hosts render /chat themselves using the example template shipped at lmchatkit/examples/chat.html as a starting point.
Routes registered:
- {prefix}/api/... — chat/tools/prompts/resources endpoints
- {prefix}/assets/... — embedded chat.js, chat.css, markdown.js
The host's AuthMiddleware (if set) wraps every handler.
type ServerEvent ¶
type ServerEvent struct {
Type string `json:"type"` // conversation_saved, conversation_deleted, conversation_renamed, prompts_changed, resources_changed
ID string `json:"id,omitempty"`
}
ServerEvent is one event pushed to SSE subscribers.
type SlashCommand ¶
type SlashCommand struct {
ID string `json:"id"` // stable hash of name
Name string `json:"name"` // canonical name (filename stem), no leading slash
Description string `json:"description,omitempty"` // from frontmatter, shown in the selection menu
ArgumentHint string `json:"argument_hint,omitempty"` // from frontmatter, e.g. "<github-handle>"
AllowedTools string `json:"allowed_tools,omitempty"` // from frontmatter, comma-separated tool names to auto-allow
Body string `json:"body"` // raw markdown (frontmatter stripped); $ARGUMENTS replaced when rendered
Source string `json:"source"` // short source hint for debugging
}
SlashCommand is one user-invokable slash command loaded from a markdown file in CommandsDir. The command name is the filename stem (e.g. help.md -> /help). The Body is the raw markdown with optional $ARGUMENTS placeholder substituted at render time. Description and ArgumentHint come from YAML frontmatter at the top of the file (Claude-style):
--- description: Open a PR and tag a reviewer argument-hint: <github-handle> ---
Review the changes by $ARGUMENTS and suggest...
type StandardHost ¶
type StandardHost struct {
// ModelsFunc returns the models available for selection. Called on each
// /api/models request so the list is always current. Required.
ModelsFunc func(ctx context.Context) ([]Model, error)
// OpenAIBaseURL is where /v1/chat/completions lives. For a self-loopback
// (llmrouter), this is "http://127.0.0.1:<port>". For an external LLM
// proxy, it's that proxy's URL. Required.
OpenAIBaseURL string
// OpenAIToken is the bearer token sent with the completion request.
// For a self-loopback this is the server's API token; for a user-scoped
// proxy it's the user's token. May be empty if the endpoint doesn't
// require auth.
OpenAIToken string
// MCPServer returns the *mcp.Server to use for tool/prompt/resource
// calls. For single-user hosts (llmrouter), return the same server
// every time. For per-user hosts (knot), extract the user from the
// context and return their server. Return nil to disable MCP for that
// request.
MCPServer func(ctx context.Context) *mcplib.Server
// SystemPromptAugmenter is called on every /api/chat request with the
// current system prompt (from the persona). The returned string
// replaces the system prompt sent to the LLM — the stored conversation
// is not modified. Use this to inject dynamic context like available
// skills. Optional: nil = pass through unchanged.
SystemPromptAugmenter func(ctx context.Context, current string) string
// HTTPClient overrides the default client. nil = use http.DefaultClient
// with no timeout (streaming needs no overall timeout).
HTTPClient *http.Client
}
StandardHost is a ready-to-use Host implementation for apps that:
- Talk to an OpenAI-compatible /v1/chat/completions endpoint
- Expose MCP tools/prompts/resources via an *mcp.Server
Hosts with different needs (non-OpenAI LLM, custom tool backends, per-user MCP servers) construct a StandardHost with the appropriate functions. Hosts with fundamentally different architectures implement Host themselves.
Auth is NOT handled here — the host wraps the lmchatkit routes with its own middleware via Config.AuthMiddleware, exactly like it protects any other route. StandardHost never sees tokens, sessions, or passwords.
func (*StandardHost) AugmentSystemPrompt ¶
func (h *StandardHost) AugmentSystemPrompt(ctx context.Context, current string) string
AugmentSystemPrompt satisfies SystemPromptAugmenter. If the function field is nil, the prompt passes through unchanged.
func (*StandardHost) CallTool ¶
func (h *StandardHost) CallTool(ctx context.Context, name string, arguments json.RawMessage) (ToolResult, error)
func (*StandardHost) Complete ¶
func (h *StandardHost) Complete(ctx context.Context, req CompleteRequest, events chan<- Event) error
func (*StandardHost) GetPrompt ¶
func (h *StandardHost) GetPrompt(ctx context.Context, name string, args map[string]string) (PromptResult, error)
func (*StandardHost) ListPrompts ¶
func (h *StandardHost) ListPrompts(ctx context.Context) ([]Prompt, error)
func (*StandardHost) ListResources ¶
func (h *StandardHost) ListResources(ctx context.Context) ([]Resource, error)
func (*StandardHost) ListTools ¶
func (h *StandardHost) ListTools(ctx context.Context) ([]Tool, error)
func (*StandardHost) ReadResource ¶
func (h *StandardHost) ReadResource(ctx context.Context, uri string) (ResourceResult, error)
type StaticCommands ¶
type StaticCommands []SlashCommand
StaticCommands is a CommandSource backed by a fixed slice.
func (StaticCommands) Commands ¶
func (s StaticCommands) Commands(ctx context.Context) ([]SlashCommand, error)
type StaticPersonas ¶
type StaticPersonas []Persona
StaticPersonas is a PersonaSource backed by a fixed slice. Useful for single-tenant hosts that have one system-defined persona (e.g. knot).
type StoredConversation ¶
type StoredConversation struct {
ConversationSummary
Messages []Message `json:"messages"`
EnabledTools []string `json:"enabled_tools,omitempty"`
}
StoredConversation is a full conversation including messages.
type SystemPromptAugmenter ¶
type SystemPromptAugmenter interface {
AugmentSystemPrompt(ctx context.Context, current string) string
}
SystemPromptAugmenter is an optional interface a Host can implement to dynamically augment the system prompt on every chat request. The chat handler calls AugmentSystemPrompt with the current system prompt content (from the persona) and uses the returned string when forwarding to the LLM. The stored conversation is not modified — the augmentation is transient, recomputed on each request so it always reflects current state (e.g. skills added/removed at runtime).
type Tool ¶
type Tool struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
InputSchema map[string]interface{} `json:"input_schema,omitempty"`
}
Tool describes one MCP tool exposed to the chat. InputSchema is the JSON schema for arguments (as exposed by MCP tools/list); the frontend uses it to render argument hints when confirming a tool call.
type ToolCall ¶
type ToolCall struct {
ID string `json:"id"`
Name string `json:"name"`
Arguments json.RawMessage `json:"arguments"`
}
ToolCall is a single tool invocation requested by the model. Arguments is the raw JSON arguments string (the host parses it according to the tool's input schema).
type ToolResult ¶
ToolResult is the outcome of a tool call. Content is the model-facing text (typically the MCP tool response). isError flags the result as an error so the model knows not to treat Content as a successful payload.