* feat(usage): add Source, APIKeyID, APIKeyName columns to UsageRecord Adds three additive columns plus UsageSource* constants. The columns are auto-migrated by InitDB. APIKeyID is a nullable foreign reference to UserAPIKey.ID; APIKeyName is snapshotted on each row so revoked keys keep showing their name in history. Refs: #9862 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(usage): backfill Source on pre-feature usage rows InitDB now classifies any pre-existing usage_record with an empty source: 'legacy-api-key' user -> legacy, everything else -> web. The backfill is idempotent (only touches NULL/empty rows). Refs: #9862 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(usage): add GetUserUsageBySource aggregator Groups by (bucket, source, api_key_id, api_key_name). Filters out legacy by default. Returns both per-bucket detail and roll-ups (by_source, by_key sorted desc and capped at 200, grand_total). The MAX(created_at) projection is iterated via Rows().Scan into a string column and parsed manually because the SQLite driver surfaces the aggregated timestamp as a string, which database/sql refuses to scan directly into time.Time. Postgres returns a real timestamp; the same string path handles its RFC3339 form too. Refs: #9862 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(usage): log Rows() errors and assert LastUsed in tests Adds rows.Err() and Rows() open-failure logging in computeSourceTotals so silent data drops surface in logs. Logs on parseLastUsedString format misses for the same reason. Strengthens the snapshot-survival test to assert LastUsed is a recent timestamp, locking the SQLite time-string parser behaviour. Refs: #9862 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(usage): add admin GetAllUsageBySource with filters and truncation Optional user_id and api_key_id filters (composed with AND). Legacy bucket is included for admin callers. truncated=true when more than 200 distinct keys would be in the by_key roll-up. Refs: #9862 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(auth): plumb auth_source and auth_apikey through Echo context tryAuthenticate now sets auth_source on every successful branch (web for session/Bearer-session, apikey for Bearer-key/x-api-key/ token-cookie, legacy for legacy env key match). For named-key branches it also stores the resolved *UserAPIKey under auth_apikey so downstream middlewares can snapshot id+name without re-validating. Refs: #9862 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(auth): expand tryAuthenticate godoc and cover Bearer-session branch Documents all three context-keys side effects (auth_source, auth_apikey, _auth_session) plus the split of responsibilities with the parent Middleware. Adds a test for the Bearer-as-session-token classification so future regressions there fail loudly. Refs: #9862 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(usage): UsageMiddleware records source + snapshots key name Reads auth_source and auth_apikey from the Echo context (set by auth.Middleware in the previous task). Snapshots UserAPIKey.ID and Name onto each row so revoked keys remain readable in history. Falls back to source=web when no auth_source is set (auth disabled or unrecognised path). Refs: #9862 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(usage): add /api/auth/usage/sources and admin variant Self endpoint filters legacy server-side; admin endpoint includes legacy and accepts user_id + api_key_id filters. Response includes buckets, totals.{by_source, by_key, grand_total}, and a truncated flag set when the per-key roll-up was capped at 200. Refs: #9862 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * docs(routes): mark test mirror handlers as keep-in-sync with production The newTestAuthApp helper duplicates production route handlers inline because it cannot use RegisterAuthRoutes (which requires a *application.Application). Naming the source path on each mirror makes the drift contract explicit for future maintainers. Refs: #9862 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(ui): add usageApi.getMySources/getAdminSources + i18n strings Refs: #9862 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(ui): add Sources tab skeleton with data fetch Adds Usage page tab that fetches /api/auth/usage/sources (or the admin variant). Renders raw totals plus a placeholder key list; real visualisations land in subsequent commits. Restructures the existing tab button block so Models and Sources are visible to non-admins (Users remains admin-only). Refs: #9862 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(ui): source mix ribbon + searchable/sortable sources table Replaces the SourcesTab placeholder rendering with two reusable components: SourceMixRibbon (one segmented bar per source class) and SourcesTable (search + sort + revoked-key dim). Pulls the current API key list to detect revoked keys. Refs: #9862 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(ui): skip revoked-key detection until the key list is known existingKeyIds defaulted to an empty Set, which made every live api_key row render as (revoked) during the brief window before apiKeysApi.list() resolved, and permanently after a fetch failure. Use null as the unknown state and suppress the revoked badge until the parent provides a real Set. Refs: #9862 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(ui): top-N stacked time chart and drill-in chip for Sources tab Top 7 sources by total tokens get distinct colours; the rest roll up into 'Other'. Clicking a row in the SourcesTable dims everything except that series in the chart; the chip is the canonical clear. Refs: #9862 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * docs(usage): document per-API-key Sources tab and endpoints Extends features/authentication.md Usage Tracking section with: - A 'Sources' tab description and source-class taxonomy - Endpoint documentation for /api/auth/usage/sources and the admin variant - Response shape example with by_source / by_key / grand_total - Migration note about pre-feature row backfill Refs: #9862 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(usage): silence errcheck on deferred rows.Close CI errcheck flagged the bare 'defer rows.Close()' in computeSourceTotals. Wrap in a closure that discards the close error explicitly; an error here is non-actionable since we have already drained the rows and logged any iteration failure. Refs: #9862 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactor(usage): bound batcher intake and add Shutdown/FlushNow hooks The pre-existing usage batcher had no cap on its add() path; the usageMaxPending=5000 constant only guarded the re-queue path after a failed write, leaving memory growth unbounded if the DB fell behind. This commit: - Adds the cap to add() so saturation drops new records (rate-limited warn at 1/1024) instead of growing unbounded. - Raises usageMaxPending to 50000 to absorb realistic inference bursts. - Replaces the package-level batcher global with a mutex-guarded pair plus a currentBatcher() accessor so Init / Shutdown cycles are race-free. - Adds ShutdownUsageRecorder() for graceful drain on process exit (not yet wired into app shutdown, just published). - Adds FlushNow() for deterministic tests; the middleware suite no longer needs 6s sleeps per spec and now runs in ~50ms instead of 18s. - Re-queue on failed flush is now cap-aware: prepends as much of the failed batch as fits alongside concurrent arrivals, instead of dropping the whole batch when full. Refs: #9862 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(usage): drain usage batcher on graceful shutdown Registers ShutdownUsageRecorder with the existing signals.RegisterGracefulTerminationHandler so SIGINT/SIGTERM synchronously flushes any in-memory usage records before the process exits. Without this, up to one flush interval (5s) of recorded usage was lost when LocalAI restarted. Refs: #9862 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
17 KiB
+++ disableToc = false title = "Authentication & Authorization" weight = 26 url = '/features/authentication' +++
LocalAI supports two authentication modes: legacy API key authentication (simple shared keys) and a full user authentication system with roles, sessions, OAuth, and per-user usage tracking.
Legacy API Key Authentication
The simplest way to protect your LocalAI instance is with API keys. Set one or more keys via environment variable or CLI flag:
# Single key
LOCALAI_API_KEY=sk-my-secret-key localai run
# Multiple keys (comma-separated)
LOCALAI_API_KEY=key1,key2,key3 localai run
Clients provide the key via any of these methods:
Authorization: Bearer <key>headerx-api-key: <key>headerxi-api-key: <key>headertokencookie
Legacy API keys grant full admin access — there is no role separation. For multi-user deployments with role-based access, use the user authentication system instead.
API keys can also be managed at runtime through the [Runtime Settings]({{%relref "features/runtime-settings" %}}) interface.
User Authentication System
The user authentication system provides:
- User accounts with email, name, and avatar
- Role-based access control (admin vs. user)
- Session-based authentication with secure cookies
- OAuth login (GitHub) and OIDC single sign-on (Keycloak, Google, Okta, Authentik, etc.)
- Per-user API keys for programmatic access
- Admin route gating — management endpoints are restricted to admins
- Per-user usage tracking with token consumption metrics
Enabling Authentication
Set LOCALAI_AUTH=true or provide a GitHub OAuth Client ID or OIDC Client ID (which auto-enables auth):
# Enable with SQLite (default, stored at {DataPath}/database.db)
LOCALAI_AUTH=true localai run
# Enable with GitHub OAuth
GITHUB_CLIENT_ID=your-client-id \
GITHUB_CLIENT_SECRET=your-client-secret \
LOCALAI_BASE_URL=http://localhost:8080 \
localai run
# Enable with OIDC provider (e.g. Keycloak)
LOCALAI_OIDC_ISSUER=https://keycloak.example.com/realms/myrealm \
LOCALAI_OIDC_CLIENT_ID=your-client-id \
LOCALAI_OIDC_CLIENT_SECRET=your-client-secret \
LOCALAI_BASE_URL=http://localhost:8080 \
localai run
# Enable with PostgreSQL
LOCALAI_AUTH=true \
LOCALAI_AUTH_DATABASE_URL=postgres://user:pass@host/dbname \
localai run
Configuration Reference
| Environment Variable | Default | Description |
|---|---|---|
LOCALAI_AUTH |
false |
Enable user authentication and authorization |
LOCALAI_AUTH_DATABASE_URL |
{DataPath}/database.db |
Database URL — postgres://... for PostgreSQL, or a file path for SQLite |
GITHUB_CLIENT_ID |
GitHub OAuth App Client ID (auto-enables auth when set) | |
GITHUB_CLIENT_SECRET |
GitHub OAuth App Client Secret | |
LOCALAI_OIDC_ISSUER |
OIDC issuer URL for auto-discovery (e.g. https://accounts.google.com) |
|
LOCALAI_OIDC_CLIENT_ID |
OIDC Client ID (auto-enables auth when set) | |
LOCALAI_OIDC_CLIENT_SECRET |
OIDC Client Secret | |
LOCALAI_BASE_URL |
Base URL for OAuth callbacks (e.g. http://localhost:8080) |
|
LOCALAI_ADMIN_EMAIL |
Email address to auto-promote to admin role on login | |
LOCALAI_REGISTRATION_MODE |
approval |
Registration mode: open, approval, or invite |
LOCALAI_DISABLE_LOCAL_AUTH |
false |
Disable local email/password registration and login (for OAuth/OIDC-only deployments) |
Disabling Local Authentication
If you want to enforce OAuth/OIDC-only login and prevent users from registering or logging in with email/password, set LOCALAI_DISABLE_LOCAL_AUTH=true (or pass --disable-local-auth):
# OAuth-only setup (no email/password)
LOCALAI_DISABLE_LOCAL_AUTH=true \
GITHUB_CLIENT_ID=your-client-id \
GITHUB_CLIENT_SECRET=your-client-secret \
LOCALAI_BASE_URL=http://localhost:8080 \
localai run
When disabled:
- The login page will not show email/password forms (the UI checks the
providerslist from/api/auth/status) POST /api/auth/registerreturns403 ForbiddenPOST /api/auth/loginreturns403 Forbidden- OAuth/OIDC login continues to work normally
Roles
There are two roles:
- Admin: Full access to all endpoints, including model management, backend configuration, system settings, traces, agents, and user management.
- User: Access to inference endpoints only — chat completions, embeddings, image/video/audio generation, TTS, MCP chat, and their own usage statistics.
The first user to sign in is automatically assigned the admin role. Additional users can be promoted to admin via the admin user management API or by setting LOCALAI_ADMIN_EMAIL to their email address.
Registration Modes
| Mode | Description |
|---|---|
open |
Anyone can register and is immediately active |
approval |
New users land in "pending" status until an admin approves them. If a valid invite code is provided during registration, the user is activated immediately (skipping the approval wait). (default) |
invite |
Registration requires a valid invite link generated by an admin. Without one, registration is rejected. |
Invite Links
Admins can generate single-use, time-limited invite links from the Users → Invites tab in the web UI, or via the API:
# Create an invite link (default: expires in 7 days)
curl -X POST http://localhost:8080/api/auth/admin/invites \
-H "Authorization: Bearer <admin-key>" \
-H "Content-Type: application/json" \
-d '{"expiresInHours": 168}'
# List all invites
curl http://localhost:8080/api/auth/admin/invites \
-H "Authorization: Bearer <admin-key>"
# Revoke an unused invite
curl -X DELETE http://localhost:8080/api/auth/admin/invites/<invite-id> \
-H "Authorization: Bearer <admin-key>"
# Check if an invite code is valid (public, no auth required)
curl http://localhost:8080/api/auth/invite/<code>/check
Share the invite URL (/invite/<code>) with the user. When they open it, the registration form is pre-filled with the invite code. Invite codes are single-use — once consumed, they cannot be reused. Expired or used invites are rejected.
For GitHub OAuth, the invite code is passed as a query parameter to the login URL (/api/auth/github/login?invite_code=<code>) and stored in a cookie during the OAuth flow.
Admin-Only Endpoints
When authentication is enabled, the following endpoints require admin role:
Model & Backend Management:
GET /api/models,POST /api/models/install/*,POST /api/models/delete/*GET /api/backends,POST /api/backends/install/*,POST /api/backends/delete/*GET /api/operations,POST /api/operations/*/cancelGET /models/available,GET /models/galleries,GET /models/jobs/*GET /backends,GET /backends/available,GET /backends/galleries
System & Monitoring:
GET /api/traces,POST /api/traces/clearGET /api/backend-traces,POST /api/backend-traces/clearGET /api/backend-logs/*,POST /api/backend-logs/*/clearGET /api/resources,GET /api/settings,POST /api/settingsGET /system,GET /backend/monitor,POST /backend/shutdown
P2P:
GET /api/p2p/*
Agents & Jobs:
- All
/api/agents/*endpoints - All
/api/agent/tasks/*and/api/agent/jobs/*endpoints
User-Accessible Endpoints (all authenticated users):
POST /v1/chat/completions,POST /v1/embeddings,POST /v1/completionsPOST /v1/images/generations,POST /v1/audio/*,POST /tts,POST /vad,POST /videoGET /v1/models,POST /v1/tokenize,POST /v1/detectionPOST /v1/mcp/chat/completions,POST /v1/messages,POST /v1/responsesPOST /stores/*,GET /api/cors-proxyGET /version,GET /api/features,GET /swagger/*,GET /metricsGET /api/auth/usage(own usage data)
Web UI Access Control
When auth is enabled, the React UI sidebar dynamically shows/hides sections based on the user's role:
- All users see: Home, Chat, Images, Video, TTS, Sound, Talk, Usage, API docs link
- Admins also see: Install Models, Agents section (Agents, Skills, Memory, MCP CI Jobs), System section (Backends, Traces, Swarm, System, Settings)
Admin-only pages are also protected at the router level — navigating directly to an admin URL redirects non-admin users to the home page.
GitHub OAuth Setup
- Create a GitHub OAuth App at Settings → Developer settings → OAuth Apps → New OAuth App
- Set the Authorization callback URL to
{LOCALAI_BASE_URL}/api/auth/github/callback - Set
GITHUB_CLIENT_IDandGITHUB_CLIENT_SECRETenvironment variables - Set
LOCALAI_BASE_URLto your publicly-accessible URL
OIDC Setup
Any OIDC-compliant identity provider can be used for single sign-on. This includes Keycloak, Google, Okta, Authentik, Azure AD, and many others.
Steps:
- Create a client/application in your OIDC provider
- Set the redirect URL to
{LOCALAI_BASE_URL}/api/auth/oidc/callback - Set the three environment variables:
LOCALAI_OIDC_ISSUER,LOCALAI_OIDC_CLIENT_ID,LOCALAI_OIDC_CLIENT_SECRET
LocalAI uses OIDC auto-discovery (the /.well-known/openid-configuration endpoint) and requests the standard scopes: openid, profile, email.
Provider examples:
# Keycloak
LOCALAI_OIDC_ISSUER=https://keycloak.example.com/realms/myrealm
# Google
LOCALAI_OIDC_ISSUER=https://accounts.google.com
# Authentik
LOCALAI_OIDC_ISSUER=https://authentik.example.com/application/o/localai/
# Okta
LOCALAI_OIDC_ISSUER=https://your-org.okta.com
For OIDC, invite codes work the same way as GitHub OAuth — the invite code is passed as a query parameter to the login URL (/api/auth/oidc/login?invite_code=<code>) and stored in a cookie during the OAuth flow.
User API Keys
Authenticated users can create personal API keys for programmatic access:
# Create an API key (requires session auth)
curl -X POST http://localhost:8080/api/auth/api-keys \
-H "Cookie: session=<session-id>" \
-H "Content-Type: application/json" \
-d '{"name": "My Script Key"}'
User API keys inherit the creating user's role. Admin keys grant admin access; user keys grant user-level access.
Auth API Endpoints
| Method | Endpoint | Description | Auth Required |
|---|---|---|---|
GET |
/api/auth/status |
Auth state, current user, providers | No |
POST |
/api/auth/logout |
End session | Yes |
GET |
/api/auth/me |
Current user info | Yes |
POST |
/api/auth/api-keys |
Create API key | Yes |
GET |
/api/auth/api-keys |
List user's API keys | Yes |
DELETE |
/api/auth/api-keys/:id |
Revoke API key | Yes |
GET |
/api/auth/usage |
User's own usage stats | Yes |
GET |
/api/auth/usage/sources |
User's own per-API-key / per-source breakdown | Yes |
GET |
/api/auth/admin/users |
List all users | Admin |
PUT |
/api/auth/admin/users/:id/role |
Change user role | Admin |
DELETE |
/api/auth/admin/users/:id |
Delete user | Admin |
GET |
/api/auth/admin/usage |
All users' usage stats | Admin |
GET |
/api/auth/admin/usage/sources |
All users' per-API-key / per-source breakdown | Admin |
POST |
/api/auth/admin/invites |
Create invite link | Admin |
GET |
/api/auth/admin/invites |
List all invites | Admin |
DELETE |
/api/auth/admin/invites/:id |
Revoke unused invite | Admin |
GET |
/api/auth/invite/:code/check |
Check if invite code is valid | No |
GET |
/api/auth/github/login |
Start GitHub OAuth | No |
GET |
/api/auth/github/callback |
GitHub OAuth callback (internal) | No |
GET |
/api/auth/oidc/login |
Start OIDC login | No |
GET |
/api/auth/oidc/callback |
OIDC callback (internal) | No |
Usage Tracking
When authentication is enabled, LocalAI automatically tracks per-user token usage for inference endpoints. Usage data includes:
- Prompt tokens, completion tokens, and total tokens per request
- Model used and endpoint called
- Request duration
- Timestamp for time-series aggregation
Viewing Usage
Usage is accessible through the Usage page in the web UI (visible to all authenticated users) or via the API:
# Get your own usage (default: last 30 days)
curl http://localhost:8080/api/auth/usage?period=month \
-H "Authorization: Bearer <key>"
# Admin: get all users' usage
curl http://localhost:8080/api/auth/admin/usage?period=week \
-H "Authorization: Bearer <admin-key>"
# Admin: filter by specific user
curl "http://localhost:8080/api/auth/admin/usage?period=month&user_id=<user-id>" \
-H "Authorization: Bearer <admin-key>"
Period values:
day— last 24 hours, bucketed by hourweek— last 7 days, bucketed by daymonth— last 30 days, bucketed by day (default)all— all time, bucketed by month
Response format:
{
"usage": [
{
"bucket": "2026-03-18",
"model": "gpt-4",
"user_id": "abc-123",
"user_name": "Alice",
"prompt_tokens": 1500,
"completion_tokens": 800,
"total_tokens": 2300,
"request_count": 12
}
],
"totals": {
"prompt_tokens": 1500,
"completion_tokens": 800,
"total_tokens": 2300,
"request_count": 12
}
}
Usage Dashboard
The web UI Usage page provides:
- Period selector - switch between day, week, month, and all-time views
- Summary cards - total requests, prompt tokens, completion tokens, total tokens
- By Model table - per-model breakdown with visual usage bars
- By User table (admin only) - per-user breakdown across all models
- Sources tab - per-API-key and per-source breakdown (described below)
Per-API-key Breakdown
The Sources tab on the Usage page surfaces a third dimension of the same data: traffic broken down by API key and by request source. Three source classes are tracked:
- API key - request authenticated with a named user API key (
Authorization: Bearer lai-...,x-api-key, ortokencookie). Each key shows up with its label (snapshotted at write time, so revoked keys still display the original name). - Web UI - request authenticated with a browser session cookie.
- Legacy - request authenticated with an env-configured
LOCALAI_API_KEY. Visible to admins only.
The Sources tab is visible to every authenticated user. Non-admins see only their own keys plus their own Web UI traffic (legacy is filtered server-side). Admins see every key from every user.
The tab is laid out as:
- A source mix ribbon showing the percentage split across the three classes.
- A top-N + Other stacked time chart (top 7 sources by total tokens; the rest roll up).
- A searchable, sortable table of every key plus the Web UI and Legacy pseudo-rows. Click a row to filter the chart to that source.
Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/auth/usage/sources |
Self | Caller's per-source breakdown. Excludes legacy. |
GET |
/api/auth/admin/usage/sources |
Admin | All users' per-source breakdown. Accepts user_id and api_key_id filters. Includes legacy. |
Both endpoints accept the same period parameter (day, week, month, all) as /api/auth/usage.
# Your own per-source usage for the last week
curl "http://localhost:8080/api/auth/usage/sources?period=week" \
-H "Authorization: Bearer <key>"
# Admin: filter to a single API key across all users
curl "http://localhost:8080/api/auth/admin/usage/sources?period=month&api_key_id=<key-id>" \
-H "Authorization: Bearer <admin-key>"
Response shape:
{
"buckets": [
{ "bucket": "2026-05-19", "source": "apikey",
"api_key_id": "uuid", "api_key_name": "ci-runner",
"total_tokens": 20000, "request_count": 142, "...": "..." },
{ "bucket": "2026-05-19", "source": "web",
"total_tokens": 300, "request_count": 11, "...": "..." }
],
"totals": {
"by_source": {
"apikey": { "tokens": 1234567, "requests": 8420 },
"web": { "tokens": 92000, "requests": 211 }
},
"by_key": [
{ "api_key_id": "uuid", "api_key_name": "ci-runner",
"tokens": 2100000, "requests": 8420,
"last_used": "2026-05-20T12:34:56Z" }
],
"grand_total": { "tokens": 1334777, "requests": 8645 }
},
"truncated": false
}
The by_key list is server-sorted by tokens descending and capped at 200 entries. When more keys would qualify, the response sets "truncated": true so the UI can show a notice.
Migration of pre-feature data
Usage rows recorded before this feature have no source column. On startup, InitDB backfills them as legacy when the synthetic legacy-api-key user_id was used, and web for everything else. The migration is idempotent; existing aggregations remain correct after the upgrade.
Combining Auth Modes
Legacy API keys and user authentication can be used simultaneously. When both are configured:
- User sessions and user API keys are checked first
- Legacy API keys are checked as fallback — they grant admin-level access
- This allows a gradual migration from shared API keys to per-user accounts
Build Requirements
The user authentication system requires CGO for SQLite support. It is enabled with the auth build tag, which is included by default in Docker builds.
# Building from source with auth support
GO_TAGS=auth make build
# Or directly with go build
go build -tags auth ./...
The default Dockerfile includes GO_TAGS="auth", so all Docker images ship with auth support. When building from source without the auth tag, setting LOCALAI_AUTH=true has no effect — the system operates without authentication.