Spaces:
Runtime error
Runtime error
feat: allow storing env variable in DB (#1802)
Browse files* feat(config): wip, make config controllable from DB, public side for now
* fix: build
* feat(config): add config manager env var toggle
* wip: make backend config use config manager
* fix: typechecks
* fix: circular import
* refactor: init hook sveltekit
* fix: disable config manager during tests
* refactor: use enum for semaphore, move delete from auto after 60s to separate `deleteAt` field
* feat: config manager checks for updates on requests
* fix: config script exit
* fix: remove top-level awaits
* fix: script await
* feat: filter config keys based on public environment variables for more security
* fix: change import to type for public environment variables
This view is limited to 50 files because it contains too many changes.
See raw diff
- .env +9 -2
- chart/env/prod.yaml +1 -0
- package.json +1 -0
- scripts/config.ts +64 -0
- src/hooks.server.ts +45 -34
- src/lib/components/AssistantSettings.svelte +2 -2
- src/lib/components/DisclaimerModal.svelte +6 -5
- src/lib/components/LoginModal.svelte +6 -5
- src/lib/components/NavMenu.svelte +5 -4
- src/lib/components/ToolsMenu.svelte +2 -2
- src/lib/components/chat/AssistantIntroduction.svelte +3 -2
- src/lib/components/chat/ChatIntroduction.svelte +6 -9
- src/lib/components/icons/Logo.svelte +8 -5
- src/lib/jobs/refresh-assistants-counts.ts +3 -4
- src/lib/jobs/refresh-conversation-stats.ts +3 -4
- src/lib/migrations/lock.ts +7 -4
- src/lib/migrations/migrations.spec.ts +12 -11
- src/lib/migrations/migrations.ts +8 -9
- src/lib/server/adminToken.ts +6 -7
- src/lib/server/auth.ts +14 -14
- src/lib/server/config.ts +182 -0
- src/lib/server/database.ts +29 -15
- src/lib/server/embeddingEndpoints/hfApi/embeddingHfApi.ts +3 -3
- src/lib/server/embeddingEndpoints/openai/embeddingEndpoints.ts +2 -2
- src/lib/server/embeddingEndpoints/tei/embeddingEndpoints.ts +2 -2
- src/lib/server/embeddingModels.ts +2 -2
- src/lib/server/endpoints/anthropic/endpointAnthropic.ts +2 -2
- src/lib/server/endpoints/cloudflare/endpointCloudflare.ts +3 -3
- src/lib/server/endpoints/cohere/endpointCohere.ts +2 -2
- src/lib/server/endpoints/google/endpointGenAI.ts +2 -2
- src/lib/server/endpoints/llamacpp/endpointLlamacpp.ts +2 -2
- src/lib/server/endpoints/local/endpointLocal.ts +2 -2
- src/lib/server/endpoints/openai/endpointOai.ts +2 -2
- src/lib/server/endpoints/tgi/endpointTgi.ts +2 -2
- src/lib/server/logger.ts +2 -2
- src/lib/server/metrics.ts +6 -6
- src/lib/server/models.ts +15 -15
- src/lib/server/sendSlack.ts +3 -3
- src/lib/server/textGeneration/assistant.ts +4 -4
- src/lib/server/textGeneration/generate.ts +2 -2
- src/lib/server/textGeneration/title.ts +2 -2
- src/lib/server/tools/index.ts +2 -2
- src/lib/server/tools/utils.ts +3 -3
- src/lib/server/usageLimits.ts +3 -3
- src/lib/server/websearch/scrape/playwright.ts +6 -6
- src/lib/server/websearch/search/endpoints.ts +12 -12
- src/lib/server/websearch/search/endpoints/bing.ts +2 -2
- src/lib/server/websearch/search/endpoints/searchApi.ts +2 -2
- src/lib/server/websearch/search/endpoints/searxng.ts +2 -2
- src/lib/server/websearch/search/endpoints/serpApi.ts +2 -2
.env
CHANGED
|
@@ -1,11 +1,17 @@
|
|
| 1 |
# Use .env.local to change these variables
|
| 2 |
# DO NOT EDIT THIS FILE WITH SENSITIVE DATA
|
| 3 |
|
|
|
|
|
|
|
|
|
|
| 4 |
### MongoDB ###
|
| 5 |
MONGODB_URL=#your mongodb URL here, use chat-ui-db image if you don't want to set this
|
| 6 |
MONGODB_DB_NAME=chat-ui
|
| 7 |
MONGODB_DIRECT_CONNECTION=false
|
| 8 |
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
### Endpoints config ###
|
| 11 |
HF_API_ROOT=https://api-inference.huggingface.co/models
|
|
@@ -85,7 +91,7 @@ COOKIE_NAME=hf-chat
|
|
| 85 |
# specify secure behaviour for cookies
|
| 86 |
COOKIE_SAMESITE=# can be "lax", "strict", "none" or left empty
|
| 87 |
COOKIE_SECURE=# set to true to only allow cookies over https
|
| 88 |
-
|
| 89 |
|
| 90 |
### Admin stuff ###
|
| 91 |
ADMIN_CLI_LOGIN=true # set to false to disable the CLI login
|
|
@@ -170,7 +176,7 @@ USE_HF_TOKEN_IN_API=false
|
|
| 170 |
HF_ORG_ADMIN=
|
| 171 |
HF_ORG_EARLY_ACCESS=
|
| 172 |
WEBHOOK_URL_REPORT_ASSISTANT=#provide slack webhook url to get notified for reports/feature requests
|
| 173 |
-
|
| 174 |
|
| 175 |
|
| 176 |
### Metrics ###
|
|
@@ -208,3 +214,4 @@ OPENID_NAME_CLAIM="name" # Change to "username" for some providers that do not p
|
|
| 208 |
OPENID_PROVIDER_URL=https://huggingface.co # for Google, use https://accounts.google.com
|
| 209 |
OPENID_TOLERANCE=
|
| 210 |
OPENID_RESOURCE=
|
|
|
|
|
|
| 1 |
# Use .env.local to change these variables
|
| 2 |
# DO NOT EDIT THIS FILE WITH SENSITIVE DATA
|
| 3 |
|
| 4 |
+
### Config ###
|
| 5 |
+
ENABLE_CONFIG_MANAGER=true
|
| 6 |
+
|
| 7 |
### MongoDB ###
|
| 8 |
MONGODB_URL=#your mongodb URL here, use chat-ui-db image if you don't want to set this
|
| 9 |
MONGODB_DB_NAME=chat-ui
|
| 10 |
MONGODB_DIRECT_CONNECTION=false
|
| 11 |
|
| 12 |
+
### Local Storage ###
|
| 13 |
+
MODELS_STORAGE_PATH= # where are .gguf for model inference stored
|
| 14 |
+
MONGO_STORAGE_PATH= # where is the db folder stored
|
| 15 |
|
| 16 |
### Endpoints config ###
|
| 17 |
HF_API_ROOT=https://api-inference.huggingface.co/models
|
|
|
|
| 91 |
# specify secure behaviour for cookies
|
| 92 |
COOKIE_SAMESITE=# can be "lax", "strict", "none" or left empty
|
| 93 |
COOKIE_SECURE=# set to true to only allow cookies over https
|
| 94 |
+
TRUSTED_EMAIL_HEADER=# header to use to get the user email, only use if you know what you are doing
|
| 95 |
|
| 96 |
### Admin stuff ###
|
| 97 |
ADMIN_CLI_LOGIN=true # set to false to disable the CLI login
|
|
|
|
| 176 |
HF_ORG_ADMIN=
|
| 177 |
HF_ORG_EARLY_ACCESS=
|
| 178 |
WEBHOOK_URL_REPORT_ASSISTANT=#provide slack webhook url to get notified for reports/feature requests
|
| 179 |
+
IP_TOKEN_SECRET=
|
| 180 |
|
| 181 |
|
| 182 |
### Metrics ###
|
|
|
|
| 214 |
OPENID_PROVIDER_URL=https://huggingface.co # for Google, use https://accounts.google.com
|
| 215 |
OPENID_TOLERANCE=
|
| 216 |
OPENID_RESOURCE=
|
| 217 |
+
EXPOSE_API=# deprecated, API is now always exposed
|
chart/env/prod.yaml
CHANGED
|
@@ -39,6 +39,7 @@ envVars:
|
|
| 39 |
COOKIE_SECURE: "true"
|
| 40 |
ENABLE_ASSISTANTS: "true"
|
| 41 |
ENABLE_ASSISTANTS_RAG: "true"
|
|
|
|
| 42 |
METRICS_PORT: 5565
|
| 43 |
LOG_LEVEL: "debug"
|
| 44 |
METRICS_ENABLED: "true"
|
|
|
|
| 39 |
COOKIE_SECURE: "true"
|
| 40 |
ENABLE_ASSISTANTS: "true"
|
| 41 |
ENABLE_ASSISTANTS_RAG: "true"
|
| 42 |
+
ENABLE_CONFIG_MANAGER: "false"
|
| 43 |
METRICS_PORT: 5565
|
| 44 |
LOG_LEVEL: "debug"
|
| 45 |
METRICS_ENABLED: "true"
|
package.json
CHANGED
|
@@ -14,6 +14,7 @@
|
|
| 14 |
"test": "vitest",
|
| 15 |
"updateLocalEnv": "vite-node --options.transformMode.ssr='/.*/' scripts/updateLocalEnv.ts",
|
| 16 |
"populate": "vite-node --options.transformMode.ssr='/.*/' scripts/populate.ts",
|
|
|
|
| 17 |
"prepare": "husky"
|
| 18 |
},
|
| 19 |
"devDependencies": {
|
|
|
|
| 14 |
"test": "vitest",
|
| 15 |
"updateLocalEnv": "vite-node --options.transformMode.ssr='/.*/' scripts/updateLocalEnv.ts",
|
| 16 |
"populate": "vite-node --options.transformMode.ssr='/.*/' scripts/populate.ts",
|
| 17 |
+
"config": "vite-node --options.transformMode.ssr='/.*/' scripts/config.ts",
|
| 18 |
"prepare": "husky"
|
| 19 |
},
|
| 20 |
"devDependencies": {
|
scripts/config.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sade from "sade";
|
| 2 |
+
|
| 3 |
+
// @ts-expect-error: vite-node makes the var available but the typescript compiler doesn't see them
|
| 4 |
+
import { config, ready } from "$lib/server/config";
|
| 5 |
+
|
| 6 |
+
const prog = sade("config");
|
| 7 |
+
await ready;
|
| 8 |
+
prog
|
| 9 |
+
.command("clear")
|
| 10 |
+
.describe("Clear all config keys")
|
| 11 |
+
.action(async () => {
|
| 12 |
+
console.log("Clearing config...");
|
| 13 |
+
await clear();
|
| 14 |
+
});
|
| 15 |
+
|
| 16 |
+
prog
|
| 17 |
+
.command("add <key> <value>")
|
| 18 |
+
.describe("Add a new config key")
|
| 19 |
+
.action(async (key: string, value: string) => {
|
| 20 |
+
await add(key, value);
|
| 21 |
+
});
|
| 22 |
+
|
| 23 |
+
prog
|
| 24 |
+
.command("remove <key>")
|
| 25 |
+
.describe("Remove a config key")
|
| 26 |
+
.action(async (key: string) => {
|
| 27 |
+
console.log(`Removing ${key}`);
|
| 28 |
+
await remove(key);
|
| 29 |
+
process.exit(0);
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
prog
|
| 33 |
+
.command("help")
|
| 34 |
+
.describe("Show help information")
|
| 35 |
+
.action(() => {
|
| 36 |
+
prog.help();
|
| 37 |
+
process.exit(0);
|
| 38 |
+
});
|
| 39 |
+
|
| 40 |
+
async function clear() {
|
| 41 |
+
await config.clear();
|
| 42 |
+
process.exit(0);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
async function add(key: string, value: string) {
|
| 46 |
+
if (!key || !value) {
|
| 47 |
+
console.error("Key and value are required");
|
| 48 |
+
process.exit(1);
|
| 49 |
+
}
|
| 50 |
+
await config.set(key as keyof typeof config.keysFromEnv, value);
|
| 51 |
+
process.exit(0);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
async function remove(key: string) {
|
| 55 |
+
if (!key) {
|
| 56 |
+
console.error("Key is required");
|
| 57 |
+
process.exit(1);
|
| 58 |
+
}
|
| 59 |
+
await config.delete(key as keyof typeof config.keysFromEnv);
|
| 60 |
+
process.exit(0);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// Parse arguments and handle help automatically
|
| 64 |
+
prog.parse(process.argv);
|
src/hooks.server.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
-
import {
|
| 2 |
-
import {
|
| 3 |
-
import type { Handle, HandleServerError } from "@sveltejs/kit";
|
| 4 |
import { collections } from "$lib/server/database";
|
| 5 |
import { base } from "$app/paths";
|
| 6 |
import { findUser, refreshSessionCookie, requiresUser } from "$lib/server/auth";
|
|
@@ -18,34 +17,39 @@ import { refreshAssistantsCounts } from "$lib/jobs/refresh-assistants-counts";
|
|
| 18 |
import { refreshConversationStats } from "$lib/jobs/refresh-conversation-stats";
|
| 19 |
import { adminTokenManager } from "$lib/server/adminToken";
|
| 20 |
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
process.env.HF_TOKEN ??= env.HF_TOKEN;
|
| 25 |
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
| 28 |
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
refreshAssistantsCounts();
|
| 32 |
-
}
|
| 33 |
-
refreshConversationStats();
|
| 34 |
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
| 40 |
|
| 41 |
-
|
|
|
|
| 42 |
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
| 47 |
}
|
| 48 |
-
}
|
| 49 |
|
| 50 |
export const handleError: HandleServerError = async ({ error, event, status, message }) => {
|
| 51 |
// handle 404
|
|
@@ -81,6 +85,10 @@ export const handleError: HandleServerError = async ({ error, event, status, mes
|
|
| 81 |
};
|
| 82 |
|
| 83 |
export const handle: Handle = async ({ event, resolve }) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
logger.debug({
|
| 85 |
locals: event.locals,
|
| 86 |
url: event.url.pathname,
|
|
@@ -101,7 +109,7 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|
| 101 |
}
|
| 102 |
|
| 103 |
if (event.url.pathname.startsWith(`${base}/admin/`) || event.url.pathname === `${base}/admin`) {
|
| 104 |
-
const ADMIN_SECRET =
|
| 105 |
|
| 106 |
if (!ADMIN_SECRET) {
|
| 107 |
return errorResponse(500, "Admin API is not configured");
|
|
@@ -112,11 +120,11 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|
| 112 |
}
|
| 113 |
}
|
| 114 |
|
| 115 |
-
const token = event.cookies.get(
|
| 116 |
|
| 117 |
// if the trusted email header is set we use it to get the user email
|
| 118 |
-
const email =
|
| 119 |
-
? event.request.headers.get(
|
| 120 |
: null;
|
| 121 |
|
| 122 |
let secretSessionId: string | null = null;
|
|
@@ -145,7 +153,10 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|
| 145 |
if (user) {
|
| 146 |
event.locals.user = user;
|
| 147 |
}
|
| 148 |
-
} else if (
|
|
|
|
|
|
|
|
|
|
| 149 |
// if the request goes to the API and no user is available in the header
|
| 150 |
// check if a bearer token is available in the Authorization header
|
| 151 |
|
|
@@ -234,7 +245,7 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|
| 234 |
|
| 235 |
const validOrigins = [
|
| 236 |
new URL(event.request.url).host,
|
| 237 |
-
...(
|
| 238 |
];
|
| 239 |
|
| 240 |
if (!validOrigins.includes(new URL(origin).host)) {
|
|
@@ -262,7 +273,7 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|
| 262 |
if (
|
| 263 |
!event.locals.user &&
|
| 264 |
requiresUser &&
|
| 265 |
-
!((
|
| 266 |
) {
|
| 267 |
return errorResponse(401, ERROR_MESSAGES.authOnly);
|
| 268 |
}
|
|
@@ -273,7 +284,7 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|
| 273 |
if (
|
| 274 |
!requiresUser &&
|
| 275 |
!event.url.pathname.startsWith(`${base}/settings`) &&
|
| 276 |
-
|
| 277 |
) {
|
| 278 |
const hasAcceptedEthicsModal = await collections.settings.countDocuments({
|
| 279 |
sessionId: event.locals.sessionId,
|
|
@@ -296,12 +307,12 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|
| 296 |
}
|
| 297 |
replaced = true;
|
| 298 |
|
| 299 |
-
return chunk.html.replace("%gaId%",
|
| 300 |
},
|
| 301 |
});
|
| 302 |
|
| 303 |
// Add CSP header to disallow framing if ALLOW_IFRAME is not "true"
|
| 304 |
-
if (
|
| 305 |
response.headers.append("Content-Security-Policy", "frame-ancestors 'none';");
|
| 306 |
}
|
| 307 |
|
|
|
|
| 1 |
+
import { config, ready } from "$lib/server/config";
|
| 2 |
+
import type { Handle, HandleServerError, ServerInit } from "@sveltejs/kit";
|
|
|
|
| 3 |
import { collections } from "$lib/server/database";
|
| 4 |
import { base } from "$app/paths";
|
| 5 |
import { findUser, refreshSessionCookie, requiresUser } from "$lib/server/auth";
|
|
|
|
| 17 |
import { refreshConversationStats } from "$lib/jobs/refresh-conversation-stats";
|
| 18 |
import { adminTokenManager } from "$lib/server/adminToken";
|
| 19 |
|
| 20 |
+
export const init: ServerInit = async () => {
|
| 21 |
+
// Wait for config to be fully loaded
|
| 22 |
+
await ready;
|
|
|
|
| 23 |
|
| 24 |
+
// TODO: move this code on a started server hook, instead of using a "building" flag
|
| 25 |
+
if (!building) {
|
| 26 |
+
// Set HF_TOKEN as a process variable for Transformers.JS to see it
|
| 27 |
+
process.env.HF_TOKEN ??= config.HF_TOKEN;
|
| 28 |
|
| 29 |
+
logger.info("Starting server...");
|
| 30 |
+
initExitHandler();
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
+
checkAndRunMigrations();
|
| 33 |
+
if (config.ENABLE_ASSISTANTS) {
|
| 34 |
+
refreshAssistantsCounts();
|
| 35 |
+
}
|
| 36 |
+
refreshConversationStats();
|
| 37 |
|
| 38 |
+
// Init metrics server
|
| 39 |
+
MetricsServer.getInstance();
|
| 40 |
|
| 41 |
+
// Init AbortedGenerations refresh process
|
| 42 |
+
AbortedGenerations.getInstance();
|
| 43 |
|
| 44 |
+
adminTokenManager.displayToken();
|
| 45 |
+
|
| 46 |
+
if (config.EXPOSE_API) {
|
| 47 |
+
logger.warn(
|
| 48 |
+
"The EXPOSE_API flag has been deprecated. The API is now required for chat-ui to work."
|
| 49 |
+
);
|
| 50 |
+
}
|
| 51 |
}
|
| 52 |
+
};
|
| 53 |
|
| 54 |
export const handleError: HandleServerError = async ({ error, event, status, message }) => {
|
| 55 |
// handle 404
|
|
|
|
| 85 |
};
|
| 86 |
|
| 87 |
export const handle: Handle = async ({ event, resolve }) => {
|
| 88 |
+
await ready.then(() => {
|
| 89 |
+
config.checkForUpdates();
|
| 90 |
+
});
|
| 91 |
+
|
| 92 |
logger.debug({
|
| 93 |
locals: event.locals,
|
| 94 |
url: event.url.pathname,
|
|
|
|
| 109 |
}
|
| 110 |
|
| 111 |
if (event.url.pathname.startsWith(`${base}/admin/`) || event.url.pathname === `${base}/admin`) {
|
| 112 |
+
const ADMIN_SECRET = config.ADMIN_API_SECRET || config.PARQUET_EXPORT_SECRET;
|
| 113 |
|
| 114 |
if (!ADMIN_SECRET) {
|
| 115 |
return errorResponse(500, "Admin API is not configured");
|
|
|
|
| 120 |
}
|
| 121 |
}
|
| 122 |
|
| 123 |
+
const token = event.cookies.get(config.COOKIE_NAME);
|
| 124 |
|
| 125 |
// if the trusted email header is set we use it to get the user email
|
| 126 |
+
const email = config.TRUSTED_EMAIL_HEADER
|
| 127 |
+
? event.request.headers.get(config.TRUSTED_EMAIL_HEADER)
|
| 128 |
: null;
|
| 129 |
|
| 130 |
let secretSessionId: string | null = null;
|
|
|
|
| 153 |
if (user) {
|
| 154 |
event.locals.user = user;
|
| 155 |
}
|
| 156 |
+
} else if (
|
| 157 |
+
event.url.pathname.startsWith(`${base}/api/`) &&
|
| 158 |
+
config.USE_HF_TOKEN_IN_API === "true"
|
| 159 |
+
) {
|
| 160 |
// if the request goes to the API and no user is available in the header
|
| 161 |
// check if a bearer token is available in the Authorization header
|
| 162 |
|
|
|
|
| 245 |
|
| 246 |
const validOrigins = [
|
| 247 |
new URL(event.request.url).host,
|
| 248 |
+
...(config.PUBLIC_ORIGIN ? [new URL(config.PUBLIC_ORIGIN).host] : []),
|
| 249 |
];
|
| 250 |
|
| 251 |
if (!validOrigins.includes(new URL(origin).host)) {
|
|
|
|
| 273 |
if (
|
| 274 |
!event.locals.user &&
|
| 275 |
requiresUser &&
|
| 276 |
+
!((config.MESSAGES_BEFORE_LOGIN ? parseInt(config.MESSAGES_BEFORE_LOGIN) : 0) > 0)
|
| 277 |
) {
|
| 278 |
return errorResponse(401, ERROR_MESSAGES.authOnly);
|
| 279 |
}
|
|
|
|
| 284 |
if (
|
| 285 |
!requiresUser &&
|
| 286 |
!event.url.pathname.startsWith(`${base}/settings`) &&
|
| 287 |
+
config.PUBLIC_APP_DISCLAIMER === "1"
|
| 288 |
) {
|
| 289 |
const hasAcceptedEthicsModal = await collections.settings.countDocuments({
|
| 290 |
sessionId: event.locals.sessionId,
|
|
|
|
| 307 |
}
|
| 308 |
replaced = true;
|
| 309 |
|
| 310 |
+
return chunk.html.replace("%gaId%", config.PUBLIC_GOOGLE_ANALYTICS_ID);
|
| 311 |
},
|
| 312 |
});
|
| 313 |
|
| 314 |
// Add CSP header to disallow framing if ALLOW_IFRAME is not "true"
|
| 315 |
+
if (config.ALLOW_IFRAME !== "true") {
|
| 316 |
response.headers.append("Content-Security-Policy", "frame-ancestors 'none';");
|
| 317 |
}
|
| 318 |
|
src/lib/components/AssistantSettings.svelte
CHANGED
|
@@ -12,7 +12,7 @@
|
|
| 12 |
import CarbonTools from "~icons/carbon/tools";
|
| 13 |
|
| 14 |
import { useSettingsStore } from "$lib/stores/settings";
|
| 15 |
-
import {
|
| 16 |
import IconInternet from "./icons/IconInternet.svelte";
|
| 17 |
import TokensCounter from "./TokensCounter.svelte";
|
| 18 |
import HoverTooltip from "./HoverTooltip.svelte";
|
|
@@ -457,7 +457,7 @@
|
|
| 457 |
>Internet access
|
| 458 |
<IconInternet classNames="inline text-sm text-blue-600" />
|
| 459 |
|
| 460 |
-
{#if isHuggingChat}
|
| 461 |
<a
|
| 462 |
href="https://huggingface.co/spaces/huggingchat/chat-ui/discussions/385"
|
| 463 |
target="_blank"
|
|
|
|
| 12 |
import CarbonTools from "~icons/carbon/tools";
|
| 13 |
|
| 14 |
import { useSettingsStore } from "$lib/stores/settings";
|
| 15 |
+
import { publicConfig } from "$lib/utils/PublicConfig.svelte";
|
| 16 |
import IconInternet from "./icons/IconInternet.svelte";
|
| 17 |
import TokensCounter from "./TokensCounter.svelte";
|
| 18 |
import HoverTooltip from "./HoverTooltip.svelte";
|
|
|
|
| 457 |
>Internet access
|
| 458 |
<IconInternet classNames="inline text-sm text-blue-600" />
|
| 459 |
|
| 460 |
+
{#if publicConfig.isHuggingChat}
|
| 461 |
<a
|
| 462 |
href="https://huggingface.co/spaces/huggingchat/chat-ui/discussions/385"
|
| 463 |
target="_blank"
|
src/lib/components/DisclaimerModal.svelte
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
<script lang="ts">
|
| 2 |
import { base } from "$app/paths";
|
| 3 |
import { page } from "$app/state";
|
| 4 |
-
import {
|
|
|
|
| 5 |
import LogoHuggingFaceBorderless from "$lib/components/icons/LogoHuggingFaceBorderless.svelte";
|
| 6 |
import Modal from "$lib/components/Modal.svelte";
|
| 7 |
import { useSettingsStore } from "$lib/stores/settings";
|
|
@@ -17,15 +18,15 @@
|
|
| 17 |
>
|
| 18 |
<h2 class="flex items-center text-2xl font-semibold text-gray-800">
|
| 19 |
<Logo classNames="mr-1" />
|
| 20 |
-
{
|
| 21 |
</h2>
|
| 22 |
|
| 23 |
<p class="text-lg font-semibold leading-snug text-gray-800" style="text-wrap: balance;">
|
| 24 |
-
{
|
| 25 |
</p>
|
| 26 |
|
| 27 |
<p class="text-sm text-gray-500">
|
| 28 |
-
{
|
| 29 |
</p>
|
| 30 |
|
| 31 |
<div class="flex w-full flex-col items-center gap-2">
|
|
@@ -61,7 +62,7 @@
|
|
| 61 |
class="flex w-full flex-wrap items-center justify-center whitespace-nowrap rounded-full border-2 border-black bg-black px-5 py-2 text-lg font-semibold text-gray-100 transition-colors hover:bg-gray-900"
|
| 62 |
>
|
| 63 |
Sign in
|
| 64 |
-
{#if
|
| 65 |
<span class="flex items-center">
|
| 66 |
with <LogoHuggingFaceBorderless classNames="text-xl mr-1 ml-1.5 flex-none" /> Hugging
|
| 67 |
Face
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
import { base } from "$app/paths";
|
| 3 |
import { page } from "$app/state";
|
| 4 |
+
import { publicConfig } from "$lib/utils/PublicConfig.svelte";
|
| 5 |
+
|
| 6 |
import LogoHuggingFaceBorderless from "$lib/components/icons/LogoHuggingFaceBorderless.svelte";
|
| 7 |
import Modal from "$lib/components/Modal.svelte";
|
| 8 |
import { useSettingsStore } from "$lib/stores/settings";
|
|
|
|
| 18 |
>
|
| 19 |
<h2 class="flex items-center text-2xl font-semibold text-gray-800">
|
| 20 |
<Logo classNames="mr-1" />
|
| 21 |
+
{publicConfig.PUBLIC_APP_NAME}
|
| 22 |
</h2>
|
| 23 |
|
| 24 |
<p class="text-lg font-semibold leading-snug text-gray-800" style="text-wrap: balance;">
|
| 25 |
+
{publicConfig.PUBLIC_APP_DESCRIPTION}
|
| 26 |
</p>
|
| 27 |
|
| 28 |
<p class="text-sm text-gray-500">
|
| 29 |
+
{publicConfig.PUBLIC_APP_DISCLAIMER_MESSAGE}
|
| 30 |
</p>
|
| 31 |
|
| 32 |
<div class="flex w-full flex-col items-center gap-2">
|
|
|
|
| 62 |
class="flex w-full flex-wrap items-center justify-center whitespace-nowrap rounded-full border-2 border-black bg-black px-5 py-2 text-lg font-semibold text-gray-100 transition-colors hover:bg-gray-900"
|
| 63 |
>
|
| 64 |
Sign in
|
| 65 |
+
{#if publicConfig.PUBLIC_APP_NAME === "HuggingChat"}
|
| 66 |
<span class="flex items-center">
|
| 67 |
with <LogoHuggingFaceBorderless classNames="text-xl mr-1 ml-1.5 flex-none" /> Hugging
|
| 68 |
Face
|
src/lib/components/LoginModal.svelte
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
<script lang="ts">
|
| 2 |
import { base } from "$app/paths";
|
| 3 |
import { page } from "$app/state";
|
| 4 |
-
import {
|
|
|
|
| 5 |
import LogoHuggingFaceBorderless from "$lib/components/icons/LogoHuggingFaceBorderless.svelte";
|
| 6 |
import Modal from "$lib/components/Modal.svelte";
|
| 7 |
import { useSettingsStore } from "$lib/stores/settings";
|
|
@@ -17,13 +18,13 @@
|
|
| 17 |
>
|
| 18 |
<h2 class="flex items-center text-2xl font-semibold text-gray-800">
|
| 19 |
<Logo classNames="mr-1" />
|
| 20 |
-
{
|
| 21 |
</h2>
|
| 22 |
<p class="text-balance text-lg font-semibold leading-snug text-gray-800">
|
| 23 |
-
{
|
| 24 |
</p>
|
| 25 |
<p class="text-balance rounded-xl border bg-white/80 p-2 text-base text-gray-800">
|
| 26 |
-
{
|
| 27 |
</p>
|
| 28 |
|
| 29 |
<form
|
|
@@ -38,7 +39,7 @@
|
|
| 38 |
class="flex w-full flex-wrap items-center justify-center whitespace-nowrap rounded-full bg-black px-5 py-2 text-center text-lg font-semibold text-gray-100 transition-colors hover:bg-gray-900"
|
| 39 |
>
|
| 40 |
Sign in
|
| 41 |
-
{#if
|
| 42 |
<span class="flex items-center">
|
| 43 |
with <LogoHuggingFaceBorderless classNames="text-xl mr-1 ml-1.5" /> Hugging Face
|
| 44 |
</span>
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
import { base } from "$app/paths";
|
| 3 |
import { page } from "$app/state";
|
| 4 |
+
import { publicConfig } from "$lib/utils/PublicConfig.svelte";
|
| 5 |
+
|
| 6 |
import LogoHuggingFaceBorderless from "$lib/components/icons/LogoHuggingFaceBorderless.svelte";
|
| 7 |
import Modal from "$lib/components/Modal.svelte";
|
| 8 |
import { useSettingsStore } from "$lib/stores/settings";
|
|
|
|
| 18 |
>
|
| 19 |
<h2 class="flex items-center text-2xl font-semibold text-gray-800">
|
| 20 |
<Logo classNames="mr-1" />
|
| 21 |
+
{publicConfig.PUBLIC_APP_NAME}
|
| 22 |
</h2>
|
| 23 |
<p class="text-balance text-lg font-semibold leading-snug text-gray-800">
|
| 24 |
+
{publicConfig.PUBLIC_APP_DESCRIPTION}
|
| 25 |
</p>
|
| 26 |
<p class="text-balance rounded-xl border bg-white/80 p-2 text-base text-gray-800">
|
| 27 |
+
{publicConfig.PUBLIC_APP_GUEST_MESSAGE}
|
| 28 |
</p>
|
| 29 |
|
| 30 |
<form
|
|
|
|
| 39 |
class="flex w-full flex-wrap items-center justify-center whitespace-nowrap rounded-full bg-black px-5 py-2 text-center text-lg font-semibold text-gray-100 transition-colors hover:bg-gray-900"
|
| 40 |
>
|
| 41 |
Sign in
|
| 42 |
+
{#if publicConfig.PUBLIC_APP_NAME === "HuggingChat"}
|
| 43 |
<span class="flex items-center">
|
| 44 |
with <LogoHuggingFaceBorderless classNames="text-xl mr-1 ml-1.5" /> Hugging Face
|
| 45 |
</span>
|
src/lib/components/NavMenu.svelte
CHANGED
|
@@ -4,7 +4,8 @@
|
|
| 4 |
import Logo from "$lib/components/icons/Logo.svelte";
|
| 5 |
import { switchTheme } from "$lib/switchTheme";
|
| 6 |
import { isAborted } from "$lib/stores/isAborted";
|
| 7 |
-
import {
|
|
|
|
| 8 |
import NavConversationItem from "./NavConversationItem.svelte";
|
| 9 |
import type { LayoutData } from "../../routes/$types";
|
| 10 |
import type { ConvSidebar } from "$lib/types/ConvSidebar";
|
|
@@ -90,10 +91,10 @@
|
|
| 90 |
>
|
| 91 |
<a
|
| 92 |
class="flex items-center rounded-xl text-lg font-semibold"
|
| 93 |
-
href="{
|
| 94 |
>
|
| 95 |
<Logo classNames="mr-1" />
|
| 96 |
-
{
|
| 97 |
</a>
|
| 98 |
{#if $page.url.pathname !== base + "/"}
|
| 99 |
<a
|
|
@@ -216,7 +217,7 @@
|
|
| 216 |
>
|
| 217 |
Settings
|
| 218 |
</a>
|
| 219 |
-
{#if
|
| 220 |
<a
|
| 221 |
href="{base}/privacy"
|
| 222 |
class="flex h-9 flex-none items-center gap-1.5 rounded-lg pl-2.5 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
|
|
|
|
| 4 |
import Logo from "$lib/components/icons/Logo.svelte";
|
| 5 |
import { switchTheme } from "$lib/switchTheme";
|
| 6 |
import { isAborted } from "$lib/stores/isAborted";
|
| 7 |
+
import { publicConfig } from "$lib/utils/PublicConfig.svelte";
|
| 8 |
+
|
| 9 |
import NavConversationItem from "./NavConversationItem.svelte";
|
| 10 |
import type { LayoutData } from "../../routes/$types";
|
| 11 |
import type { ConvSidebar } from "$lib/types/ConvSidebar";
|
|
|
|
| 91 |
>
|
| 92 |
<a
|
| 93 |
class="flex items-center rounded-xl text-lg font-semibold"
|
| 94 |
+
href="{publicConfig.PUBLIC_ORIGIN}{base}/"
|
| 95 |
>
|
| 96 |
<Logo classNames="mr-1" />
|
| 97 |
+
{publicConfig.PUBLIC_APP_NAME}
|
| 98 |
</a>
|
| 99 |
{#if $page.url.pathname !== base + "/"}
|
| 100 |
<a
|
|
|
|
| 217 |
>
|
| 218 |
Settings
|
| 219 |
</a>
|
| 220 |
+
{#if publicConfig.PUBLIC_APP_NAME === "HuggingChat"}
|
| 221 |
<a
|
| 222 |
href="{base}/privacy"
|
| 223 |
class="flex h-9 flex-none items-center gap-1.5 rounded-lg pl-2.5 pr-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
|
src/lib/components/ToolsMenu.svelte
CHANGED
|
@@ -4,7 +4,7 @@
|
|
| 4 |
import { clickOutside } from "$lib/actions/clickOutside";
|
| 5 |
import { useSettingsStore } from "$lib/stores/settings";
|
| 6 |
import type { ToolFront } from "$lib/types/Tool";
|
| 7 |
-
import {
|
| 8 |
import IconTool from "./icons/IconTool.svelte";
|
| 9 |
import CarbonInformation from "~icons/carbon/information";
|
| 10 |
import CarbonGlobe from "~icons/carbon/earth-filled";
|
|
@@ -71,7 +71,7 @@
|
|
| 71 |
<div class="grid grid-cols-2 gap-x-6 gap-y-1 p-3">
|
| 72 |
<div class="col-span-2 flex items-center gap-1.5 text-sm text-gray-500">
|
| 73 |
Available tools
|
| 74 |
-
{#if isHuggingChat}
|
| 75 |
<a
|
| 76 |
href="https://huggingface.co/spaces/huggingchat/chat-ui/discussions/470"
|
| 77 |
target="_blank"
|
|
|
|
| 4 |
import { clickOutside } from "$lib/actions/clickOutside";
|
| 5 |
import { useSettingsStore } from "$lib/stores/settings";
|
| 6 |
import type { ToolFront } from "$lib/types/Tool";
|
| 7 |
+
import { publicConfig } from "$lib/utils/PublicConfig.svelte";
|
| 8 |
import IconTool from "./icons/IconTool.svelte";
|
| 9 |
import CarbonInformation from "~icons/carbon/information";
|
| 10 |
import CarbonGlobe from "~icons/carbon/earth-filled";
|
|
|
|
| 71 |
<div class="grid grid-cols-2 gap-x-6 gap-y-1 p-3">
|
| 72 |
<div class="col-span-2 flex items-center gap-1.5 text-sm text-gray-500">
|
| 73 |
Available tools
|
| 74 |
+
{#if publicConfig.isHuggingChat}
|
| 75 |
<a
|
| 76 |
href="https://huggingface.co/spaces/huggingchat/chat-ui/discussions/470"
|
| 77 |
target="_blank"
|
src/lib/components/chat/AssistantIntroduction.svelte
CHANGED
|
@@ -15,7 +15,8 @@
|
|
| 15 |
import CarbonTools from "~icons/carbon/tools";
|
| 16 |
|
| 17 |
import { share } from "$lib/utils/share";
|
| 18 |
-
import {
|
|
|
|
| 19 |
import { page } from "$app/state";
|
| 20 |
|
| 21 |
interface Props {
|
|
@@ -48,7 +49,7 @@
|
|
| 48 |
);
|
| 49 |
|
| 50 |
const prefix =
|
| 51 |
-
|
| 52 |
|
| 53 |
let shareUrl = $derived(`${prefix}/assistant/${assistant?._id}`);
|
| 54 |
|
|
|
|
| 15 |
import CarbonTools from "~icons/carbon/tools";
|
| 16 |
|
| 17 |
import { share } from "$lib/utils/share";
|
| 18 |
+
import { publicConfig } from "$lib/utils/PublicConfig.svelte";
|
| 19 |
+
|
| 20 |
import { page } from "$app/state";
|
| 21 |
|
| 22 |
interface Props {
|
|
|
|
| 49 |
);
|
| 50 |
|
| 51 |
const prefix =
|
| 52 |
+
publicConfig.PUBLIC_SHARE_PREFIX || `${publicConfig.PUBLIC_ORIGIN || page.url.origin}${base}`;
|
| 53 |
|
| 54 |
let shareUrl = $derived(`${prefix}/assistant/${assistant?._id}`);
|
| 55 |
|
src/lib/components/chat/ChatIntroduction.svelte
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
<script lang="ts">
|
| 2 |
-
import {
|
|
|
|
| 3 |
import Logo from "$lib/components/icons/Logo.svelte";
|
| 4 |
import { createEventDispatcher } from "svelte";
|
| 5 |
import IconGear from "~icons/bi/gear-fill";
|
|
@@ -15,10 +16,6 @@
|
|
| 15 |
|
| 16 |
let { currentModel }: Props = $props();
|
| 17 |
|
| 18 |
-
const announcementBanners = envPublic.PUBLIC_ANNOUNCEMENT_BANNERS
|
| 19 |
-
? JSON5.parse(envPublic.PUBLIC_ANNOUNCEMENT_BANNERS)
|
| 20 |
-
: [];
|
| 21 |
-
|
| 22 |
const dispatch = createEventDispatcher<{ message: string }>();
|
| 23 |
</script>
|
| 24 |
|
|
@@ -27,21 +24,21 @@
|
|
| 27 |
<div>
|
| 28 |
<div class="mb-3 flex items-center text-2xl font-semibold">
|
| 29 |
<Logo classNames="mr-1 flex-none" />
|
| 30 |
-
{
|
| 31 |
<div
|
| 32 |
class="ml-3 flex h-6 items-center rounded-lg border border-gray-100 bg-gray-50 px-2 text-base text-gray-400 dark:border-gray-700/60 dark:bg-gray-800"
|
| 33 |
>
|
| 34 |
-
v{
|
| 35 |
</div>
|
| 36 |
</div>
|
| 37 |
<p class="text-base text-gray-600 dark:text-gray-400">
|
| 38 |
-
{
|
| 39 |
"Making the community's best AI chat models available to everyone."}
|
| 40 |
</p>
|
| 41 |
</div>
|
| 42 |
</div>
|
| 43 |
<div class="lg:col-span-2 lg:pl-24">
|
| 44 |
-
{#each
|
| 45 |
<AnnouncementBanner classNames="mb-4" title={banner.title}>
|
| 46 |
<a
|
| 47 |
target={banner.external ? "_blank" : "_self"}
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
+
import { publicConfig } from "$lib/utils/PublicConfig.svelte";
|
| 3 |
+
|
| 4 |
import Logo from "$lib/components/icons/Logo.svelte";
|
| 5 |
import { createEventDispatcher } from "svelte";
|
| 6 |
import IconGear from "~icons/bi/gear-fill";
|
|
|
|
| 16 |
|
| 17 |
let { currentModel }: Props = $props();
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
const dispatch = createEventDispatcher<{ message: string }>();
|
| 20 |
</script>
|
| 21 |
|
|
|
|
| 24 |
<div>
|
| 25 |
<div class="mb-3 flex items-center text-2xl font-semibold">
|
| 26 |
<Logo classNames="mr-1 flex-none" />
|
| 27 |
+
{publicConfig.PUBLIC_APP_NAME}
|
| 28 |
<div
|
| 29 |
class="ml-3 flex h-6 items-center rounded-lg border border-gray-100 bg-gray-50 px-2 text-base text-gray-400 dark:border-gray-700/60 dark:bg-gray-800"
|
| 30 |
>
|
| 31 |
+
v{publicConfig.PUBLIC_VERSION}
|
| 32 |
</div>
|
| 33 |
</div>
|
| 34 |
<p class="text-base text-gray-600 dark:text-gray-400">
|
| 35 |
+
{publicConfig.PUBLIC_APP_DESCRIPTION ||
|
| 36 |
"Making the community's best AI chat models available to everyone."}
|
| 37 |
</p>
|
| 38 |
</div>
|
| 39 |
</div>
|
| 40 |
<div class="lg:col-span-2 lg:pl-24">
|
| 41 |
+
{#each JSON5.parse(publicConfig.PUBLIC_ANNOUNCEMENT_BANNERS || "[]") as banner}
|
| 42 |
<AnnouncementBanner classNames="mb-4" title={banner.title}>
|
| 43 |
<a
|
| 44 |
target={banner.external ? "_blank" : "_self"}
|
src/lib/components/icons/Logo.svelte
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
<script lang="ts">
|
| 2 |
import { page } from "$app/state";
|
| 3 |
-
import {
|
|
|
|
| 4 |
import { base } from "$app/paths";
|
| 5 |
|
| 6 |
interface Props {
|
|
@@ -13,13 +14,14 @@
|
|
| 13 |
<svelte:head>
|
| 14 |
<link
|
| 15 |
rel="preload"
|
| 16 |
-
href="{
|
|
|
|
| 17 |
as="image"
|
| 18 |
type="image/svg+xml"
|
| 19 |
/>
|
| 20 |
</svelte:head>
|
| 21 |
|
| 22 |
-
{#if
|
| 23 |
<svg
|
| 24 |
height="30"
|
| 25 |
width="30"
|
|
@@ -35,7 +37,8 @@
|
|
| 35 |
{:else}
|
| 36 |
<img
|
| 37 |
class={classNames}
|
| 38 |
-
alt="{
|
| 39 |
-
src="{
|
|
|
|
| 40 |
/>
|
| 41 |
{/if}
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
import { page } from "$app/state";
|
| 3 |
+
import { publicConfig } from "$lib/utils/PublicConfig.svelte";
|
| 4 |
+
|
| 5 |
import { base } from "$app/paths";
|
| 6 |
|
| 7 |
interface Props {
|
|
|
|
| 14 |
<svelte:head>
|
| 15 |
<link
|
| 16 |
rel="preload"
|
| 17 |
+
href="{publicConfig.PUBLIC_ORIGIN ||
|
| 18 |
+
page.url.origin}{base}/{publicConfig.PUBLIC_APP_ASSETS}/logo.svg"
|
| 19 |
as="image"
|
| 20 |
type="image/svg+xml"
|
| 21 |
/>
|
| 22 |
</svelte:head>
|
| 23 |
|
| 24 |
+
{#if publicConfig.PUBLIC_APP_ASSETS === "chatui"}
|
| 25 |
<svg
|
| 26 |
height="30"
|
| 27 |
width="30"
|
|
|
|
| 37 |
{:else}
|
| 38 |
<img
|
| 39 |
class={classNames}
|
| 40 |
+
alt="{publicConfig.PUBLIC_APP_NAME} logo"
|
| 41 |
+
src="{publicConfig.PUBLIC_ORIGIN ||
|
| 42 |
+
page.url.origin}{base}/{publicConfig.PUBLIC_APP_ASSETS}/logo.svg"
|
| 43 |
/>
|
| 44 |
{/if}
|
src/lib/jobs/refresh-assistants-counts.ts
CHANGED
|
@@ -4,8 +4,7 @@ import type { ObjectId } from "mongodb";
|
|
| 4 |
import { subDays } from "date-fns";
|
| 5 |
import { logger } from "$lib/server/logger";
|
| 6 |
import { collections } from "$lib/server/database";
|
| 7 |
-
|
| 8 |
-
|
| 9 |
let hasLock = false;
|
| 10 |
let lockId: ObjectId | null = null;
|
| 11 |
|
|
@@ -76,13 +75,13 @@ async function refreshAssistantsCountsHelper() {
|
|
| 76 |
|
| 77 |
async function maintainLock() {
|
| 78 |
if (hasLock && lockId) {
|
| 79 |
-
hasLock = await refreshLock(
|
| 80 |
|
| 81 |
if (!hasLock) {
|
| 82 |
lockId = null;
|
| 83 |
}
|
| 84 |
} else if (!hasLock) {
|
| 85 |
-
lockId = (await acquireLock(
|
| 86 |
hasLock = !!lockId;
|
| 87 |
}
|
| 88 |
|
|
|
|
| 4 |
import { subDays } from "date-fns";
|
| 5 |
import { logger } from "$lib/server/logger";
|
| 6 |
import { collections } from "$lib/server/database";
|
| 7 |
+
import { Semaphores } from "$lib/types/Semaphore";
|
|
|
|
| 8 |
let hasLock = false;
|
| 9 |
let lockId: ObjectId | null = null;
|
| 10 |
|
|
|
|
| 75 |
|
| 76 |
async function maintainLock() {
|
| 77 |
if (hasLock && lockId) {
|
| 78 |
+
hasLock = await refreshLock(Semaphores.ASSISTANTS_COUNT, lockId);
|
| 79 |
|
| 80 |
if (!hasLock) {
|
| 81 |
lockId = null;
|
| 82 |
}
|
| 83 |
} else if (!hasLock) {
|
| 84 |
+
lockId = (await acquireLock(Semaphores.ASSISTANTS_COUNT)) || null;
|
| 85 |
hasLock = !!lockId;
|
| 86 |
}
|
| 87 |
|
src/lib/jobs/refresh-conversation-stats.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { CONVERSATION_STATS_COLLECTION, collections } from "$lib/server/database
|
|
| 3 |
import { logger } from "$lib/server/logger";
|
| 4 |
import type { ObjectId } from "mongodb";
|
| 5 |
import { acquireLock, refreshLock } from "$lib/migrations/lock";
|
|
|
|
| 6 |
|
| 7 |
async function getLastComputationTime(): Promise<Date> {
|
| 8 |
const lastStats = await collections.conversationStats.findOne({}, { sort: { "date.at": -1 } });
|
|
@@ -234,20 +235,18 @@ async function computeStats(params: {
|
|
| 234 |
);
|
| 235 |
}
|
| 236 |
|
| 237 |
-
const LOCK_KEY = "conversation.stats";
|
| 238 |
-
|
| 239 |
let hasLock = false;
|
| 240 |
let lockId: ObjectId | null = null;
|
| 241 |
|
| 242 |
async function maintainLock() {
|
| 243 |
if (hasLock && lockId) {
|
| 244 |
-
hasLock = await refreshLock(
|
| 245 |
|
| 246 |
if (!hasLock) {
|
| 247 |
lockId = null;
|
| 248 |
}
|
| 249 |
} else if (!hasLock) {
|
| 250 |
-
lockId = (await acquireLock(
|
| 251 |
hasLock = !!lockId;
|
| 252 |
}
|
| 253 |
|
|
|
|
| 3 |
import { logger } from "$lib/server/logger";
|
| 4 |
import type { ObjectId } from "mongodb";
|
| 5 |
import { acquireLock, refreshLock } from "$lib/migrations/lock";
|
| 6 |
+
import { Semaphores } from "$lib/types/Semaphore";
|
| 7 |
|
| 8 |
async function getLastComputationTime(): Promise<Date> {
|
| 9 |
const lastStats = await collections.conversationStats.findOne({}, { sort: { "date.at": -1 } });
|
|
|
|
| 235 |
);
|
| 236 |
}
|
| 237 |
|
|
|
|
|
|
|
| 238 |
let hasLock = false;
|
| 239 |
let lockId: ObjectId | null = null;
|
| 240 |
|
| 241 |
async function maintainLock() {
|
| 242 |
if (hasLock && lockId) {
|
| 243 |
+
hasLock = await refreshLock(Semaphores.CONVERSATION_STATS, lockId);
|
| 244 |
|
| 245 |
if (!hasLock) {
|
| 246 |
lockId = null;
|
| 247 |
}
|
| 248 |
} else if (!hasLock) {
|
| 249 |
+
lockId = (await acquireLock(Semaphores.CONVERSATION_STATS)) || null;
|
| 250 |
hasLock = !!lockId;
|
| 251 |
}
|
| 252 |
|
src/lib/migrations/lock.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
| 1 |
import { collections } from "$lib/server/database";
|
| 2 |
import { ObjectId } from "mongodb";
|
|
|
|
| 3 |
|
| 4 |
/**
|
| 5 |
* Returns the lock id if the lock was acquired, false otherwise
|
| 6 |
*/
|
| 7 |
-
export async function acquireLock(key:
|
| 8 |
try {
|
| 9 |
const id = new ObjectId();
|
| 10 |
|
|
@@ -13,6 +14,7 @@ export async function acquireLock(key: string): Promise<ObjectId | false> {
|
|
| 13 |
key,
|
| 14 |
createdAt: new Date(),
|
| 15 |
updatedAt: new Date(),
|
|
|
|
| 16 |
});
|
| 17 |
|
| 18 |
return insert.acknowledged ? id : false; // true if the document was inserted
|
|
@@ -22,21 +24,21 @@ export async function acquireLock(key: string): Promise<ObjectId | false> {
|
|
| 22 |
}
|
| 23 |
}
|
| 24 |
|
| 25 |
-
export async function releaseLock(key:
|
| 26 |
await collections.semaphores.deleteOne({
|
| 27 |
_id: lockId,
|
| 28 |
key,
|
| 29 |
});
|
| 30 |
}
|
| 31 |
|
| 32 |
-
export async function isDBLocked(key:
|
| 33 |
const res = await collections.semaphores.countDocuments({
|
| 34 |
key,
|
| 35 |
});
|
| 36 |
return res > 0;
|
| 37 |
}
|
| 38 |
|
| 39 |
-
export async function refreshLock(key:
|
| 40 |
const result = await collections.semaphores.updateOne(
|
| 41 |
{
|
| 42 |
_id: lockId,
|
|
@@ -45,6 +47,7 @@ export async function refreshLock(key: string, lockId: ObjectId): Promise<boolea
|
|
| 45 |
{
|
| 46 |
$set: {
|
| 47 |
updatedAt: new Date(),
|
|
|
|
| 48 |
},
|
| 49 |
}
|
| 50 |
);
|
|
|
|
| 1 |
import { collections } from "$lib/server/database";
|
| 2 |
import { ObjectId } from "mongodb";
|
| 3 |
+
import type { Semaphores } from "$lib/types/Semaphore";
|
| 4 |
|
| 5 |
/**
|
| 6 |
* Returns the lock id if the lock was acquired, false otherwise
|
| 7 |
*/
|
| 8 |
+
export async function acquireLock(key: Semaphores): Promise<ObjectId | false> {
|
| 9 |
try {
|
| 10 |
const id = new ObjectId();
|
| 11 |
|
|
|
|
| 14 |
key,
|
| 15 |
createdAt: new Date(),
|
| 16 |
updatedAt: new Date(),
|
| 17 |
+
deleteAt: new Date(Date.now() + 1000 * 60 * 3), // 3 minutes
|
| 18 |
});
|
| 19 |
|
| 20 |
return insert.acknowledged ? id : false; // true if the document was inserted
|
|
|
|
| 24 |
}
|
| 25 |
}
|
| 26 |
|
| 27 |
+
export async function releaseLock(key: Semaphores, lockId: ObjectId) {
|
| 28 |
await collections.semaphores.deleteOne({
|
| 29 |
_id: lockId,
|
| 30 |
key,
|
| 31 |
});
|
| 32 |
}
|
| 33 |
|
| 34 |
+
export async function isDBLocked(key: Semaphores): Promise<boolean> {
|
| 35 |
const res = await collections.semaphores.countDocuments({
|
| 36 |
key,
|
| 37 |
});
|
| 38 |
return res > 0;
|
| 39 |
}
|
| 40 |
|
| 41 |
+
export async function refreshLock(key: Semaphores, lockId: ObjectId): Promise<boolean> {
|
| 42 |
const result = await collections.semaphores.updateOne(
|
| 43 |
{
|
| 44 |
_id: lockId,
|
|
|
|
| 47 |
{
|
| 48 |
$set: {
|
| 49 |
updatedAt: new Date(),
|
| 50 |
+
deleteAt: new Date(Date.now() + 1000 * 60 * 3), // 3 minutes
|
| 51 |
},
|
| 52 |
}
|
| 53 |
);
|
src/lib/migrations/migrations.spec.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
| 1 |
import { afterEach, assert, describe, expect, it } from "vitest";
|
| 2 |
import { migrations } from "./routines";
|
| 3 |
import { acquireLock, isDBLocked, refreshLock, releaseLock } from "./lock";
|
|
|
|
| 4 |
import { collections } from "$lib/server/database";
|
| 5 |
|
| 6 |
-
const LOCK_KEY = "migrations.test";
|
| 7 |
-
|
| 8 |
describe(
|
| 9 |
"migrations",
|
| 10 |
{
|
|
@@ -18,7 +17,9 @@ describe(
|
|
| 18 |
});
|
| 19 |
|
| 20 |
it("should acquire only one lock on DB", async () => {
|
| 21 |
-
const results = await Promise.all(
|
|
|
|
|
|
|
| 22 |
const locks = results.filter((r) => r);
|
| 23 |
|
| 24 |
const semaphores = await collections.semaphores.find({}).toArray();
|
|
@@ -26,20 +27,20 @@ describe(
|
|
| 26 |
expect(locks.length).toBe(1);
|
| 27 |
expect(semaphores).toBeDefined();
|
| 28 |
expect(semaphores.length).toBe(1);
|
| 29 |
-
expect(semaphores?.[0].key).toBe(
|
| 30 |
});
|
| 31 |
|
| 32 |
it("should read the lock correctly", async () => {
|
| 33 |
-
const lockId = await acquireLock(
|
| 34 |
assert(lockId);
|
| 35 |
-
expect(await isDBLocked(
|
| 36 |
-
expect(!!(await acquireLock(
|
| 37 |
-
await releaseLock(
|
| 38 |
-
expect(await isDBLocked(
|
| 39 |
});
|
| 40 |
|
| 41 |
it("should refresh the lock", async () => {
|
| 42 |
-
const lockId = await acquireLock(
|
| 43 |
|
| 44 |
assert(lockId);
|
| 45 |
|
|
@@ -47,7 +48,7 @@ describe(
|
|
| 47 |
|
| 48 |
const updatedAtInitially = (await collections.semaphores.findOne({}))?.updatedAt;
|
| 49 |
|
| 50 |
-
await refreshLock(
|
| 51 |
|
| 52 |
const updatedAtAfterRefresh = (await collections.semaphores.findOne({}))?.updatedAt;
|
| 53 |
|
|
|
|
| 1 |
import { afterEach, assert, describe, expect, it } from "vitest";
|
| 2 |
import { migrations } from "./routines";
|
| 3 |
import { acquireLock, isDBLocked, refreshLock, releaseLock } from "./lock";
|
| 4 |
+
import { Semaphores } from "$lib/types/Semaphore";
|
| 5 |
import { collections } from "$lib/server/database";
|
| 6 |
|
|
|
|
|
|
|
| 7 |
describe(
|
| 8 |
"migrations",
|
| 9 |
{
|
|
|
|
| 17 |
});
|
| 18 |
|
| 19 |
it("should acquire only one lock on DB", async () => {
|
| 20 |
+
const results = await Promise.all(
|
| 21 |
+
new Array(1000).fill(0).map(() => acquireLock(Semaphores.TEST_MIGRATION))
|
| 22 |
+
);
|
| 23 |
const locks = results.filter((r) => r);
|
| 24 |
|
| 25 |
const semaphores = await collections.semaphores.find({}).toArray();
|
|
|
|
| 27 |
expect(locks.length).toBe(1);
|
| 28 |
expect(semaphores).toBeDefined();
|
| 29 |
expect(semaphores.length).toBe(1);
|
| 30 |
+
expect(semaphores?.[0].key).toBe(Semaphores.TEST_MIGRATION);
|
| 31 |
});
|
| 32 |
|
| 33 |
it("should read the lock correctly", async () => {
|
| 34 |
+
const lockId = await acquireLock(Semaphores.TEST_MIGRATION);
|
| 35 |
assert(lockId);
|
| 36 |
+
expect(await isDBLocked(Semaphores.TEST_MIGRATION)).toBe(true);
|
| 37 |
+
expect(!!(await acquireLock(Semaphores.TEST_MIGRATION))).toBe(false);
|
| 38 |
+
await releaseLock(Semaphores.TEST_MIGRATION, lockId);
|
| 39 |
+
expect(await isDBLocked(Semaphores.TEST_MIGRATION)).toBe(false);
|
| 40 |
});
|
| 41 |
|
| 42 |
it("should refresh the lock", async () => {
|
| 43 |
+
const lockId = await acquireLock(Semaphores.TEST_MIGRATION);
|
| 44 |
|
| 45 |
assert(lockId);
|
| 46 |
|
|
|
|
| 48 |
|
| 49 |
const updatedAtInitially = (await collections.semaphores.findOne({}))?.updatedAt;
|
| 50 |
|
| 51 |
+
await refreshLock(Semaphores.TEST_MIGRATION, lockId);
|
| 52 |
|
| 53 |
const updatedAtAfterRefresh = (await collections.semaphores.findOne({}))?.updatedAt;
|
| 54 |
|
src/lib/migrations/migrations.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
| 1 |
import { Database } from "$lib/server/database";
|
| 2 |
import { migrations } from "./routines";
|
| 3 |
import { acquireLock, releaseLock, isDBLocked, refreshLock } from "./lock";
|
| 4 |
-
import {
|
| 5 |
import { logger } from "$lib/server/logger";
|
| 6 |
-
|
| 7 |
-
const LOCK_KEY = "migrations";
|
| 8 |
|
| 9 |
export async function checkAndRunMigrations() {
|
| 10 |
// make sure all GUIDs are unique
|
|
@@ -23,7 +22,7 @@ export async function checkAndRunMigrations() {
|
|
| 23 |
// connect to the database
|
| 24 |
const connectedClient = await (await Database.getInstance()).getClient().connect();
|
| 25 |
|
| 26 |
-
const lockId = await acquireLock(
|
| 27 |
|
| 28 |
if (!lockId) {
|
| 29 |
// another instance already has the lock, so we exit early
|
|
@@ -33,7 +32,7 @@ export async function checkAndRunMigrations() {
|
|
| 33 |
|
| 34 |
// Todo: is this necessary? Can we just return?
|
| 35 |
// block until the lock is released
|
| 36 |
-
while (await isDBLocked(
|
| 37 |
await new Promise((resolve) => setTimeout(resolve, 1000));
|
| 38 |
}
|
| 39 |
return;
|
|
@@ -42,7 +41,7 @@ export async function checkAndRunMigrations() {
|
|
| 42 |
// once here, we have the lock
|
| 43 |
// make sure to refresh it regularly while it's running
|
| 44 |
const refreshInterval = setInterval(async () => {
|
| 45 |
-
await refreshLock(
|
| 46 |
}, 1000 * 10);
|
| 47 |
|
| 48 |
// iterate over all migrations
|
|
@@ -58,8 +57,8 @@ export async function checkAndRunMigrations() {
|
|
| 58 |
} else {
|
| 59 |
// check the modifiers to see if some cases match
|
| 60 |
if (
|
| 61 |
-
(migration.runForHuggingChat === "only" && !isHuggingChat) ||
|
| 62 |
-
(migration.runForHuggingChat === "never" && isHuggingChat)
|
| 63 |
) {
|
| 64 |
logger.debug(
|
| 65 |
`[MIGRATIONS] "${migration.name}" should not be applied for this run. Skipping...`
|
|
@@ -115,5 +114,5 @@ export async function checkAndRunMigrations() {
|
|
| 115 |
logger.debug("[MIGRATIONS] All migrations applied. Releasing lock");
|
| 116 |
|
| 117 |
clearInterval(refreshInterval);
|
| 118 |
-
await releaseLock(
|
| 119 |
}
|
|
|
|
| 1 |
import { Database } from "$lib/server/database";
|
| 2 |
import { migrations } from "./routines";
|
| 3 |
import { acquireLock, releaseLock, isDBLocked, refreshLock } from "./lock";
|
| 4 |
+
import { Semaphores } from "$lib/types/Semaphore";
|
| 5 |
import { logger } from "$lib/server/logger";
|
| 6 |
+
import { config } from "$lib/server/config";
|
|
|
|
| 7 |
|
| 8 |
export async function checkAndRunMigrations() {
|
| 9 |
// make sure all GUIDs are unique
|
|
|
|
| 22 |
// connect to the database
|
| 23 |
const connectedClient = await (await Database.getInstance()).getClient().connect();
|
| 24 |
|
| 25 |
+
const lockId = await acquireLock(Semaphores.MIGRATION);
|
| 26 |
|
| 27 |
if (!lockId) {
|
| 28 |
// another instance already has the lock, so we exit early
|
|
|
|
| 32 |
|
| 33 |
// Todo: is this necessary? Can we just return?
|
| 34 |
// block until the lock is released
|
| 35 |
+
while (await isDBLocked(Semaphores.MIGRATION)) {
|
| 36 |
await new Promise((resolve) => setTimeout(resolve, 1000));
|
| 37 |
}
|
| 38 |
return;
|
|
|
|
| 41 |
// once here, we have the lock
|
| 42 |
// make sure to refresh it regularly while it's running
|
| 43 |
const refreshInterval = setInterval(async () => {
|
| 44 |
+
await refreshLock(Semaphores.MIGRATION, lockId);
|
| 45 |
}, 1000 * 10);
|
| 46 |
|
| 47 |
// iterate over all migrations
|
|
|
|
| 57 |
} else {
|
| 58 |
// check the modifiers to see if some cases match
|
| 59 |
if (
|
| 60 |
+
(migration.runForHuggingChat === "only" && !config.isHuggingChat) ||
|
| 61 |
+
(migration.runForHuggingChat === "never" && config.isHuggingChat)
|
| 62 |
) {
|
| 63 |
logger.debug(
|
| 64 |
`[MIGRATIONS] "${migration.name}" should not be applied for this run. Skipping...`
|
|
|
|
| 114 |
logger.debug("[MIGRATIONS] All migrations applied. Releasing lock");
|
| 115 |
|
| 116 |
clearInterval(refreshInterval);
|
| 117 |
+
await releaseLock(Semaphores.MIGRATION, lockId);
|
| 118 |
}
|
src/lib/server/adminToken.ts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
| 1 |
-
import {
|
| 2 |
-
import { env as envPublic } from "$env/dynamic/public";
|
| 3 |
import type { Session } from "$lib/types/Session";
|
| 4 |
import { logger } from "./logger";
|
| 5 |
import { v4 } from "uuid";
|
| 6 |
|
| 7 |
class AdminTokenManager {
|
| 8 |
-
private token =
|
| 9 |
// contains all session ids that are currently admin sessions
|
| 10 |
private adminSessions: Array<Session["sessionId"]> = [];
|
| 11 |
|
| 12 |
public get enabled() {
|
| 13 |
// if open id is configured, disable the feature
|
| 14 |
-
return
|
| 15 |
}
|
| 16 |
public isAdmin(sessionId: Session["sessionId"]) {
|
| 17 |
if (!this.enabled) return false;
|
|
@@ -23,7 +22,7 @@ class AdminTokenManager {
|
|
| 23 |
if (token === this.token) {
|
| 24 |
logger.info(`[ADMIN] Token validated`);
|
| 25 |
this.adminSessions.push(sessionId);
|
| 26 |
-
this.token =
|
| 27 |
return true;
|
| 28 |
}
|
| 29 |
|
|
@@ -36,7 +35,7 @@ class AdminTokenManager {
|
|
| 36 |
|
| 37 |
public displayToken() {
|
| 38 |
// if admin token is set, don't display it
|
| 39 |
-
if (!this.enabled ||
|
| 40 |
|
| 41 |
let port = process.argv.includes("--port")
|
| 42 |
? parseInt(process.argv[process.argv.indexOf("--port") + 1])
|
|
@@ -53,7 +52,7 @@ class AdminTokenManager {
|
|
| 53 |
}
|
| 54 |
}
|
| 55 |
|
| 56 |
-
const url = (
|
| 57 |
logger.info(`[ADMIN] You can login with ${url + this.token}`);
|
| 58 |
}
|
| 59 |
}
|
|
|
|
| 1 |
+
import { config } from "$lib/server/config";
|
|
|
|
| 2 |
import type { Session } from "$lib/types/Session";
|
| 3 |
import { logger } from "./logger";
|
| 4 |
import { v4 } from "uuid";
|
| 5 |
|
| 6 |
class AdminTokenManager {
|
| 7 |
+
private token = config.ADMIN_TOKEN || v4();
|
| 8 |
// contains all session ids that are currently admin sessions
|
| 9 |
private adminSessions: Array<Session["sessionId"]> = [];
|
| 10 |
|
| 11 |
public get enabled() {
|
| 12 |
// if open id is configured, disable the feature
|
| 13 |
+
return config.ADMIN_CLI_LOGIN === "true";
|
| 14 |
}
|
| 15 |
public isAdmin(sessionId: Session["sessionId"]) {
|
| 16 |
if (!this.enabled) return false;
|
|
|
|
| 22 |
if (token === this.token) {
|
| 23 |
logger.info(`[ADMIN] Token validated`);
|
| 24 |
this.adminSessions.push(sessionId);
|
| 25 |
+
this.token = config.ADMIN_TOKEN || v4();
|
| 26 |
return true;
|
| 27 |
}
|
| 28 |
|
|
|
|
| 35 |
|
| 36 |
public displayToken() {
|
| 37 |
// if admin token is set, don't display it
|
| 38 |
+
if (!this.enabled || config.ADMIN_TOKEN) return;
|
| 39 |
|
| 40 |
let port = process.argv.includes("--port")
|
| 41 |
? parseInt(process.argv[process.argv.indexOf("--port") + 1])
|
|
|
|
| 52 |
}
|
| 53 |
}
|
| 54 |
|
| 55 |
+
const url = (config.PUBLIC_ORIGIN || `http://localhost:${port}`) + "?token=";
|
| 56 |
logger.info(`[ADMIN] You can login with ${url + this.token}`);
|
| 57 |
}
|
| 58 |
}
|
src/lib/server/auth.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
| 6 |
custom,
|
| 7 |
} from "openid-client";
|
| 8 |
import { addHours, addWeeks } from "date-fns";
|
| 9 |
-
import {
|
| 10 |
import { sha256 } from "$lib/utils/sha256";
|
| 11 |
import { z } from "zod";
|
| 12 |
import { dev } from "$app/environment";
|
|
@@ -32,34 +32,34 @@ const stringWithDefault = (value: string) =>
|
|
| 32 |
|
| 33 |
export const OIDConfig = z
|
| 34 |
.object({
|
| 35 |
-
CLIENT_ID: stringWithDefault(
|
| 36 |
-
CLIENT_SECRET: stringWithDefault(
|
| 37 |
-
PROVIDER_URL: stringWithDefault(
|
| 38 |
-
SCOPES: stringWithDefault(
|
| 39 |
-
NAME_CLAIM: stringWithDefault(
|
| 40 |
(el) => !["preferred_username", "email", "picture", "sub"].includes(el),
|
| 41 |
{ message: "nameClaim cannot be one of the restricted keys." }
|
| 42 |
),
|
| 43 |
-
TOLERANCE: stringWithDefault(
|
| 44 |
-
RESOURCE: stringWithDefault(
|
| 45 |
ID_TOKEN_SIGNED_RESPONSE_ALG: z.string().optional(),
|
| 46 |
})
|
| 47 |
-
.parse(JSON5.parse(
|
| 48 |
|
| 49 |
export const requiresUser = !!OIDConfig.CLIENT_ID && !!OIDConfig.CLIENT_SECRET;
|
| 50 |
|
| 51 |
const sameSite = z
|
| 52 |
.enum(["lax", "none", "strict"])
|
| 53 |
-
.default(dev ||
|
| 54 |
-
.parse(
|
| 55 |
|
| 56 |
const secure = z
|
| 57 |
.boolean()
|
| 58 |
-
.default(!(dev ||
|
| 59 |
-
.parse(
|
| 60 |
|
| 61 |
export function refreshSessionCookie(cookies: Cookies, sessionId: string) {
|
| 62 |
-
cookies.set(
|
| 63 |
path: "/",
|
| 64 |
// So that it works inside the space's iframe
|
| 65 |
sameSite,
|
|
|
|
| 6 |
custom,
|
| 7 |
} from "openid-client";
|
| 8 |
import { addHours, addWeeks } from "date-fns";
|
| 9 |
+
import { config } from "$lib/server/config";
|
| 10 |
import { sha256 } from "$lib/utils/sha256";
|
| 11 |
import { z } from "zod";
|
| 12 |
import { dev } from "$app/environment";
|
|
|
|
| 32 |
|
| 33 |
export const OIDConfig = z
|
| 34 |
.object({
|
| 35 |
+
CLIENT_ID: stringWithDefault(config.OPENID_CLIENT_ID),
|
| 36 |
+
CLIENT_SECRET: stringWithDefault(config.OPENID_CLIENT_SECRET),
|
| 37 |
+
PROVIDER_URL: stringWithDefault(config.OPENID_PROVIDER_URL),
|
| 38 |
+
SCOPES: stringWithDefault(config.OPENID_SCOPES),
|
| 39 |
+
NAME_CLAIM: stringWithDefault(config.OPENID_NAME_CLAIM).refine(
|
| 40 |
(el) => !["preferred_username", "email", "picture", "sub"].includes(el),
|
| 41 |
{ message: "nameClaim cannot be one of the restricted keys." }
|
| 42 |
),
|
| 43 |
+
TOLERANCE: stringWithDefault(config.OPENID_TOLERANCE),
|
| 44 |
+
RESOURCE: stringWithDefault(config.OPENID_RESOURCE),
|
| 45 |
ID_TOKEN_SIGNED_RESPONSE_ALG: z.string().optional(),
|
| 46 |
})
|
| 47 |
+
.parse(JSON5.parse(config.OPENID_CONFIG || "{}"));
|
| 48 |
|
| 49 |
export const requiresUser = !!OIDConfig.CLIENT_ID && !!OIDConfig.CLIENT_SECRET;
|
| 50 |
|
| 51 |
const sameSite = z
|
| 52 |
.enum(["lax", "none", "strict"])
|
| 53 |
+
.default(dev || config.ALLOW_INSECURE_COOKIES === "true" ? "lax" : "none")
|
| 54 |
+
.parse(config.COOKIE_SAMESITE === "" ? undefined : config.COOKIE_SAMESITE);
|
| 55 |
|
| 56 |
const secure = z
|
| 57 |
.boolean()
|
| 58 |
+
.default(!(dev || config.ALLOW_INSECURE_COOKIES === "true"))
|
| 59 |
+
.parse(config.COOKIE_SECURE === "" ? undefined : config.COOKIE_SECURE === "true");
|
| 60 |
|
| 61 |
export function refreshSessionCookie(cookies: Cookies, sessionId: string) {
|
| 62 |
+
cookies.set(config.COOKIE_NAME, sessionId, {
|
| 63 |
path: "/",
|
| 64 |
// So that it works inside the space's iframe
|
| 65 |
sameSite,
|
src/lib/server/config.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { env as publicEnv } from "$env/dynamic/public";
|
| 2 |
+
import { env as serverEnv } from "$env/dynamic/private";
|
| 3 |
+
import { publicConfig } from "$lib/utils/PublicConfig.svelte";
|
| 4 |
+
import { building } from "$app/environment";
|
| 5 |
+
import type { Collection } from "mongodb";
|
| 6 |
+
import type { ConfigKey as ConfigKeyType } from "$lib/types/ConfigKey";
|
| 7 |
+
import type { Semaphore } from "$lib/types/Semaphore";
|
| 8 |
+
import { Semaphores } from "$lib/types/Semaphore";
|
| 9 |
+
|
| 10 |
+
export type PublicConfigKey = keyof typeof publicEnv;
|
| 11 |
+
const keysFromEnv = { ...publicEnv, ...serverEnv };
|
| 12 |
+
export type ConfigKey = keyof typeof keysFromEnv;
|
| 13 |
+
|
| 14 |
+
class ConfigManager {
|
| 15 |
+
private keysFromDB: Partial<Record<ConfigKey, string>> = {};
|
| 16 |
+
private isInitialized = false;
|
| 17 |
+
|
| 18 |
+
private configCollection: Collection<ConfigKeyType> | undefined;
|
| 19 |
+
private semaphoreCollection: Collection<Semaphore> | undefined;
|
| 20 |
+
private lastConfigUpdate: Date | undefined;
|
| 21 |
+
|
| 22 |
+
async init() {
|
| 23 |
+
if (this.isInitialized) return;
|
| 24 |
+
|
| 25 |
+
if (import.meta.env.MODE === "test") {
|
| 26 |
+
this.isInitialized = true;
|
| 27 |
+
return;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
const { collections, ready } = await import("./database");
|
| 31 |
+
await ready;
|
| 32 |
+
if (!collections) {
|
| 33 |
+
throw new Error("Database not initialized");
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
this.configCollection = collections.config;
|
| 37 |
+
this.semaphoreCollection = collections.semaphores;
|
| 38 |
+
|
| 39 |
+
await this.checkForUpdates().then(() => {
|
| 40 |
+
this.isInitialized = true;
|
| 41 |
+
});
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
get ConfigManagerEnabled() {
|
| 45 |
+
return serverEnv.ENABLE_CONFIG_MANAGER === "true" && import.meta.env.MODE !== "test";
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
get isHuggingChat() {
|
| 49 |
+
return this.get("PUBLIC_APP_ASSETS") === "huggingchat";
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
async checkForUpdates() {
|
| 53 |
+
if (await this.isConfigStale()) {
|
| 54 |
+
await this.updateConfig();
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
async isConfigStale(): Promise<boolean> {
|
| 59 |
+
if (!this.lastConfigUpdate || !this.isInitialized) {
|
| 60 |
+
return true;
|
| 61 |
+
}
|
| 62 |
+
const count = await this.semaphoreCollection?.countDocuments({
|
| 63 |
+
key: Semaphores.CONFIG_UPDATE,
|
| 64 |
+
updatedAt: { $gt: this.lastConfigUpdate },
|
| 65 |
+
});
|
| 66 |
+
return count !== undefined && count > 0;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
async updateConfig() {
|
| 70 |
+
const configs = (await this.configCollection?.find({}).toArray()) ?? [];
|
| 71 |
+
this.keysFromDB = configs.reduce(
|
| 72 |
+
(acc, curr) => {
|
| 73 |
+
acc[curr.key as ConfigKey] = curr.value;
|
| 74 |
+
return acc;
|
| 75 |
+
},
|
| 76 |
+
{} as Record<ConfigKey, string>
|
| 77 |
+
);
|
| 78 |
+
|
| 79 |
+
this.lastConfigUpdate = new Date();
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
get(key: ConfigKey): string {
|
| 83 |
+
if (!this.ConfigManagerEnabled) {
|
| 84 |
+
return keysFromEnv[key] || "";
|
| 85 |
+
}
|
| 86 |
+
return this.keysFromDB[key] || keysFromEnv[key] || "";
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
async updateSemaphore() {
|
| 90 |
+
await this.semaphoreCollection?.updateOne(
|
| 91 |
+
{ key: Semaphores.CONFIG_UPDATE },
|
| 92 |
+
{
|
| 93 |
+
$set: {
|
| 94 |
+
updatedAt: new Date(),
|
| 95 |
+
},
|
| 96 |
+
$setOnInsert: {
|
| 97 |
+
createdAt: new Date(),
|
| 98 |
+
},
|
| 99 |
+
},
|
| 100 |
+
{ upsert: true }
|
| 101 |
+
);
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
async set(key: ConfigKey, value: string) {
|
| 105 |
+
if (!this.ConfigManagerEnabled) throw new Error("Config manager is disabled");
|
| 106 |
+
await this.configCollection?.updateOne({ key }, { $set: { value } }, { upsert: true });
|
| 107 |
+
this.keysFromDB[key] = value;
|
| 108 |
+
await this.updateSemaphore();
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
async delete(key: ConfigKey) {
|
| 112 |
+
if (!this.ConfigManagerEnabled) throw new Error("Config manager is disabled");
|
| 113 |
+
await this.configCollection?.deleteOne({ key });
|
| 114 |
+
delete this.keysFromDB[key];
|
| 115 |
+
await this.updateSemaphore();
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
async clear() {
|
| 119 |
+
if (!this.ConfigManagerEnabled) throw new Error("Config manager is disabled");
|
| 120 |
+
await this.configCollection?.deleteMany({});
|
| 121 |
+
this.keysFromDB = {};
|
| 122 |
+
await this.updateSemaphore();
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
getPublicConfig() {
|
| 126 |
+
let config = {
|
| 127 |
+
...Object.fromEntries(
|
| 128 |
+
Object.entries(keysFromEnv).filter(([key]) => key.startsWith("PUBLIC_"))
|
| 129 |
+
),
|
| 130 |
+
} as Record<PublicConfigKey, string>;
|
| 131 |
+
|
| 132 |
+
if (this.ConfigManagerEnabled) {
|
| 133 |
+
config = {
|
| 134 |
+
...config,
|
| 135 |
+
...Object.fromEntries(
|
| 136 |
+
Object.entries(this.keysFromDB).filter(([key]) => key.startsWith("PUBLIC_"))
|
| 137 |
+
),
|
| 138 |
+
};
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
const publicEnvKeys = Object.keys(publicEnv);
|
| 142 |
+
|
| 143 |
+
return Object.fromEntries(
|
| 144 |
+
Object.entries(config).filter(([key]) => publicEnvKeys.includes(key))
|
| 145 |
+
) as Record<PublicConfigKey, string>;
|
| 146 |
+
}
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
// Create the instance and initialize it.
|
| 150 |
+
const configManager = new ConfigManager();
|
| 151 |
+
|
| 152 |
+
export const ready = (async () => {
|
| 153 |
+
if (!building) {
|
| 154 |
+
await configManager.init().then(() => {
|
| 155 |
+
publicConfig.init(configManager.getPublicConfig());
|
| 156 |
+
});
|
| 157 |
+
}
|
| 158 |
+
})();
|
| 159 |
+
|
| 160 |
+
type ConfigProxy = ConfigManager & { [K in ConfigKey]: string };
|
| 161 |
+
|
| 162 |
+
export const config: ConfigProxy = new Proxy(configManager, {
|
| 163 |
+
get(target, prop, receiver) {
|
| 164 |
+
if (prop in target) {
|
| 165 |
+
return Reflect.get(target, prop, receiver);
|
| 166 |
+
}
|
| 167 |
+
if (typeof prop === "string") {
|
| 168 |
+
return target.get(prop as ConfigKey);
|
| 169 |
+
}
|
| 170 |
+
return undefined;
|
| 171 |
+
},
|
| 172 |
+
set(target, prop, value, receiver) {
|
| 173 |
+
if (prop in target) {
|
| 174 |
+
return Reflect.set(target, prop, value, receiver);
|
| 175 |
+
}
|
| 176 |
+
if (typeof prop === "string") {
|
| 177 |
+
target.set(prop as ConfigKey, value);
|
| 178 |
+
return true;
|
| 179 |
+
}
|
| 180 |
+
return false;
|
| 181 |
+
},
|
| 182 |
+
}) as ConfigProxy;
|
src/lib/server/database.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
import { env } from "$env/dynamic/private";
|
| 2 |
import { GridFSBucket, MongoClient } from "mongodb";
|
| 3 |
import type { Conversation } from "$lib/types/Conversation";
|
| 4 |
import type { SharedConversation } from "$lib/types/SharedConversation";
|
|
@@ -23,12 +22,11 @@ import { fileURLToPath } from "url";
|
|
| 23 |
import { dirname, join } from "path";
|
| 24 |
import { existsSync, mkdirSync } from "fs";
|
| 25 |
import { findRepoRoot } from "./findRepoRoot";
|
|
|
|
|
|
|
| 26 |
|
| 27 |
export const CONVERSATION_STATS_COLLECTION = "conversations.stats";
|
| 28 |
|
| 29 |
-
export const DB_FOLDER =
|
| 30 |
-
env.MONGO_STORAGE_PATH || join(findRepoRoot(dirname(fileURLToPath(import.meta.url))), "db");
|
| 31 |
-
|
| 32 |
export class Database {
|
| 33 |
private client?: MongoClient;
|
| 34 |
private mongoServer?: MongoMemoryServer;
|
|
@@ -36,7 +34,11 @@ export class Database {
|
|
| 36 |
private static instance: Database;
|
| 37 |
|
| 38 |
private async init() {
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
logger.warn("No MongoDB URL found, using in-memory server");
|
| 41 |
|
| 42 |
logger.info(`Using database path: ${DB_FOLDER}`);
|
|
@@ -48,7 +50,7 @@ export class Database {
|
|
| 48 |
|
| 49 |
this.mongoServer = await MongoMemoryServer.create({
|
| 50 |
instance: {
|
| 51 |
-
dbName:
|
| 52 |
dbPath: DB_FOLDER,
|
| 53 |
},
|
| 54 |
binary: {
|
|
@@ -56,11 +58,11 @@ export class Database {
|
|
| 56 |
},
|
| 57 |
});
|
| 58 |
this.client = new MongoClient(this.mongoServer.getUri(), {
|
| 59 |
-
directConnection:
|
| 60 |
});
|
| 61 |
} else {
|
| 62 |
-
this.client = new MongoClient(
|
| 63 |
-
directConnection:
|
| 64 |
});
|
| 65 |
}
|
| 66 |
|
|
@@ -68,7 +70,7 @@ export class Database {
|
|
| 68 |
logger.error(err, "Connection error");
|
| 69 |
process.exit(1);
|
| 70 |
});
|
| 71 |
-
this.client.db(
|
| 72 |
this.client.on("open", () => this.initDatabase());
|
| 73 |
|
| 74 |
// Disconnect DB on exit
|
|
@@ -108,7 +110,7 @@ export class Database {
|
|
| 108 |
}
|
| 109 |
|
| 110 |
const db = this.client.db(
|
| 111 |
-
|
| 112 |
);
|
| 113 |
|
| 114 |
const conversations = db.collection<Conversation>("conversations");
|
|
@@ -127,6 +129,7 @@ export class Database {
|
|
| 127 |
const semaphores = db.collection<Semaphore>("semaphores");
|
| 128 |
const tokenCaches = db.collection<TokenCache>("tokens");
|
| 129 |
const tools = db.collection<CommunityToolDB>("tools");
|
|
|
|
| 130 |
|
| 131 |
return {
|
| 132 |
conversations,
|
|
@@ -145,6 +148,7 @@ export class Database {
|
|
| 145 |
semaphores,
|
| 146 |
tokenCaches,
|
| 147 |
tools,
|
|
|
|
| 148 |
};
|
| 149 |
}
|
| 150 |
|
|
@@ -168,6 +172,7 @@ export class Database {
|
|
| 168 |
semaphores,
|
| 169 |
tokenCaches,
|
| 170 |
tools,
|
|
|
|
| 171 |
} = this.getCollections();
|
| 172 |
|
| 173 |
conversations
|
|
@@ -258,7 +263,7 @@ export class Database {
|
|
| 258 |
// Unique index for semaphore and migration results
|
| 259 |
semaphores.createIndex({ key: 1 }, { unique: true }).catch((e) => logger.error(e));
|
| 260 |
semaphores
|
| 261 |
-
.createIndex({
|
| 262 |
.catch((e) => logger.error(e));
|
| 263 |
tokenCaches
|
| 264 |
.createIndex({ createdAt: 1 }, { expireAfterSeconds: 5 * 60 })
|
|
@@ -281,9 +286,18 @@ export class Database {
|
|
| 281 |
sessionId: 1,
|
| 282 |
})
|
| 283 |
.catch((e) => logger.error(e));
|
|
|
|
|
|
|
| 284 |
}
|
| 285 |
}
|
| 286 |
|
| 287 |
-
export
|
| 288 |
-
|
| 289 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import { GridFSBucket, MongoClient } from "mongodb";
|
| 2 |
import type { Conversation } from "$lib/types/Conversation";
|
| 3 |
import type { SharedConversation } from "$lib/types/SharedConversation";
|
|
|
|
| 22 |
import { dirname, join } from "path";
|
| 23 |
import { existsSync, mkdirSync } from "fs";
|
| 24 |
import { findRepoRoot } from "./findRepoRoot";
|
| 25 |
+
import type { ConfigKey } from "$lib/types/ConfigKey";
|
| 26 |
+
import { config } from "$lib/server/config";
|
| 27 |
|
| 28 |
export const CONVERSATION_STATS_COLLECTION = "conversations.stats";
|
| 29 |
|
|
|
|
|
|
|
|
|
|
| 30 |
export class Database {
|
| 31 |
private client?: MongoClient;
|
| 32 |
private mongoServer?: MongoMemoryServer;
|
|
|
|
| 34 |
private static instance: Database;
|
| 35 |
|
| 36 |
private async init() {
|
| 37 |
+
const DB_FOLDER =
|
| 38 |
+
config.MONGO_STORAGE_PATH ||
|
| 39 |
+
join(findRepoRoot(dirname(fileURLToPath(import.meta.url))), "db");
|
| 40 |
+
|
| 41 |
+
if (!config.MONGODB_URL) {
|
| 42 |
logger.warn("No MongoDB URL found, using in-memory server");
|
| 43 |
|
| 44 |
logger.info(`Using database path: ${DB_FOLDER}`);
|
|
|
|
| 50 |
|
| 51 |
this.mongoServer = await MongoMemoryServer.create({
|
| 52 |
instance: {
|
| 53 |
+
dbName: config.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : ""),
|
| 54 |
dbPath: DB_FOLDER,
|
| 55 |
},
|
| 56 |
binary: {
|
|
|
|
| 58 |
},
|
| 59 |
});
|
| 60 |
this.client = new MongoClient(this.mongoServer.getUri(), {
|
| 61 |
+
directConnection: config.MONGODB_DIRECT_CONNECTION === "true",
|
| 62 |
});
|
| 63 |
} else {
|
| 64 |
+
this.client = new MongoClient(config.MONGODB_URL, {
|
| 65 |
+
directConnection: config.MONGODB_DIRECT_CONNECTION === "true",
|
| 66 |
});
|
| 67 |
}
|
| 68 |
|
|
|
|
| 70 |
logger.error(err, "Connection error");
|
| 71 |
process.exit(1);
|
| 72 |
});
|
| 73 |
+
this.client.db(config.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : ""));
|
| 74 |
this.client.on("open", () => this.initDatabase());
|
| 75 |
|
| 76 |
// Disconnect DB on exit
|
|
|
|
| 110 |
}
|
| 111 |
|
| 112 |
const db = this.client.db(
|
| 113 |
+
config.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : "")
|
| 114 |
);
|
| 115 |
|
| 116 |
const conversations = db.collection<Conversation>("conversations");
|
|
|
|
| 129 |
const semaphores = db.collection<Semaphore>("semaphores");
|
| 130 |
const tokenCaches = db.collection<TokenCache>("tokens");
|
| 131 |
const tools = db.collection<CommunityToolDB>("tools");
|
| 132 |
+
const configCollection = db.collection<ConfigKey>("config");
|
| 133 |
|
| 134 |
return {
|
| 135 |
conversations,
|
|
|
|
| 148 |
semaphores,
|
| 149 |
tokenCaches,
|
| 150 |
tools,
|
| 151 |
+
config: configCollection,
|
| 152 |
};
|
| 153 |
}
|
| 154 |
|
|
|
|
| 172 |
semaphores,
|
| 173 |
tokenCaches,
|
| 174 |
tools,
|
| 175 |
+
config,
|
| 176 |
} = this.getCollections();
|
| 177 |
|
| 178 |
conversations
|
|
|
|
| 263 |
// Unique index for semaphore and migration results
|
| 264 |
semaphores.createIndex({ key: 1 }, { unique: true }).catch((e) => logger.error(e));
|
| 265 |
semaphores
|
| 266 |
+
.createIndex({ deleteAt: 1 }, { expireAfterSeconds: 1 })
|
| 267 |
.catch((e) => logger.error(e));
|
| 268 |
tokenCaches
|
| 269 |
.createIndex({ createdAt: 1 }, { expireAfterSeconds: 5 * 60 })
|
|
|
|
| 286 |
sessionId: 1,
|
| 287 |
})
|
| 288 |
.catch((e) => logger.error(e));
|
| 289 |
+
|
| 290 |
+
config.createIndex({ key: 1 }, { unique: true }).catch((e) => logger.error(e));
|
| 291 |
}
|
| 292 |
}
|
| 293 |
|
| 294 |
+
export let collections: ReturnType<typeof Database.prototype.getCollections>;
|
| 295 |
+
|
| 296 |
+
export const ready = (async () => {
|
| 297 |
+
if (!building) {
|
| 298 |
+
await Database.getInstance();
|
| 299 |
+
collections = await Database.getInstance().then((db) => db.getCollections());
|
| 300 |
+
} else {
|
| 301 |
+
collections = {} as unknown as ReturnType<typeof Database.prototype.getCollections>;
|
| 302 |
+
}
|
| 303 |
+
})();
|
src/lib/server/embeddingEndpoints/hfApi/embeddingHfApi.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
import { z } from "zod";
|
| 2 |
import type { EmbeddingEndpoint, Embedding } from "../embeddingEndpoints";
|
| 3 |
import { chunk } from "$lib/utils/chunk";
|
| 4 |
-
import {
|
| 5 |
import { logger } from "$lib/server/logger";
|
| 6 |
|
| 7 |
export const embeddingEndpointHfApiSchema = z.object({
|
|
@@ -11,14 +11,14 @@ export const embeddingEndpointHfApiSchema = z.object({
|
|
| 11 |
authorization: z
|
| 12 |
.string()
|
| 13 |
.optional()
|
| 14 |
-
.transform((v) => (!v &&
|
| 15 |
});
|
| 16 |
|
| 17 |
export async function embeddingEndpointHfApi(
|
| 18 |
input: z.input<typeof embeddingEndpointHfApiSchema>
|
| 19 |
): Promise<EmbeddingEndpoint> {
|
| 20 |
const { model, authorization } = embeddingEndpointHfApiSchema.parse(input);
|
| 21 |
-
const url = `${
|
| 22 |
|
| 23 |
return async ({ inputs }) => {
|
| 24 |
const batchesInputs = chunk(inputs, 128);
|
|
|
|
| 1 |
import { z } from "zod";
|
| 2 |
import type { EmbeddingEndpoint, Embedding } from "../embeddingEndpoints";
|
| 3 |
import { chunk } from "$lib/utils/chunk";
|
| 4 |
+
import { config } from "$lib/server/config";
|
| 5 |
import { logger } from "$lib/server/logger";
|
| 6 |
|
| 7 |
export const embeddingEndpointHfApiSchema = z.object({
|
|
|
|
| 11 |
authorization: z
|
| 12 |
.string()
|
| 13 |
.optional()
|
| 14 |
+
.transform((v) => (!v && config.HF_TOKEN ? "Bearer " + config.HF_TOKEN : v)), // if the header is not set but HF_TOKEN is, use it as the authorization header
|
| 15 |
});
|
| 16 |
|
| 17 |
export async function embeddingEndpointHfApi(
|
| 18 |
input: z.input<typeof embeddingEndpointHfApiSchema>
|
| 19 |
): Promise<EmbeddingEndpoint> {
|
| 20 |
const { model, authorization } = embeddingEndpointHfApiSchema.parse(input);
|
| 21 |
+
const url = `${config.HF_API_ROOT}/${model.id}`;
|
| 22 |
|
| 23 |
return async ({ inputs }) => {
|
| 24 |
const batchesInputs = chunk(inputs, 128);
|
src/lib/server/embeddingEndpoints/openai/embeddingEndpoints.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
| 1 |
import { z } from "zod";
|
| 2 |
import type { EmbeddingEndpoint, Embedding } from "../embeddingEndpoints";
|
| 3 |
import { chunk } from "$lib/utils/chunk";
|
| 4 |
-
import {
|
| 5 |
|
| 6 |
export const embeddingEndpointOpenAIParametersSchema = z.object({
|
| 7 |
weight: z.number().int().positive().default(1),
|
| 8 |
model: z.any(),
|
| 9 |
type: z.literal("openai"),
|
| 10 |
url: z.string().url().default("https://api.openai.com/v1/embeddings"),
|
| 11 |
-
apiKey: z.string().default(
|
| 12 |
defaultHeaders: z.record(z.string()).default({}),
|
| 13 |
});
|
| 14 |
|
|
|
|
| 1 |
import { z } from "zod";
|
| 2 |
import type { EmbeddingEndpoint, Embedding } from "../embeddingEndpoints";
|
| 3 |
import { chunk } from "$lib/utils/chunk";
|
| 4 |
+
import { config } from "$lib/server/config";
|
| 5 |
|
| 6 |
export const embeddingEndpointOpenAIParametersSchema = z.object({
|
| 7 |
weight: z.number().int().positive().default(1),
|
| 8 |
model: z.any(),
|
| 9 |
type: z.literal("openai"),
|
| 10 |
url: z.string().url().default("https://api.openai.com/v1/embeddings"),
|
| 11 |
+
apiKey: z.string().default(config.OPENAI_API_KEY),
|
| 12 |
defaultHeaders: z.record(z.string()).default({}),
|
| 13 |
});
|
| 14 |
|
src/lib/server/embeddingEndpoints/tei/embeddingEndpoints.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
import { z } from "zod";
|
| 2 |
import type { EmbeddingEndpoint, Embedding } from "../embeddingEndpoints";
|
| 3 |
import { chunk } from "$lib/utils/chunk";
|
| 4 |
-
import {
|
| 5 |
import { logger } from "$lib/server/logger";
|
| 6 |
|
| 7 |
export const embeddingEndpointTeiParametersSchema = z.object({
|
|
@@ -12,7 +12,7 @@ export const embeddingEndpointTeiParametersSchema = z.object({
|
|
| 12 |
authorization: z
|
| 13 |
.string()
|
| 14 |
.optional()
|
| 15 |
-
.transform((v) => (!v &&
|
| 16 |
});
|
| 17 |
|
| 18 |
const getModelInfoByUrl = async (url: string, authorization?: string) => {
|
|
|
|
| 1 |
import { z } from "zod";
|
| 2 |
import type { EmbeddingEndpoint, Embedding } from "../embeddingEndpoints";
|
| 3 |
import { chunk } from "$lib/utils/chunk";
|
| 4 |
+
import { config } from "$lib/server/config";
|
| 5 |
import { logger } from "$lib/server/logger";
|
| 6 |
|
| 7 |
export const embeddingEndpointTeiParametersSchema = z.object({
|
|
|
|
| 12 |
authorization: z
|
| 13 |
.string()
|
| 14 |
.optional()
|
| 15 |
+
.transform((v) => (!v && config.HF_TOKEN ? "Bearer " + config.HF_TOKEN : v)), // if the header is not set but HF_TOKEN is, use it as the authorization header
|
| 16 |
});
|
| 17 |
|
| 18 |
const getModelInfoByUrl = async (url: string, authorization?: string) => {
|
src/lib/server/embeddingModels.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import {
|
| 2 |
|
| 3 |
import { z } from "zod";
|
| 4 |
import { sum } from "$lib/utils/sum";
|
|
@@ -29,7 +29,7 @@ const modelConfig = z.object({
|
|
| 29 |
|
| 30 |
// Default embedding model for backward compatibility
|
| 31 |
const rawEmbeddingModelJSON =
|
| 32 |
-
|
| 33 |
`[
|
| 34 |
{
|
| 35 |
"name": "Xenova/gte-small",
|
|
|
|
| 1 |
+
import { config } from "$lib/server/config";
|
| 2 |
|
| 3 |
import { z } from "zod";
|
| 4 |
import { sum } from "$lib/utils/sum";
|
|
|
|
| 29 |
|
| 30 |
// Default embedding model for backward compatibility
|
| 31 |
const rawEmbeddingModelJSON =
|
| 32 |
+
config.TEXT_EMBEDDING_MODELS ||
|
| 33 |
`[
|
| 34 |
{
|
| 35 |
"name": "Xenova/gte-small",
|
src/lib/server/endpoints/anthropic/endpointAnthropic.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import { z } from "zod";
|
| 2 |
import type { Endpoint } from "../endpoints";
|
| 3 |
-
import {
|
| 4 |
import type { TextGenerationStreamOutput } from "@huggingface/inference";
|
| 5 |
import { createImageProcessorOptionsValidator } from "../images";
|
| 6 |
import { endpointMessagesToAnthropicMessages, addToolResults } from "./utils";
|
|
@@ -22,7 +22,7 @@ export const endpointAnthropicParametersSchema = z.object({
|
|
| 22 |
model: z.any(),
|
| 23 |
type: z.literal("anthropic"),
|
| 24 |
baseURL: z.string().url().default("https://api.anthropic.com"),
|
| 25 |
-
apiKey: z.string().default(
|
| 26 |
defaultHeaders: z.record(z.string()).optional(),
|
| 27 |
defaultQuery: z.record(z.string()).optional(),
|
| 28 |
multimodal: z
|
|
|
|
| 1 |
import { z } from "zod";
|
| 2 |
import type { Endpoint } from "../endpoints";
|
| 3 |
+
import { config } from "$lib/server/config";
|
| 4 |
import type { TextGenerationStreamOutput } from "@huggingface/inference";
|
| 5 |
import { createImageProcessorOptionsValidator } from "../images";
|
| 6 |
import { endpointMessagesToAnthropicMessages, addToolResults } from "./utils";
|
|
|
|
| 22 |
model: z.any(),
|
| 23 |
type: z.literal("anthropic"),
|
| 24 |
baseURL: z.string().url().default("https://api.anthropic.com"),
|
| 25 |
+
apiKey: z.string().default(config.ANTHROPIC_API_KEY ?? "sk-"),
|
| 26 |
defaultHeaders: z.record(z.string()).optional(),
|
| 27 |
defaultQuery: z.record(z.string()).optional(),
|
| 28 |
multimodal: z
|
src/lib/server/endpoints/cloudflare/endpointCloudflare.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
| 1 |
import { z } from "zod";
|
| 2 |
import type { Endpoint } from "../endpoints";
|
| 3 |
import type { TextGenerationStreamOutput } from "@huggingface/inference";
|
| 4 |
-
import {
|
| 5 |
import { logger } from "$lib/server/logger";
|
| 6 |
|
| 7 |
export const endpointCloudflareParametersSchema = z.object({
|
| 8 |
weight: z.number().int().positive().default(1),
|
| 9 |
model: z.any(),
|
| 10 |
type: z.literal("cloudflare"),
|
| 11 |
-
accountId: z.string().default(
|
| 12 |
-
apiToken: z.string().default(
|
| 13 |
});
|
| 14 |
|
| 15 |
export async function endpointCloudflare(
|
|
|
|
| 1 |
import { z } from "zod";
|
| 2 |
import type { Endpoint } from "../endpoints";
|
| 3 |
import type { TextGenerationStreamOutput } from "@huggingface/inference";
|
| 4 |
+
import { config } from "$lib/server/config";
|
| 5 |
import { logger } from "$lib/server/logger";
|
| 6 |
|
| 7 |
export const endpointCloudflareParametersSchema = z.object({
|
| 8 |
weight: z.number().int().positive().default(1),
|
| 9 |
model: z.any(),
|
| 10 |
type: z.literal("cloudflare"),
|
| 11 |
+
accountId: z.string().default(config.CLOUDFLARE_ACCOUNT_ID),
|
| 12 |
+
apiToken: z.string().default(config.CLOUDFLARE_API_TOKEN),
|
| 13 |
});
|
| 14 |
|
| 15 |
export async function endpointCloudflare(
|
src/lib/server/endpoints/cohere/endpointCohere.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import { z } from "zod";
|
| 2 |
-
import {
|
| 3 |
import type { Endpoint } from "../endpoints";
|
| 4 |
import type { TextGenerationStreamOutput } from "@huggingface/inference";
|
| 5 |
import type { Cohere, CohereClient } from "cohere-ai";
|
|
@@ -12,7 +12,7 @@ export const endpointCohereParametersSchema = z.object({
|
|
| 12 |
weight: z.number().int().positive().default(1),
|
| 13 |
model: z.any(),
|
| 14 |
type: z.literal("cohere"),
|
| 15 |
-
apiKey: z.string().default(
|
| 16 |
clientName: z.string().optional(),
|
| 17 |
raw: z.boolean().default(false),
|
| 18 |
forceSingleStep: z.boolean().default(true),
|
|
|
|
| 1 |
import { z } from "zod";
|
| 2 |
+
import { config } from "$lib/server/config";
|
| 3 |
import type { Endpoint } from "../endpoints";
|
| 4 |
import type { TextGenerationStreamOutput } from "@huggingface/inference";
|
| 5 |
import type { Cohere, CohereClient } from "cohere-ai";
|
|
|
|
| 12 |
weight: z.number().int().positive().default(1),
|
| 13 |
model: z.any(),
|
| 14 |
type: z.literal("cohere"),
|
| 15 |
+
apiKey: z.string().default(config.COHERE_API_TOKEN),
|
| 16 |
clientName: z.string().optional(),
|
| 17 |
raw: z.boolean().default(false),
|
| 18 |
forceSingleStep: z.boolean().default(true),
|
src/lib/server/endpoints/google/endpointGenAI.ts
CHANGED
|
@@ -6,13 +6,13 @@ import type { TextGenerationStreamOutput } from "@huggingface/inference";
|
|
| 6 |
import type { Endpoint } from "../endpoints";
|
| 7 |
import { createImageProcessorOptionsValidator, makeImageProcessor } from "../images";
|
| 8 |
import type { ImageProcessorOptions } from "../images";
|
| 9 |
-
import {
|
| 10 |
|
| 11 |
export const endpointGenAIParametersSchema = z.object({
|
| 12 |
weight: z.number().int().positive().default(1),
|
| 13 |
model: z.any(),
|
| 14 |
type: z.literal("genai"),
|
| 15 |
-
apiKey: z.string().default(
|
| 16 |
safetyThreshold: z
|
| 17 |
.enum([
|
| 18 |
HarmBlockThreshold.HARM_BLOCK_THRESHOLD_UNSPECIFIED,
|
|
|
|
| 6 |
import type { Endpoint } from "../endpoints";
|
| 7 |
import { createImageProcessorOptionsValidator, makeImageProcessor } from "../images";
|
| 8 |
import type { ImageProcessorOptions } from "../images";
|
| 9 |
+
import { config } from "$lib/server/config";
|
| 10 |
|
| 11 |
export const endpointGenAIParametersSchema = z.object({
|
| 12 |
weight: z.number().int().positive().default(1),
|
| 13 |
model: z.any(),
|
| 14 |
type: z.literal("genai"),
|
| 15 |
+
apiKey: z.string().default(config.GOOGLE_GENAI_API_KEY),
|
| 16 |
safetyThreshold: z
|
| 17 |
.enum([
|
| 18 |
HarmBlockThreshold.HARM_BLOCK_THRESHOLD_UNSPECIFIED,
|
src/lib/server/endpoints/llamacpp/endpointLlamacpp.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import {
|
| 2 |
import { buildPrompt } from "$lib/buildPrompt";
|
| 3 |
import type { TextGenerationStreamOutput } from "@huggingface/inference";
|
| 4 |
import type { Endpoint } from "../endpoints";
|
|
@@ -11,7 +11,7 @@ export const endpointLlamacppParametersSchema = z.object({
|
|
| 11 |
type: z.literal("llamacpp"),
|
| 12 |
url: z.string().url().default("http://127.0.0.1:8080"), // legacy, feel free to remove in breaking change update
|
| 13 |
baseURL: z.string().url().optional(),
|
| 14 |
-
accessToken: z.string().default(
|
| 15 |
});
|
| 16 |
|
| 17 |
export function endpointLlamacpp(
|
|
|
|
| 1 |
+
import { config } from "$lib/server/config";
|
| 2 |
import { buildPrompt } from "$lib/buildPrompt";
|
| 3 |
import type { TextGenerationStreamOutput } from "@huggingface/inference";
|
| 4 |
import type { Endpoint } from "../endpoints";
|
|
|
|
| 11 |
type: z.literal("llamacpp"),
|
| 12 |
url: z.string().url().default("http://127.0.0.1:8080"), // legacy, feel free to remove in breaking change update
|
| 13 |
baseURL: z.string().url().optional(),
|
| 14 |
+
accessToken: z.string().default(config.HF_TOKEN ?? config.HF_ACCESS_TOKEN),
|
| 15 |
});
|
| 16 |
|
| 17 |
export function endpointLlamacpp(
|
src/lib/server/endpoints/local/endpointLocal.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import {
|
| 2 |
import type {
|
| 3 |
Endpoint,
|
| 4 |
EndpointMessage,
|
|
@@ -50,7 +50,7 @@ export async function endpointLocal(
|
|
| 50 |
// Setup model path and folder
|
| 51 |
const path = modelPathInput ?? `hf:${model.id ?? model.name}`;
|
| 52 |
const modelFolder =
|
| 53 |
-
|
| 54 |
join(findRepoRoot(dirname(fileURLToPath(import.meta.url))), "models");
|
| 55 |
|
| 56 |
// Initialize Llama model
|
|
|
|
| 1 |
+
import { config } from "$lib/server/config";
|
| 2 |
import type {
|
| 3 |
Endpoint,
|
| 4 |
EndpointMessage,
|
|
|
|
| 50 |
// Setup model path and folder
|
| 51 |
const path = modelPathInput ?? `hf:${model.id ?? model.name}`;
|
| 52 |
const modelFolder =
|
| 53 |
+
config.MODELS_STORAGE_PATH ||
|
| 54 |
join(findRepoRoot(dirname(fileURLToPath(import.meta.url))), "models");
|
| 55 |
|
| 56 |
// Initialize Llama model
|
src/lib/server/endpoints/openai/endpointOai.ts
CHANGED
|
@@ -12,7 +12,7 @@ import type {
|
|
| 12 |
} from "openai/resources/chat/completions";
|
| 13 |
import type { FunctionDefinition, FunctionParameters } from "openai/resources/shared";
|
| 14 |
import { buildPrompt } from "$lib/buildPrompt";
|
| 15 |
-
import {
|
| 16 |
import type { Endpoint } from "../endpoints";
|
| 17 |
import type OpenAI from "openai";
|
| 18 |
import { createImageProcessorOptionsValidator, makeImageProcessor } from "../images";
|
|
@@ -90,7 +90,7 @@ export const endpointOAIParametersSchema = z.object({
|
|
| 90 |
model: z.any(),
|
| 91 |
type: z.literal("openai"),
|
| 92 |
baseURL: z.string().url().default("https://api.openai.com/v1"),
|
| 93 |
-
apiKey: z.string().default(
|
| 94 |
completion: z
|
| 95 |
.union([z.literal("completions"), z.literal("chat_completions")])
|
| 96 |
.default("chat_completions"),
|
|
|
|
| 12 |
} from "openai/resources/chat/completions";
|
| 13 |
import type { FunctionDefinition, FunctionParameters } from "openai/resources/shared";
|
| 14 |
import { buildPrompt } from "$lib/buildPrompt";
|
| 15 |
+
import { config } from "$lib/server/config";
|
| 16 |
import type { Endpoint } from "../endpoints";
|
| 17 |
import type OpenAI from "openai";
|
| 18 |
import { createImageProcessorOptionsValidator, makeImageProcessor } from "../images";
|
|
|
|
| 90 |
model: z.any(),
|
| 91 |
type: z.literal("openai"),
|
| 92 |
baseURL: z.string().url().default("https://api.openai.com/v1"),
|
| 93 |
+
apiKey: z.string().default(config.OPENAI_API_KEY || config.HF_TOKEN || "sk-"),
|
| 94 |
completion: z
|
| 95 |
.union([z.literal("completions"), z.literal("chat_completions")])
|
| 96 |
.default("chat_completions"),
|
src/lib/server/endpoints/tgi/endpointTgi.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import {
|
| 2 |
import { buildPrompt } from "$lib/buildPrompt";
|
| 3 |
import { textGenerationStream } from "@huggingface/inference";
|
| 4 |
import type { Endpoint, EndpointMessage } from "../endpoints";
|
|
@@ -14,7 +14,7 @@ export const endpointTgiParametersSchema = z.object({
|
|
| 14 |
model: z.any(),
|
| 15 |
type: z.literal("tgi"),
|
| 16 |
url: z.string().url(),
|
| 17 |
-
accessToken: z.string().default(
|
| 18 |
authorization: z.string().optional(),
|
| 19 |
multimodal: z
|
| 20 |
.object({
|
|
|
|
| 1 |
+
import { config } from "$lib/server/config";
|
| 2 |
import { buildPrompt } from "$lib/buildPrompt";
|
| 3 |
import { textGenerationStream } from "@huggingface/inference";
|
| 4 |
import type { Endpoint, EndpointMessage } from "../endpoints";
|
|
|
|
| 14 |
model: z.any(),
|
| 15 |
type: z.literal("tgi"),
|
| 16 |
url: z.string().url(),
|
| 17 |
+
accessToken: z.string().default(config.HF_TOKEN ?? config.HF_ACCESS_TOKEN),
|
| 18 |
authorization: z.string().optional(),
|
| 19 |
multimodal: z
|
| 20 |
.object({
|
src/lib/server/logger.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import pino from "pino";
|
| 2 |
import { dev } from "$app/environment";
|
| 3 |
-
import {
|
| 4 |
|
| 5 |
let options: pino.LoggerOptions = {};
|
| 6 |
|
|
@@ -15,4 +15,4 @@ if (dev) {
|
|
| 15 |
};
|
| 16 |
}
|
| 17 |
|
| 18 |
-
export const logger = pino({ ...options, level:
|
|
|
|
| 1 |
import pino from "pino";
|
| 2 |
import { dev } from "$app/environment";
|
| 3 |
+
import { config } from "$lib/server/config";
|
| 4 |
|
| 5 |
let options: pino.LoggerOptions = {};
|
| 6 |
|
|
|
|
| 15 |
};
|
| 16 |
}
|
| 17 |
|
| 18 |
+
export const logger = pino({ ...options, level: config.LOG_LEVEL ?? "info" });
|
src/lib/server/metrics.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
import { collectDefaultMetrics, Registry, Counter, Summary } from "prom-client";
|
| 2 |
import express from "express";
|
| 3 |
import { logger } from "$lib/server/logger";
|
| 4 |
-
import {
|
| 5 |
import type { Model } from "$lib/types/Model";
|
| 6 |
import { onExit } from "./exitHandler";
|
| 7 |
import { promisify } from "util";
|
|
@@ -41,15 +41,15 @@ export class MetricsServer {
|
|
| 41 |
private constructor() {
|
| 42 |
const app = express();
|
| 43 |
|
| 44 |
-
const port = Number(
|
| 45 |
if (isNaN(port) || port < 0 || port > 65535) {
|
| 46 |
-
logger.warn(`Invalid value for METRICS_PORT: ${
|
| 47 |
}
|
| 48 |
|
| 49 |
-
if (
|
| 50 |
-
logger.warn(`Invalid value for METRICS_ENABLED: ${
|
| 51 |
}
|
| 52 |
-
if (
|
| 53 |
const server = app.listen(port, () => {
|
| 54 |
logger.info(`Metrics server listening on port ${port}`);
|
| 55 |
});
|
|
|
|
| 1 |
import { collectDefaultMetrics, Registry, Counter, Summary } from "prom-client";
|
| 2 |
import express from "express";
|
| 3 |
import { logger } from "$lib/server/logger";
|
| 4 |
+
import { config } from "$lib/server/config";
|
| 5 |
import type { Model } from "$lib/types/Model";
|
| 6 |
import { onExit } from "./exitHandler";
|
| 7 |
import { promisify } from "util";
|
|
|
|
| 41 |
private constructor() {
|
| 42 |
const app = express();
|
| 43 |
|
| 44 |
+
const port = Number(config.METRICS_PORT || "5565");
|
| 45 |
if (isNaN(port) || port < 0 || port > 65535) {
|
| 46 |
+
logger.warn(`Invalid value for METRICS_PORT: ${config.METRICS_PORT}`);
|
| 47 |
}
|
| 48 |
|
| 49 |
+
if (config.METRICS_ENABLED !== "false" && config.METRICS_ENABLED !== "true") {
|
| 50 |
+
logger.warn(`Invalid value for METRICS_ENABLED: ${config.METRICS_ENABLED}`);
|
| 51 |
}
|
| 52 |
+
if (config.METRICS_ENABLED === "true") {
|
| 53 |
const server = app.listen(port, () => {
|
| 54 |
logger.info(`Metrics server listening on port ${port}`);
|
| 55 |
});
|
src/lib/server/models.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import {
|
| 2 |
import type { ChatTemplateInput } from "$lib/types/Template";
|
| 3 |
import { compileTemplate } from "$lib/utils/template";
|
| 4 |
import { z } from "zod";
|
|
@@ -13,7 +13,6 @@ import JSON5 from "json5";
|
|
| 13 |
import { getTokenizer } from "$lib/utils/getTokenizer";
|
| 14 |
import { logger } from "$lib/server/logger";
|
| 15 |
import { ToolResultStatus, type ToolInput } from "$lib/types/Tool";
|
| 16 |
-
import { isHuggingChat } from "$lib/utils/isHuggingChat";
|
| 17 |
import { join, dirname } from "path";
|
| 18 |
import { resolveModelFile, readGgufFileInfo } from "node-llama-cpp";
|
| 19 |
import { fileURLToPath } from "url";
|
|
@@ -22,7 +21,8 @@ import { Template } from "@huggingface/jinja";
|
|
| 22 |
import { readdirSync } from "fs";
|
| 23 |
|
| 24 |
export const MODELS_FOLDER =
|
| 25 |
-
|
|
|
|
| 26 |
|
| 27 |
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
|
| 28 |
|
|
@@ -137,9 +137,9 @@ const turnStringIntoLocalModel = z.preprocess((obj: unknown) => {
|
|
| 137 |
} satisfies z.input<typeof modelConfig>;
|
| 138 |
}, modelConfig);
|
| 139 |
|
| 140 |
-
let modelsRaw = z.array(turnStringIntoLocalModel).parse(JSON5.parse(
|
| 141 |
|
| 142 |
-
if (
|
| 143 |
const parsedGgufModels = z.array(modelConfig).parse(ggufModelsConfig);
|
| 144 |
modelsRaw = [...modelsRaw, ...parsedGgufModels];
|
| 145 |
}
|
|
@@ -240,7 +240,7 @@ async function getChatPromptRender(
|
|
| 240 |
// or use the `rag` mode without the citations
|
| 241 |
const id = m.id ?? m.name;
|
| 242 |
|
| 243 |
-
if (isHuggingChat && id.startsWith("CohereForAI")) {
|
| 244 |
formattedMessages = [
|
| 245 |
{
|
| 246 |
role: "user",
|
|
@@ -267,7 +267,7 @@ async function getChatPromptRender(
|
|
| 267 |
},
|
| 268 |
...formattedMessages,
|
| 269 |
];
|
| 270 |
-
} else if (isHuggingChat && id.startsWith("meta-llama")) {
|
| 271 |
const results = toolResults.flatMap((result) => {
|
| 272 |
if (result.status === ToolResultStatus.Error) {
|
| 273 |
return [
|
|
@@ -361,8 +361,8 @@ const addEndpoint = (m: Awaited<ReturnType<typeof processModel>>) => ({
|
|
| 361 |
if (!m.endpoints) {
|
| 362 |
return endpointTgi({
|
| 363 |
type: "tgi",
|
| 364 |
-
url: `${
|
| 365 |
-
accessToken:
|
| 366 |
weight: 1,
|
| 367 |
model: m,
|
| 368 |
});
|
|
@@ -416,7 +416,7 @@ const addEndpoint = (m: Awaited<ReturnType<typeof processModel>>) => ({
|
|
| 416 |
},
|
| 417 |
});
|
| 418 |
|
| 419 |
-
const inferenceApiIds = isHuggingChat
|
| 420 |
? await fetch(
|
| 421 |
"https://huggingface.co/api/models?pipeline_tag=text-generation&inference=warm&filter=conversational"
|
| 422 |
)
|
|
@@ -447,7 +447,7 @@ export const validModelIdSchema = z.enum(models.map((m) => m.id) as [string, ...
|
|
| 447 |
export const defaultModel = models[0];
|
| 448 |
|
| 449 |
// Models that have been deprecated
|
| 450 |
-
export const oldModels =
|
| 451 |
? z
|
| 452 |
.array(
|
| 453 |
z.object({
|
|
@@ -457,7 +457,7 @@ export const oldModels = env.OLD_MODELS
|
|
| 457 |
transferTo: validModelIdSchema.optional(),
|
| 458 |
})
|
| 459 |
)
|
| 460 |
-
.parse(JSON5.parse(
|
| 461 |
.map((m) => ({ ...m, id: m.id || m.name, displayName: m.displayName || m.name }))
|
| 462 |
: [];
|
| 463 |
|
|
@@ -469,9 +469,9 @@ export const validateModel = (_models: BackendModel[]) => {
|
|
| 469 |
// if `TASK_MODEL` is string & name of a model in `MODELS`, then we use `MODELS[TASK_MODEL]`, else we try to parse `TASK_MODEL` as a model config itself
|
| 470 |
|
| 471 |
export const taskModel = addEndpoint(
|
| 472 |
-
|
| 473 |
-
? ((models.find((m) => m.name ===
|
| 474 |
-
(await processModel(modelConfig.parse(JSON5.parse(
|
| 475 |
defaultModel)
|
| 476 |
: defaultModel
|
| 477 |
);
|
|
|
|
| 1 |
+
import { config } from "$lib/server/config";
|
| 2 |
import type { ChatTemplateInput } from "$lib/types/Template";
|
| 3 |
import { compileTemplate } from "$lib/utils/template";
|
| 4 |
import { z } from "zod";
|
|
|
|
| 13 |
import { getTokenizer } from "$lib/utils/getTokenizer";
|
| 14 |
import { logger } from "$lib/server/logger";
|
| 15 |
import { ToolResultStatus, type ToolInput } from "$lib/types/Tool";
|
|
|
|
| 16 |
import { join, dirname } from "path";
|
| 17 |
import { resolveModelFile, readGgufFileInfo } from "node-llama-cpp";
|
| 18 |
import { fileURLToPath } from "url";
|
|
|
|
| 21 |
import { readdirSync } from "fs";
|
| 22 |
|
| 23 |
export const MODELS_FOLDER =
|
| 24 |
+
config.MODELS_STORAGE_PATH ||
|
| 25 |
+
join(findRepoRoot(dirname(fileURLToPath(import.meta.url))), "models");
|
| 26 |
|
| 27 |
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
|
| 28 |
|
|
|
|
| 137 |
} satisfies z.input<typeof modelConfig>;
|
| 138 |
}, modelConfig);
|
| 139 |
|
| 140 |
+
let modelsRaw = z.array(turnStringIntoLocalModel).parse(JSON5.parse(config.MODELS ?? "[]"));
|
| 141 |
|
| 142 |
+
if (config.LOAD_GGUF_MODELS === "true" || modelsRaw.length === 0) {
|
| 143 |
const parsedGgufModels = z.array(modelConfig).parse(ggufModelsConfig);
|
| 144 |
modelsRaw = [...modelsRaw, ...parsedGgufModels];
|
| 145 |
}
|
|
|
|
| 240 |
// or use the `rag` mode without the citations
|
| 241 |
const id = m.id ?? m.name;
|
| 242 |
|
| 243 |
+
if (config.isHuggingChat && id.startsWith("CohereForAI")) {
|
| 244 |
formattedMessages = [
|
| 245 |
{
|
| 246 |
role: "user",
|
|
|
|
| 267 |
},
|
| 268 |
...formattedMessages,
|
| 269 |
];
|
| 270 |
+
} else if (config.isHuggingChat && id.startsWith("meta-llama")) {
|
| 271 |
const results = toolResults.flatMap((result) => {
|
| 272 |
if (result.status === ToolResultStatus.Error) {
|
| 273 |
return [
|
|
|
|
| 361 |
if (!m.endpoints) {
|
| 362 |
return endpointTgi({
|
| 363 |
type: "tgi",
|
| 364 |
+
url: `${config.HF_API_ROOT}/${m.name}`,
|
| 365 |
+
accessToken: config.HF_TOKEN ?? config.HF_ACCESS_TOKEN,
|
| 366 |
weight: 1,
|
| 367 |
model: m,
|
| 368 |
});
|
|
|
|
| 416 |
},
|
| 417 |
});
|
| 418 |
|
| 419 |
+
const inferenceApiIds = config.isHuggingChat
|
| 420 |
? await fetch(
|
| 421 |
"https://huggingface.co/api/models?pipeline_tag=text-generation&inference=warm&filter=conversational"
|
| 422 |
)
|
|
|
|
| 447 |
export const defaultModel = models[0];
|
| 448 |
|
| 449 |
// Models that have been deprecated
|
| 450 |
+
export const oldModels = config.OLD_MODELS
|
| 451 |
? z
|
| 452 |
.array(
|
| 453 |
z.object({
|
|
|
|
| 457 |
transferTo: validModelIdSchema.optional(),
|
| 458 |
})
|
| 459 |
)
|
| 460 |
+
.parse(JSON5.parse(config.OLD_MODELS))
|
| 461 |
.map((m) => ({ ...m, id: m.id || m.name, displayName: m.displayName || m.name }))
|
| 462 |
: [];
|
| 463 |
|
|
|
|
| 469 |
// if `TASK_MODEL` is string & name of a model in `MODELS`, then we use `MODELS[TASK_MODEL]`, else we try to parse `TASK_MODEL` as a model config itself
|
| 470 |
|
| 471 |
export const taskModel = addEndpoint(
|
| 472 |
+
config.TASK_MODEL
|
| 473 |
+
? ((models.find((m) => m.name === config.TASK_MODEL) ||
|
| 474 |
+
(await processModel(modelConfig.parse(JSON5.parse(config.TASK_MODEL))))) ??
|
| 475 |
defaultModel)
|
| 476 |
: defaultModel
|
| 477 |
);
|
src/lib/server/sendSlack.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
| 1 |
-
import {
|
| 2 |
import { logger } from "$lib/server/logger";
|
| 3 |
|
| 4 |
export async function sendSlack(text: string) {
|
| 5 |
-
if (!
|
| 6 |
logger.warn("WEBHOOK_URL_REPORT_ASSISTANT is not set, tried to send a slack message.");
|
| 7 |
return;
|
| 8 |
}
|
| 9 |
|
| 10 |
-
const res = await fetch(
|
| 11 |
method: "POST",
|
| 12 |
headers: {
|
| 13 |
"Content-type": "application/json",
|
|
|
|
| 1 |
+
import { config } from "$lib/server/config";
|
| 2 |
import { logger } from "$lib/server/logger";
|
| 3 |
|
| 4 |
export async function sendSlack(text: string) {
|
| 5 |
+
if (!config.WEBHOOK_URL_REPORT_ASSISTANT) {
|
| 6 |
logger.warn("WEBHOOK_URL_REPORT_ASSISTANT is not set, tried to send a slack message.");
|
| 7 |
return;
|
| 8 |
}
|
| 9 |
|
| 10 |
+
const res = await fetch(config.WEBHOOK_URL_REPORT_ASSISTANT, {
|
| 11 |
method: "POST",
|
| 12 |
headers: {
|
| 13 |
"Content-type": "application/json",
|
src/lib/server/textGeneration/assistant.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import { isURLLocal } from "../isURLLocal";
|
| 2 |
-
import {
|
| 3 |
import { collections } from "$lib/server/database";
|
| 4 |
import type { Assistant } from "$lib/types/Assistant";
|
| 5 |
import type { ObjectId } from "mongodb";
|
|
@@ -20,7 +20,7 @@ export async function processPreprompt(preprompt: string, user_message: string |
|
|
| 20 |
const urlString = match[2];
|
| 21 |
try {
|
| 22 |
const url = new URL(urlString);
|
| 23 |
-
if ((await isURLLocal(url)) &&
|
| 24 |
throw new Error("URL couldn't be fetched, it resolved to a local address.");
|
| 25 |
}
|
| 26 |
|
|
@@ -62,7 +62,7 @@ export async function getAssistantById(id?: ObjectId) {
|
|
| 62 |
|
| 63 |
export function assistantHasWebSearch(assistant?: Pick<Assistant, "rag"> | null) {
|
| 64 |
return (
|
| 65 |
-
|
| 66 |
!!assistant?.rag &&
|
| 67 |
(assistant.rag.allowedLinks.length > 0 ||
|
| 68 |
assistant.rag.allowedDomains.length > 0 ||
|
|
@@ -71,5 +71,5 @@ export function assistantHasWebSearch(assistant?: Pick<Assistant, "rag"> | null)
|
|
| 71 |
}
|
| 72 |
|
| 73 |
export function assistantHasDynamicPrompt(assistant?: Pick<Assistant, "dynamicPrompt">) {
|
| 74 |
-
return
|
| 75 |
}
|
|
|
|
| 1 |
import { isURLLocal } from "../isURLLocal";
|
| 2 |
+
import { config } from "$lib/server/config";
|
| 3 |
import { collections } from "$lib/server/database";
|
| 4 |
import type { Assistant } from "$lib/types/Assistant";
|
| 5 |
import type { ObjectId } from "mongodb";
|
|
|
|
| 20 |
const urlString = match[2];
|
| 21 |
try {
|
| 22 |
const url = new URL(urlString);
|
| 23 |
+
if ((await isURLLocal(url)) && config.ENABLE_LOCAL_FETCH !== "true") {
|
| 24 |
throw new Error("URL couldn't be fetched, it resolved to a local address.");
|
| 25 |
}
|
| 26 |
|
|
|
|
| 62 |
|
| 63 |
export function assistantHasWebSearch(assistant?: Pick<Assistant, "rag"> | null) {
|
| 64 |
return (
|
| 65 |
+
config.ENABLE_ASSISTANTS_RAG === "true" &&
|
| 66 |
!!assistant?.rag &&
|
| 67 |
(assistant.rag.allowedLinks.length > 0 ||
|
| 68 |
assistant.rag.allowedDomains.length > 0 ||
|
|
|
|
| 71 |
}
|
| 72 |
|
| 73 |
export function assistantHasDynamicPrompt(assistant?: Pick<Assistant, "dynamicPrompt">) {
|
| 74 |
+
return config.ENABLE_ASSISTANTS_RAG === "true" && Boolean(assistant?.dynamicPrompt);
|
| 75 |
}
|
src/lib/server/textGeneration/generate.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import {
|
| 2 |
import type { ToolResult, Tool } from "$lib/types/Tool";
|
| 3 |
import {
|
| 4 |
MessageReasoningUpdateType,
|
|
@@ -171,7 +171,7 @@ Do not use prefixes such as Response: or Answer: when answering to the user.`,
|
|
| 171 |
|
| 172 |
// create a new status every 5 seconds
|
| 173 |
if (
|
| 174 |
-
|
| 175 |
new Date().getTime() - lastReasoningUpdate.getTime() > 4000
|
| 176 |
) {
|
| 177 |
lastReasoningUpdate = new Date();
|
|
|
|
| 1 |
+
import { config } from "$lib/server/config";
|
| 2 |
import type { ToolResult, Tool } from "$lib/types/Tool";
|
| 3 |
import {
|
| 4 |
MessageReasoningUpdateType,
|
|
|
|
| 171 |
|
| 172 |
// create a new status every 5 seconds
|
| 173 |
if (
|
| 174 |
+
config.REASONING_SUMMARY === "true" &&
|
| 175 |
new Date().getTime() - lastReasoningUpdate.getTime() > 4000
|
| 176 |
) {
|
| 177 |
lastReasoningUpdate = new Date();
|
src/lib/server/textGeneration/title.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import {
|
| 2 |
import { generateFromDefaultEndpoint } from "$lib/server/generateFromDefaultEndpoint";
|
| 3 |
import { logger } from "$lib/server/logger";
|
| 4 |
import { MessageUpdateType, type MessageUpdate } from "$lib/types/MessageUpdate";
|
|
@@ -29,7 +29,7 @@ export async function* generateTitleForConversation(
|
|
| 29 |
}
|
| 30 |
|
| 31 |
export async function generateTitle(prompt: string) {
|
| 32 |
-
if (
|
| 33 |
return prompt.split(/\s+/g).slice(0, 5).join(" ");
|
| 34 |
}
|
| 35 |
|
|
|
|
| 1 |
+
import { config } from "$lib/server/config";
|
| 2 |
import { generateFromDefaultEndpoint } from "$lib/server/generateFromDefaultEndpoint";
|
| 3 |
import { logger } from "$lib/server/logger";
|
| 4 |
import { MessageUpdateType, type MessageUpdate } from "$lib/types/MessageUpdate";
|
|
|
|
| 29 |
}
|
| 30 |
|
| 31 |
export async function generateTitle(prompt: string) {
|
| 32 |
+
if (config.LLM_SUMMARIZATION !== "true") {
|
| 33 |
return prompt.split(/\s+/g).slice(0, 5).join(" ");
|
| 34 |
}
|
| 35 |
|
src/lib/server/tools/index.ts
CHANGED
|
@@ -12,7 +12,7 @@ import type { TextGenerationContext } from "../textGeneration/types";
|
|
| 12 |
|
| 13 |
import { z } from "zod";
|
| 14 |
import JSON5 from "json5";
|
| 15 |
-
import {
|
| 16 |
|
| 17 |
import jp from "jsonpath";
|
| 18 |
import calculator from "./calculator";
|
|
@@ -306,4 +306,4 @@ export function getCallMethod(tool: Omit<BaseTool, "call">): BackendCall {
|
|
| 306 |
};
|
| 307 |
}
|
| 308 |
|
| 309 |
-
export const toolFromConfigs = configTools.parse(JSON5.parse(
|
|
|
|
| 12 |
|
| 13 |
import { z } from "zod";
|
| 14 |
import JSON5 from "json5";
|
| 15 |
+
import { config } from "$lib/server/config";
|
| 16 |
|
| 17 |
import jp from "jsonpath";
|
| 18 |
import calculator from "./calculator";
|
|
|
|
| 306 |
};
|
| 307 |
}
|
| 308 |
|
| 309 |
+
export const toolFromConfigs = configTools.parse(JSON5.parse(config.TOOLS)) satisfies ConfigTool[];
|
src/lib/server/tools/utils.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import {
|
| 2 |
import { Client } from "@gradio/client";
|
| 3 |
import { SignJWT } from "jose";
|
| 4 |
import JSON5 from "json5";
|
|
@@ -28,7 +28,7 @@ export async function* callSpace<TInput extends unknown[], TOutput extends unkno
|
|
| 28 |
const client = await CustomClient.connect(name, {
|
| 29 |
hf_token: ipToken // dont pass the hf token if we have an ip token
|
| 30 |
? undefined
|
| 31 |
-
: ((
|
| 32 |
events: ["status", "data"],
|
| 33 |
});
|
| 34 |
|
|
@@ -63,7 +63,7 @@ export async function* callSpace<TInput extends unknown[], TOutput extends unkno
|
|
| 63 |
}
|
| 64 |
|
| 65 |
export async function getIpToken(ip: string, username?: string) {
|
| 66 |
-
const ipTokenSecret =
|
| 67 |
if (!ipTokenSecret) {
|
| 68 |
return;
|
| 69 |
}
|
|
|
|
| 1 |
+
import { config } from "$lib/server/config";
|
| 2 |
import { Client } from "@gradio/client";
|
| 3 |
import { SignJWT } from "jose";
|
| 4 |
import JSON5 from "json5";
|
|
|
|
| 28 |
const client = await CustomClient.connect(name, {
|
| 29 |
hf_token: ipToken // dont pass the hf token if we have an ip token
|
| 30 |
? undefined
|
| 31 |
+
: ((config.HF_TOKEN ?? config.HF_ACCESS_TOKEN) as unknown as `hf_${string}`),
|
| 32 |
events: ["status", "data"],
|
| 33 |
});
|
| 34 |
|
|
|
|
| 63 |
}
|
| 64 |
|
| 65 |
export async function getIpToken(ip: string, username?: string) {
|
| 66 |
+
const ipTokenSecret = config.IP_TOKEN_SECRET;
|
| 67 |
if (!ipTokenSecret) {
|
| 68 |
return;
|
| 69 |
}
|
src/lib/server/usageLimits.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import { z } from "zod";
|
| 2 |
-
import {
|
| 3 |
import JSON5 from "json5";
|
| 4 |
|
| 5 |
// RATE_LIMIT is the legacy way to define messages per minute limit
|
|
@@ -12,7 +12,7 @@ export const usageLimitsSchema = z
|
|
| 12 |
messagesPerMinute: z
|
| 13 |
.preprocess((val) => {
|
| 14 |
if (val === undefined) {
|
| 15 |
-
return
|
| 16 |
}
|
| 17 |
return val;
|
| 18 |
}, z.coerce.number().optional())
|
|
@@ -21,4 +21,4 @@ export const usageLimitsSchema = z
|
|
| 21 |
})
|
| 22 |
.optional();
|
| 23 |
|
| 24 |
-
export const usageLimits = usageLimitsSchema.parse(JSON5.parse(
|
|
|
|
| 1 |
import { z } from "zod";
|
| 2 |
+
import { config } from "$lib/server/config";
|
| 3 |
import JSON5 from "json5";
|
| 4 |
|
| 5 |
// RATE_LIMIT is the legacy way to define messages per minute limit
|
|
|
|
| 12 |
messagesPerMinute: z
|
| 13 |
.preprocess((val) => {
|
| 14 |
if (val === undefined) {
|
| 15 |
+
return config.RATE_LIMIT;
|
| 16 |
}
|
| 17 |
return val;
|
| 18 |
}, z.coerce.number().optional())
|
|
|
|
| 21 |
})
|
| 22 |
.optional();
|
| 23 |
|
| 24 |
+
export const usageLimits = usageLimitsSchema.parse(JSON5.parse(config.USAGE_LIMITS));
|
src/lib/server/websearch/scrape/playwright.ts
CHANGED
|
@@ -7,16 +7,16 @@ import {
|
|
| 7 |
type Browser,
|
| 8 |
} from "playwright";
|
| 9 |
import { PlaywrightBlocker } from "@cliqz/adblocker-playwright";
|
| 10 |
-
import {
|
| 11 |
import { logger } from "$lib/server/logger";
|
| 12 |
import { onExit } from "$lib/server/exitHandler";
|
| 13 |
|
| 14 |
const blocker =
|
| 15 |
-
|
| 16 |
? await PlaywrightBlocker.fromPrebuiltAdsAndTracking(fetch)
|
| 17 |
.then((blker) => {
|
| 18 |
const mostBlocked = blker.blockFonts().blockMedias().blockFrames().blockImages();
|
| 19 |
-
if (
|
| 20 |
return mostBlocked;
|
| 21 |
})
|
| 22 |
.catch((err) => {
|
|
@@ -68,7 +68,7 @@ export async function withPage<T>(
|
|
| 68 |
|
| 69 |
try {
|
| 70 |
const page = await ctx.newPage();
|
| 71 |
-
if (
|
| 72 |
await blocker.enableBlockingInPage(page);
|
| 73 |
}
|
| 74 |
|
|
@@ -82,10 +82,10 @@ export async function withPage<T>(
|
|
| 82 |
});
|
| 83 |
|
| 84 |
const res = await page
|
| 85 |
-
.goto(url, { waitUntil: "load", timeout: parseInt(
|
| 86 |
.catch(() => {
|
| 87 |
console.warn(
|
| 88 |
-
`Failed to load page within ${parseInt(
|
| 89 |
);
|
| 90 |
});
|
| 91 |
|
|
|
|
| 7 |
type Browser,
|
| 8 |
} from "playwright";
|
| 9 |
import { PlaywrightBlocker } from "@cliqz/adblocker-playwright";
|
| 10 |
+
import { config } from "$lib/server/config";
|
| 11 |
import { logger } from "$lib/server/logger";
|
| 12 |
import { onExit } from "$lib/server/exitHandler";
|
| 13 |
|
| 14 |
const blocker =
|
| 15 |
+
config.PLAYWRIGHT_ADBLOCKER === "true"
|
| 16 |
? await PlaywrightBlocker.fromPrebuiltAdsAndTracking(fetch)
|
| 17 |
.then((blker) => {
|
| 18 |
const mostBlocked = blker.blockFonts().blockMedias().blockFrames().blockImages();
|
| 19 |
+
if (config.WEBSEARCH_JAVASCRIPT === "false") return mostBlocked.blockScripts();
|
| 20 |
return mostBlocked;
|
| 21 |
})
|
| 22 |
.catch((err) => {
|
|
|
|
| 68 |
|
| 69 |
try {
|
| 70 |
const page = await ctx.newPage();
|
| 71 |
+
if (config.PLAYWRIGHT_ADBLOCKER === "true") {
|
| 72 |
await blocker.enableBlockingInPage(page);
|
| 73 |
}
|
| 74 |
|
|
|
|
| 82 |
});
|
| 83 |
|
| 84 |
const res = await page
|
| 85 |
+
.goto(url, { waitUntil: "load", timeout: parseInt(config.WEBSEARCH_TIMEOUT) })
|
| 86 |
.catch(() => {
|
| 87 |
console.warn(
|
| 88 |
+
`Failed to load page within ${parseInt(config.WEBSEARCH_TIMEOUT) / 1000}s: ${url}`
|
| 89 |
);
|
| 90 |
});
|
| 91 |
|
src/lib/server/websearch/search/endpoints.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import { WebSearchProvider, type WebSearchSource } from "$lib/types/WebSearch";
|
| 2 |
-
import {
|
| 3 |
import searchSerper from "./endpoints/serper";
|
| 4 |
import searchSerpApi from "./endpoints/serpApi";
|
| 5 |
import searchSerpStack from "./endpoints/serpStack";
|
|
@@ -10,22 +10,22 @@ import searchSearchApi from "./endpoints/searchApi";
|
|
| 10 |
import searchBing from "./endpoints/bing";
|
| 11 |
|
| 12 |
export function getWebSearchProvider() {
|
| 13 |
-
if (
|
| 14 |
-
if (
|
| 15 |
-
if (
|
| 16 |
return WebSearchProvider.GOOGLE;
|
| 17 |
}
|
| 18 |
|
| 19 |
/** Searches the web using the first available provider, based on the env */
|
| 20 |
export async function searchWeb(query: string): Promise<WebSearchSource[]> {
|
| 21 |
-
if (
|
| 22 |
-
if (
|
| 23 |
-
if (
|
| 24 |
-
if (
|
| 25 |
-
if (
|
| 26 |
-
if (
|
| 27 |
-
if (
|
| 28 |
-
if (
|
| 29 |
throw new Error(
|
| 30 |
"No configuration found for web search. Please set USE_LOCAL_WEBSEARCH, SEARXNG_QUERY_URL, SERPER_API_KEY, YDC_API_KEY, SERPSTACK_API_KEY, or SEARCHAPI_KEY in your environment variables."
|
| 31 |
);
|
|
|
|
| 1 |
import { WebSearchProvider, type WebSearchSource } from "$lib/types/WebSearch";
|
| 2 |
+
import { config } from "$lib/server/config";
|
| 3 |
import searchSerper from "./endpoints/serper";
|
| 4 |
import searchSerpApi from "./endpoints/serpApi";
|
| 5 |
import searchSerpStack from "./endpoints/serpStack";
|
|
|
|
| 10 |
import searchBing from "./endpoints/bing";
|
| 11 |
|
| 12 |
export function getWebSearchProvider() {
|
| 13 |
+
if (config.YDC_API_KEY) return WebSearchProvider.YOU;
|
| 14 |
+
if (config.SEARXNG_QUERY_URL) return WebSearchProvider.SEARXNG;
|
| 15 |
+
if (config.BING_SUBSCRIPTION_KEY) return WebSearchProvider.BING;
|
| 16 |
return WebSearchProvider.GOOGLE;
|
| 17 |
}
|
| 18 |
|
| 19 |
/** Searches the web using the first available provider, based on the env */
|
| 20 |
export async function searchWeb(query: string): Promise<WebSearchSource[]> {
|
| 21 |
+
if (config.USE_LOCAL_WEBSEARCH) return searchWebLocal(query);
|
| 22 |
+
if (config.SEARXNG_QUERY_URL) return searchSearxng(query);
|
| 23 |
+
if (config.SERPER_API_KEY) return searchSerper(query);
|
| 24 |
+
if (config.YDC_API_KEY) return searchYouApi(query);
|
| 25 |
+
if (config.SERPAPI_KEY) return searchSerpApi(query);
|
| 26 |
+
if (config.SERPSTACK_API_KEY) return searchSerpStack(query);
|
| 27 |
+
if (config.SEARCHAPI_KEY) return searchSearchApi(query);
|
| 28 |
+
if (config.BING_SUBSCRIPTION_KEY) return searchBing(query);
|
| 29 |
throw new Error(
|
| 30 |
"No configuration found for web search. Please set USE_LOCAL_WEBSEARCH, SEARXNG_QUERY_URL, SERPER_API_KEY, YDC_API_KEY, SERPSTACK_API_KEY, or SEARCHAPI_KEY in your environment variables."
|
| 31 |
);
|
src/lib/server/websearch/search/endpoints/bing.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import type { WebSearchSource } from "$lib/types/WebSearch";
|
| 2 |
-
import {
|
| 3 |
|
| 4 |
export default async function search(query: string): Promise<WebSearchSource[]> {
|
| 5 |
// const params = {
|
|
@@ -12,7 +12,7 @@ export default async function search(query: string): Promise<WebSearchSource[]>
|
|
| 12 |
{
|
| 13 |
method: "GET",
|
| 14 |
headers: {
|
| 15 |
-
"Ocp-Apim-Subscription-Key":
|
| 16 |
"Content-type": "application/json",
|
| 17 |
},
|
| 18 |
}
|
|
|
|
| 1 |
import type { WebSearchSource } from "$lib/types/WebSearch";
|
| 2 |
+
import { config } from "$lib/server/config";
|
| 3 |
|
| 4 |
export default async function search(query: string): Promise<WebSearchSource[]> {
|
| 5 |
// const params = {
|
|
|
|
| 12 |
{
|
| 13 |
method: "GET",
|
| 14 |
headers: {
|
| 15 |
+
"Ocp-Apim-Subscription-Key": config.BING_SUBSCRIPTION_KEY,
|
| 16 |
"Content-type": "application/json",
|
| 17 |
},
|
| 18 |
}
|
src/lib/server/websearch/search/endpoints/searchApi.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import {
|
| 2 |
import type { WebSearchSource } from "$lib/types/WebSearch";
|
| 3 |
|
| 4 |
export default async function search(query: string): Promise<WebSearchSource[]> {
|
|
@@ -7,7 +7,7 @@ export default async function search(query: string): Promise<WebSearchSource[]>
|
|
| 7 |
{
|
| 8 |
method: "GET",
|
| 9 |
headers: {
|
| 10 |
-
Authorization: `Bearer ${
|
| 11 |
"Content-type": "application/json",
|
| 12 |
},
|
| 13 |
}
|
|
|
|
| 1 |
+
import { config } from "$lib/server/config";
|
| 2 |
import type { WebSearchSource } from "$lib/types/WebSearch";
|
| 3 |
|
| 4 |
export default async function search(query: string): Promise<WebSearchSource[]> {
|
|
|
|
| 7 |
{
|
| 8 |
method: "GET",
|
| 9 |
headers: {
|
| 10 |
+
Authorization: `Bearer ${config.SEARCHAPI_KEY}`,
|
| 11 |
"Content-type": "application/json",
|
| 12 |
},
|
| 13 |
}
|
src/lib/server/websearch/search/endpoints/searxng.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import {
|
| 2 |
import { logger } from "$lib/server/logger";
|
| 3 |
import type { WebSearchSource } from "$lib/types/WebSearch";
|
| 4 |
import { isURL } from "$lib/utils/isUrl";
|
|
@@ -8,7 +8,7 @@ export default async function searchSearxng(query: string): Promise<WebSearchSou
|
|
| 8 |
setTimeout(() => abortController.abort(), 10000);
|
| 9 |
|
| 10 |
// Insert the query into the URL template
|
| 11 |
-
let url =
|
| 12 |
|
| 13 |
// Check if "&format=json" already exists in the URL
|
| 14 |
if (!url.includes("&format=json")) {
|
|
|
|
| 1 |
+
import { config } from "$lib/server/config";
|
| 2 |
import { logger } from "$lib/server/logger";
|
| 3 |
import type { WebSearchSource } from "$lib/types/WebSearch";
|
| 4 |
import { isURL } from "$lib/utils/isUrl";
|
|
|
|
| 8 |
setTimeout(() => abortController.abort(), 10000);
|
| 9 |
|
| 10 |
// Insert the query into the URL template
|
| 11 |
+
let url = config.SEARXNG_QUERY_URL.replace("<query>", query);
|
| 12 |
|
| 13 |
// Check if "&format=json" already exists in the URL
|
| 14 |
if (!url.includes("&format=json")) {
|
src/lib/server/websearch/search/endpoints/serpApi.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import {
|
| 2 |
import { getJson, type GoogleParameters } from "serpapi";
|
| 3 |
import type { WebSearchSource } from "$lib/types/WebSearch";
|
| 4 |
import { isURL } from "$lib/utils/isUrl";
|
|
@@ -15,7 +15,7 @@ export default async function searchWebSerpApi(query: string): Promise<WebSearch
|
|
| 15 |
hl: "en",
|
| 16 |
gl: "us",
|
| 17 |
google_domain: "google.com",
|
| 18 |
-
api_key:
|
| 19 |
} satisfies GoogleParameters;
|
| 20 |
|
| 21 |
// Show result as JSON
|
|
|
|
| 1 |
+
import { config } from "$lib/server/config";
|
| 2 |
import { getJson, type GoogleParameters } from "serpapi";
|
| 3 |
import type { WebSearchSource } from "$lib/types/WebSearch";
|
| 4 |
import { isURL } from "$lib/utils/isUrl";
|
|
|
|
| 15 |
hl: "en",
|
| 16 |
gl: "us",
|
| 17 |
google_domain: "google.com",
|
| 18 |
+
api_key: config.SERPAPI_KEY,
|
| 19 |
} satisfies GoogleParameters;
|
| 20 |
|
| 21 |
// Show result as JSON
|