nsarrazin commited on
Commit
48059af
·
unverified ·
1 Parent(s): 31a4a42

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
Files changed (50) hide show
  1. .env +9 -2
  2. chart/env/prod.yaml +1 -0
  3. package.json +1 -0
  4. scripts/config.ts +64 -0
  5. src/hooks.server.ts +45 -34
  6. src/lib/components/AssistantSettings.svelte +2 -2
  7. src/lib/components/DisclaimerModal.svelte +6 -5
  8. src/lib/components/LoginModal.svelte +6 -5
  9. src/lib/components/NavMenu.svelte +5 -4
  10. src/lib/components/ToolsMenu.svelte +2 -2
  11. src/lib/components/chat/AssistantIntroduction.svelte +3 -2
  12. src/lib/components/chat/ChatIntroduction.svelte +6 -9
  13. src/lib/components/icons/Logo.svelte +8 -5
  14. src/lib/jobs/refresh-assistants-counts.ts +3 -4
  15. src/lib/jobs/refresh-conversation-stats.ts +3 -4
  16. src/lib/migrations/lock.ts +7 -4
  17. src/lib/migrations/migrations.spec.ts +12 -11
  18. src/lib/migrations/migrations.ts +8 -9
  19. src/lib/server/adminToken.ts +6 -7
  20. src/lib/server/auth.ts +14 -14
  21. src/lib/server/config.ts +182 -0
  22. src/lib/server/database.ts +29 -15
  23. src/lib/server/embeddingEndpoints/hfApi/embeddingHfApi.ts +3 -3
  24. src/lib/server/embeddingEndpoints/openai/embeddingEndpoints.ts +2 -2
  25. src/lib/server/embeddingEndpoints/tei/embeddingEndpoints.ts +2 -2
  26. src/lib/server/embeddingModels.ts +2 -2
  27. src/lib/server/endpoints/anthropic/endpointAnthropic.ts +2 -2
  28. src/lib/server/endpoints/cloudflare/endpointCloudflare.ts +3 -3
  29. src/lib/server/endpoints/cohere/endpointCohere.ts +2 -2
  30. src/lib/server/endpoints/google/endpointGenAI.ts +2 -2
  31. src/lib/server/endpoints/llamacpp/endpointLlamacpp.ts +2 -2
  32. src/lib/server/endpoints/local/endpointLocal.ts +2 -2
  33. src/lib/server/endpoints/openai/endpointOai.ts +2 -2
  34. src/lib/server/endpoints/tgi/endpointTgi.ts +2 -2
  35. src/lib/server/logger.ts +2 -2
  36. src/lib/server/metrics.ts +6 -6
  37. src/lib/server/models.ts +15 -15
  38. src/lib/server/sendSlack.ts +3 -3
  39. src/lib/server/textGeneration/assistant.ts +4 -4
  40. src/lib/server/textGeneration/generate.ts +2 -2
  41. src/lib/server/textGeneration/title.ts +2 -2
  42. src/lib/server/tools/index.ts +2 -2
  43. src/lib/server/tools/utils.ts +3 -3
  44. src/lib/server/usageLimits.ts +3 -3
  45. src/lib/server/websearch/scrape/playwright.ts +6 -6
  46. src/lib/server/websearch/search/endpoints.ts +12 -12
  47. src/lib/server/websearch/search/endpoints/bing.ts +2 -2
  48. src/lib/server/websearch/search/endpoints/searchApi.ts +2 -2
  49. src/lib/server/websearch/search/endpoints/searxng.ts +2 -2
  50. 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 { env } from "$env/dynamic/private";
