Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,15 @@ GOOGLE_KEY=user_provided
# Vertex AI model for image generation (defaults to gemini-2.5-flash-image)
# GEMINI_IMAGE_MODEL=gemini-2.5-flash-image

#===================#
# Pollinations API #
#===================#

# Pollinations is the primary generation backend for text, image, and video.
# Get your API key at: https://enter.pollinations.ai
POLLINATIONS_API_KEY=your_pollinations_api_key
# POLLINATIONS_BASE_URL=https://gen.pollinations.ai

#============#
# OpenAI #
#============#
Expand Down Expand Up @@ -454,6 +463,12 @@ ILLEGAL_MODEL_REQ_SCORE=5
# Registration and Login #
#========================#

# Set GUEST_MODE=true to bypass all login requirements.
# Visitors are automatically authenticated as a shared guest user — no account needed.
# The guest user is created on first launch and reused on every subsequent request.
# Everything else (chat, conversations, settings) works as normal.
GUEST_MODE=false

ALLOW_EMAIL_LOGIN=true
ALLOW_REGISTRATION=true
ALLOW_SOCIAL_LOGIN=false
Expand Down
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,34 @@ Open source, actively developed, and built for anyone who values control over th

---

## 🌸 Pollinations Integration

LibreChat ships with **[Pollinations](https://pollinations.ai)** as its primary generation backend.
Every message is automatically routed through a lightweight intent classifier before reaching the model:

| Intent | Flow |
|--------|------|
| **Text** | Classified by `nova-fast` → answered by `nova-fast` (or any model you choose) |
| **Image** | Classified by `nova-fast` → prompt enhanced by `nova-fast` → image generated with `zimage` |
| **Video** | Classified by `nova-fast` → _coming soon_ placeholder returned |

### Setup

1. Get a free API key at <https://enter.pollinations.ai>.
2. Add it to your `.env`:
```
POLLINATIONS_API_KEY=your_pollinations_api_key
```
3. Copy `librechat.example.yaml` → `librechat.yaml` (or use `CONFIG_PATH`). The **Pollinations** endpoint is already the first entry under `endpoints.custom`.

### Available models (sample)

`nova-fast` · `openai` · `openai-fast` · `openai-large` · `gemini-fast` · `claude-fast` · `deepseek` · `mistral` · `qwen-coder` · `grok` · `perplexity-fast` · `kimi` — plus many more. Set `fetch: true` in the YAML to auto-populate all current models from the Pollinations `/v1/models` endpoint.

> **Key:** Use an `sk_` prefixed secret key for server-side requests. `pk_` keys work client-side but are rate-limited.

---

## 🌐 Resources

**GitHub Repo:**
Expand Down
45 changes: 45 additions & 0 deletions api/server/controllers/AuthController.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
const cookies = require('cookie');
const jwt = require('jsonwebtoken');
const openIdClient = require('openid-client');
const { randomBytes } = require('node:crypto');
const { logger } = require('@librechat/data-schemas');
const { isEnabled, findOpenIDUser } = require('@librechat/api');
const { SystemRoles } = require('librechat-data-provider');
const {
requestPasswordReset,
setOpenIDAuthTokens,
Expand All @@ -13,13 +15,47 @@ const {
const {
deleteAllUserSessions,
getUserById,
createUser,
findSession,
updateUser,
findUser,
} = require('~/models');
const { getGraphApiToken } = require('~/server/services/GraphTokenService');
const { getOpenIdConfig, getOpenIdEmail } = require('~/strategies');

const GUEST_EMAIL = process.env.GUEST_USER_EMAIL || 'guest@librechat.local';

/**
* Finds or creates the shared guest user used in GUEST_MODE.
* The guest user is a regular local user with a stable email address and no password.
* It is created once and reused for every unauthenticated request.
* @returns {Promise<object>} User document without sensitive fields
*/
async function getOrCreateGuestUser() {
let user = await findUser({ email: GUEST_EMAIL }, '-password -__v -totpSecret -backupCodes');
if (!user) {
const created = await createUser(
{
provider: 'local',
email: GUEST_EMAIL,
username: 'guest',
name: 'Guest',
avatar: null,
role: SystemRoles.USER,
emailVerified: true,
// Use a random, non-bcrypt-hashed value so the guest account
// can never be used to log in via password-based local auth.
password: randomBytes(32).toString('hex'),
},
undefined,
true,
false,
);
user = await getUserById(created._id.toString(), '-password -__v -totpSecret -backupCodes');
}
return user;
}

const registrationController = async (req, res) => {
try {
const response = await registerUser(req.body);
Expand Down Expand Up @@ -130,6 +166,15 @@ const refreshController = async (req, res) => {
/** For non-OpenID users, read refresh token from cookies */
const refreshToken = parsedCookies.refreshToken;
if (!refreshToken) {
if (isEnabled(process.env.GUEST_MODE)) {
try {
const guestUser = await getOrCreateGuestUser();
const token = await setAuthTokens(guestUser._id.toString(), res);
return res.status(200).send({ token, user: guestUser });
} catch (err) {
logger.error('[refreshController] Guest mode auto-auth failed:', err);
}
}
return res.status(200).send('Refresh token not provided');
}

Expand Down
2 changes: 2 additions & 0 deletions api/server/routes/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const router = express.Router();
const emailLoginEnabled =
process.env.ALLOW_EMAIL_LOGIN === undefined || isEnabled(process.env.ALLOW_EMAIL_LOGIN);
const passwordResetEnabled = isEnabled(process.env.ALLOW_PASSWORD_RESET);
const guestMode = isEnabled(process.env.GUEST_MODE);

const sharedLinksEnabled =
process.env.ALLOW_SHARED_LINKS === undefined || isEnabled(process.env.ALLOW_SHARED_LINKS);
Expand Down Expand Up @@ -77,6 +78,7 @@ function buildSharedPayload() {
publicSharedLinksEnabled,
analyticsGtmId: process.env.ANALYTICS_GTM_ID,
openidReuseTokens,
guestMode,
};

const minPasswordLength = parseInt(process.env.MIN_PASSWORD_LENGTH, 10);
Expand Down
10 changes: 7 additions & 3 deletions client/src/hooks/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
useLoginUserMutation,
useLogoutUserMutation,
useRefreshTokenMutation,
useGetStartupConfig,
} from '~/data-provider';
import { TAuthConfig, TUserContext, TAuthContext, TResError } from '~/common';
import { SESSION_KEY, isSafeRedirect, getPostLoginRedirect } from '~/utils';
Expand All @@ -47,6 +48,9 @@ const AuthContextProvider = ({
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
const setQueriesEnabled = useSetRecoilState<boolean>(store.queriesEnabled);

const { data: startupConfig } = useGetStartupConfig();
const guestMode = startupConfig?.guestMode === true;

const { data: userRole = null } = useGetRole(SystemRoles.USER, {
enabled: !!(isAuthenticated && (user?.role ?? '')),
});
Expand Down Expand Up @@ -191,7 +195,7 @@ const AuthContextProvider = ({
return;
}
console.log('Token is not present. User is not authenticated.');
if (authConfig?.test === true) {
if (authConfig?.test === true || guestMode) {
return;
}
navigate(buildLoginRedirectUrl());
Expand All @@ -201,7 +205,7 @@ const AuthContextProvider = ({
return;
}
console.log('refreshToken mutation error:', error);
if (authConfig?.test === true) {
if (authConfig?.test === true || guestMode) {
return;
}
navigate(buildLoginRedirectUrl());
Expand All @@ -216,7 +220,7 @@ const AuthContextProvider = ({
}
if (userQuery.data) {
setUser(userQuery.data);
} else if (userQuery.isError) {
} else if (userQuery.isError && !guestMode) {
doSetError((userQuery.error as Error).message);
navigate(buildLoginRedirectUrl(), { replace: true });
}
Expand Down
2 changes: 1 addition & 1 deletion client/src/routes/ChatRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ export default function ChatRoute() {
);
}

if (!isAuthenticated) {
if (!isAuthenticated && !startupConfig?.guestMode) {
return null;
}

Expand Down
6 changes: 5 additions & 1 deletion client/src/routes/Layouts/Startup.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import { Navigate, Outlet, useNavigate, useLocation } from 'react-router-dom';
import type { TStartupConfig } from 'librechat-data-provider';
import { TranslationKeys, useLocalize } from '~/hooks';
import { useGetStartupConfig } from '~/data-provider';
Expand Down Expand Up @@ -52,6 +52,10 @@ export default function StartupLayout({ isAuthenticated }: { isAuthenticated?: b
setHeaderText(null);
}, [location.pathname]);

if (data?.guestMode) {
return <Navigate to="/c/new" replace />;
}

const contextValue = {
error,
setError,
Expand Down
2 changes: 1 addition & 1 deletion client/src/routes/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export default function Root() {
logout('/login?redirect=false');
};

if (!isAuthenticated) {
if (!isAuthenticated && !config?.guestMode) {
return null;
}

Expand Down
8 changes: 7 additions & 1 deletion client/src/routes/useAuthRedirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ import { useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { buildLoginRedirectUrl } from 'librechat-data-provider';
import { useAuthContext } from '~/hooks';
import { useGetStartupConfig } from '~/data-provider';

export default function useAuthRedirect() {
const { user, roles, isAuthenticated } = useAuthContext();
const { data: startupConfig } = useGetStartupConfig();
const navigate = useNavigate();
const location = useLocation();

useEffect(() => {
if (startupConfig?.guestMode) {
return;
}

const timeout = setTimeout(() => {
if (isAuthenticated) {
return;
Expand All @@ -22,7 +28,7 @@ export default function useAuthRedirect() {
return () => {
clearTimeout(timeout);
};
}, [isAuthenticated, navigate, location]);
}, [isAuthenticated, navigate, location, startupConfig?.guestMode]);

return {
user,
Expand Down
Loading