Back to Localai

API Endpoints and Authentication

.agents/api-endpoints-and-auth.md

4.3.615.7 KB
Original Source

API Endpoints and Authentication

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.

Architecture overview

Authentication and authorization flow through three layers:

  1. Global auth middleware (core/http/auth/middleware.goauth.Middleware) — applied to every request in core/http/app.go. Handles session cookies, Bearer tokens, API keys, and legacy API keys. Populates auth_user and auth_role in the Echo context.
  2. 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.
  3. 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).

Adding a new API endpoint

Step 1: Create the handler

Write the endpoint handler in the appropriate package under core/http/endpoints/. Follow existing patterns:

go
// 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)
    }
}

Step 2: Register routes

Add routes in the appropriate file under core/http/routes/. The file you use depends on the endpoint category:

FileCategory
routes/openai.goOpenAI-compatible API endpoints (/v1/...)
routes/localai.goLocalAI-specific endpoints (/api/..., /models/..., /backends/...)
routes/agents.goAgent pool endpoints (/api/agents/...)
routes/auth.goAuth endpoints (/api/auth/...)
routes/ui_api.goUI backend API endpoints

Step 3: Apply the right middleware

Choose the appropriate protection level:

No auth required (public)

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.

Standard auth (any authenticated user)

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.

go
router.GET("/v1/my-endpoint", myHandler)  // auth enforced by global middleware

Admin only

Pass adminMiddleware to the route. This is set up in app.go and passed to Register*Routes functions:

go
// 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)
}

Feature-gated

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)

go
// 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:

go
var RouteFeatureRegistry = []RouteFeature{
    // ... existing entries ...
    {"POST", "/v1/my-endpoint", FeatureMyFeature},
}

Adding a new feature

When you need a new toggleable feature (not just a new endpoint under an existing feature):

1. Define the feature constant

Add to core/http/auth/permissions.go:

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:

go
// Default OFF — user must be explicitly granted access:
var AgentFeatures = []string{..., FeatureMyFeature}

// Default ON — user has access unless explicitly revoked:
var APIFeatures = []string{..., FeatureMyFeature}

2. Add feature metadata

In core/http/auth/features.go, add to the appropriate FeatureMetas function so the admin UI can display it:

go
func AgentFeatureMetas() []FeatureMeta {
    return []FeatureMeta{
        // ... existing ...
        {FeatureMyFeature, "My Feature", false},  // false = default OFF
    }
}

3. Wire up the middleware

In core/http/app.go:

go
myFeatureMw := auth.RequireFeature(application.AuthDB(), auth.FeatureMyFeature)

Then pass it to the route registration function.

4. Register route-feature mappings (if applicable)

If your feature gates standard API endpoints (like /v1/...), add entries to RouteFeatureRegistry in features.go instead of using per-route middleware.

Accessing the authenticated user in handlers

go
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 composition patterns

Middleware can be composed at different levels. Here are the patterns used in the codebase:

Group-level middleware (agents pattern)

go
// All routes in the group share the middleware
g := e.Group("/api/agents", poolReadyMw, agentsMw)
g.GET("", listHandler)
g.POST("", createHandler)

Per-route middleware (localai pattern)

go
// Individual routes get middleware as extra arguments
router.POST("/models/apply", applyHandler, adminMiddleware)
router.GET("/metrics", metricsHandler, adminMiddleware)

Middleware slice (openai pattern)

go
// Build a middleware chain for a handler
chatMiddleware := []echo.MiddlewareFunc{
    usageMiddleware,
    traceMiddleware,
    modelFilterMiddleware,
}
app.POST("/v1/chat/completions", chatHandler, chatMiddleware...)

Error response format

Always use schema.ErrorResponse for auth/permission errors to stay consistent with the OpenAI-compatible API:

go
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 provided
  • 403 Forbidden — authenticated but lacking permission
  • 429 Too Many Requests — rate limited (auth endpoints)

Usage tracking

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.

Advertising surfaces — where to register a new capability

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.

1. Swagger @Tags annotation (mandatory)

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:

go
// 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:

bash
make protogen-go         # ensures gRPC codegen is fresh first
make swagger             # regenerates swagger/swagger.json

2. /api/instructions registry (for new capability areas)

core/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:

go
{
    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.

3. capabilities.js symbol (for new model-config FLAG_* flags)

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 in core/config/backend_capabilities.go
  • UsecaseInfoMap entry mapping the string to its flag + gRPC method
  • FLAG_<NAME> bitmask in core/config/model_config.go
  • GetAllModelConfigUsecases() map entry (otherwise the YAML loader silently ignores the string)
  • ModalityGroups membership if the flag should affect IsMultimodal() (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 capability
  • usecaseFilters in core/http/routes/ui_api.go (drives the gallery filter dropdown)
  • Models.jsx FILTERS array + matching filters.<camelCase> i18n key in core/http/react-ui/public/locales/en/models.json
  • core/http/react-ui/src/utils/capabilities.js:
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.

4. docs/content/ (user-facing documentation)

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.

Path protection rules

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 in appConfig.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.

Checklist

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 RouteFeatureRegistry in core/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 (APIFeatures default-ON / AgentFeatures default-OFF), metadata in features.go *FeatureMetas()
  • If feature uses group middleware: wired in core/http/app.go and passed to the route registration function
  • If new path prefix: added to isAPIPath() in middleware.go
  • If token-counting: usageMiddleware added 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 instructionDefs in core/http/endpoints/localai/api_instructions.go + test count bumped in api_instructions_test.go
  • If new FLAG_* usecase flag: matching CAP_* symbol exported from core/http/react-ui/src/utils/capabilities.js
  • docs/content/features/<feature>.md created; cross-links from related feature pages; entry in docs/content/whats-new.md

Quality

  • Error responses use schema.ErrorResponse format (or echo.NewHTTPError with a mapped gRPC status — see the mapBackendError helper in core/http/endpoints/localai/images.go)
  • Tests cover both authenticated and unauthenticated access
  • Swagger regenerated (make swagger) if you changed any @Router/@Tags/@Param annotation

Companion: MCP admin tool surface

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 LocalAIClient interface method, implement it in both inproc and httpapi, register the tool with a Tool* constant, update the skill prompts, and add the route to toolToHTTPRoute in pkg/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 TestToolHTTPRouteMappingComplete test in pkg/mcp/localaitools is a partial guard (it checks every Tool* 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.