2
- import { env as envPublic } from "$env/dynamic/public";
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
- // TODO: move this code on a started server hook, instead of using a "building" flag
22
- if (!building) {
23
- // Set HF_TOKEN as a process variable for Transformers.JS to see it
24
- process.env.HF_TOKEN ??= env.HF_TOKEN;
25
 
26
- logger.info("Starting server...");
27
- initExitHandler();
 
 
28
 
29
- checkAndRunMigrations();
30
- if (env.ENABLE_ASSISTANTS) {
31
- refreshAssistantsCounts();
32
- }
33
- refreshConversationStats();
34
 
35
- // Init metrics server
36
- MetricsServer.getInstance();
 
 
 
37
 
38
- // Init AbortedGenerations refresh process
39
- AbortedGenerations.getInstance();
40
 
41
- adminTokenManager.displayToken();
 
42
 
43
- if (env.EXPOSE_API) {
44
- logger.warn(
45
- "The EXPOSE_API flag has been deprecated. The API is now required for chat-ui to work."
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 = env.ADMIN_API_SECRET || env.PARQUET_EXPORT_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(env.COOKIE_NAME);
116
 
117
  // if the trusted email header is set we use it to get the user email
118
- const email = env.TRUSTED_EMAIL_HEADER
119
- ? event.request.headers.get(env.TRUSTED_EMAIL_HEADER)
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 (event.url.pathname.startsWith(`${base}/api/`) && env.USE_HF_TOKEN_IN_API === "true") {
 
 
 
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
- ...(envPublic.PUBLIC_ORIGIN ? [new URL(envPublic.PUBLIC_ORIGIN).host] : []),
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
- !((env.MESSAGES_BEFORE_LOGIN ? parseInt(env.MESSAGES_BEFORE_LOGIN) : 0) > 0)
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
- envPublic.PUBLIC_APP_DISCLAIMER === "1"
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%", envPublic.PUBLIC_GOOGLE_ANALYTICS_ID);
300
  },
301
  });
302
 
303
  // Add CSP header to disallow framing if ALLOW_IFRAME is not "true"
304
- if (env.ALLOW_IFRAME !== "true") {
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 { isHuggingChat } from "$lib/utils/isHuggingChat";
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 { env as envPublic } from "$env/dynamic/public";
 
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
- {envPublic.PUBLIC_APP_NAME}
21
  </h2>
22
 
23
  <p class="text-lg font-semibold leading-snug text-gray-800" style="text-wrap: balance;">
24
- {envPublic.PUBLIC_APP_DESCRIPTION}
25
  </p>
26
 
27
  <p class="text-sm text-gray-500">
28
- {envPublic.PUBLIC_APP_DISCLAIMER_MESSAGE}
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 envPublic.PUBLIC_APP_NAME === "HuggingChat"}
65
  <span class="flex items-center">
66
  &nbsp;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
  &nbsp;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 { env as envPublic } from "$env/dynamic/public";
 
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
- {envPublic.PUBLIC_APP_NAME}
21
  </h2>
22
  <p class="text-balance text-lg font-semibold leading-snug text-gray-800">
23
- {envPublic.PUBLIC_APP_DESCRIPTION}
24
  </p>
25
  <p class="text-balance rounded-xl border bg-white/80 p-2 text-base text-gray-800">
26
- {envPublic.PUBLIC_APP_GUEST_MESSAGE}
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 envPublic.PUBLIC_APP_NAME === "HuggingChat"}
42
  <span class="flex items-center">
43
  &nbsp;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
  &nbsp;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 { env as envPublic } from "$env/dynamic/public";
 
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="{envPublic.PUBLIC_ORIGIN}{base}/"
94
  >
95
  <Logo classNames="mr-1" />
96
- {envPublic.PUBLIC_APP_NAME}
97
  </a>
