package lua // Plugin runtime. // // A plugin is a single .lua file, loaded from its own directory, that extends // the gateway in two ways: // // 1. Hooks: it registers callbacks on the request pipeline's stages // (request_start, response_end, …). A hook receives a JSON table and // returns either nil (no opinion) or a JSON object. // 2. UI: at boot it returns HTML/CSS/JS fragments that the kernel injects // into the WebUI — either as a whole new page, or as an extra element on // an existing page. // // Design notes that are load-bearing (each one cost something to learn): // // - Plugins run in a SEPARATE VM from adapters, and each plugin gets its own // elastic pool, exactly like an adapter. Sharing one state would let a // plugin's globals corrupt an adapter's protocol translation (or vice // versa), and a plugin is third-party code while an adapter is core. // // - A plugin that errors must NEVER break request forwarding. Hook calls are // wrapped so a plugin error is logged and the original value is returned // unchanged. A broken plugin is a missing feature, not an outage — the same // reason transform_stream_chunk swallows errors today. // // - The billing plugin therefore cannot be trusted to be the source of truth // for anything the gateway must enforce. It reads usage off the hook // payload and accumulates in its own state; the gateway's own quota // accounting (internal/gateway/stats.go) stays authoritative for limits. // Two accounting paths that disagree is worse than one that is slightly // less featureful, so the split is explicit and documented. import ( "encoding/json" "fmt" "os" "path/filepath" "sort" "strings" "sync" "sync/atomic" golua "github.com/aarzilli/golua/lua" ) // Stage identifies a point in the request pipeline. Plugins may register a // function for any stage; unknown stages are ignored at call time so an old // plugin survives a gateway that grew new stages. type Stage string // The pipeline stages. The order here is the order they fire in; it is the // contract plugins are written against. const ( // StageRequestStart fires after the gateway has parsed and authorized a // request but BEFORE any upstream slot is chosen. payload: // stage, type ("chat"|"stream"|"image"), model (as requested), // key (masked gateway key id), role, source (empty), stream (bool), // messages_count, tools_count, ts (unix seconds). StageRequestStart Stage = "request_start" // StageRouted fires once a (source, model) slot has been chosen and before // the upstream call. payload adds: source, model (the resolved one), // tier (AUTO tier, -1 for the direct path), stream. StageRouted Stage = "routed" // StageRequestEnd fires exactly once per request, after the client response // has been produced (or after a failure was recorded). payload adds: // source, model, ok, status, latency_ms, first_byte_ms, prompt_tokens, // completion_tokens, cache_hit_tokens, cache_miss_tokens, image_count, // error ("" when ok). // // This is the stage a billing plugin should read: it carries the final // accounting for the request, including the upstream's own usage numbers. StageRequestEnd Stage = "request_end" ) // AllStages is the firing order, used by the docs and by the hook listing. var AllStages = []Stage{ StageRequestStart, StageRouted, StageRequestEnd, } // UIExtension is what a plugin contributes to the WebUI at boot. type UIExtension struct { // Page is a whole new sidebar entry + pane. Requires PageID and Title. // The kernel renders Page's HTML into a pane whose id is "tab-"+PageID and // adds a sidebar button with data-tab="". Page *UIPage `json:"page,omitempty"` // Elements are snippets injected into EXISTING pages, keyed by target page // id (e.g. "status", "keys"). Order within a target is plugin load order. Elements []UIElement `json:"elements,omitempty"` } // UIPage is a plugin-provided page. type UIPage struct { PageID string `json:"page_id"` // kebab-case; becomes data-tab and #tab- Title string `json:"title"` // sidebar label Icon string `json:"icon"` // optional inline SVG or short glyph Order int `json:"order"` // sidebar sort key (default 100) // Mount is the page body. It may contain