This guide covers how to add new API endpoints and properly integrate them with the auth/permissions system.
Before you ship a new endpoint or capability surface, re-read the checklist at the bottom of this file. LocalAI advertises its feature surface in several independent places — miss any one of them and clients/admins/UI won't know the endpoint exists.
Authentication and authorization flow through three layers:
- Global auth middleware (
core/http/auth/middleware.go→auth.Middleware) — applied to every request incore/http/app.go. Handles session cookies, Bearer tokens, API keys, and legacy API keys. Populatesauth_userandauth_rolein the Echo context. - Feature middleware (
auth.RequireFeature) — per-feature access control applied to route groups or individual routes. Checks if the authenticated user has the specific feature enabled. - Admin middleware (
auth.RequireAdmin) — restricts endpoints to admin users only.
When auth is disabled (no auth DB, no legacy API keys), all middleware becomes pass-through (auth.NoopMiddleware).
Write the endpoint handler in the appropriate package under core/http/endpoints/. Follow existing patterns:
// core/http/endpoints/localai/my_feature.go
func MyFeatureEndpoint(app *application.Application) echo.HandlerFunc {
return func(c echo.Context) error {
// Use auth.GetUser(c) to get the authenticated user (may be nil if auth is disabled)
user := auth.GetUser(c)
// Your logic here
return c.JSON(http.StatusOK, result)
}
}Add routes in the appropriate file under core/http/routes/. The file you use depends on the endpoint category:
| File | Category |
|---|---|
routes/openai.go |
OpenAI-compatible API endpoints (/v1/...) |
routes/localai.go |
LocalAI-specific endpoints (/api/..., /models/..., /backends/...) |
routes/agents.go |
Agent pool endpoints (/api/agents/...) |
routes/auth.go |
Auth endpoints (/api/auth/...) |
routes/ui_api.go |
UI backend API endpoints |
Choose the appropriate protection level:
Exempt paths bypass auth entirely. Add to isExemptPath() in middleware.go or use the /api/auth/ prefix (always exempt). Use sparingly — most endpoints should require auth.
The global middleware already handles this. API paths (/api/, /v1/, etc.) automatically require authentication when auth is enabled. You don't need to add any extra middleware.
router.GET("/v1/my-endpoint", myHandler) // auth enforced by global middlewarePass adminMiddleware to the route. This is set up in app.go and passed to Register*Routes functions:
// In the Register function signature, accept the middleware:
func RegisterMyRoutes(router *echo.Echo, app *application.Application, adminMiddleware echo.MiddlewareFunc) {
router.POST("/models/apply", myHandler, adminMiddleware)
}For endpoints that should be toggleable per-user, use feature middleware. There are two approaches:
Approach A: Route-level middleware (preferred for groups of related endpoints)
// In app.go, create the feature middleware:
myFeatureMw := auth.RequireFeature(application.AuthDB(), auth.FeatureMyFeature)
// Pass it to the route registration function:
routes.RegisterMyRoutes(e, app, myFeatureMw)
// In the routes file, apply to a group:
g := e.Group("/api/my-feature", myFeatureMw)
g.GET("", listHandler)
g.POST("", createHandler)Approach B: RouteFeatureRegistry (preferred for individual OpenAI-compatible endpoints)
Add an entry to RouteFeatureRegistry in core/http/auth/features.go. The RequireRouteFeature global middleware will automatically enforce it:
var RouteFeatureRegistry = []RouteFeature{
// ... existing entries ...
{"POST", "/v1/my-endpoint", FeatureMyFeature},
}When you need a new toggleable feature (not just a new endpoint under an existing feature):
Add to core/http/auth/permissions.go:
const (
// Add to the appropriate group:
// Agent features (default OFF for new users)
FeatureMyFeature = "my_feature"
// OR API features (default ON for new users)
FeatureMyFeature = "my_feature"
)Then add it to the appropriate slice:
// Default OFF — user must be explicitly granted access:
var AgentFeatures = []string{..., FeatureMyFeature}
// Default ON — user has access unless explicitly revoked:
var APIFeatures = []string{..., FeatureMyFeature}In core/http/auth/features.go, add to the appropriate FeatureMetas function so the admin UI can display it:
func AgentFeatureMetas() []FeatureMeta {
return []FeatureMeta{
// ... existing ...
{FeatureMyFeature, "My Feature", false}, // false = default OFF
}
}In core/http/app.go:
myFeatureMw := auth.RequireFeature(application.AuthDB(), auth.FeatureMyFeature)Then pass it to the route registration function.
If your feature gates standard API endpoints (like /v1/...), add entries to RouteFeatureRegistry in features.go instead of using per-route middleware.
import "github.com/mudler/LocalAI/core/http/auth"
func MyHandler(c echo.Context) error {
// Get the user (nil when auth is disabled or unauthenticated)
user := auth.GetUser(c)
if user == nil {
// Handle unauthenticated — or let middleware handle it
}
// Check role
if user.Role == auth.RoleAdmin {
// admin-specific logic
}
// Check feature access programmatically (when you need conditional behavior, not full blocking)
if auth.HasFeatureAccess(db, user, auth.FeatureMyFeature) {
// feature-specific logic
}
// Check model access
if !auth.IsModelAllowed(db, user, modelName) {
return c.JSON(http.StatusForbidden, ...)
}
}Middleware can be composed at different levels. Here are the patterns used in the codebase:
// All routes in the group share the middleware
g := e.Group("/api/agents", poolReadyMw, agentsMw)
g.GET("", listHandler)
g.POST("", createHandler)// Individual routes get middleware as extra arguments
router.POST("/models/apply", applyHandler, adminMiddleware)
router.GET("/metrics", metricsHandler, adminMiddleware)// Build a middleware chain for a handler
chatMiddleware := []echo.MiddlewareFunc{
usageMiddleware,
traceMiddleware,
modelFilterMiddleware,
}
app.POST("/v1/chat/completions", chatHandler, chatMiddleware...)Always use schema.ErrorResponse for auth/permission errors to stay consistent with the OpenAI-compatible API:
return c.JSON(http.StatusForbidden, schema.ErrorResponse{
Error: &schema.APIError{
Message: "feature not enabled for your account",
Code: http.StatusForbidden,
Type: "authorization_error",
},
})Use these HTTP status codes:
401 Unauthorized— no valid credentials provided403 Forbidden— authenticated but lacking permission429 Too Many Requests— rate limited (auth endpoints)
If your endpoint should be tracked for usage (token counts, request counts), add the usageMiddleware to its middleware chain. See core/http/middleware/usage.go and how it's applied in routes/openai.go.
Beyond routing and auth, LocalAI publishes its capability surface in four independent places. When you add an endpoint — especially one introducing a net-new capability like a new media type or a new auth-gated feature — you must update every relevant surface. These aren't optional: missing them means the endpoint works but is invisible to clients, admins, and the UI.
Every handler needs a swagger block so the endpoint appears in /swagger/index.html and in the /api/instructions output. The @Tags value is what groups the endpoint into a capability area:
// MyEndpoint does X.
// @Summary Do X.
// @Tags my-capability
// @Param request body schema.MyRequest true "payload"
// @Success 200 {object} schema.MyResponse "Response"
// @Router /v1/my-endpoint [post]
func MyEndpoint(...) echo.HandlerFunc { ... }Use an existing tag when the endpoint extends an existing area (e.g. audio, images, face-recognition). Create a new tag only when the endpoint introduces a genuinely new capability surface — and in that case, also register it in step 2.
After adding endpoints, regenerate the embedded spec so the runtime serves it:
make protogen-go # ensures gRPC codegen is fresh first
make swagger # regenerates swagger/swagger.jsoncore/http/endpoints/localai/api_instructions.go defines instructionDefs — a lightweight, machine-readable index of capability areas that groups swagger endpoints by tag. It's the primary discovery surface for agents and SDKs ("what can this server do?").
When to update: only when adding a new capability area (a new swagger tag). Existing-tag additions automatically surface without any change here.
Add an entry to instructionDefs:
{
Name: "my-capability", // URL segment at /api/instructions/my-capability
Description: "Short sentence describing the capability",
Tags: []string{"my-capability"}, // must match swagger @Tags
Intro: "Optional gotcha/context that isn't in the swagger descriptions (caveats, defaults, cross-references to other endpoints).",
},Also bump the expected-length count in api_instructions_test.go and add the name to the ContainElements assertion.
If your feature needs a new FLAG_* usecase flag in core/config/model_config.go (so users can filter gallery models by it, and so /v1/models surfaces it), you need to update all of:
Usecase<Name>string constant incore/config/backend_capabilities.goUsecaseInfoMapentry mapping the string to its flag + gRPC methodFLAG_<NAME>bitmask incore/config/model_config.goGetAllModelConfigUsecases()map entry (otherwise the YAML loader silently ignores the string)ModalityGroupsmembership if the flag should affectIsMultimodal()(e.g. realtime_audio is in both speech-input and audio-output groups so a lone flag still reads as multimodal)GuessUsecases()branch listing the backends that own this capabilityusecaseFiltersincore/http/routes/ui_api.go(drives the gallery filter dropdown)Models.jsxFILTERSarray + matchingfilters.<camelCase>i18n key incore/http/react-ui/public/locales/en/models.jsoncore/http/react-ui/src/utils/capabilities.js:
export const CAP_MY_CAPABILITY = 'FLAG_MY_CAPABILITY'React pages that want to filter the ModelSelector by capability import this symbol. Declare it even if you're not building the UI page yet — the declaration keeps the Go/JS vocabularies in sync.
A new capability deserves its own page under docs/content/features/, plus cross-links from related features and an entry in docs/content/whats-new.md. See the pattern used by face-recognition.md / object-detection.md.
The global auth middleware classifies paths as API paths or non-API paths:
- API paths (always require auth when auth is enabled):
/api/,/v1/,/models/,/backends/,/backend/,/tts,/vad,/video,/stores/,/system,/ws/,/metrics - Exempt paths (never require auth):
/api/auth/prefix, anything inappConfig.PathWithoutAuth - Non-API paths (UI, static assets): pass through without auth — the React UI handles login redirects client-side
If you add endpoints under a new top-level path prefix, add it to isAPIPath() in middleware.go to ensure it requires authentication.
When adding a new endpoint:
Routing & auth
- Handler in
core/http/endpoints/ - Route registered in appropriate
core/http/routes/file - Auth level chosen: public / standard / admin / feature-gated
- Entry added to
RouteFeatureRegistryincore/http/auth/features.go(one row per route/method — all /v1/* routes gate through this, not per-route middleware) - If new feature: constant in
permissions.go, added to the right slice (APIFeaturesdefault-ON /AgentFeaturesdefault-OFF), metadata infeatures.go*FeatureMetas() - If feature uses group middleware: wired in
core/http/app.goand passed to the route registration function - If new path prefix: added to
isAPIPath()inmiddleware.go - If token-counting:
usageMiddlewareadded to middleware chain
Advertising surfaces (easy to miss — see the Advertising surfaces section)
- Swagger block on the handler:
@Summary,@Tags,@Param,@Success,@Router - If new capability area (new swagger tag): entry in
instructionDefsincore/http/endpoints/localai/api_instructions.go+ test count bumped inapi_instructions_test.go - If new
FLAG_*usecase flag: matchingCAP_*symbol exported fromcore/http/react-ui/src/utils/capabilities.js -
docs/content/features/<feature>.mdcreated; cross-links from related feature pages; entry indocs/content/whats-new.md
Quality
- Error responses use
schema.ErrorResponseformat (orecho.NewHTTPErrorwith a mapped gRPC status — see themapBackendErrorhelper incore/http/endpoints/localai/images.go) - Tests cover both authenticated and unauthenticated access
- Swagger regenerated (
make swagger) if you changed any@Router/@Tags/@Paramannotation
Required for admin endpoints. Every new admin endpoint MUST be considered for the MCP admin tool surface — the REST API and the MCP tool catalog can drift silently otherwise, and both the LocalAI Assistant chat modality and the standalone local-ai mcp-server rely on pkg/mcp/localaitools/ to mirror REST.
Two outcomes are acceptable; one is not:
- Tool added. The new endpoint is something an admin would manage conversationally (install, list, edit, toggle, upgrade). Follow the full checklist in .agents/localai-assistant-mcp.md: add a
LocalAIClientinterface method, implement it in bothinprocandhttpapi, register the tool with aTool*constant, update the skill prompts, and add the route totoolToHTTPRouteinpkg/mcp/localaitools/coverage_test.go. - Tool deliberately skipped. The endpoint is internal/diagnostic and adding a chat path would be misleading. Document the decision in the PR description; no code action.
- Forgot. This breaks the contract. The
TestToolHTTPRouteMappingCompletetest inpkg/mcp/localaitoolsis a partial guard (it checks everyTool*has a route mapping), but it does NOT detect new REST endpoints without a tool — that's still a process check on the PR author.
Add to the bottom of the checklist below:
- If admin: decided whether MCP coverage is needed; if yes, tool registered + map updated; if no, skip-reason in PR description.