98
  {#if $page.url.pathname !== base + "/"}
99
  <a
@@ -216,7 +217,7 @@
216
  >
217
  Settings
218
  </a>
219
- {#if envPublic.PUBLIC_APP_NAME === "HuggingChat"}
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 { isHuggingChat } from "$lib/utils/isHuggingChat";
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 { env as envPublic } from "$env/dynamic/public";
 
19
  import { page } from "$app/state";
20
 
21
  interface Props {
@@ -48,7 +49,7 @@
48
  );
49
 
50
  const prefix =
51
- envPublic.PUBLIC_SHARE_PREFIX || `${envPublic.PUBLIC_ORIGIN || page.url.origin}${base}`;
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 { env as envPublic } from "$env/dynamic/public";
 
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
- {envPublic.PUBLIC_APP_NAME}
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{envPublic.PUBLIC_VERSION}
35
  </div>
36
  </div>
37
  <p class="text-base text-gray-600 dark:text-gray-400">
38
- {envPublic.PUBLIC_APP_DESCRIPTION ||
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 announcementBanners as banner}
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 { env as envPublic } from "$env/dynamic/public";
 
4
  import { base } from "$app/paths";
5
 
6
  interface Props {
@@ -13,13 +14,14 @@
13
  <svelte:head>
14
  <link
15
  rel="preload"
16
- href="{envPublic.PUBLIC_ORIGIN || page.url.origin}{base}/{envPublic.PUBLIC_APP_ASSETS}/logo.svg"
 
17
  as="image"
18
  type="image/svg+xml"
19
  />
20
  </svelte:head>
21
 
22
- {#if envPublic.PUBLIC_APP_ASSETS === "chatui"}
23
  <svg
24
  height="30"
25
  width="30"
@@ -35,7 +37,8 @@
35
  {:else}
36
  <img
37
  class={classNames}
38
- alt="{envPublic.PUBLIC_APP_NAME} logo"
39
- src="{envPublic.PUBLIC_ORIGIN || page.url.origin}{base}/{envPublic.PUBLIC_APP_ASSETS}/logo.svg"
 
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
- const LOCK_KEY = "assistants.count";
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(LOCK_KEY, lockId);
80
 
81
  if (!hasLock) {
82
  lockId = null;
83
  }
84
  } else if (!hasLock) {
85
- lockId = (await acquireLock(LOCK_KEY)) || null;
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(LOCK_KEY, lockId);
245
 
246
  if (!hasLock) {
247
  lockId = null;
248
  }
249
  } else if (!hasLock) {
250
- lockId = (await acquireLock(LOCK_KEY)) || null;
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: string): Promise<ObjectId | false> {
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: string, lockId: ObjectId) {
26
  await collections.semaphores.deleteOne({
27
  _id: lockId,
28
  key,
29
  });
30
  }
31
 
32
- export async function isDBLocked(key: string): Promise<boolean> {
33
  const res = await collections.semaphores.countDocuments({
34
  key,
35
  });
36
  return res > 0;
37
  }
38
 
39
- export async function refreshLock(key: string, lockId: ObjectId): Promise<boolean> {
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(new Array(1000).fill(0).map(() => acquireLock(LOCK_KEY)));
 
 
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(LOCK_KEY);
30
  });
31
 
32
  it("should read the lock correctly", async () => {
33
- const lockId = await acquireLock(LOCK_KEY);
34
  assert(lockId);
35
- expect(await isDBLocked(LOCK_KEY)).toBe(true);
36
- expect(!!(await acquireLock(LOCK_KEY))).toBe(false);
37
- await releaseLock(LOCK_KEY, lockId);
38
- expect(await isDBLocked(LOCK_KEY)).toBe(false);
39
  });
40
 
41
  it("should refresh the lock", async () => {
42
- const lockId = await acquireLock(LOCK_KEY);
43
 
44
  assert(lockId);
45
 
@@ -47,7 +48,7 @@ describe(
47
 
48
  const updatedAtInitially = (await collections.semaphores.findOne({}))?.updatedAt;
49
 
50
- await refreshLock(LOCK_KEY, lockId);
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 { isHuggingChat } from "$lib/utils/isHuggingChat";
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(LOCK_KEY);
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(LOCK_KEY)) {
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(LOCK_KEY, lockId);
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(LOCK_KEY, lockId);
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 { env } from "$env/dynamic/private";
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 = env.ADMIN_TOKEN || v4();
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 env.ADMIN_CLI_LOGIN === "true";
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 = env.ADMIN_TOKEN || v4();
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 || env.ADMIN_TOKEN) return;
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 = (envPublic.PUBLIC_ORIGIN || `http://localhost:${port}`) + "?token=";
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 { env } from "$env/dynamic/private";
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(env.OPENID_CLIENT_ID),
36
- CLIENT_SECRET: stringWithDefault(env.OPENID_CLIENT_SECRET),
37
- PROVIDER_URL: stringWithDefault(env.OPENID_PROVIDER_URL),
38
- SCOPES: stringWithDefault(env.OPENID_SCOPES),
39
- NAME_CLAIM: stringWithDefault(env.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(env.OPENID_TOLERANCE),
44
- RESOURCE: stringWithDefault(env.OPENID_RESOURCE),
45
  ID_TOKEN_SIGNED_RESPONSE_ALG: z.string().optional(),
46
  })
47
- .parse(JSON5.parse(env.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 || env.ALLOW_INSECURE_COOKIES === "true" ? "lax" : "none")
54
- .parse(env.COOKIE_SAMESITE === "" ? undefined : env.COOKIE_SAMESITE);
55
 
56
  const secure = z
57
  .boolean()
58
- .default(!(dev || env.ALLOW_INSECURE_COOKIES === "true"))
59
- .parse(env.COOKIE_SECURE === "" ? undefined : env.COOKIE_SECURE === "true");
60
 
61
  export function refreshSessionCookie(cookies: Cookies, sessionId: string) {
62
- cookies.set(env.COOKIE_NAME, sessionId, {
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
- if (!env.MONGODB_URL) {
 
 
 
 
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: env.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : ""),
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: env.MONGODB_DIRECT_CONNECTION === "true",
60
  });
61
  } else {
62
- this.client = new MongoClient(env.MONGODB_URL, {
63
- directConnection: env.MONGODB_DIRECT_CONNECTION === "true",
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(env.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : ""));
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
- env.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : "")
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({ createdAt: 1 }, { expireAfterSeconds: 60 })
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 const collections = building
288
- ? ({} as unknown as ReturnType<typeof Database.prototype.getCollections>)
289
- : await Database.getInstance().then((db) => db.getCollections());
 
 
 
 
 
 
 
 
 
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 { env } from "$env/dynamic/private";
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 && env.HF_TOKEN ? "Bearer " + env.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 = `${env.HF_API_ROOT}/${model.id}`;
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 { env } from "$env/dynamic/private";
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(env.OPENAI_API_KEY),
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 { env } from "$env/dynamic/private";
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 && env.HF_TOKEN ? "Bearer " + env.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) => {
 
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 { env } from "$env/dynamic/private";
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
- env.TEXT_EMBEDDING_MODELS ||
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 { env } from "$env/dynamic/private";
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(env.ANTHROPIC_API_KEY ?? "sk-"),
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 { env } from "$env/dynamic/private";
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(env.CLOUDFLARE_ACCOUNT_ID),
12
- apiToken: z.string().default(env.CLOUDFLARE_API_TOKEN),
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 { env } from "$env/dynamic/private";
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(env.COHERE_API_TOKEN),
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 { env } from "$env/dynamic/private";
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(env.GOOGLE_GENAI_API_KEY),
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 { env } from "$env/dynamic/private";
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(env.HF_TOKEN ?? env.HF_ACCESS_TOKEN),
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 { env } from "$env/dynamic/private";
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
- env.MODELS_STORAGE_PATH ||
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 { env } from "$env/dynamic/private";
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(env.OPENAI_API_KEY || env.HF_TOKEN || "sk-"),
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 { env } from "$env/dynamic/private";
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(env.HF_TOKEN ?? env.HF_ACCESS_TOKEN),
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 { env } from "$env/dynamic/private";
4
 
5
  let options: pino.LoggerOptions = {};
6
 
@@ -15,4 +15,4 @@ if (dev) {
15
  };
16
  }
17
 
18
- export const logger = pino({ ...options, level: env.LOG_LEVEL ?? "info" });
 
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 { env } from "$env/dynamic/private";
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(env.METRICS_PORT || "5565");
45
  if (isNaN(port) || port < 0 || port > 65535) {
46
- logger.warn(`Invalid value for METRICS_PORT: ${env.METRICS_PORT}`);
47
  }
48
 
49
- if (env.METRICS_ENABLED !== "false" && env.METRICS_ENABLED !== "true") {
50
- logger.warn(`Invalid value for METRICS_ENABLED: ${env.METRICS_ENABLED}`);
51
  }
52
- if (env.METRICS_ENABLED === "true") {
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 { env } from "$env/dynamic/private";
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
- env.MODELS_STORAGE_PATH || 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,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(env.MODELS ?? "[]"));
141
 
142
- if (env.LOAD_GGUF_MODELS === "true" || modelsRaw.length === 0) {
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: `${env.HF_API_ROOT}/${m.name}`,
365
- accessToken: env.HF_TOKEN ?? env.HF_ACCESS_TOKEN,
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 = env.OLD_MODELS
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(env.OLD_MODELS))
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
- env.TASK_MODEL
473
- ? ((models.find((m) => m.name === env.TASK_MODEL) ||
474
- (await processModel(modelConfig.parse(JSON5.parse(env.TASK_MODEL))))) ??
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 { env } from "$env/dynamic/private";
2
  import { logger } from "$lib/server/logger";
3
 
4
  export async function sendSlack(text: string) {
5
- if (!env.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(env.WEBHOOK_URL_REPORT_ASSISTANT, {
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 { env } from "$env/dynamic/private";
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)) && env.ENABLE_LOCAL_FETCH !== "true") {
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
- env.ENABLE_ASSISTANTS_RAG === "true" &&
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 env.ENABLE_ASSISTANTS_RAG === "true" && Boolean(assistant?.dynamicPrompt);
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 { env } from "$env/dynamic/private";
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
- env.REASONING_SUMMARY === "true" &&
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 { env } from "$env/dynamic/private";
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 (env.LLM_SUMMARIZATION !== "true") {
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 { env } from "$env/dynamic/private";
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(env.TOOLS)) satisfies ConfigTool[];
 
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 { env } from "$env/dynamic/private";
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
- : ((env.HF_TOKEN ?? env.HF_ACCESS_TOKEN) as unknown as `hf_${string}`),
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 = env.IP_TOKEN_SECRET;
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 { env } from "$env/dynamic/private";
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 env.RATE_LIMIT;
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(env.USAGE_LIMITS));
 
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 { env } from "$env/dynamic/private";
11
  import { logger } from "$lib/server/logger";
12
  import { onExit } from "$lib/server/exitHandler";
13
 
14
  const blocker =
15
- env.PLAYWRIGHT_ADBLOCKER === "true"
16
  ? await PlaywrightBlocker.fromPrebuiltAdsAndTracking(fetch)
17
  .then((blker) => {
18
  const mostBlocked = blker.blockFonts().blockMedias().blockFrames().blockImages();
19
- if (env.WEBSEARCH_JAVASCRIPT === "false") return mostBlocked.blockScripts();
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 (env.PLAYWRIGHT_ADBLOCKER === "true") {
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(env.WEBSEARCH_TIMEOUT) })
86
  .catch(() => {
87
  console.warn(
88
- `Failed to load page within ${parseInt(env.WEBSEARCH_TIMEOUT) / 1000}s: ${url}`
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 { env } from "$env/dynamic/private";
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 (env.YDC_API_KEY) return WebSearchProvider.YOU;
14
- if (env.SEARXNG_QUERY_URL) return WebSearchProvider.SEARXNG;
15
- if (env.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 (env.USE_LOCAL_WEBSEARCH) return searchWebLocal(query);
22
- if (env.SEARXNG_QUERY_URL) return searchSearxng(query);
23
- if (env.SERPER_API_KEY) return searchSerper(query);
24
- if (env.YDC_API_KEY) return searchYouApi(query);
25
- if (env.SERPAPI_KEY) return searchSerpApi(query);
26
- if (env.SERPSTACK_API_KEY) return searchSerpStack(query);
27
- if (env.SEARCHAPI_KEY) return searchSearchApi(query);
28
- if (env.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
  );
 
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 { env } from "$env/dynamic/private";
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": env.BING_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 { env } from "$env/dynamic/private";
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 ${env.SEARCHAPI_KEY}`,
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 { env } from "$env/dynamic/private";
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 = env.SEARXNG_QUERY_URL.replace("<query>", query);
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 { env } from "$env/dynamic/private";
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: env.SERPAPI_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