Skip to content
Open
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
19 changes: 19 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
{
"name": "Node.js & TypeScript",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm",

// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],

// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "yarn install",

// Configure tool-specific properties.
// "customizations": {},

// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ COOKIE_NAME=hf-chat
COOKIE_SAMESITE=# can be "lax", "strict", "none" or left empty
COOKIE_SECURE=# set to true to only allow cookies over https
TRUSTED_EMAIL_HEADER=# header to use to get the user email, only use if you know what you are doing

USE_HFCO_COOKIE=#only if you deploy huggingchat
### Admin stuff ###
ADMIN_CLI_LOGIN=true # set to false to disable the CLI login
ADMIN_TOKEN=#We recommend leaving this empty, you can get the token from the terminal.
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

![Chat UI repository thumbnail](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/chat-ui/Frame%2013.png)

A chat interface using open source models, eg OpenAssistant or Llama. It is a SvelteKit app and it powers the [HuggingChat app on hf.co/chat](https://huggingface.co/chat).
A chat interface using open source models, eg Qwen, GPT-OSS, or Llama. It is a SvelteKit app and it powers the [HuggingChat app on hf.co/chat](https://huggingface.co/chat).

0. [Quickstart](#quickstart)
1. [Database Options](#database-options)
Expand All @@ -11,11 +11,11 @@ A chat interface using open source models, eg OpenAssistant or Llama. It is a Sv
4. [Extra parameters](#extra-parameters)
5. [Building](#building)

> Note on models: Chat UI only supports OpenAI-compatible APIs via `OPENAI_BASE_URL` and the `/models` endpoint. Provider-specific integrations (legacy `MODELS` env var, GGUF discovery, embeddings, web-search helpers, etc.) are removed, but any service that speaks the OpenAI protocol—Hugging Face router, llama.cpp server, Ollama’s OpenAI bridge, OpenRouter, Anthropic-on-OpenRouter, etc.—will work.
> Note on models: Chat UI v2 only supports OpenAI-compatible APIs via `OPENAI_BASE_URL` and the `/models` endpoint. Provider-specific integrations (legacy `MODELS` env var, GGUF discovery, embeddings, web-search helpers, etc.) are removed, but any service that speaks the OpenAI protocol—Hugging Face router, llama.cpp server, Ollama’s OpenAI bridge, OpenRouter, Anthropic-on-OpenRouter, etc.—will work.

## Quickstart

Chat UI speaks to OpenAI-compatible APIs only. The fastest way to get running is with the Hugging Face Inference Providers router plus your personal Hugging Face access token.
Chat UI speaks to OpenAI-compatible APIs only. The fastest way to get running is with the [Hugging Face Inference Providers router](https://huggingface.co/docs/inference-providers) plus your personal Hugging Face access token.

**Step 1 – Create `.env.local`:**

Expand Down
17 changes: 12 additions & 5 deletions src/hooks.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { config, ready } from "$lib/server/config";
import type { Handle, HandleServerError, ServerInit, HandleFetch } from "@sveltejs/kit";
import { collections } from "$lib/server/database";
import { base } from "$app/paths";
import { authenticateRequest, refreshSessionCookie, requiresUser } from "$lib/server/auth";
import {
authenticateFromHfCookie,
authenticateRequest,
refreshSessionCookie,
requiresUser,
} from "$lib/server/auth";
import { ERROR_MESSAGES } from "$lib/stores/errors";
import { addWeeks } from "date-fns";
import { checkAndRunMigrations } from "$lib/migrations/migrations";
Expand Down Expand Up @@ -121,10 +126,12 @@ export const handle: Handle = async ({ event, resolve }) => {
}
}

const auth = await authenticateRequest(
{ type: "svelte", value: event.request.headers },
{ type: "svelte", value: event.cookies }
);
const auth = config.USE_HFCO_COOKIE
? await authenticateFromHfCookie(event.request.headers, event.cookies)
: await authenticateRequest(
{ type: "svelte", value: event.request.headers },
{ type: "svelte", value: event.cookies }
);

event.locals.user = auth.user || undefined;
event.locals.sessionId = auth.sessionId;
Expand Down
78 changes: 78 additions & 0 deletions src/lib/server/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { logger } from "$lib/server/logger";
import { ObjectId } from "mongodb";
import type { Cookie } from "elysia";
import { adminTokenManager } from "./adminToken";
import { updateUser } from "../../routes/login/callback/updateUser";

export interface OIDCSettings {
redirectURI: string;
Expand Down Expand Up @@ -311,3 +312,80 @@ export async function authenticateRequest(

return { user: undefined, sessionId, secretSessionId, isAdmin: false };
}

export async function authenticateFromHfCookie(
headers: Headers,
cookies: Cookies
): Promise<App.Locals & { secretSessionId: string }> {
const cookieHfco = cookies.get("token");

if (cookieHfco) {
const hash = await sha256(cookieHfco);
const sessionId = hash;
const secretSessionId = hash;

const cacheHit = await collections.tokenCaches.findOne({ tokenHash: hash });
if (cacheHit) {
const user = await collections.users.findOne({ hfUserId: cacheHit.userId });
if (!user) {
throw new Error("User not found");
}
return {
user,
sessionId,
secretSessionId,
isAdmin: user.isAdmin || adminTokenManager.isAdmin(sessionId),
};
}

/// Pass the cookie as an actual cookie, not a Bearer token.
const response = await fetch("https://huggingface.co/api/whoami-v2", {
headers: { Cookie: `token=${cookieHfco}` },
});

if (!response.ok) {
throw new Error("Unauthorized");
}

const userData = z
.object({
id: z.string(),
name: z.string(),
avatarUrl: z.string(),
})
.parse(await response.json());

const locals = {
user: userData,
sessionId,
secretSessionId,
isAdmin: adminTokenManager.isAdmin(sessionId),
};
await updateUser({
userData,
locals,
cookies,
userAgent: headers.get("user-agent") ?? undefined,
});
await collections.tokenCaches.insertOne({
tokenHash: hash,
userId: userData.id,
createdAt: new Date(),
updatedAt: new Date(),
});

/// TODO: Get a JWT token for inference from the Hub

return locals;
}

// Generate new session if none exists
const secretSessionId = crypto.randomUUID();
const sessionId = await sha256(secretSessionId);

if (await collections.sessions.findOne({ sessionId })) {
throw new Error("Session ID collision");
}

return { user: undefined, sessionId, secretSessionId, isAdmin: false };
}