From 8ac33f0931274ba544b2ff68cebc1ee9a83a766c Mon Sep 17 00:00:00 2001
From: Nathan Sarrazin
Date: Fri, 14 Feb 2025 10:46:06 +0000
Subject: [PATCH 01/34] feat(API): refactor API with Elysia
---
package-lock.json | 135 +++++++++++++++++++++++++++
package.json | 3 +
src/hooks.server.ts | 112 ++--------------------
src/lib/server/auth.ts | 111 ++++++++++++++++++++++
src/routes/api/[...slugs]/+server.ts | 26 ++++++
5 files changed, 284 insertions(+), 103 deletions(-)
create mode 100644 src/routes/api/[...slugs]/+server.ts
diff --git a/package-lock.json b/package-lock.json
index 23cd9e569c3..c4cada16fbc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -57,6 +57,8 @@
"zod": "^3.22.3"
},
"devDependencies": {
+ "@elysiajs/eden": "^1.2.0",
+ "@elysiajs/node": "^1.2.5",
"@faker-js/faker": "^8.4.1",
"@iconify-json/carbon": "^1.1.16",
"@iconify-json/eos-icons": "^1.1.6",
@@ -79,6 +81,7 @@
"@typescript-eslint/eslint-plugin": "^6.x",
"@typescript-eslint/parser": "^6.x",
"dompurify": "^3.1.6",
+ "elysia": "^1.2.12",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte": "^2.45.1",
@@ -1428,6 +1431,38 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
+ "node_modules/@elysiajs/eden": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@elysiajs/eden/-/eden-1.2.0.tgz",
+ "integrity": "sha512-MpV45ahuF+iFZUg4tyJbLr9qxzY99m8clJVgQrDrz7Qh6eOKQ8MY6vjYMj3Wh21pTIRHPHzOLhVorRGby1/Owg==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "elysia": ">= 1.2.0"
+ }
+ },
+ "node_modules/@elysiajs/node": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/@elysiajs/node/-/node-1.2.5.tgz",
+ "integrity": "sha512-g5iE2csoixsx4KT4Q57BVjQqjcjJPigWF1ruGHguxrwmI/nfTlWNnpKAR6XU+MERhr3Cf7dd6GI826BBNgo0xw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "formidable": "^3.5.2",
+ "ws": "^8.18.0"
+ },
+ "peerDependencies": {
+ "bufferutil": ">= 4.0.1",
+ "elysia": ">= 1.2.7",
+ "formidable": ">= 3.5.2",
+ "ws": ">= 8.18.0"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@emnapi/runtime": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz",
@@ -3579,6 +3614,13 @@
"node": ">= 8.0.0"
}
},
+ "node_modules/@sinclair/typebox": {
+ "version": "0.34.21",
+ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.21.tgz",
+ "integrity": "sha512-G+7YVIZalo+N+h7KapWBxRH2at9yO4yIHq49PkQLTQQkPTM0ZYaPOfPDpeNv/ukWjm8a1GdnO3yDC7bietT23A==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@smithy/abort-controller": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.1.tgz",
@@ -5110,6 +5152,13 @@
"node": ">=8"
}
},
+ "node_modules/asap": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
+ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/assertion-error": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
@@ -6138,6 +6187,17 @@
"integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==",
"dev": true
},
+ "node_modules/dezalgo": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
+ "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "asap": "^2.0.0",
+ "wrappy": "1"
+ }
+ },
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -6284,6 +6344,42 @@
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.18.tgz",
"integrity": "sha512-1OfuVACu+zKlmjsNdcJuVQuVE61sZOLbNM4JAQ1Rvh6EOj0/EUKhMJjRH73InPlXSh8HIJk1cVZ8pyOV/FMdUQ=="
},
+ "node_modules/elysia": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/elysia/-/elysia-1.2.12.tgz",
+ "integrity": "sha512-X1bZo09qe8/Poa/5tz08Y+sE/77B/wLwnA5xDDENU3FCrsUtYJuBVcy6BPXGRCgnJ1fPQpc0Ov2ZU5MYJXluTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.34.15",
+ "cookie": "^1.0.2",
+ "memoirist": "^0.3.0",
+ "openapi-types": "^12.1.3"
+ },
+ "peerDependencies": {
+ "@sinclair/typebox": ">= 0.34.0",
+ "openapi-types": ">= 12.0.0",
+ "typescript": ">= 5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "openapi-types": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/elysia/node_modules/cookie": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
+ "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/emoji-regex": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
@@ -7276,6 +7372,21 @@
"node": ">= 12.20"
}
},
+ "node_modules/formidable": {
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.2.tgz",
+ "integrity": "sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dezalgo": "^1.0.4",
+ "hexoid": "^2.0.0",
+ "once": "^1.4.0"
+ },
+ "funding": {
+ "url": "https://ko-fi.com/tunnckoCore/commissions"
+ }
+ },
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -7669,6 +7780,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/hexoid": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz",
+ "integrity": "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/highlight.js": {
"version": "11.10.0",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.10.0.tgz",
@@ -8908,6 +9029,13 @@
"node": ">= 0.6"
}
},
+ "node_modules/memoirist": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/memoirist/-/memoirist-0.3.0.tgz",
+ "integrity": "sha512-wR+4chMgVPq+T6OOsk40u9Wlpw1Pjx66NMNiYxCQQ4EUJ7jDs3D9kTCeKdBOkvAiqXlHLVJlvYL01PvIJ1MPNg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/memory-pager": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
@@ -9799,6 +9927,13 @@
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"optional": true
},
+ "node_modules/openapi-types": {
+ "version": "12.1.3",
+ "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
+ "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/openid-client": {
"version": "5.6.5",
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.5.tgz",
diff --git a/package.json b/package.json
index 354ddd6a98f..cd4700d8dc3 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,8 @@
"prepare": "husky"
},
"devDependencies": {
+ "@elysiajs/eden": "^1.2.0",
+ "@elysiajs/node": "^1.2.5",
"@faker-js/faker": "^8.4.1",
"@iconify-json/carbon": "^1.1.16",
"@iconify-json/eos-icons": "^1.1.6",
@@ -39,6 +41,7 @@
"@typescript-eslint/eslint-plugin": "^6.x",
"@typescript-eslint/parser": "^6.x",
"dompurify": "^3.1.6",
+ "elysia": "^1.2.12",
"eslint": "^8.28.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte": "^2.45.1",
diff --git a/src/hooks.server.ts b/src/hooks.server.ts
index 964d53c6254..25dcfff5d22 100644
--- a/src/hooks.server.ts
+++ b/src/hooks.server.ts
@@ -3,9 +3,8 @@ import { env as envPublic } from "$env/dynamic/public";
import type { Handle, HandleServerError } from "@sveltejs/kit";
import { collections } from "$lib/server/database";
import { base } from "$app/paths";
-import { findUser, refreshSessionCookie, requiresUser } from "$lib/server/auth";
+import { authenticateRequest, refreshSessionCookie, requiresUser } from "$lib/server/auth";
import { ERROR_MESSAGES } from "$lib/stores/errors";
-import { sha256 } from "$lib/utils/sha256";
import { addWeeks } from "date-fns";
import { checkAndRunMigrations } from "$lib/migrations/migrations";
import { building } from "$app/environment";
@@ -13,7 +12,6 @@ import { logger } from "$lib/server/logger";
import { AbortedGenerations } from "$lib/server/abortedGenerations";
import { MetricsServer } from "$lib/server/metrics";
import { initExitHandler } from "$lib/server/exitHandler";
-import { ObjectId } from "mongodb";
import { refreshAssistantsCounts } from "$lib/jobs/refresh-assistants-counts";
import { refreshConversationStats } from "$lib/jobs/refresh-conversation-stats";
@@ -109,105 +107,13 @@ export const handle: Handle = async ({ event, resolve }) => {
}
}
- const token = event.cookies.get(env.COOKIE_NAME);
-
- // if the trusted email header is set we use it to get the user email
- const email = env.TRUSTED_EMAIL_HEADER
- ? event.request.headers.get(env.TRUSTED_EMAIL_HEADER)
- : null;
-
- let secretSessionId: string | null = null;
- let sessionId: string | null = null;
-
- if (email) {
- secretSessionId = sessionId = await sha256(email);
-
- event.locals.user = {
- // generate id based on email
- _id: new ObjectId(sessionId.slice(0, 24)),
- name: email,
- email,
- createdAt: new Date(),
- updatedAt: new Date(),
- hfUserId: email,
- avatarUrl: "",
- logoutDisabled: true,
- };
- } else if (token) {
- secretSessionId = token;
- sessionId = await sha256(token);
-
- const user = await findUser(sessionId);
-
- if (user) {
- event.locals.user = user;
- }
- } else if (event.url.pathname.startsWith(`${base}/api/`) && env.USE_HF_TOKEN_IN_API === "true") {
- // if the request goes to the API and no user is available in the header
- // check if a bearer token is available in the Authorization header
-
- const authorization = event.request.headers.get("Authorization");
-
- if (authorization && authorization.startsWith("Bearer ")) {
- const token = authorization.slice(7);
-
- const hash = await sha256(token);
-
- sessionId = secretSessionId = hash;
-
- // check if the hash is in the DB and get the user
- // else check against https://huggingface.co/api/whoami-v2
-
- const cacheHit = await collections.tokenCaches.findOne({ tokenHash: hash });
-
- if (cacheHit) {
- const user = await collections.users.findOne({ hfUserId: cacheHit.userId });
-
- if (!user) {
- return errorResponse(500, "User not found");
- }
-
- event.locals.user = user;
- } else {
- const response = await fetch("https://huggingface.co/api/whoami-v2", {
- headers: {
- Authorization: `Bearer ${token}`,
- },
- });
-
- if (!response.ok) {
- return errorResponse(401, "Unauthorized");
- }
-
- const data = await response.json();
- const user = await collections.users.findOne({ hfUserId: data.id });
-
- if (!user) {
- return errorResponse(500, "User not found");
- }
-
- await collections.tokenCaches.insertOne({
- tokenHash: hash,
- userId: data.id,
- createdAt: new Date(),
- updatedAt: new Date(),
- });
-
- event.locals.user = user;
- }
- }
- }
-
- if (!sessionId || !secretSessionId) {
- secretSessionId = crypto.randomUUID();
- sessionId = await sha256(secretSessionId);
-
- if (await collections.sessions.findOne({ sessionId })) {
- return errorResponse(500, "Session ID collision");
- }
- }
+ const auth = await authenticateRequest(
+ { type: "svelte", value: event.request.headers },
+ { type: "svelte", value: event.cookies }
+ );
- event.locals.sessionId = sessionId;
+ event.locals.user = auth.user || undefined;
+ event.locals.sessionId = auth.sessionId;
// CSRF protection
const requestContentType = event.request.headers.get("content-type")?.split(";")[0] ?? "";
@@ -239,10 +145,10 @@ export const handle: Handle = async ({ event, resolve }) => {
if (event.request.method === "POST") {
// if the request is a POST request we refresh the cookie
- refreshSessionCookie(event.cookies, secretSessionId);
+ refreshSessionCookie(event.cookies, auth.secretSessionId);
await collections.sessions.updateOne(
- { sessionId },
+ { sessionId: auth.sessionId },
{ $set: { updatedAt: new Date(), expiresAt: addWeeks(new Date(), 2) } }
);
}
diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts
index ae170a8bc3f..20baedc88c8 100644
--- a/src/lib/server/auth.ts
+++ b/src/lib/server/auth.ts
@@ -14,6 +14,8 @@ import type { Cookies } from "@sveltejs/kit";
import { collections } from "$lib/server/database";
import JSON5 from "json5";
import { logger } from "$lib/server/logger";
+import { ObjectId } from "mongodb";
+import type { Cookie } from "elysia";
export interface OIDCSettings {
redirectURI: string;
@@ -175,3 +177,112 @@ export async function validateAndParseCsrfToken(
}
return null;
}
+
+type CookieRecord =
+ | { type: "elysia"; value: Record> }
+ | { type: "svelte"; value: Cookies };
+type HeaderRecord =
+ | { type: "elysia"; value: Record }
+ | { type: "svelte"; value: Headers };
+
+export async function authenticateRequest(
+ headers: HeaderRecord,
+ cookie: CookieRecord,
+ isApi?: boolean
+) {
+ const token =
+ cookie.type === "elysia"
+ ? cookie.value[env.COOKIE_NAME].value
+ : cookie.value.get(env.COOKIE_NAME);
+
+ let email = null;
+ if (env.TRUSTED_EMAIL_HEADER) {
+ if (headers.type === "elysia") {
+ email = headers.value[env.TRUSTED_EMAIL_HEADER];
+ } else {
+ email = headers.value.get(env.TRUSTED_EMAIL_HEADER);
+ }
+ }
+
+ let secretSessionId: string | null = null;
+ let sessionId: string | null = null;
+
+ if (email) {
+ secretSessionId = sessionId = await sha256(email);
+ return {
+ user: {
+ _id: new ObjectId(sessionId.slice(0, 24)),
+ name: email,
+ email,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ hfUserId: email,
+ avatarUrl: "",
+ logoutDisabled: true,
+ },
+ sessionId,
+ secretSessionId,
+ };
+ }
+
+ if (token) {
+ secretSessionId = token;
+ sessionId = await sha256(token);
+ const user = await findUser(sessionId);
+ return { user, sessionId, secretSessionId };
+ }
+
+ if (isApi) {
+ const authorization =
+ headers.type === "elysia"
+ ? headers.value["Authorization"]
+ : headers.value.get("Authorization");
+ if (authorization?.startsWith("Bearer ")) {
+ const token = authorization.slice(7);
+ const hash = await sha256(token);
+ sessionId = 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 };
+ }
+
+ const response = await fetch("https://huggingface.co/api/whoami-v2", {
+ headers: { Authorization: `Bearer ${token}` },
+ });
+
+ if (!response.ok) {
+ throw new Error("Unauthorized");
+ }
+
+ const data = await response.json();
+ const user = await collections.users.findOne({ hfUserId: data.id });
+ if (!user) {
+ throw new Error("User not found");
+ }
+
+ await collections.tokenCaches.insertOne({
+ tokenHash: hash,
+ userId: data.id,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ });
+
+ return { user, sessionId, secretSessionId };
+ }
+ }
+
+ // Generate new session if none exists
+ secretSessionId = crypto.randomUUID();
+ sessionId = await sha256(secretSessionId);
+
+ if (await collections.sessions.findOne({ sessionId })) {
+ throw new Error("Session ID collision");
+ }
+
+ return { user: null, sessionId, secretSessionId };
+}
diff --git a/src/routes/api/[...slugs]/+server.ts b/src/routes/api/[...slugs]/+server.ts
new file mode 100644
index 00000000000..11c55872577
--- /dev/null
+++ b/src/routes/api/[...slugs]/+server.ts
@@ -0,0 +1,26 @@
+import { Elysia } from "elysia";
+import { base } from "$app/paths";
+import { authenticateRequest } from "$lib/server/auth";
+
+const app = new Elysia({ prefix: `${base}/api` })
+ .derive(async ({ headers, cookie }) => ({
+ locals: await authenticateRequest(
+ { type: "elysia", value: headers },
+ { type: "elysia", value: cookie },
+ true
+ ),
+ }))
+ .get("/foo", ({ locals }) => {
+ return locals.user?._id;
+ })
+ .get("/bar", ({ locals }) => {
+ return locals.user?.name;
+ });
+
+type RequestHandler = (v: { request: Request; locals: App.Locals }) => Response | Promise;
+
+export const GET: RequestHandler = ({ request }) => app.handle(request);
+export const POST: RequestHandler = ({ request }) => app.handle(request);
+export const PUT: RequestHandler = ({ request }) => app.handle(request);
+export const PATCH: RequestHandler = ({ request }) => app.handle(request);
+export const DELETE: RequestHandler = ({ request }) => app.handle(request);
From 57c6db5412378e23aef3f3d968c3201bd6ae1df7 Mon Sep 17 00:00:00 2001
From: Nathan Sarrazin
Date: Thu, 20 Feb 2025 13:09:10 +0000
Subject: [PATCH 02/34] feat: initial elysia setup
---
.vscode/settings.json | 3 +
package-lock.json | 108 +++++++++++-
package.json | 1 +
src/hooks.server.ts | 3 +
src/lib/server/api/authPlugin.ts | 24 +++
.../server/api/routes/groups/assistants.ts | 45 +++++
.../server/api/routes/groups/conversations.ts | 163 ++++++++++++++++++
src/lib/server/api/routes/groups/tools.ts | 48 ++++++
src/lib/server/api/routes/groups/user.ts | 36 ++++
src/lib/server/api/server.ts | 57 ++++++
src/lib/server/auth.ts | 15 +-
src/lib/utils/api.ts | 12 ++
src/routes/api/v2/[...slugs]/+server.ts | 9 +
13 files changed, 513 insertions(+), 11 deletions(-)
create mode 100644 src/lib/server/api/authPlugin.ts
create mode 100644 src/lib/server/api/routes/groups/assistants.ts
create mode 100644 src/lib/server/api/routes/groups/conversations.ts
create mode 100644 src/lib/server/api/routes/groups/tools.ts
create mode 100644 src/lib/server/api/routes/groups/user.ts
create mode 100644 src/lib/server/api/server.ts
create mode 100644 src/lib/utils/api.ts
create mode 100644 src/routes/api/v2/[...slugs]/+server.ts
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 4a65d631069..670fc9d919e 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -7,5 +7,8 @@
"eslint.validate": ["javascript", "svelte"],
"[svelte]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[typescript]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
diff --git a/package-lock.json b/package-lock.json
index c4cada16fbc..401ba1d6ee2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,7 @@
"dependencies": {
"@aws-sdk/credential-providers": "^3.592.0",
"@cliqz/adblocker-playwright": "^1.27.2",
+ "@elysiajs/swagger": "^1.2.0",
"@gradio/client": "^1.8.0",
"@huggingface/hub": "^0.5.1",
"@huggingface/inference": "^2.8.1",
@@ -1463,6 +1464,21 @@
}
}
},
+ "node_modules/@elysiajs/swagger": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@elysiajs/swagger/-/swagger-1.2.0.tgz",
+ "integrity": "sha512-OPx93DP6rM2VHjA3D44Xiz5MYm9AYlO2NGWPsnSsdyvaOCiL9wJj529583h7arX4iIEYE5LiLB0/A45unqbopw==",
+ "license": "MIT",
+ "dependencies": {
+ "@scalar/themes": "^0.9.52",
+ "@scalar/types": "^0.0.12",
+ "openapi-types": "^12.1.3",
+ "pathe": "^1.1.2"
+ },
+ "peerDependencies": {
+ "elysia": ">= 1.2.0"
+ }
+ },
"node_modules/@emnapi/runtime": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz",
@@ -3594,6 +3610,62 @@
"win32"
]
},
+ "node_modules/@scalar/openapi-types": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@scalar/openapi-types/-/openapi-types-0.1.1.tgz",
+ "integrity": "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@scalar/themes": {
+ "version": "0.9.66",
+ "resolved": "https://registry.npmjs.org/@scalar/themes/-/themes-0.9.66.tgz",
+ "integrity": "sha512-Fm2dUlIQoWCG83yZ2QNdIG7j+3eHgmSQHSnGOfd59+XIC/JxmCVbiOCYyhzfCXl1Zb8YcPlu6Ka2wY++GlrEeQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@scalar/types": "0.0.32"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@scalar/themes/node_modules/@scalar/openapi-types": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/@scalar/openapi-types/-/openapi-types-0.1.7.tgz",
+ "integrity": "sha512-oOTG3JQifg55U3DhKB7WdNIxFnJzbPJe7rqdyWdio977l8IkxQTVmObftJhdNIMvhV2K+1f/bDoMQGu6yTaD0A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@scalar/themes/node_modules/@scalar/types": {
+ "version": "0.0.32",
+ "resolved": "https://registry.npmjs.org/@scalar/types/-/types-0.0.32.tgz",
+ "integrity": "sha512-WHMkFQw4cu1mrG4pEiTUXVBBs205kHECdLM/5F7ATI0A7Axv6G1GgofkwbyCAayUjNk82uaCXzSOgPojbq4iGQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@scalar/openapi-types": "0.1.7",
+ "@unhead/schema": "^1.11.11"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@scalar/types": {
+ "version": "0.0.12",
+ "resolved": "https://registry.npmjs.org/@scalar/types/-/types-0.0.12.tgz",
+ "integrity": "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@scalar/openapi-types": "0.1.1",
+ "@unhead/schema": "^1.9.5"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@sec-ant/readable-stream": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
@@ -3618,7 +3690,6 @@
"version": "0.34.21",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.21.tgz",
"integrity": "sha512-G+7YVIZalo+N+h7KapWBxRH2at9yO4yIHq49PkQLTQQkPTM0ZYaPOfPDpeNv/ukWjm8a1GdnO3yDC7bietT23A==",
- "dev": true,
"license": "MIT"
},
"node_modules/@smithy/abort-controller": {
@@ -4867,6 +4938,19 @@
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
"dev": true
},
+ "node_modules/@unhead/schema": {
+ "version": "1.11.19",
+ "resolved": "https://registry.npmjs.org/@unhead/schema/-/schema-1.11.19.tgz",
+ "integrity": "sha512-7VhYHWK7xHgljdv+C01MepCSYZO2v6OhgsfKWPxRQBDDGfUKCUaChox0XMq3tFvXP6u4zSp6yzcDw2yxCfVMwg==",
+ "license": "MIT",
+ "dependencies": {
+ "hookable": "^5.5.3",
+ "zhead": "^2.2.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/harlan-zw"
+ }
+ },
"node_modules/@vitest/expect": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz",
@@ -6348,7 +6432,6 @@
"version": "1.2.12",
"resolved": "https://registry.npmjs.org/elysia/-/elysia-1.2.12.tgz",
"integrity": "sha512-X1bZo09qe8/Poa/5tz08Y+sE/77B/wLwnA5xDDENU3FCrsUtYJuBVcy6BPXGRCgnJ1fPQpc0Ov2ZU5MYJXluTg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.15",
@@ -6374,7 +6457,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
@@ -7798,6 +7880,12 @@
"node": ">=12.0.0"
}
},
+ "node_modules/hookable": {
+ "version": "5.5.3",
+ "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
+ "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
+ "license": "MIT"
+ },
"node_modules/html-encoding-sniffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
@@ -9033,7 +9121,6 @@
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/memoirist/-/memoirist-0.3.0.tgz",
"integrity": "sha512-wR+4chMgVPq+T6OOsk40u9Wlpw1Pjx66NMNiYxCQQ4EUJ7jDs3D9kTCeKdBOkvAiqXlHLVJlvYL01PvIJ1MPNg==",
- "dev": true,
"license": "MIT"
},
"node_modules/memory-pager": {
@@ -9931,7 +10018,6 @@
"version": "12.1.3",
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
- "dev": true,
"license": "MIT"
},
"node_modules/openid-client": {
@@ -10176,8 +10262,7 @@
"node_modules/pathe": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
- "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
- "dev": true
+ "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="
},
"node_modules/pathval": {
"version": "2.0.0",
@@ -14440,6 +14525,15 @@
"resolved": "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz",
"integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA=="
},
+ "node_modules/zhead": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/zhead/-/zhead-2.2.4.tgz",
+ "integrity": "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/harlan-zw"
+ }
+ },
"node_modules/zimmerframe": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",
diff --git a/package.json b/package.json
index cd4700d8dc3..3a1b08f48ed 100644
--- a/package.json
+++ b/package.json
@@ -67,6 +67,7 @@
"dependencies": {
"@aws-sdk/credential-providers": "^3.592.0",
"@cliqz/adblocker-playwright": "^1.27.2",
+ "@elysiajs/swagger": "^1.2.0",
"@gradio/client": "^1.8.0",
"@huggingface/hub": "^0.5.1",
"@huggingface/inference": "^2.8.1",
diff --git a/src/hooks.server.ts b/src/hooks.server.ts
index 25dcfff5d22..f716bfd8083 100644
--- a/src/hooks.server.ts
+++ b/src/hooks.server.ts
@@ -198,6 +198,9 @@ export const handle: Handle = async ({ event, resolve }) => {
return chunk.html.replace("%gaId%", envPublic.PUBLIC_GOOGLE_ANALYTICS_ID);
},
+ filterSerializedResponseHeaders: (header) => {
+ return header.includes("content-type");
+ },
});
// Add CSP header to disallow framing if ALLOW_IFRAME is not "true"
diff --git a/src/lib/server/api/authPlugin.ts b/src/lib/server/api/authPlugin.ts
new file mode 100644
index 00000000000..db6dcb7db00
--- /dev/null
+++ b/src/lib/server/api/authPlugin.ts
@@ -0,0 +1,24 @@
+import Elysia from "elysia";
+import { authenticateRequest } from "../auth";
+
+export const authPlugin = new Elysia({ name: "auth" }).derive(
+ { as: "scoped" },
+ async ({
+ headers,
+ cookie,
+ }): Promise<{
+ locals: App.Locals;
+ }> => {
+ const auth = await authenticateRequest(
+ { type: "elysia", value: headers },
+ { type: "elysia", value: cookie },
+ true
+ );
+ return {
+ locals: {
+ user: auth?.user,
+ sessionId: auth?.sessionId,
+ },
+ };
+ }
+);
diff --git a/src/lib/server/api/routes/groups/assistants.ts b/src/lib/server/api/routes/groups/assistants.ts
new file mode 100644
index 00000000000..6a50fd49a91
--- /dev/null
+++ b/src/lib/server/api/routes/groups/assistants.ts
@@ -0,0 +1,45 @@
+import { Elysia } from "elysia";
+import { authPlugin } from "$lib/server/api/authPlugin";
+
+export const assistantGroup = new Elysia().use(authPlugin).group("/assistants", (app) => {
+ return app
+ .get("/", () => {
+ // todo: get assistants
+ return "aa";
+ })
+ .post("/", () => {
+ // todo: post new assistant
+ return "aa";
+ })
+ .group("/:id", (app) => {
+ return app
+ .get("/", () => {
+ // todo: get assistant
+ return "aa";
+ })
+ .patch("/", () => {
+ // todo: patch assistant
+ return "aa";
+ })
+ .delete("/", () => {
+ // todo: delete assistant
+ return "aa";
+ })
+ .post("/report", () => {
+ // todo: report assistant
+ return "aa";
+ })
+ .patch("/review", () => {
+ // todo: review assistant
+ return "aa";
+ })
+ .post("/subscribe", () => {
+ // todo: subscribe to assistant
+ return "aa";
+ })
+ .delete("/subscribe", () => {
+ // todo: unsubscribe from assistant
+ return "aa";
+ });
+ });
+});
diff --git a/src/lib/server/api/routes/groups/conversations.ts b/src/lib/server/api/routes/groups/conversations.ts
new file mode 100644
index 00000000000..ef87d22b5f4
--- /dev/null
+++ b/src/lib/server/api/routes/groups/conversations.ts
@@ -0,0 +1,163 @@
+import { Elysia, t } from "elysia";
+import { authPlugin } from "$lib/server/api/authPlugin";
+import { collections } from "$lib/server/database";
+import { ObjectId } from "mongodb";
+import { authCondition } from "$lib/server/auth";
+import { models } from "$lib/server/models";
+import { convertLegacyConversation } from "$lib/utils/tree/convertLegacyConversation";
+import type { Conversation } from "$lib/types/Conversation";
+import type { Assistant } from "$lib/types/Assistant";
+
+export type GETConversationResponse = Pick<
+ Conversation,
+ "messages" | "title" | "model" | "preprompt" | "rootMessageId" | "updatedAt" | "assistantId"
+> & {
+ shared: boolean;
+ modelTools: boolean;
+ assistant: Assistant | undefined;
+ id: string;
+ modelId: Conversation["model"];
+};
+
+export const conversationGroup = new Elysia().use(authPlugin).group("/conversations", (app) => {
+ return app
+ .get("", () => {
+ // todo: get conversations
+ return "aa";
+ })
+ .post("", () => {
+ // todo: post new conversation
+ return "aa";
+ })
+ .group(
+ "/:id",
+ {
+ params: t.Object({
+ id: t.String(),
+ }),
+ },
+ (app) => {
+ return app
+ .derive(async ({ locals, params, error }) => {
+ let conversation;
+ let shared = false;
+
+ // if the conver
+ if (params.id.length === 7) {
+ // shared link of length 7
+ conversation = await collections.sharedConversations.findOne({
+ _id: params.id,
+ });
+ shared = true;
+
+ if (!conversation) {
+ return error(404, "Conversation not found");
+ }
+ } else {
+ // todo: add validation on params.id
+ try {
+ new ObjectId(params.id);
+ } catch {
+ return error(400, "Invalid conversation ID format");
+ }
+ conversation = await collections.conversations.findOne({
+ _id: new ObjectId(params.id),
+ ...authCondition(locals),
+ });
+
+ if (!conversation) {
+ const conversationExists =
+ (await collections.conversations.countDocuments({
+ _id: new ObjectId(params.id),
+ })) !== 0;
+
+ if (conversationExists) {
+ return error(
+ 403,
+ "You don't have access to this conversation. If someone gave you this link, ask them to use the 'share' feature instead."
+ );
+ }
+
+ return error(404, "Conversation not found.");
+ }
+ }
+
+ const convertedConv = {
+ ...conversation,
+ ...convertLegacyConversation(conversation),
+ shared,
+ };
+
+ return { conversation: convertedConv };
+ })
+ .get("", async ({ conversation }) => {
+ return {
+ messages: conversation.messages,
+ title: conversation.title,
+ model: conversation.model,
+ preprompt: conversation.preprompt,
+ rootMessageId: conversation.rootMessageId,
+ assistant: conversation.assistantId
+ ? JSON.parse(
+ JSON.stringify(
+ await collections.assistants.findOne({
+ _id: new ObjectId(conversation.assistantId),
+ })
+ )
+ )
+ : undefined,
+ id: conversation._id.toString(),
+ updatedAt: conversation.updatedAt,
+ modelId: conversation.model,
+ assistantId: conversation.assistantId,
+ modelTools: models.find((m) => m.id == conversation.model)?.tools ?? false,
+ shared: conversation.shared,
+ } satisfies GETConversationResponse;
+ })
+ .post("", () => {
+ // todo: post new message
+ return "aa";
+ })
+ .get("/output/:sha256", () => {
+ // todo: get output
+ return "aa";
+ })
+ .post("/share", () => {
+ // todo: share conversation
+ return "aa";
+ })
+ .post("/stop-generating", () => {
+ // todo: stop generating
+ return "aa";
+ })
+ .group(
+ "messages/:messageId",
+ {
+ params: t.Object({
+ id: t.String(),
+ messageId: t.Optional(t.String()),
+ }),
+ },
+ (app) => {
+ return app
+ .get("/", () => {
+ // todo: get message
+ return "aa";
+ })
+ .delete("/", () => {
+ // todo: delete message
+ return "aa";
+ })
+ .get("/prompt", () => {
+ // todo: get message prompt
+ return "aa";
+ })
+ .post("/vote", () => {
+ // todo: vote on message
+ return "aa";
+ });
+ }
+ );
+ }
+ );
+});
diff --git a/src/lib/server/api/routes/groups/tools.ts b/src/lib/server/api/routes/groups/tools.ts
new file mode 100644
index 00000000000..7075efa14e5
--- /dev/null
+++ b/src/lib/server/api/routes/groups/tools.ts
@@ -0,0 +1,48 @@
+import { Elysia } from "elysia";
+import { authPlugin } from "$lib/server/api/authPlugin";
+
+export const toolGroup = new Elysia().use(authPlugin).group("/tools", (app) => {
+ return app
+ .get("/", () => {
+ // todo: get tools
+ return "aa";
+ })
+ .get("/search", () => {
+ // todo: search tools
+ return "aa";
+ })
+ .group("/:id", (app) => {
+ return app
+ .get("/", () => {
+ // todo: get tool
+ return "aa";
+ })
+ .post("/", () => {
+ // todo: post new tool
+ return "aa";
+ })
+ .group("/:toolId", (app) => {
+ return app
+ .get("/", () => {
+ // todo: get tool
+ return "aa";
+ })
+ .patch("/", () => {
+ // todo: patch tool
+ return "aa";
+ })
+ .delete("/", () => {
+ // todo: delete tool
+ return "aa";
+ })
+ .post("/report", () => {
+ // todo: report tool
+ return "aa";
+ })
+ .patch("/review", () => {
+ // todo: review tool
+ return "aa";
+ });
+ });
+ });
+});
diff --git a/src/lib/server/api/routes/groups/user.ts b/src/lib/server/api/routes/groups/user.ts
new file mode 100644
index 00000000000..f059fa1df3a
--- /dev/null
+++ b/src/lib/server/api/routes/groups/user.ts
@@ -0,0 +1,36 @@
+import { Elysia } from "elysia";
+import { authPlugin } from "$lib/server/api/authPlugin";
+
+export const userGroup = new Elysia()
+ .use(authPlugin)
+ .post("/login", () => {
+ // todo: login
+ return "aa";
+ })
+ .get("/login/callback", () => {
+ // todo: login callback
+ return "aa";
+ })
+ .post("/logout", () => {
+ // todo: logout
+ return "aa";
+ })
+ .group("/user", (app) => {
+ return app
+ .get("/", () => {
+ // todo: get user
+ return "aa";
+ })
+ .get("/settings", () => {
+ // todo: get user settings
+ return "aa";
+ })
+ .patch("/settings", () => {
+ // todo: patch user settings
+ return "aa";
+ })
+ .get("/assistants", () => {
+ // todo: get user assistants
+ return "aa";
+ });
+ });
diff --git a/src/lib/server/api/server.ts b/src/lib/server/api/server.ts
new file mode 100644
index 00000000000..a5d6f6c762f
--- /dev/null
+++ b/src/lib/server/api/server.ts
@@ -0,0 +1,57 @@
+import { Elysia } from "elysia";
+import { base } from "$app/paths";
+import { authPlugin } from "$lib/server/api/authPlugin";
+import { conversationGroup } from "$lib/server/api/routes/groups/conversations";
+import { assistantGroup } from "$lib/server/api/routes/groups/assistants";
+import { userGroup } from "$lib/server/api/routes/groups/user";
+import { toolGroup } from "$lib/server/api/routes/groups/tools";
+import { swagger } from "@elysiajs/swagger";
+import { models } from "$lib/server/models";
+
+const prefix = `${base}/api/v2` as unknown as "";
+
+export const app = new Elysia({ prefix })
+ .use(
+ swagger({
+ documentation: {
+ info: {
+ title: "Elysia Documentation",
+ version: "1.0.0",
+ },
+ },
+ provider: "swagger-ui",
+ })
+ )
+ .use(authPlugin)
+ .use(conversationGroup)
+ .use(toolGroup)
+ .use(assistantGroup)
+ .use(userGroup)
+ .get("/models", () => {
+ return models
+ .filter((m) => m.unlisted == false)
+ .map((model) => ({
+ id: model.id,
+ name: model.name,
+ websiteUrl: model.websiteUrl ?? "https://huggingface.co",
+ modelUrl: model.modelUrl ?? "https://huggingface.co",
+ tokenizer: model.tokenizer,
+ datasetName: model.datasetName,
+ datasetUrl: model.datasetUrl,
+ displayName: model.displayName,
+ description: model.description ?? "",
+ logoUrl: model.logoUrl,
+ promptExamples: model.promptExamples ?? [],
+ preprompt: model.preprompt ?? "",
+ multimodal: model.multimodal ?? false,
+ unlisted: model.unlisted ?? false,
+ tools: model.tools ?? false,
+ hasInferenceAPI: model.hasInferenceAPI ?? false,
+ }));
+ })
+ .get("/spaces-config", () => {
+ // todo: get spaces config
+ return;
+ });
+
+export type App = typeof app;
diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts
index 20baedc88c8..c935e869b4b 100644
--- a/src/lib/server/auth.ts
+++ b/src/lib/server/auth.ts
@@ -189,7 +189,10 @@ export async function authenticateRequest(
headers: HeaderRecord,
cookie: CookieRecord,
isApi?: boolean
-) {
+): Promise {
+ // once the entire API has been moved to elysia
+ // we can move this function to authPlugin.ts
+ // and get rid of the isApi && type: "svelte" options
const token =
cookie.type === "elysia"
? cookie.value[env.COOKIE_NAME].value
@@ -229,7 +232,7 @@ export async function authenticateRequest(
secretSessionId = token;
sessionId = await sha256(token);
const user = await findUser(sessionId);
- return { user, sessionId, secretSessionId };
+ return { user: user ?? undefined, sessionId, secretSessionId };
}
if (isApi) {
@@ -272,7 +275,11 @@ export async function authenticateRequest(
updatedAt: new Date(),
});
- return { user, sessionId, secretSessionId };
+ return {
+ user,
+ sessionId,
+ secretSessionId,
+ };
}
}
@@ -284,5 +291,5 @@ export async function authenticateRequest(
throw new Error("Session ID collision");
}
- return { user: null, sessionId, secretSessionId };
+ return { user: undefined, sessionId, secretSessionId };
}
diff --git a/src/lib/utils/api.ts b/src/lib/utils/api.ts
new file mode 100644
index 00000000000..ec5cced9cb4
--- /dev/null
+++ b/src/lib/utils/api.ts
@@ -0,0 +1,12 @@
+import type { App } from "$lib/server/api/server";
+import { edenFetch } from "@elysiajs/eden";
+
+type Fetch = typeof fetch;
+
+export function useEdenFetch({ fetch }: { fetch: Fetch }) {
+ const app = edenFetch("http://localhost:5173/chat/api/v2", {
+ fetcher: fetch,
+ });
+
+ return app;
+}
diff --git a/src/routes/api/v2/[...slugs]/+server.ts b/src/routes/api/v2/[...slugs]/+server.ts
new file mode 100644
index 00000000000..23f2c81be38
--- /dev/null
+++ b/src/routes/api/v2/[...slugs]/+server.ts
@@ -0,0 +1,9 @@
+import { app } from "$lib/server/api/server";
+
+type RequestHandler = (v: { request: Request; locals: App.Locals }) => Response | Promise;
+
+export const GET: RequestHandler = ({ request }) => app.handle(request);
+export const POST: RequestHandler = ({ request }) => app.handle(request);
+export const PUT: RequestHandler = ({ request }) => app.handle(request);
+export const PATCH: RequestHandler = ({ request }) => app.handle(request);
+export const DELETE: RequestHandler = ({ request }) => app.handle(request);
From 46eec827c1066be25fd3921752335a5459c24a33 Mon Sep 17 00:00:00 2001
From: Nathan Sarrazin
Date: Thu, 20 Feb 2025 13:09:36 +0000
Subject: [PATCH 03/34] feat: replace conv/[id] load function with universal
---
src/routes/conversation/[id]/+page.server.ts | 68 --------------------
src/routes/conversation/[id]/+page.ts | 21 ++++++
2 files changed, 21 insertions(+), 68 deletions(-)
delete mode 100644 src/routes/conversation/[id]/+page.server.ts
create mode 100644 src/routes/conversation/[id]/+page.ts
diff --git a/src/routes/conversation/[id]/+page.server.ts b/src/routes/conversation/[id]/+page.server.ts
deleted file mode 100644
index bd431a07a63..00000000000
--- a/src/routes/conversation/[id]/+page.server.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import { collections } from "$lib/server/database";
-import { ObjectId } from "mongodb";
-import { error } from "@sveltejs/kit";
-import { authCondition } from "$lib/server/auth";
-import { UrlDependency } from "$lib/types/UrlDependency";
-import { convertLegacyConversation } from "$lib/utils/tree/convertLegacyConversation.js";
-
-export const load = async ({ params, depends, locals }) => {
- let conversation;
- let shared = false;
-
- // if the conver
- if (params.id.length === 7) {
- // shared link of length 7
- conversation = await collections.sharedConversations.findOne({
- _id: params.id,
- });
- shared = true;
-
- if (!conversation) {
- error(404, "Conversation not found");
- }
- } else {
- // todo: add validation on params.id
- conversation = await collections.conversations.findOne({
- _id: new ObjectId(params.id),
- ...authCondition(locals),
- });
-
- depends(UrlDependency.Conversation);
-
- if (!conversation) {
- const conversationExists =
- (await collections.conversations.countDocuments({
- _id: new ObjectId(params.id),
- })) !== 0;
-
- if (conversationExists) {
- error(
- 403,
- "You don't have access to this conversation. If someone gave you this link, ask them to use the 'share' feature instead."
- );
- }
-
- error(404, "Conversation not found.");
- }
- }
-
- const convertedConv = { ...conversation, ...convertLegacyConversation(conversation) };
-
- return {
- messages: convertedConv.messages,
- title: convertedConv.title,
- model: convertedConv.model,
- preprompt: convertedConv.preprompt,
- rootMessageId: convertedConv.rootMessageId,
- assistant: convertedConv.assistantId
- ? JSON.parse(
- JSON.stringify(
- await collections.assistants.findOne({
- _id: new ObjectId(convertedConv.assistantId),
- })
- )
- )
- : null,
- shared,
- };
-};
diff --git a/src/routes/conversation/[id]/+page.ts b/src/routes/conversation/[id]/+page.ts
new file mode 100644
index 00000000000..2e90f3d4c97
--- /dev/null
+++ b/src/routes/conversation/[id]/+page.ts
@@ -0,0 +1,21 @@
+import type { GETConversationResponse } from "$lib/server/api/routes/groups/conversations.js";
+import { UrlDependency } from "$lib/types/UrlDependency";
+import { useEdenFetch } from "$lib/utils/api.js";
+import { error } from "@sveltejs/kit";
+export const load = async ({ params, depends, fetch }) => {
+ depends(UrlDependency.Conversation);
+
+ const edenFetch = useEdenFetch({ fetch });
+
+ const { data, error: e } = await edenFetch("/conversations/:id", {
+ params: {
+ id: params.id,
+ },
+ });
+
+ if (e) {
+ error(e.status, e.message);
+ }
+
+ return data as GETConversationResponse;
+};
From 43eb901bdbae93b93c0f4323d0429fae4a61aaa8 Mon Sep 17 00:00:00 2001
From: Nathan Sarrazin
Date: Thu, 20 Feb 2025 14:17:43 +0000
Subject: [PATCH 04/34] fix: delete v1 catchall
---
src/routes/api/[...slugs]/+server.ts | 26 --------------------------
1 file changed, 26 deletions(-)
delete mode 100644 src/routes/api/[...slugs]/+server.ts
diff --git a/src/routes/api/[...slugs]/+server.ts b/src/routes/api/[...slugs]/+server.ts
deleted file mode 100644
index 11c55872577..00000000000
--- a/src/routes/api/[...slugs]/+server.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { Elysia } from "elysia";
-import { base } from "$app/paths";
-import { authenticateRequest } from "$lib/server/auth";
-
-const app = new Elysia({ prefix: `${base}/api` })
- .derive(async ({ headers, cookie }) => ({
- locals: await authenticateRequest(
- { type: "elysia", value: headers },
- { type: "elysia", value: cookie },
- true
- ),
- }))
- .get("/foo", ({ locals }) => {
- return locals.user?._id;
- })
- .get("/bar", ({ locals }) => {
- return locals.user?.name;
- });
-
-type RequestHandler = (v: { request: Request; locals: App.Locals }) => Response | Promise;
-
-export const GET: RequestHandler = ({ request }) => app.handle(request);
-export const POST: RequestHandler = ({ request }) => app.handle(request);
-export const PUT: RequestHandler = ({ request }) => app.handle(request);
-export const PATCH: RequestHandler = ({ request }) => app.handle(request);
-export const DELETE: RequestHandler = ({ request }) => app.handle(request);
From 8df50a209d9913bab4d64c4385f53f1f0c6788b0 Mon Sep 17 00:00:00 2001
From: Nathan Sarrazin
Date: Sun, 23 Feb 2025 22:59:41 +0000
Subject: [PATCH 05/34] wip
---
.../chat/AssistantIntroduction.svelte | 4 +-
src/lib/components/chat/ChatInput.svelte | 3 +-
src/lib/components/chat/ChatWindow.svelte | 3 +-
.../server/api/routes/groups/assistants.ts | 55 +++++++++++++++----
.../server/api/routes/groups/conversations.ts | 15 +++--
src/lib/server/api/server.ts | 3 +-
src/lib/types/UrlDependency.ts | 2 +-
src/lib/utils/serialize.ts | 13 +++++
src/routes/+layout.server.ts | 3 +-
src/routes/+page.svelte | 4 +-
.../api/assistant/[id]/subscribe/+server.ts | 1 +
.../assistant/[assistantId]/+page.server.ts | 42 --------------
src/routes/assistant/[assistantId]/+page.ts | 33 +++++++++++
src/routes/assistants/+page.server.ts | 3 +-
src/routes/tools/+page.server.ts | 3 +-
src/routes/tools/+page.svelte | 9 ++-
16 files changed, 120 insertions(+), 76 deletions(-)
create mode 100644 src/lib/utils/serialize.ts
delete mode 100644 src/routes/assistant/[assistantId]/+page.server.ts
create mode 100644 src/routes/assistant/[assistantId]/+page.ts
diff --git a/src/lib/components/chat/AssistantIntroduction.svelte b/src/lib/components/chat/AssistantIntroduction.svelte
index f84188dbcf1..00cbca98e03 100644
--- a/src/lib/components/chat/AssistantIntroduction.svelte
+++ b/src/lib/components/chat/AssistantIntroduction.svelte
@@ -17,11 +17,11 @@
import { share } from "$lib/utils/share";
import { env as envPublic } from "$env/dynamic/public";
import { page } from "$app/state";
-
+ import type { Serialize } from "$lib/utils/serialize";
interface Props {
models: Model[];
assistant: Pick<
- Assistant,
+ Serialize,
| "avatar"
| "name"
| "rag"
diff --git a/src/lib/components/chat/ChatInput.svelte b/src/lib/components/chat/ChatInput.svelte
index bf616351128..46b12093251 100644
--- a/src/lib/components/chat/ChatInput.svelte
+++ b/src/lib/components/chat/ChatInput.svelte
@@ -24,6 +24,7 @@
import { captureScreen } from "$lib/utils/screenshot";
import IconScreenshot from "../icons/IconScreenshot.svelte";
import { loginModalOpen } from "$lib/stores/loginModal";
+ import type { Serialize } from "$lib/utils/serialize";
interface Props {
files?: File[];
@@ -32,7 +33,7 @@
placeholder?: string;
loading?: boolean;
disabled?: boolean;
- assistant?: Assistant | undefined;
+ assistant?: Serialize | undefined;
modelHasTools?: boolean;
modelIsMultimodal?: boolean;
children?: import("svelte").Snippet;
diff --git a/src/lib/components/chat/ChatWindow.svelte b/src/lib/components/chat/ChatWindow.svelte
index b16a9c1a31e..b56bc2d2a2b 100644
--- a/src/lib/components/chat/ChatWindow.svelte
+++ b/src/lib/components/chat/ChatWindow.svelte
@@ -37,6 +37,7 @@
import { cubicInOut } from "svelte/easing";
import type { ToolFront } from "$lib/types/Tool";
import { loginModalOpen } from "$lib/stores/loginModal";
+ import type { Serialize } from "$lib/utils/serialize";
interface Props {
messages?: Message[];
@@ -46,7 +47,7 @@
shared?: boolean;
currentModel: Model;
models: Model[];
- assistant?: Assistant | undefined;
+ assistant?: Serialize | undefined;
preprompt?: string | undefined;
files?: File[];
}
diff --git a/src/lib/server/api/routes/groups/assistants.ts b/src/lib/server/api/routes/groups/assistants.ts
index 6a50fd49a91..832fec95480 100644
--- a/src/lib/server/api/routes/groups/assistants.ts
+++ b/src/lib/server/api/routes/groups/assistants.ts
@@ -1,5 +1,9 @@
import { Elysia } from "elysia";
import { authPlugin } from "$lib/server/api/authPlugin";
+import { collections } from "$lib/server/database";
+import { ObjectId } from "mongodb";
+import { authCondition } from "$lib/server/auth";
+import { jsonSerialize } from "$lib/utils/serialize";
export const assistantGroup = new Elysia().use(authPlugin).group("/assistants", (app) => {
return app
@@ -13,11 +17,21 @@ export const assistantGroup = new Elysia().use(authPlugin).group("/assistants",
})
.group("/:id", (app) => {
return app
- .get("/", () => {
- // todo: get assistant
- return "aa";
+ .derive(async ({ params, error }) => {
+ const assistant = await collections.assistants.findOne({
+ _id: new ObjectId(params.id),
+ });
+
+ if (!assistant) {
+ return error(404, "Assistant not found");
+ }
+
+ return { assistant };
})
- .patch("/", () => {
+ .get("", ({ assistant }) => {
+ return jsonSerialize(assistant);
+ })
+ .patch("", () => {
// todo: patch assistant
return "aa";
})
@@ -33,13 +47,34 @@ export const assistantGroup = new Elysia().use(authPlugin).group("/assistants",
// todo: review assistant
return "aa";
})
- .post("/subscribe", () => {
- // todo: subscribe to assistant
- return "aa";
+ .post("/subscribe", async ({ locals, assistant }) => {
+ const result = await collections.settings.updateOne(authCondition(locals), {
+ $addToSet: { assistants: assistant._id },
+ $set: { activeModel: assistant._id.toString() },
+ });
+
+ if (result.modifiedCount > 0) {
+ await collections.assistants.updateOne(
+ { _id: assistant._id },
+ { $inc: { userCount: 1 } }
+ );
+ }
+
+ return new Response("Assistant subscribed", { status: 200 });
})
- .delete("/subscribe", () => {
- // todo: unsubscribe from assistant
- return "aa";
+ .delete("/subscribe", async ({ locals, assistant }) => {
+ const result = await collections.settings.updateOne(authCondition(locals), {
+ $pull: { assistants: assistant._id },
+ });
+
+ if (result.modifiedCount > 0) {
+ await collections.assistants.updateOne(
+ { _id: assistant._id },
+ { $inc: { userCount: -1 } }
+ );
+ }
+
+ return new Response("Assistant unsubscribed", { status: 200 });
});
});
});
diff --git a/src/lib/server/api/routes/groups/conversations.ts b/src/lib/server/api/routes/groups/conversations.ts
index ef87d22b5f4..e9302052d47 100644
--- a/src/lib/server/api/routes/groups/conversations.ts
+++ b/src/lib/server/api/routes/groups/conversations.ts
@@ -7,14 +7,15 @@ import { models } from "$lib/server/models";
import { convertLegacyConversation } from "$lib/utils/tree/convertLegacyConversation";
import type { Conversation } from "$lib/types/Conversation";
import type { Assistant } from "$lib/types/Assistant";
-
+import type { Serialize } from "$lib/utils/serialize";
+import { jsonSerialize } from "$lib/utils/serialize";
export type GETConversationResponse = Pick<
Conversation,
"messages" | "title" | "model" | "preprompt" | "rootMessageId" | "updatedAt" | "assistantId"
> & {
shared: boolean;
modelTools: boolean;
- assistant: Assistant | undefined;
+ assistant: Serialize | undefined;
id: string;
modelId: Conversation["model"];
};
@@ -98,12 +99,10 @@ export const conversationGroup = new Elysia().use(authPlugin).group("/conversati
preprompt: conversation.preprompt,
rootMessageId: conversation.rootMessageId,
assistant: conversation.assistantId
- ? JSON.parse(
- JSON.stringify(
- await collections.assistants.findOne({
- _id: new ObjectId(conversation.assistantId),
- })
- )
+ ? jsonSerialize(
+ (await collections.assistants.findOne({
+ _id: new ObjectId(conversation.assistantId),
+ })) ?? undefined
)
: undefined,
id: conversation._id.toString(),
diff --git a/src/lib/server/api/server.ts b/src/lib/server/api/server.ts
index a5d6f6c762f..ac69883f49e 100644
--- a/src/lib/server/api/server.ts
+++ b/src/lib/server/api/server.ts
@@ -7,10 +7,11 @@ import { userGroup } from "$lib/server/api/routes/groups/user";
import { toolGroup } from "$lib/server/api/routes/groups/tools";
import { swagger } from "@elysiajs/swagger";
import { models } from "$lib/server/models";
+import { node } from "@elysiajs/node";
const prefix = `${base}/api/v2` as unknown as "";
-export const app = new Elysia({ prefix })
+export const app = new Elysia({ prefix, adapter: node() })
.use(
swagger({
documentation: {
diff --git a/src/lib/types/UrlDependency.ts b/src/lib/types/UrlDependency.ts
index dca26f87f49..c8b901f2ef5 100644
--- a/src/lib/types/UrlDependency.ts
+++ b/src/lib/types/UrlDependency.ts
@@ -1,5 +1,5 @@
/* eslint-disable no-shadow */
export enum UrlDependency {
ConversationList = "conversation:list",
- Conversation = "conversation",
+ Conversation = "conversation:id",
}
diff --git a/src/lib/utils/serialize.ts b/src/lib/utils/serialize.ts
new file mode 100644
index 00000000000..20e5250f2cc
--- /dev/null
+++ b/src/lib/utils/serialize.ts
@@ -0,0 +1,13 @@
+import { ObjectId } from "mongodb";
+
+export type Serialize = T extends ObjectId | Date
+ ? string
+ : T extends Array
+ ? Array>
+ : T extends object
+ ? { [K in keyof T]: Serialize }
+ : T;
+
+export function jsonSerialize(data: T): Serialize {
+ return JSON.parse(JSON.stringify(data)) as Serialize;
+}
diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts
index f2caa374834..c54dea29cdb 100644
--- a/src/routes/+layout.server.ts
+++ b/src/routes/+layout.server.ts
@@ -13,6 +13,7 @@ import { MetricsServer } from "$lib/server/metrics";
import type { ToolFront, ToolInputFile } from "$lib/types/Tool";
import { ReviewStatus } from "$lib/types/Review";
import { base } from "$app/paths";
+import { jsonSerialize } from "../lib/utils/serialize";
export const load: LayoutServerLoad = async ({ locals, depends, fetch }) => {
depends(UrlDependency.ConversationList);
@@ -273,7 +274,7 @@ export const load: LayoutServerLoad = async ({ locals, depends, fetch }) => {
isAdmin: locals.user.isAdmin ?? false,
isEarlyAccess: locals.user.isEarlyAccess ?? false,
},
- assistant: assistant ? JSON.parse(JSON.stringify(assistant)) : null,
+ assistant: assistant ? jsonSerialize(assistant) : undefined,
enableAssistants,
enableAssistantsRAG: env.ENABLE_ASSISTANTS_RAG === "true",
enableCommunityTools: env.COMMUNITY_TOOLS === "true",
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 2e1f106febd..42c3ffdc9c5 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -30,8 +30,8 @@
if (validModels.includes($settings.activeModel)) {
model = $settings.activeModel;
} else {
- if (validModels.includes(data.assistant?.modelId)) {
- model = data.assistant?.modelId;
+ if (data.assistant?.modelId && validModels.includes(data.assistant.modelId)) {
+ model = data.assistant.modelId;
} else {
model = data.models[0].id;
}
diff --git a/src/routes/api/assistant/[id]/subscribe/+server.ts b/src/routes/api/assistant/[id]/subscribe/+server.ts
index fe67acf644d..ffc7ec60265 100644
--- a/src/routes/api/assistant/[id]/subscribe/+server.ts
+++ b/src/routes/api/assistant/[id]/subscribe/+server.ts
@@ -23,6 +23,7 @@ export async function POST({ params, locals }) {
const result = await collections.settings.updateOne(authCondition(locals), {
$addToSet: { assistants: assistant._id },
+ $set: { activeModel: assistant._id.toString() },
});
// reduce count only if push succeeded
diff --git a/src/routes/assistant/[assistantId]/+page.server.ts b/src/routes/assistant/[assistantId]/+page.server.ts
deleted file mode 100644
index 072b3516c8e..00000000000
--- a/src/routes/assistant/[assistantId]/+page.server.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { base } from "$app/paths";
-import { collections } from "$lib/server/database";
-import { redirect } from "@sveltejs/kit";
-import { ObjectId } from "mongodb";
-import { authCondition } from "$lib/server/auth.js";
-
-export async function load({ params, locals }) {
- try {
- const assistant = await collections.assistants.findOne({
- _id: new ObjectId(params.assistantId),
- });
-
- if (!assistant) {
- redirect(302, `${base}`);
- }
-
- if (locals.user?._id ?? locals.sessionId) {
- await collections.settings.updateOne(
- authCondition(locals),
- {
- $set: {
- activeModel: assistant._id.toString(),
- updatedAt: new Date(),
- },
- $push: { assistants: assistant._id },
- $setOnInsert: {
- createdAt: new Date(),
- },
- },
- {
- upsert: true,
- }
- );
- }
-
- return {
- assistant: JSON.parse(JSON.stringify(assistant)),
- };
- } catch {
- redirect(302, `${base}`);
- }
-}
diff --git a/src/routes/assistant/[assistantId]/+page.ts b/src/routes/assistant/[assistantId]/+page.ts
new file mode 100644
index 00000000000..78540097aed
--- /dev/null
+++ b/src/routes/assistant/[assistantId]/+page.ts
@@ -0,0 +1,33 @@
+import { useEdenFetch } from "$lib/utils/api";
+import { error } from "@sveltejs/kit";
+import type { Assistant } from "$lib/types/Assistant";
+import type { Serialize } from "$lib/utils/serialize";
+
+export async function load({ fetch, params }) {
+ const edenFetch = useEdenFetch({ fetch });
+
+ const { data, error: e } = await edenFetch("/assistants/:id", {
+ method: "GET",
+ params: {
+ id: params.assistantId,
+ },
+ });
+
+ if (e) {
+ error(e.status, e.message);
+ }
+
+ const { error: subscribeError } = await edenFetch("/assistants/:id/subscribe", {
+ method: "POST",
+ params: {
+ id: params.assistantId,
+ },
+ });
+
+ if (subscribeError) {
+ console.error(subscribeError);
+ error(subscribeError.status, subscribeError.message);
+ }
+
+ return { assistant: data as Serialize };
+}
diff --git a/src/routes/assistants/+page.server.ts b/src/routes/assistants/+page.server.ts
index cc5ab3ab195..d74701e63b6 100644
--- a/src/routes/assistants/+page.server.ts
+++ b/src/routes/assistants/+page.server.ts
@@ -7,6 +7,7 @@ import { generateQueryTokens } from "$lib/utils/searchTokens.js";
import { error, redirect } from "@sveltejs/kit";
import type { Filter } from "mongodb";
import { ReviewStatus } from "$lib/types/Review";
+import { jsonSerialize } from "$lib/utils/serialize";
const NUM_PER_PAGE = 24;
export const load = async ({ url, locals }) => {
@@ -75,7 +76,7 @@ export const load = async ({ url, locals }) => {
.assistants.countDocuments(filter);
return {
- assistants: JSON.parse(JSON.stringify(assistants)) as Array,
+ assistants: jsonSerialize(assistants),
selectedModel: modelId ?? "",
numTotalItems,
numItemsPerPage: NUM_PER_PAGE,
diff --git a/src/routes/tools/+page.server.ts b/src/routes/tools/+page.server.ts
index 7aca65796ec..b704a889930 100644
--- a/src/routes/tools/+page.server.ts
+++ b/src/routes/tools/+page.server.ts
@@ -7,6 +7,7 @@ import { ReviewStatus } from "$lib/types/Review";
import type { CommunityToolDB } from "$lib/types/Tool.js";
import type { User } from "$lib/types/User.js";
import { generateQueryTokens, generateSearchTokens } from "$lib/utils/searchTokens.js";
+import { jsonSerialize } from "$lib/utils/serialize";
import { error } from "@sveltejs/kit";
import { ObjectId, type Filter } from "mongodb";
@@ -89,7 +90,7 @@ export const load = async ({ url, locals }) => {
toolFromConfigs.length;
return {
- tools: JSON.parse(JSON.stringify(tools)) as CommunityToolDB[],
+ tools: jsonSerialize(tools),
numTotalItems,
numItemsPerPage: NUM_PER_PAGE,
query,
diff --git a/src/routes/tools/+page.svelte b/src/routes/tools/+page.svelte
index 920fb10dd79..16e98e49570 100644
--- a/src/routes/tools/+page.svelte
+++ b/src/routes/tools/+page.svelte
@@ -266,15 +266,14 @@
diff --git a/src/lib/components/LoginModal.svelte b/src/lib/components/LoginModal.svelte
index 8f46878e654..ba22c16989d 100644
--- a/src/lib/components/LoginModal.svelte
+++ b/src/lib/components/LoginModal.svelte
@@ -26,20 +26,10 @@
{envPublic.PUBLIC_APP_GUEST_MESSAGE}
-