diff --git a/.env b/.env index 98db06b8318cebff6a561a163e7d5c378c22a2a5..2d8eaa0101c26cfcddeb66e230236a9e64952ea6 100644 --- a/.env +++ b/.env @@ -1,11 +1,17 @@ # Use .env.local to change these variables # DO NOT EDIT THIS FILE WITH SENSITIVE DATA +### Config ### +ENABLE_CONFIG_MANAGER=true + ### MongoDB ### MONGODB_URL=#your mongodb URL here, use chat-ui-db image if you don't want to set this MONGODB_DB_NAME=chat-ui MONGODB_DIRECT_CONNECTION=false +### Local Storage ### +MODELS_STORAGE_PATH= # where are .gguf for model inference stored +MONGO_STORAGE_PATH= # where is the db folder stored ### Endpoints config ### HF_API_ROOT=https://api-inference.huggingface.co/models @@ -85,7 +91,7 @@ COOKIE_NAME=hf-chat # specify secure behaviour for cookies COOKIE_SAMESITE=# can be "lax", "strict", "none" or left empty COOKIE_SECURE=# set to true to only allow cookies over https - +TRUSTED_EMAIL_HEADER=# header to use to get the user email, only use if you know what you are doing ### Admin stuff ### ADMIN_CLI_LOGIN=true # set to false to disable the CLI login @@ -170,7 +176,7 @@ USE_HF_TOKEN_IN_API=false HF_ORG_ADMIN= HF_ORG_EARLY_ACCESS= WEBHOOK_URL_REPORT_ASSISTANT=#provide slack webhook url to get notified for reports/feature requests - +IP_TOKEN_SECRET= ### Metrics ### @@ -208,3 +214,4 @@ OPENID_NAME_CLAIM="name" # Change to "username" for some providers that do not p OPENID_PROVIDER_URL=https://huggingface.co # for Google, use https://accounts.google.com OPENID_TOLERANCE= OPENID_RESOURCE= +EXPOSE_API=# deprecated, API is now always exposed diff --git a/chart/env/prod.yaml b/chart/env/prod.yaml index e49f764ec7767f87ba6a6e6d498136de395f367f..050e1ecc5edc2921911cc1d909e7e48ad253f3c7 100644 --- a/chart/env/prod.yaml +++ b/chart/env/prod.yaml @@ -39,6 +39,7 @@ envVars: COOKIE_SECURE: "true" ENABLE_ASSISTANTS: "true" ENABLE_ASSISTANTS_RAG: "true" + ENABLE_CONFIG_MANAGER: "false" METRICS_PORT: 5565 LOG_LEVEL: "debug" METRICS_ENABLED: "true" diff --git a/package.json b/package.json index 83a910bdf235137ad4e92f3fb4dac6e4d0228fd4..6701d30f3c7778969d746eff3fbbe8f09f333748 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "test": "vitest", "updateLocalEnv": "vite-node --options.transformMode.ssr='/.*/' scripts/updateLocalEnv.ts", "populate": "vite-node --options.transformMode.ssr='/.*/' scripts/populate.ts", + "config": "vite-node --options.transformMode.ssr='/.*/' scripts/config.ts", "prepare": "husky" }, "devDependencies": { diff --git a/scripts/config.ts b/scripts/config.ts new file mode 100644 index 0000000000000000000000000000000000000000..2757ee96115b3b8ad2431f135a213e7e38bac0b4 --- /dev/null +++ b/scripts/config.ts @@ -0,0 +1,64 @@ +import sade from "sade"; + +// @ts-expect-error: vite-node makes the var available but the typescript compiler doesn't see them +import { config, ready } from "$lib/server/config"; + +const prog = sade("config"); +await ready; +prog + .command("clear") + .describe("Clear all config keys") + .action(async () => { + console.log("Clearing config..."); + await clear(); + }); + +prog + .command("add ") + .describe("Add a new config key") + .action(async (key: string, value: string) => { + await add(key, value); + }); + +prog + .command("remove ") + .describe("Remove a config key") + .action(async (key: string) => { + console.log(`Removing ${key}`); + await remove(key); + process.exit(0); + }); + +prog + .command("help") + .describe("Show help information") + .action(() => { + prog.help(); + process.exit(0); + }); + +async function clear() { + await config.clear(); + process.exit(0); +} + +async function add(key: string, value: string) { + if (!key || !value) { + console.error("Key and value are required"); + process.exit(1); + } + await config.set(key as keyof typeof config.keysFromEnv, value); + process.exit(0); +} + +async function remove(key: string) { + if (!key) { + console.error("Key is required"); + process.exit(1); + } + await config.delete(key as keyof typeof config.keysFromEnv); + process.exit(0); +} + +// Parse arguments and handle help automatically +prog.parse(process.argv); diff --git a/src/hooks.server.ts b/src/hooks.server.ts index e2361c24e96cbd9743a7c0b51188df83fdbfff66..94d869a7e7b3e412944d85388a588ee886de058a 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,6 +1,5 @@ -import { env } from "$env/dynamic/private"; -import { env as envPublic } from "$env/dynamic/public"; -import type { Handle, HandleServerError } from "@sveltejs/kit"; +import { config, ready } from "$lib/server/config"; +import type { Handle, HandleServerError, ServerInit } from "@sveltejs/kit"; import { collections } from "$lib/server/database"; import { base } from "$app/paths"; import { findUser, refreshSessionCookie, requiresUser } from "$lib/server/auth"; @@ -18,34 +17,39 @@ import { refreshAssistantsCounts } from "$lib/jobs/refresh-assistants-counts"; import { refreshConversationStats } from "$lib/jobs/refresh-conversation-stats"; import { adminTokenManager } from "$lib/server/adminToken"; -// TODO: move this code on a started server hook, instead of using a "building" flag -if (!building) { - // Set HF_TOKEN as a process variable for Transformers.JS to see it - process.env.HF_TOKEN ??= env.HF_TOKEN; +export const init: ServerInit = async () => { + // Wait for config to be fully loaded + await ready; - logger.info("Starting server..."); - initExitHandler(); + // TODO: move this code on a started server hook, instead of using a "building" flag + if (!building) { + // Set HF_TOKEN as a process variable for Transformers.JS to see it + process.env.HF_TOKEN ??= config.HF_TOKEN; - checkAndRunMigrations(); - if (env.ENABLE_ASSISTANTS) { - refreshAssistantsCounts(); - } - refreshConversationStats(); + logger.info("Starting server..."); + initExitHandler(); - // Init metrics server - MetricsServer.getInstance(); + checkAndRunMigrations(); + if (config.ENABLE_ASSISTANTS) { + refreshAssistantsCounts(); + } + refreshConversationStats(); - // Init AbortedGenerations refresh process - AbortedGenerations.getInstance(); + // Init metrics server + MetricsServer.getInstance(); - adminTokenManager.displayToken(); + // Init AbortedGenerations refresh process + AbortedGenerations.getInstance(); - if (env.EXPOSE_API) { - logger.warn( - "The EXPOSE_API flag has been deprecated. The API is now required for chat-ui to work." - ); + adminTokenManager.displayToken(); + + if (config.EXPOSE_API) { + logger.warn( + "The EXPOSE_API flag has been deprecated. The API is now required for chat-ui to work." + ); + } } -} +}; export const handleError: HandleServerError = async ({ error, event, status, message }) => { // handle 404 @@ -81,6 +85,10 @@ export const handleError: HandleServerError = async ({ error, event, status, mes }; export const handle: Handle = async ({ event, resolve }) => { + await ready.then(() => { + config.checkForUpdates(); + }); + logger.debug({ locals: event.locals, url: event.url.pathname, @@ -101,7 +109,7 @@ export const handle: Handle = async ({ event, resolve }) => { } if (event.url.pathname.startsWith(`${base}/admin/`) || event.url.pathname === `${base}/admin`) { - const ADMIN_SECRET = env.ADMIN_API_SECRET || env.PARQUET_EXPORT_SECRET; + const ADMIN_SECRET = config.ADMIN_API_SECRET || config.PARQUET_EXPORT_SECRET; if (!ADMIN_SECRET) { return errorResponse(500, "Admin API is not configured"); @@ -112,11 +120,11 @@ export const handle: Handle = async ({ event, resolve }) => { } } - const token = event.cookies.get(env.COOKIE_NAME); + const token = event.cookies.get(config.COOKIE_NAME); // if the trusted email header is set we use it to get the user email - const email = env.TRUSTED_EMAIL_HEADER - ? event.request.headers.get(env.TRUSTED_EMAIL_HEADER) + const email = config.TRUSTED_EMAIL_HEADER + ? event.request.headers.get(config.TRUSTED_EMAIL_HEADER) : null; let secretSessionId: string | null = null; @@ -145,7 +153,10 @@ export const handle: Handle = async ({ event, resolve }) => { if (user) { event.locals.user = user; } - } else if (event.url.pathname.startsWith(`${base}/api/`) && env.USE_HF_TOKEN_IN_API === "true") { + } else if ( + event.url.pathname.startsWith(`${base}/api/`) && + config.USE_HF_TOKEN_IN_API === "true" + ) { // if the request goes to the API and no user is available in the header // check if a bearer token is available in the Authorization header @@ -234,7 +245,7 @@ export const handle: Handle = async ({ event, resolve }) => { const validOrigins = [ new URL(event.request.url).host, - ...(envPublic.PUBLIC_ORIGIN ? [new URL(envPublic.PUBLIC_ORIGIN).host] : []), + ...(config.PUBLIC_ORIGIN ? [new URL(config.PUBLIC_ORIGIN).host] : []), ]; if (!validOrigins.includes(new URL(origin).host)) { @@ -262,7 +273,7 @@ export const handle: Handle = async ({ event, resolve }) => { if ( !event.locals.user && requiresUser && - !((env.MESSAGES_BEFORE_LOGIN ? parseInt(env.MESSAGES_BEFORE_LOGIN) : 0) > 0) + !((config.MESSAGES_BEFORE_LOGIN ? parseInt(config.MESSAGES_BEFORE_LOGIN) : 0) > 0) ) { return errorResponse(401, ERROR_MESSAGES.authOnly); } @@ -273,7 +284,7 @@ export const handle: Handle = async ({ event, resolve }) => { if ( !requiresUser && !event.url.pathname.startsWith(`${base}/settings`) && - envPublic.PUBLIC_APP_DISCLAIMER === "1" + config.PUBLIC_APP_DISCLAIMER === "1" ) { const hasAcceptedEthicsModal = await collections.settings.countDocuments({ sessionId: event.locals.sessionId, @@ -296,12 +307,12 @@ export const handle: Handle = async ({ event, resolve }) => { } replaced = true; - return chunk.html.replace("%gaId%", envPublic.PUBLIC_GOOGLE_ANALYTICS_ID); + return chunk.html.replace("%gaId%", config.PUBLIC_GOOGLE_ANALYTICS_ID); }, }); // Add CSP header to disallow framing if ALLOW_IFRAME is not "true" - if (env.ALLOW_IFRAME !== "true") { + if (config.ALLOW_IFRAME !== "true") { response.headers.append("Content-Security-Policy", "frame-ancestors 'none';"); } diff --git a/src/lib/components/AssistantSettings.svelte b/src/lib/components/AssistantSettings.svelte index eac3fa6e9fec2ea19be97f91d3b62885e66d8e4d..b00ac6b559677bef97c495e5756161726ba4ab58 100644 --- a/src/lib/components/AssistantSettings.svelte +++ b/src/lib/components/AssistantSettings.svelte @@ -12,7 +12,7 @@ import CarbonTools from "~icons/carbon/tools"; import { useSettingsStore } from "$lib/stores/settings"; - import { isHuggingChat } from "$lib/utils/isHuggingChat"; + import { publicConfig } from "$lib/utils/PublicConfig.svelte"; import IconInternet from "./icons/IconInternet.svelte"; import TokensCounter from "./TokensCounter.svelte"; import HoverTooltip from "./HoverTooltip.svelte"; @@ -457,7 +457,7 @@ >Internet access - {#if isHuggingChat} + {#if publicConfig.isHuggingChat} import { base } from "$app/paths"; import { page } from "$app/state"; - import { env as envPublic } from "$env/dynamic/public"; + import { publicConfig } from "$lib/utils/PublicConfig.svelte"; + import LogoHuggingFaceBorderless from "$lib/components/icons/LogoHuggingFaceBorderless.svelte"; import Modal from "$lib/components/Modal.svelte"; import { useSettingsStore } from "$lib/stores/settings"; @@ -17,15 +18,15 @@ >

- {envPublic.PUBLIC_APP_NAME} + {publicConfig.PUBLIC_APP_NAME}

- {envPublic.PUBLIC_APP_DESCRIPTION} + {publicConfig.PUBLIC_APP_DESCRIPTION}

- {envPublic.PUBLIC_APP_DISCLAIMER_MESSAGE} + {publicConfig.PUBLIC_APP_DISCLAIMER_MESSAGE}

@@ -61,7 +62,7 @@ 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" > Sign in - {#if envPublic.PUBLIC_APP_NAME === "HuggingChat"} + {#if publicConfig.PUBLIC_APP_NAME === "HuggingChat"}  with Hugging Face diff --git a/src/lib/components/LoginModal.svelte b/src/lib/components/LoginModal.svelte index f78188fa68fb9e1d059cf47440ac381d817077d7..6960afcdfa41f3cbd45af268b39203d77ef3f253 100644 --- a/src/lib/components/LoginModal.svelte +++ b/src/lib/components/LoginModal.svelte @@ -1,7 +1,8 @@ @@ -27,21 +24,21 @@
- {envPublic.PUBLIC_APP_NAME} + {publicConfig.PUBLIC_APP_NAME}
- v{envPublic.PUBLIC_VERSION} + v{publicConfig.PUBLIC_VERSION}

- {envPublic.PUBLIC_APP_DESCRIPTION || + {publicConfig.PUBLIC_APP_DESCRIPTION || "Making the community's best AI chat models available to everyone."}

- {#each announcementBanners as banner} + {#each JSON5.parse(publicConfig.PUBLIC_ANNOUNCEMENT_BANNERS || "[]") as banner} import { page } from "$app/state"; - import { env as envPublic } from "$env/dynamic/public"; + import { publicConfig } from "$lib/utils/PublicConfig.svelte"; + import { base } from "$app/paths"; interface Props { @@ -13,13 +14,14 @@ -{#if envPublic.PUBLIC_APP_ASSETS === "chatui"} +{#if publicConfig.PUBLIC_APP_ASSETS === "chatui"} {/if} diff --git a/src/lib/jobs/refresh-assistants-counts.ts b/src/lib/jobs/refresh-assistants-counts.ts index d9c79a3d454e5b75135c63765be8f7596306955d..a722ad907286e475e4af3182ce7946e3aab64303 100644 --- a/src/lib/jobs/refresh-assistants-counts.ts +++ b/src/lib/jobs/refresh-assistants-counts.ts @@ -4,8 +4,7 @@ import type { ObjectId } from "mongodb"; import { subDays } from "date-fns"; import { logger } from "$lib/server/logger"; import { collections } from "$lib/server/database"; -const LOCK_KEY = "assistants.count"; - +import { Semaphores } from "$lib/types/Semaphore"; let hasLock = false; let lockId: ObjectId | null = null; @@ -76,13 +75,13 @@ async function refreshAssistantsCountsHelper() { async function maintainLock() { if (hasLock && lockId) { - hasLock = await refreshLock(LOCK_KEY, lockId); + hasLock = await refreshLock(Semaphores.ASSISTANTS_COUNT, lockId); if (!hasLock) { lockId = null; } } else if (!hasLock) { - lockId = (await acquireLock(LOCK_KEY)) || null; + lockId = (await acquireLock(Semaphores.ASSISTANTS_COUNT)) || null; hasLock = !!lockId; } diff --git a/src/lib/jobs/refresh-conversation-stats.ts b/src/lib/jobs/refresh-conversation-stats.ts index b4e4bcd0c97b80b9d7a1d9e79553d30f9d6ec221..cebf4c6e29d8e883991bf6ea5c2d81589a215f23 100644 --- a/src/lib/jobs/refresh-conversation-stats.ts +++ b/src/lib/jobs/refresh-conversation-stats.ts @@ -3,6 +3,7 @@ import { CONVERSATION_STATS_COLLECTION, collections } from "$lib/server/database import { logger } from "$lib/server/logger"; import type { ObjectId } from "mongodb"; import { acquireLock, refreshLock } from "$lib/migrations/lock"; +import { Semaphores } from "$lib/types/Semaphore"; async function getLastComputationTime(): Promise { const lastStats = await collections.conversationStats.findOne({}, { sort: { "date.at": -1 } }); @@ -234,20 +235,18 @@ async function computeStats(params: { ); } -const LOCK_KEY = "conversation.stats"; - let hasLock = false; let lockId: ObjectId | null = null; async function maintainLock() { if (hasLock && lockId) { - hasLock = await refreshLock(LOCK_KEY, lockId); + hasLock = await refreshLock(Semaphores.CONVERSATION_STATS, lockId); if (!hasLock) { lockId = null; } } else if (!hasLock) { - lockId = (await acquireLock(LOCK_KEY)) || null; + lockId = (await acquireLock(Semaphores.CONVERSATION_STATS)) || null; hasLock = !!lockId; } diff --git a/src/lib/migrations/lock.ts b/src/lib/migrations/lock.ts index 6df5d261b418f68f6f7af2ca7ca9ecccae1f54e6..875bc0df9c18b56ca54102f9a9a9d099cc5793aa 100644 --- a/src/lib/migrations/lock.ts +++ b/src/lib/migrations/lock.ts @@ -1,10 +1,11 @@ import { collections } from "$lib/server/database"; import { ObjectId } from "mongodb"; +import type { Semaphores } from "$lib/types/Semaphore"; /** * Returns the lock id if the lock was acquired, false otherwise */ -export async function acquireLock(key: string): Promise { +export async function acquireLock(key: Semaphores): Promise { try { const id = new ObjectId(); @@ -13,6 +14,7 @@ export async function acquireLock(key: string): Promise { key, createdAt: new Date(), updatedAt: new Date(), + deleteAt: new Date(Date.now() + 1000 * 60 * 3), // 3 minutes }); return insert.acknowledged ? id : false; // true if the document was inserted @@ -22,21 +24,21 @@ export async function acquireLock(key: string): Promise { } } -export async function releaseLock(key: string, lockId: ObjectId) { +export async function releaseLock(key: Semaphores, lockId: ObjectId) { await collections.semaphores.deleteOne({ _id: lockId, key, }); } -export async function isDBLocked(key: string): Promise { +export async function isDBLocked(key: Semaphores): Promise { const res = await collections.semaphores.countDocuments({ key, }); return res > 0; } -export async function refreshLock(key: string, lockId: ObjectId): Promise { +export async function refreshLock(key: Semaphores, lockId: ObjectId): Promise { const result = await collections.semaphores.updateOne( { _id: lockId, @@ -45,6 +47,7 @@ export async function refreshLock(key: string, lockId: ObjectId): Promise { - const results = await Promise.all(new Array(1000).fill(0).map(() => acquireLock(LOCK_KEY))); + const results = await Promise.all( + new Array(1000).fill(0).map(() => acquireLock(Semaphores.TEST_MIGRATION)) + ); const locks = results.filter((r) => r); const semaphores = await collections.semaphores.find({}).toArray(); @@ -26,20 +27,20 @@ describe( expect(locks.length).toBe(1); expect(semaphores).toBeDefined(); expect(semaphores.length).toBe(1); - expect(semaphores?.[0].key).toBe(LOCK_KEY); + expect(semaphores?.[0].key).toBe(Semaphores.TEST_MIGRATION); }); it("should read the lock correctly", async () => { - const lockId = await acquireLock(LOCK_KEY); + const lockId = await acquireLock(Semaphores.TEST_MIGRATION); assert(lockId); - expect(await isDBLocked(LOCK_KEY)).toBe(true); - expect(!!(await acquireLock(LOCK_KEY))).toBe(false); - await releaseLock(LOCK_KEY, lockId); - expect(await isDBLocked(LOCK_KEY)).toBe(false); + expect(await isDBLocked(Semaphores.TEST_MIGRATION)).toBe(true); + expect(!!(await acquireLock(Semaphores.TEST_MIGRATION))).toBe(false); + await releaseLock(Semaphores.TEST_MIGRATION, lockId); + expect(await isDBLocked(Semaphores.TEST_MIGRATION)).toBe(false); }); it("should refresh the lock", async () => { - const lockId = await acquireLock(LOCK_KEY); + const lockId = await acquireLock(Semaphores.TEST_MIGRATION); assert(lockId); @@ -47,7 +48,7 @@ describe( const updatedAtInitially = (await collections.semaphores.findOne({}))?.updatedAt; - await refreshLock(LOCK_KEY, lockId); + await refreshLock(Semaphores.TEST_MIGRATION, lockId); const updatedAtAfterRefresh = (await collections.semaphores.findOne({}))?.updatedAt; diff --git a/src/lib/migrations/migrations.ts b/src/lib/migrations/migrations.ts index 0d67a7147c3e3eed365ecb7ddc95cf86f6b0110f..386982fd3825b1727788fe8df2bdec913ebb4572 100644 --- a/src/lib/migrations/migrations.ts +++ b/src/lib/migrations/migrations.ts @@ -1,10 +1,9 @@ import { Database } from "$lib/server/database"; import { migrations } from "./routines"; import { acquireLock, releaseLock, isDBLocked, refreshLock } from "./lock"; -import { isHuggingChat } from "$lib/utils/isHuggingChat"; +import { Semaphores } from "$lib/types/Semaphore"; import { logger } from "$lib/server/logger"; - -const LOCK_KEY = "migrations"; +import { config } from "$lib/server/config"; export async function checkAndRunMigrations() { // make sure all GUIDs are unique @@ -23,7 +22,7 @@ export async function checkAndRunMigrations() { // connect to the database const connectedClient = await (await Database.getInstance()).getClient().connect(); - const lockId = await acquireLock(LOCK_KEY); + const lockId = await acquireLock(Semaphores.MIGRATION); if (!lockId) { // another instance already has the lock, so we exit early @@ -33,7 +32,7 @@ export async function checkAndRunMigrations() { // Todo: is this necessary? Can we just return? // block until the lock is released - while (await isDBLocked(LOCK_KEY)) { + while (await isDBLocked(Semaphores.MIGRATION)) { await new Promise((resolve) => setTimeout(resolve, 1000)); } return; @@ -42,7 +41,7 @@ export async function checkAndRunMigrations() { // once here, we have the lock // make sure to refresh it regularly while it's running const refreshInterval = setInterval(async () => { - await refreshLock(LOCK_KEY, lockId); + await refreshLock(Semaphores.MIGRATION, lockId); }, 1000 * 10); // iterate over all migrations @@ -58,8 +57,8 @@ export async function checkAndRunMigrations() { } else { // check the modifiers to see if some cases match if ( - (migration.runForHuggingChat === "only" && !isHuggingChat) || - (migration.runForHuggingChat === "never" && isHuggingChat) + (migration.runForHuggingChat === "only" && !config.isHuggingChat) || + (migration.runForHuggingChat === "never" && config.isHuggingChat) ) { logger.debug( `[MIGRATIONS] "${migration.name}" should not be applied for this run. Skipping...` @@ -115,5 +114,5 @@ export async function checkAndRunMigrations() { logger.debug("[MIGRATIONS] All migrations applied. Releasing lock"); clearInterval(refreshInterval); - await releaseLock(LOCK_KEY, lockId); + await releaseLock(Semaphores.MIGRATION, lockId); } diff --git a/src/lib/server/adminToken.ts b/src/lib/server/adminToken.ts index 7b4ab0e1f6575a7971099ae48bc8261a2f12b66b..8ba4fdd60f7746cdd3f0f8f5208e165887d3bcdd 100644 --- a/src/lib/server/adminToken.ts +++ b/src/lib/server/adminToken.ts @@ -1,17 +1,16 @@ -import { env } from "$env/dynamic/private"; -import { env as envPublic } from "$env/dynamic/public"; +import { config } from "$lib/server/config"; import type { Session } from "$lib/types/Session"; import { logger } from "./logger"; import { v4 } from "uuid"; class AdminTokenManager { - private token = env.ADMIN_TOKEN || v4(); + private token = config.ADMIN_TOKEN || v4(); // contains all session ids that are currently admin sessions private adminSessions: Array = []; public get enabled() { // if open id is configured, disable the feature - return env.ADMIN_CLI_LOGIN === "true"; + return config.ADMIN_CLI_LOGIN === "true"; } public isAdmin(sessionId: Session["sessionId"]) { if (!this.enabled) return false; @@ -23,7 +22,7 @@ class AdminTokenManager { if (token === this.token) { logger.info(`[ADMIN] Token validated`); this.adminSessions.push(sessionId); - this.token = env.ADMIN_TOKEN || v4(); + this.token = config.ADMIN_TOKEN || v4(); return true; } @@ -36,7 +35,7 @@ class AdminTokenManager { public displayToken() { // if admin token is set, don't display it - if (!this.enabled || env.ADMIN_TOKEN) return; + if (!this.enabled || config.ADMIN_TOKEN) return; let port = process.argv.includes("--port") ? parseInt(process.argv[process.argv.indexOf("--port") + 1]) @@ -53,7 +52,7 @@ class AdminTokenManager { } } - const url = (envPublic.PUBLIC_ORIGIN || `http://localhost:${port}`) + "?token="; + const url = (config.PUBLIC_ORIGIN || `http://localhost:${port}`) + "?token="; logger.info(`[ADMIN] You can login with ${url + this.token}`); } } diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index ae170a8bc3fafd533e5b12d0e90a83846ea9b6c7..a922b767b3d00239bf86e2293ca44e0d41ace2b2 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -6,7 +6,7 @@ import { custom, } from "openid-client"; import { addHours, addWeeks } from "date-fns"; -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import { sha256 } from "$lib/utils/sha256"; import { z } from "zod"; import { dev } from "$app/environment"; @@ -32,34 +32,34 @@ const stringWithDefault = (value: string) => export const OIDConfig = z .object({ - CLIENT_ID: stringWithDefault(env.OPENID_CLIENT_ID), - CLIENT_SECRET: stringWithDefault(env.OPENID_CLIENT_SECRET), - PROVIDER_URL: stringWithDefault(env.OPENID_PROVIDER_URL), - SCOPES: stringWithDefault(env.OPENID_SCOPES), - NAME_CLAIM: stringWithDefault(env.OPENID_NAME_CLAIM).refine( + CLIENT_ID: stringWithDefault(config.OPENID_CLIENT_ID), + CLIENT_SECRET: stringWithDefault(config.OPENID_CLIENT_SECRET), + PROVIDER_URL: stringWithDefault(config.OPENID_PROVIDER_URL), + SCOPES: stringWithDefault(config.OPENID_SCOPES), + NAME_CLAIM: stringWithDefault(config.OPENID_NAME_CLAIM).refine( (el) => !["preferred_username", "email", "picture", "sub"].includes(el), { message: "nameClaim cannot be one of the restricted keys." } ), - TOLERANCE: stringWithDefault(env.OPENID_TOLERANCE), - RESOURCE: stringWithDefault(env.OPENID_RESOURCE), + TOLERANCE: stringWithDefault(config.OPENID_TOLERANCE), + RESOURCE: stringWithDefault(config.OPENID_RESOURCE), ID_TOKEN_SIGNED_RESPONSE_ALG: z.string().optional(), }) - .parse(JSON5.parse(env.OPENID_CONFIG || "{}")); + .parse(JSON5.parse(config.OPENID_CONFIG || "{}")); export const requiresUser = !!OIDConfig.CLIENT_ID && !!OIDConfig.CLIENT_SECRET; const sameSite = z .enum(["lax", "none", "strict"]) - .default(dev || env.ALLOW_INSECURE_COOKIES === "true" ? "lax" : "none") - .parse(env.COOKIE_SAMESITE === "" ? undefined : env.COOKIE_SAMESITE); + .default(dev || config.ALLOW_INSECURE_COOKIES === "true" ? "lax" : "none") + .parse(config.COOKIE_SAMESITE === "" ? undefined : config.COOKIE_SAMESITE); const secure = z .boolean() - .default(!(dev || env.ALLOW_INSECURE_COOKIES === "true")) - .parse(env.COOKIE_SECURE === "" ? undefined : env.COOKIE_SECURE === "true"); + .default(!(dev || config.ALLOW_INSECURE_COOKIES === "true")) + .parse(config.COOKIE_SECURE === "" ? undefined : config.COOKIE_SECURE === "true"); export function refreshSessionCookie(cookies: Cookies, sessionId: string) { - cookies.set(env.COOKIE_NAME, sessionId, { + cookies.set(config.COOKIE_NAME, sessionId, { path: "/", // So that it works inside the space's iframe sameSite, diff --git a/src/lib/server/config.ts b/src/lib/server/config.ts new file mode 100644 index 0000000000000000000000000000000000000000..6bc507aa43d6c5153795f0b4f98dffa93c874e6f --- /dev/null +++ b/src/lib/server/config.ts @@ -0,0 +1,182 @@ +import { env as publicEnv } from "$env/dynamic/public"; +import { env as serverEnv } from "$env/dynamic/private"; +import { publicConfig } from "$lib/utils/PublicConfig.svelte"; +import { building } from "$app/environment"; +import type { Collection } from "mongodb"; +import type { ConfigKey as ConfigKeyType } from "$lib/types/ConfigKey"; +import type { Semaphore } from "$lib/types/Semaphore"; +import { Semaphores } from "$lib/types/Semaphore"; + +export type PublicConfigKey = keyof typeof publicEnv; +const keysFromEnv = { ...publicEnv, ...serverEnv }; +export type ConfigKey = keyof typeof keysFromEnv; + +class ConfigManager { + private keysFromDB: Partial> = {}; + private isInitialized = false; + + private configCollection: Collection | undefined; + private semaphoreCollection: Collection | undefined; + private lastConfigUpdate: Date | undefined; + + async init() { + if (this.isInitialized) return; + + if (import.meta.env.MODE === "test") { + this.isInitialized = true; + return; + } + + const { collections, ready } = await import("./database"); + await ready; + if (!collections) { + throw new Error("Database not initialized"); + } + + this.configCollection = collections.config; + this.semaphoreCollection = collections.semaphores; + + await this.checkForUpdates().then(() => { + this.isInitialized = true; + }); + } + + get ConfigManagerEnabled() { + return serverEnv.ENABLE_CONFIG_MANAGER === "true" && import.meta.env.MODE !== "test"; + } + + get isHuggingChat() { + return this.get("PUBLIC_APP_ASSETS") === "huggingchat"; + } + + async checkForUpdates() { + if (await this.isConfigStale()) { + await this.updateConfig(); + } + } + + async isConfigStale(): Promise { + if (!this.lastConfigUpdate || !this.isInitialized) { + return true; + } + const count = await this.semaphoreCollection?.countDocuments({ + key: Semaphores.CONFIG_UPDATE, + updatedAt: { $gt: this.lastConfigUpdate }, + }); + return count !== undefined && count > 0; + } + + async updateConfig() { + const configs = (await this.configCollection?.find({}).toArray()) ?? []; + this.keysFromDB = configs.reduce( + (acc, curr) => { + acc[curr.key as ConfigKey] = curr.value; + return acc; + }, + {} as Record + ); + + this.lastConfigUpdate = new Date(); + } + + get(key: ConfigKey): string { + if (!this.ConfigManagerEnabled) { + return keysFromEnv[key] || ""; + } + return this.keysFromDB[key] || keysFromEnv[key] || ""; + } + + async updateSemaphore() { + await this.semaphoreCollection?.updateOne( + { key: Semaphores.CONFIG_UPDATE }, + { + $set: { + updatedAt: new Date(), + }, + $setOnInsert: { + createdAt: new Date(), + }, + }, + { upsert: true } + ); + } + + async set(key: ConfigKey, value: string) { + if (!this.ConfigManagerEnabled) throw new Error("Config manager is disabled"); + await this.configCollection?.updateOne({ key }, { $set: { value } }, { upsert: true }); + this.keysFromDB[key] = value; + await this.updateSemaphore(); + } + + async delete(key: ConfigKey) { + if (!this.ConfigManagerEnabled) throw new Error("Config manager is disabled"); + await this.configCollection?.deleteOne({ key }); + delete this.keysFromDB[key]; + await this.updateSemaphore(); + } + + async clear() { + if (!this.ConfigManagerEnabled) throw new Error("Config manager is disabled"); + await this.configCollection?.deleteMany({}); + this.keysFromDB = {}; + await this.updateSemaphore(); + } + + getPublicConfig() { + let config = { + ...Object.fromEntries( + Object.entries(keysFromEnv).filter(([key]) => key.startsWith("PUBLIC_")) + ), + } as Record; + + if (this.ConfigManagerEnabled) { + config = { + ...config, + ...Object.fromEntries( + Object.entries(this.keysFromDB).filter(([key]) => key.startsWith("PUBLIC_")) + ), + }; + } + + const publicEnvKeys = Object.keys(publicEnv); + + return Object.fromEntries( + Object.entries(config).filter(([key]) => publicEnvKeys.includes(key)) + ) as Record; + } +} + +// Create the instance and initialize it. +const configManager = new ConfigManager(); + +export const ready = (async () => { + if (!building) { + await configManager.init().then(() => { + publicConfig.init(configManager.getPublicConfig()); + }); + } +})(); + +type ConfigProxy = ConfigManager & { [K in ConfigKey]: string }; + +export const config: ConfigProxy = new Proxy(configManager, { + get(target, prop, receiver) { + if (prop in target) { + return Reflect.get(target, prop, receiver); + } + if (typeof prop === "string") { + return target.get(prop as ConfigKey); + } + return undefined; + }, + set(target, prop, value, receiver) { + if (prop in target) { + return Reflect.set(target, prop, value, receiver); + } + if (typeof prop === "string") { + target.set(prop as ConfigKey, value); + return true; + } + return false; + }, +}) as ConfigProxy; diff --git a/src/lib/server/database.ts b/src/lib/server/database.ts index ffe385de5674f9e2d866adb8bfcefdc3ee7cb0b8..604a0c8256270849dd2b23c64c3101a307a5d4e1 100644 --- a/src/lib/server/database.ts +++ b/src/lib/server/database.ts @@ -1,4 +1,3 @@ -import { env } from "$env/dynamic/private"; import { GridFSBucket, MongoClient } from "mongodb"; import type { Conversation } from "$lib/types/Conversation"; import type { SharedConversation } from "$lib/types/SharedConversation"; @@ -23,12 +22,11 @@ import { fileURLToPath } from "url"; import { dirname, join } from "path"; import { existsSync, mkdirSync } from "fs"; import { findRepoRoot } from "./findRepoRoot"; +import type { ConfigKey } from "$lib/types/ConfigKey"; +import { config } from "$lib/server/config"; export const CONVERSATION_STATS_COLLECTION = "conversations.stats"; -export const DB_FOLDER = - env.MONGO_STORAGE_PATH || join(findRepoRoot(dirname(fileURLToPath(import.meta.url))), "db"); - export class Database { private client?: MongoClient; private mongoServer?: MongoMemoryServer; @@ -36,7 +34,11 @@ export class Database { private static instance: Database; private async init() { - if (!env.MONGODB_URL) { + const DB_FOLDER = + config.MONGO_STORAGE_PATH || + join(findRepoRoot(dirname(fileURLToPath(import.meta.url))), "db"); + + if (!config.MONGODB_URL) { logger.warn("No MongoDB URL found, using in-memory server"); logger.info(`Using database path: ${DB_FOLDER}`); @@ -48,7 +50,7 @@ export class Database { this.mongoServer = await MongoMemoryServer.create({ instance: { - dbName: env.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : ""), + dbName: config.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : ""), dbPath: DB_FOLDER, }, binary: { @@ -56,11 +58,11 @@ export class Database { }, }); this.client = new MongoClient(this.mongoServer.getUri(), { - directConnection: env.MONGODB_DIRECT_CONNECTION === "true", + directConnection: config.MONGODB_DIRECT_CONNECTION === "true", }); } else { - this.client = new MongoClient(env.MONGODB_URL, { - directConnection: env.MONGODB_DIRECT_CONNECTION === "true", + this.client = new MongoClient(config.MONGODB_URL, { + directConnection: config.MONGODB_DIRECT_CONNECTION === "true", }); } @@ -68,7 +70,7 @@ export class Database { logger.error(err, "Connection error"); process.exit(1); }); - this.client.db(env.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : "")); + this.client.db(config.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : "")); this.client.on("open", () => this.initDatabase()); // Disconnect DB on exit @@ -108,7 +110,7 @@ export class Database { } const db = this.client.db( - env.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : "") + config.MONGODB_DB_NAME + (import.meta.env.MODE === "test" ? "-test" : "") ); const conversations = db.collection("conversations"); @@ -127,6 +129,7 @@ export class Database { const semaphores = db.collection("semaphores"); const tokenCaches = db.collection("tokens"); const tools = db.collection("tools"); + const configCollection = db.collection("config"); return { conversations, @@ -145,6 +148,7 @@ export class Database { semaphores, tokenCaches, tools, + config: configCollection, }; } @@ -168,6 +172,7 @@ export class Database { semaphores, tokenCaches, tools, + config, } = this.getCollections(); conversations @@ -258,7 +263,7 @@ export class Database { // Unique index for semaphore and migration results semaphores.createIndex({ key: 1 }, { unique: true }).catch((e) => logger.error(e)); semaphores - .createIndex({ createdAt: 1 }, { expireAfterSeconds: 60 }) + .createIndex({ deleteAt: 1 }, { expireAfterSeconds: 1 }) .catch((e) => logger.error(e)); tokenCaches .createIndex({ createdAt: 1 }, { expireAfterSeconds: 5 * 60 }) @@ -281,9 +286,18 @@ export class Database { sessionId: 1, }) .catch((e) => logger.error(e)); + + config.createIndex({ key: 1 }, { unique: true }).catch((e) => logger.error(e)); } } -export const collections = building - ? ({} as unknown as ReturnType) - : await Database.getInstance().then((db) => db.getCollections()); +export let collections: ReturnType; + +export const ready = (async () => { + if (!building) { + await Database.getInstance(); + collections = await Database.getInstance().then((db) => db.getCollections()); + } else { + collections = {} as unknown as ReturnType; + } +})(); diff --git a/src/lib/server/embeddingEndpoints/hfApi/embeddingHfApi.ts b/src/lib/server/embeddingEndpoints/hfApi/embeddingHfApi.ts index 367d11ddb846c878b9ebae9e4895a23f1a17b954..d60e37c9f83ffa41e07b64f3036b64e6fab40f6d 100644 --- a/src/lib/server/embeddingEndpoints/hfApi/embeddingHfApi.ts +++ b/src/lib/server/embeddingEndpoints/hfApi/embeddingHfApi.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import type { EmbeddingEndpoint, Embedding } from "../embeddingEndpoints"; import { chunk } from "$lib/utils/chunk"; -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import { logger } from "$lib/server/logger"; export const embeddingEndpointHfApiSchema = z.object({ @@ -11,14 +11,14 @@ export const embeddingEndpointHfApiSchema = z.object({ authorization: z .string() .optional() - .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 + .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 }); export async function embeddingEndpointHfApi( input: z.input ): Promise { const { model, authorization } = embeddingEndpointHfApiSchema.parse(input); - const url = `${env.HF_API_ROOT}/${model.id}`; + const url = `${config.HF_API_ROOT}/${model.id}`; return async ({ inputs }) => { const batchesInputs = chunk(inputs, 128); diff --git a/src/lib/server/embeddingEndpoints/openai/embeddingEndpoints.ts b/src/lib/server/embeddingEndpoints/openai/embeddingEndpoints.ts index d1725ffad1a62de34ecc03a94846be644ea3e845..d3c1edf161f10a6c5565091991a61d1c7f12e223 100644 --- a/src/lib/server/embeddingEndpoints/openai/embeddingEndpoints.ts +++ b/src/lib/server/embeddingEndpoints/openai/embeddingEndpoints.ts @@ -1,14 +1,14 @@ import { z } from "zod"; import type { EmbeddingEndpoint, Embedding } from "../embeddingEndpoints"; import { chunk } from "$lib/utils/chunk"; -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; export const embeddingEndpointOpenAIParametersSchema = z.object({ weight: z.number().int().positive().default(1), model: z.any(), type: z.literal("openai"), url: z.string().url().default("https://api.openai.com/v1/embeddings"), - apiKey: z.string().default(env.OPENAI_API_KEY), + apiKey: z.string().default(config.OPENAI_API_KEY), defaultHeaders: z.record(z.string()).default({}), }); diff --git a/src/lib/server/embeddingEndpoints/tei/embeddingEndpoints.ts b/src/lib/server/embeddingEndpoints/tei/embeddingEndpoints.ts index c999ceba7da550ce5f7eb1ef6e263fb3142f4101..fa096aee58c95d0c29f9104f695afc6b440f109c 100644 --- a/src/lib/server/embeddingEndpoints/tei/embeddingEndpoints.ts +++ b/src/lib/server/embeddingEndpoints/tei/embeddingEndpoints.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import type { EmbeddingEndpoint, Embedding } from "../embeddingEndpoints"; import { chunk } from "$lib/utils/chunk"; -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import { logger } from "$lib/server/logger"; export const embeddingEndpointTeiParametersSchema = z.object({ @@ -12,7 +12,7 @@ export const embeddingEndpointTeiParametersSchema = z.object({ authorization: z .string() .optional() - .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 + .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 }); const getModelInfoByUrl = async (url: string, authorization?: string) => { diff --git a/src/lib/server/embeddingModels.ts b/src/lib/server/embeddingModels.ts index 67ad8fe5b1edc61aa6cde738e9ee18ca477f304f..9bbe8cf9c0613a18257c4e40e77410fb8877288e 100644 --- a/src/lib/server/embeddingModels.ts +++ b/src/lib/server/embeddingModels.ts @@ -1,4 +1,4 @@ -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import { z } from "zod"; import { sum } from "$lib/utils/sum"; @@ -29,7 +29,7 @@ const modelConfig = z.object({ // Default embedding model for backward compatibility const rawEmbeddingModelJSON = - env.TEXT_EMBEDDING_MODELS || + config.TEXT_EMBEDDING_MODELS || `[ { "name": "Xenova/gte-small", diff --git a/src/lib/server/endpoints/anthropic/endpointAnthropic.ts b/src/lib/server/endpoints/anthropic/endpointAnthropic.ts index 2677bdd07b6104232c2e0731809a9b31da92963a..a542e932d94be28f62a63402a7abb5fa054c4c19 100644 --- a/src/lib/server/endpoints/anthropic/endpointAnthropic.ts +++ b/src/lib/server/endpoints/anthropic/endpointAnthropic.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import type { Endpoint } from "../endpoints"; -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import type { TextGenerationStreamOutput } from "@huggingface/inference"; import { createImageProcessorOptionsValidator } from "../images"; import { endpointMessagesToAnthropicMessages, addToolResults } from "./utils"; @@ -22,7 +22,7 @@ export const endpointAnthropicParametersSchema = z.object({ model: z.any(), type: z.literal("anthropic"), baseURL: z.string().url().default("https://api.anthropic.com"), - apiKey: z.string().default(env.ANTHROPIC_API_KEY ?? "sk-"), + apiKey: z.string().default(config.ANTHROPIC_API_KEY ?? "sk-"), defaultHeaders: z.record(z.string()).optional(), defaultQuery: z.record(z.string()).optional(), multimodal: z diff --git a/src/lib/server/endpoints/cloudflare/endpointCloudflare.ts b/src/lib/server/endpoints/cloudflare/endpointCloudflare.ts index ca6390b9621d01382ca608b9dac84fcb6a7c65b8..933c78ffab54691f93ab87d93481922735a3a450 100644 --- a/src/lib/server/endpoints/cloudflare/endpointCloudflare.ts +++ b/src/lib/server/endpoints/cloudflare/endpointCloudflare.ts @@ -1,15 +1,15 @@ import { z } from "zod"; import type { Endpoint } from "../endpoints"; import type { TextGenerationStreamOutput } from "@huggingface/inference"; -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import { logger } from "$lib/server/logger"; export const endpointCloudflareParametersSchema = z.object({ weight: z.number().int().positive().default(1), model: z.any(), type: z.literal("cloudflare"), - accountId: z.string().default(env.CLOUDFLARE_ACCOUNT_ID), - apiToken: z.string().default(env.CLOUDFLARE_API_TOKEN), + accountId: z.string().default(config.CLOUDFLARE_ACCOUNT_ID), + apiToken: z.string().default(config.CLOUDFLARE_API_TOKEN), }); export async function endpointCloudflare( diff --git a/src/lib/server/endpoints/cohere/endpointCohere.ts b/src/lib/server/endpoints/cohere/endpointCohere.ts index c257f40422daf68dee4c52b8e608f0917e2ab9f7..92fcdc7ce5bc061e5e65c0f0bb258149fbbe260e 100644 --- a/src/lib/server/endpoints/cohere/endpointCohere.ts +++ b/src/lib/server/endpoints/cohere/endpointCohere.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import type { Endpoint } from "../endpoints"; import type { TextGenerationStreamOutput } from "@huggingface/inference"; import type { Cohere, CohereClient } from "cohere-ai"; @@ -12,7 +12,7 @@ export const endpointCohereParametersSchema = z.object({ weight: z.number().int().positive().default(1), model: z.any(), type: z.literal("cohere"), - apiKey: z.string().default(env.COHERE_API_TOKEN), + apiKey: z.string().default(config.COHERE_API_TOKEN), clientName: z.string().optional(), raw: z.boolean().default(false), forceSingleStep: z.boolean().default(true), diff --git a/src/lib/server/endpoints/google/endpointGenAI.ts b/src/lib/server/endpoints/google/endpointGenAI.ts index 0a8540810444e50230084a5c8fc61fa7a1f9978a..032fc5969b9508eedf50b86bda49017a16f887aa 100644 --- a/src/lib/server/endpoints/google/endpointGenAI.ts +++ b/src/lib/server/endpoints/google/endpointGenAI.ts @@ -6,13 +6,13 @@ import type { TextGenerationStreamOutput } from "@huggingface/inference"; import type { Endpoint } from "../endpoints"; import { createImageProcessorOptionsValidator, makeImageProcessor } from "../images"; import type { ImageProcessorOptions } from "../images"; -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; export const endpointGenAIParametersSchema = z.object({ weight: z.number().int().positive().default(1), model: z.any(), type: z.literal("genai"), - apiKey: z.string().default(env.GOOGLE_GENAI_API_KEY), + apiKey: z.string().default(config.GOOGLE_GENAI_API_KEY), safetyThreshold: z .enum([ HarmBlockThreshold.HARM_BLOCK_THRESHOLD_UNSPECIFIED, diff --git a/src/lib/server/endpoints/llamacpp/endpointLlamacpp.ts b/src/lib/server/endpoints/llamacpp/endpointLlamacpp.ts index 944dbf4999652aeb8f4e5f8fd9374b9f9553f706..671c1a50e8e38ab00d7ae8ae583d63ffb5e2ce58 100644 --- a/src/lib/server/endpoints/llamacpp/endpointLlamacpp.ts +++ b/src/lib/server/endpoints/llamacpp/endpointLlamacpp.ts @@ -1,4 +1,4 @@ -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import { buildPrompt } from "$lib/buildPrompt"; import type { TextGenerationStreamOutput } from "@huggingface/inference"; import type { Endpoint } from "../endpoints"; @@ -11,7 +11,7 @@ export const endpointLlamacppParametersSchema = z.object({ type: z.literal("llamacpp"), url: z.string().url().default("http://127.0.0.1:8080"), // legacy, feel free to remove in breaking change update baseURL: z.string().url().optional(), - accessToken: z.string().default(env.HF_TOKEN ?? env.HF_ACCESS_TOKEN), + accessToken: z.string().default(config.HF_TOKEN ?? config.HF_ACCESS_TOKEN), }); export function endpointLlamacpp( diff --git a/src/lib/server/endpoints/local/endpointLocal.ts b/src/lib/server/endpoints/local/endpointLocal.ts index 7c6ba4d8efe1e542b7f457780ecc2a9ef0a740b2..828ddefaceb1e4141fb9e7e34ac9fd7b43481aa9 100644 --- a/src/lib/server/endpoints/local/endpointLocal.ts +++ b/src/lib/server/endpoints/local/endpointLocal.ts @@ -1,4 +1,4 @@ -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import type { Endpoint, EndpointMessage, @@ -50,7 +50,7 @@ export async function endpointLocal( // Setup model path and folder const path = modelPathInput ?? `hf:${model.id ?? model.name}`; const modelFolder = - env.MODELS_STORAGE_PATH || + config.MODELS_STORAGE_PATH || join(findRepoRoot(dirname(fileURLToPath(import.meta.url))), "models"); // Initialize Llama model diff --git a/src/lib/server/endpoints/openai/endpointOai.ts b/src/lib/server/endpoints/openai/endpointOai.ts index 24d7dcb577e8337710fbe32cb541d727b880e907..2fa7ec2e88f1ca593c38f4fd83414710dd74bc63 100644 --- a/src/lib/server/endpoints/openai/endpointOai.ts +++ b/src/lib/server/endpoints/openai/endpointOai.ts @@ -12,7 +12,7 @@ import type { } from "openai/resources/chat/completions"; import type { FunctionDefinition, FunctionParameters } from "openai/resources/shared"; import { buildPrompt } from "$lib/buildPrompt"; -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import type { Endpoint } from "../endpoints"; import type OpenAI from "openai"; import { createImageProcessorOptionsValidator, makeImageProcessor } from "../images"; @@ -90,7 +90,7 @@ export const endpointOAIParametersSchema = z.object({ model: z.any(), type: z.literal("openai"), baseURL: z.string().url().default("https://api.openai.com/v1"), - apiKey: z.string().default(env.OPENAI_API_KEY || env.HF_TOKEN || "sk-"), + apiKey: z.string().default(config.OPENAI_API_KEY || config.HF_TOKEN || "sk-"), completion: z .union([z.literal("completions"), z.literal("chat_completions")]) .default("chat_completions"), diff --git a/src/lib/server/endpoints/tgi/endpointTgi.ts b/src/lib/server/endpoints/tgi/endpointTgi.ts index a9e6d47a6de52cfbb4fc471003002c9af7af1d32..0fc9410c06d5c802da13208306ff4a1435b6bffe 100644 --- a/src/lib/server/endpoints/tgi/endpointTgi.ts +++ b/src/lib/server/endpoints/tgi/endpointTgi.ts @@ -1,4 +1,4 @@ -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import { buildPrompt } from "$lib/buildPrompt"; import { textGenerationStream } from "@huggingface/inference"; import type { Endpoint, EndpointMessage } from "../endpoints"; @@ -14,7 +14,7 @@ export const endpointTgiParametersSchema = z.object({ model: z.any(), type: z.literal("tgi"), url: z.string().url(), - accessToken: z.string().default(env.HF_TOKEN ?? env.HF_ACCESS_TOKEN), + accessToken: z.string().default(config.HF_TOKEN ?? config.HF_ACCESS_TOKEN), authorization: z.string().optional(), multimodal: z .object({ diff --git a/src/lib/server/logger.ts b/src/lib/server/logger.ts index b01b7692e3a33b20df5a86572cd8d748113ffda7..18ae84e537ac389c3fc62888b68af51d400d0769 100644 --- a/src/lib/server/logger.ts +++ b/src/lib/server/logger.ts @@ -1,6 +1,6 @@ import pino from "pino"; import { dev } from "$app/environment"; -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; let options: pino.LoggerOptions = {}; @@ -15,4 +15,4 @@ if (dev) { }; } -export const logger = pino({ ...options, level: env.LOG_LEVEL ?? "info" }); +export const logger = pino({ ...options, level: config.LOG_LEVEL ?? "info" }); diff --git a/src/lib/server/metrics.ts b/src/lib/server/metrics.ts index 42d9fb9c84082fef2f621e554f202888a4ba7e33..d198830edf77ec1c64ce69c1b9b8476bfe7cd0a0 100644 --- a/src/lib/server/metrics.ts +++ b/src/lib/server/metrics.ts @@ -1,7 +1,7 @@ import { collectDefaultMetrics, Registry, Counter, Summary } from "prom-client"; import express from "express"; import { logger } from "$lib/server/logger"; -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import type { Model } from "$lib/types/Model"; import { onExit } from "./exitHandler"; import { promisify } from "util"; @@ -41,15 +41,15 @@ export class MetricsServer { private constructor() { const app = express(); - const port = Number(env.METRICS_PORT || "5565"); + const port = Number(config.METRICS_PORT || "5565"); if (isNaN(port) || port < 0 || port > 65535) { - logger.warn(`Invalid value for METRICS_PORT: ${env.METRICS_PORT}`); + logger.warn(`Invalid value for METRICS_PORT: ${config.METRICS_PORT}`); } - if (env.METRICS_ENABLED !== "false" && env.METRICS_ENABLED !== "true") { - logger.warn(`Invalid value for METRICS_ENABLED: ${env.METRICS_ENABLED}`); + if (config.METRICS_ENABLED !== "false" && config.METRICS_ENABLED !== "true") { + logger.warn(`Invalid value for METRICS_ENABLED: ${config.METRICS_ENABLED}`); } - if (env.METRICS_ENABLED === "true") { + if (config.METRICS_ENABLED === "true") { const server = app.listen(port, () => { logger.info(`Metrics server listening on port ${port}`); }); diff --git a/src/lib/server/models.ts b/src/lib/server/models.ts index 2a5288ae09498932be5b886b573db85cc0a53021..3c34c4696cc4b4f58affa8dfdbccf0ccd106d4be 100644 --- a/src/lib/server/models.ts +++ b/src/lib/server/models.ts @@ -1,4 +1,4 @@ -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import type { ChatTemplateInput } from "$lib/types/Template"; import { compileTemplate } from "$lib/utils/template"; import { z } from "zod"; @@ -13,7 +13,6 @@ import JSON5 from "json5"; import { getTokenizer } from "$lib/utils/getTokenizer"; import { logger } from "$lib/server/logger"; import { ToolResultStatus, type ToolInput } from "$lib/types/Tool"; -import { isHuggingChat } from "$lib/utils/isHuggingChat"; import { join, dirname } from "path"; import { resolveModelFile, readGgufFileInfo } from "node-llama-cpp"; import { fileURLToPath } from "url"; @@ -22,7 +21,8 @@ import { Template } from "@huggingface/jinja"; import { readdirSync } from "fs"; export const MODELS_FOLDER = - env.MODELS_STORAGE_PATH || join(findRepoRoot(dirname(fileURLToPath(import.meta.url))), "models"); + config.MODELS_STORAGE_PATH || + join(findRepoRoot(dirname(fileURLToPath(import.meta.url))), "models"); type Optional = Pick, K> & Omit; @@ -137,9 +137,9 @@ const turnStringIntoLocalModel = z.preprocess((obj: unknown) => { } satisfies z.input; }, modelConfig); -let modelsRaw = z.array(turnStringIntoLocalModel).parse(JSON5.parse(env.MODELS ?? "[]")); +let modelsRaw = z.array(turnStringIntoLocalModel).parse(JSON5.parse(config.MODELS ?? "[]")); -if (env.LOAD_GGUF_MODELS === "true" || modelsRaw.length === 0) { +if (config.LOAD_GGUF_MODELS === "true" || modelsRaw.length === 0) { const parsedGgufModels = z.array(modelConfig).parse(ggufModelsConfig); modelsRaw = [...modelsRaw, ...parsedGgufModels]; } @@ -240,7 +240,7 @@ async function getChatPromptRender( // or use the `rag` mode without the citations const id = m.id ?? m.name; - if (isHuggingChat && id.startsWith("CohereForAI")) { + if (config.isHuggingChat && id.startsWith("CohereForAI")) { formattedMessages = [ { role: "user", @@ -267,7 +267,7 @@ async function getChatPromptRender( }, ...formattedMessages, ]; - } else if (isHuggingChat && id.startsWith("meta-llama")) { + } else if (config.isHuggingChat && id.startsWith("meta-llama")) { const results = toolResults.flatMap((result) => { if (result.status === ToolResultStatus.Error) { return [ @@ -361,8 +361,8 @@ const addEndpoint = (m: Awaited>) => ({ if (!m.endpoints) { return endpointTgi({ type: "tgi", - url: `${env.HF_API_ROOT}/${m.name}`, - accessToken: env.HF_TOKEN ?? env.HF_ACCESS_TOKEN, + url: `${config.HF_API_ROOT}/${m.name}`, + accessToken: config.HF_TOKEN ?? config.HF_ACCESS_TOKEN, weight: 1, model: m, }); @@ -416,7 +416,7 @@ const addEndpoint = (m: Awaited>) => ({ }, }); -const inferenceApiIds = isHuggingChat +const inferenceApiIds = config.isHuggingChat ? await fetch( "https://huggingface.co/api/models?pipeline_tag=text-generation&inference=warm&filter=conversational" ) @@ -447,7 +447,7 @@ export const validModelIdSchema = z.enum(models.map((m) => m.id) as [string, ... export const defaultModel = models[0]; // Models that have been deprecated -export const oldModels = env.OLD_MODELS +export const oldModels = config.OLD_MODELS ? z .array( z.object({ @@ -457,7 +457,7 @@ export const oldModels = env.OLD_MODELS transferTo: validModelIdSchema.optional(), }) ) - .parse(JSON5.parse(env.OLD_MODELS)) + .parse(JSON5.parse(config.OLD_MODELS)) .map((m) => ({ ...m, id: m.id || m.name, displayName: m.displayName || m.name })) : []; @@ -469,9 +469,9 @@ export const validateModel = (_models: BackendModel[]) => { // 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 export const taskModel = addEndpoint( - env.TASK_MODEL - ? ((models.find((m) => m.name === env.TASK_MODEL) || - (await processModel(modelConfig.parse(JSON5.parse(env.TASK_MODEL))))) ?? + config.TASK_MODEL + ? ((models.find((m) => m.name === config.TASK_MODEL) || + (await processModel(modelConfig.parse(JSON5.parse(config.TASK_MODEL))))) ?? defaultModel) : defaultModel ); diff --git a/src/lib/server/sendSlack.ts b/src/lib/server/sendSlack.ts index 2b0d11da3ab766b6d09ac839c397d1709e822563..cd892b34b9a25c4313ac94f5d5a12f0f8d1f9c08 100644 --- a/src/lib/server/sendSlack.ts +++ b/src/lib/server/sendSlack.ts @@ -1,13 +1,13 @@ -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import { logger } from "$lib/server/logger"; export async function sendSlack(text: string) { - if (!env.WEBHOOK_URL_REPORT_ASSISTANT) { + if (!config.WEBHOOK_URL_REPORT_ASSISTANT) { logger.warn("WEBHOOK_URL_REPORT_ASSISTANT is not set, tried to send a slack message."); return; } - const res = await fetch(env.WEBHOOK_URL_REPORT_ASSISTANT, { + const res = await fetch(config.WEBHOOK_URL_REPORT_ASSISTANT, { method: "POST", headers: { "Content-type": "application/json", diff --git a/src/lib/server/textGeneration/assistant.ts b/src/lib/server/textGeneration/assistant.ts index 1410513c7928cd6117113c5970bcca0914e412c0..ae986c63c6d612c17d3c4c65b5c7a04c9cf95cc2 100644 --- a/src/lib/server/textGeneration/assistant.ts +++ b/src/lib/server/textGeneration/assistant.ts @@ -1,5 +1,5 @@ import { isURLLocal } from "../isURLLocal"; -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import { collections } from "$lib/server/database"; import type { Assistant } from "$lib/types/Assistant"; import type { ObjectId } from "mongodb"; @@ -20,7 +20,7 @@ export async function processPreprompt(preprompt: string, user_message: string | const urlString = match[2]; try { const url = new URL(urlString); - if ((await isURLLocal(url)) && env.ENABLE_LOCAL_FETCH !== "true") { + if ((await isURLLocal(url)) && config.ENABLE_LOCAL_FETCH !== "true") { throw new Error("URL couldn't be fetched, it resolved to a local address."); } @@ -62,7 +62,7 @@ export async function getAssistantById(id?: ObjectId) { export function assistantHasWebSearch(assistant?: Pick | null) { return ( - env.ENABLE_ASSISTANTS_RAG === "true" && + config.ENABLE_ASSISTANTS_RAG === "true" && !!assistant?.rag && (assistant.rag.allowedLinks.length > 0 || assistant.rag.allowedDomains.length > 0 || @@ -71,5 +71,5 @@ export function assistantHasWebSearch(assistant?: Pick | null) } export function assistantHasDynamicPrompt(assistant?: Pick) { - return env.ENABLE_ASSISTANTS_RAG === "true" && Boolean(assistant?.dynamicPrompt); + return config.ENABLE_ASSISTANTS_RAG === "true" && Boolean(assistant?.dynamicPrompt); } diff --git a/src/lib/server/textGeneration/generate.ts b/src/lib/server/textGeneration/generate.ts index fe13d548e9a184acbd5adcc84620f3ca093fc77c..4ed2c9f8c2f86d289eda18c089b4384f2b31ad17 100644 --- a/src/lib/server/textGeneration/generate.ts +++ b/src/lib/server/textGeneration/generate.ts @@ -1,4 +1,4 @@ -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import type { ToolResult, Tool } from "$lib/types/Tool"; import { MessageReasoningUpdateType, @@ -171,7 +171,7 @@ Do not use prefixes such as Response: or Answer: when answering to the user.`, // create a new status every 5 seconds if ( - env.REASONING_SUMMARY === "true" && + config.REASONING_SUMMARY === "true" && new Date().getTime() - lastReasoningUpdate.getTime() > 4000 ) { lastReasoningUpdate = new Date(); diff --git a/src/lib/server/textGeneration/title.ts b/src/lib/server/textGeneration/title.ts index c75a92cda78afaadabedb8bcd3e7f36c8a744bef..2a908c9f3bd5fbc1f2143ed1fa42a88c6f037bb3 100644 --- a/src/lib/server/textGeneration/title.ts +++ b/src/lib/server/textGeneration/title.ts @@ -1,4 +1,4 @@ -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import { generateFromDefaultEndpoint } from "$lib/server/generateFromDefaultEndpoint"; import { logger } from "$lib/server/logger"; import { MessageUpdateType, type MessageUpdate } from "$lib/types/MessageUpdate"; @@ -29,7 +29,7 @@ export async function* generateTitleForConversation( } export async function generateTitle(prompt: string) { - if (env.LLM_SUMMARIZATION !== "true") { + if (config.LLM_SUMMARIZATION !== "true") { return prompt.split(/\s+/g).slice(0, 5).join(" "); } diff --git a/src/lib/server/tools/index.ts b/src/lib/server/tools/index.ts index 2d3e312e3fbbdb42fd5463b44a081b69b5c681c4..71b17533982cb2bce6e03fd554c420e6467e88e5 100644 --- a/src/lib/server/tools/index.ts +++ b/src/lib/server/tools/index.ts @@ -12,7 +12,7 @@ import type { TextGenerationContext } from "../textGeneration/types"; import { z } from "zod"; import JSON5 from "json5"; -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import jp from "jsonpath"; import calculator from "./calculator"; @@ -306,4 +306,4 @@ export function getCallMethod(tool: Omit): BackendCall { }; } -export const toolFromConfigs = configTools.parse(JSON5.parse(env.TOOLS)) satisfies ConfigTool[]; +export const toolFromConfigs = configTools.parse(JSON5.parse(config.TOOLS)) satisfies ConfigTool[]; diff --git a/src/lib/server/tools/utils.ts b/src/lib/server/tools/utils.ts index fc0bd281847874c141584dc0993455cde70e2169..5f496a1088d83a2994bf6d77d796145a4820081c 100644 --- a/src/lib/server/tools/utils.ts +++ b/src/lib/server/tools/utils.ts @@ -1,4 +1,4 @@ -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import { Client } from "@gradio/client"; import { SignJWT } from "jose"; import JSON5 from "json5"; @@ -28,7 +28,7 @@ export async function* callSpace { if (val === undefined) { - return env.RATE_LIMIT; + return config.RATE_LIMIT; } return val; }, z.coerce.number().optional()) @@ -21,4 +21,4 @@ export const usageLimitsSchema = z }) .optional(); -export const usageLimits = usageLimitsSchema.parse(JSON5.parse(env.USAGE_LIMITS)); +export const usageLimits = usageLimitsSchema.parse(JSON5.parse(config.USAGE_LIMITS)); diff --git a/src/lib/server/websearch/scrape/playwright.ts b/src/lib/server/websearch/scrape/playwright.ts index b4b84dce450d4752c44ed1dee5a7e809394a9526..910eea043e0a1b5c9e23603b04412ac7408e0e8c 100644 --- a/src/lib/server/websearch/scrape/playwright.ts +++ b/src/lib/server/websearch/scrape/playwright.ts @@ -7,16 +7,16 @@ import { type Browser, } from "playwright"; import { PlaywrightBlocker } from "@cliqz/adblocker-playwright"; -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import { logger } from "$lib/server/logger"; import { onExit } from "$lib/server/exitHandler"; const blocker = - env.PLAYWRIGHT_ADBLOCKER === "true" + config.PLAYWRIGHT_ADBLOCKER === "true" ? await PlaywrightBlocker.fromPrebuiltAdsAndTracking(fetch) .then((blker) => { const mostBlocked = blker.blockFonts().blockMedias().blockFrames().blockImages(); - if (env.WEBSEARCH_JAVASCRIPT === "false") return mostBlocked.blockScripts(); + if (config.WEBSEARCH_JAVASCRIPT === "false") return mostBlocked.blockScripts(); return mostBlocked; }) .catch((err) => { @@ -68,7 +68,7 @@ export async function withPage( try { const page = await ctx.newPage(); - if (env.PLAYWRIGHT_ADBLOCKER === "true") { + if (config.PLAYWRIGHT_ADBLOCKER === "true") { await blocker.enableBlockingInPage(page); } @@ -82,10 +82,10 @@ export async function withPage( }); const res = await page - .goto(url, { waitUntil: "load", timeout: parseInt(env.WEBSEARCH_TIMEOUT) }) + .goto(url, { waitUntil: "load", timeout: parseInt(config.WEBSEARCH_TIMEOUT) }) .catch(() => { console.warn( - `Failed to load page within ${parseInt(env.WEBSEARCH_TIMEOUT) / 1000}s: ${url}` + `Failed to load page within ${parseInt(config.WEBSEARCH_TIMEOUT) / 1000}s: ${url}` ); }); diff --git a/src/lib/server/websearch/search/endpoints.ts b/src/lib/server/websearch/search/endpoints.ts index 303ce28ca0b66ea889e8c7f8c5e95e588c1ddfdd..76bf2d746b29e8841ee900957211d8ad4ed03d7c 100644 --- a/src/lib/server/websearch/search/endpoints.ts +++ b/src/lib/server/websearch/search/endpoints.ts @@ -1,5 +1,5 @@ import { WebSearchProvider, type WebSearchSource } from "$lib/types/WebSearch"; -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import searchSerper from "./endpoints/serper"; import searchSerpApi from "./endpoints/serpApi"; import searchSerpStack from "./endpoints/serpStack"; @@ -10,22 +10,22 @@ import searchSearchApi from "./endpoints/searchApi"; import searchBing from "./endpoints/bing"; export function getWebSearchProvider() { - if (env.YDC_API_KEY) return WebSearchProvider.YOU; - if (env.SEARXNG_QUERY_URL) return WebSearchProvider.SEARXNG; - if (env.BING_SUBSCRIPTION_KEY) return WebSearchProvider.BING; + if (config.YDC_API_KEY) return WebSearchProvider.YOU; + if (config.SEARXNG_QUERY_URL) return WebSearchProvider.SEARXNG; + if (config.BING_SUBSCRIPTION_KEY) return WebSearchProvider.BING; return WebSearchProvider.GOOGLE; } /** Searches the web using the first available provider, based on the env */ export async function searchWeb(query: string): Promise { - if (env.USE_LOCAL_WEBSEARCH) return searchWebLocal(query); - if (env.SEARXNG_QUERY_URL) return searchSearxng(query); - if (env.SERPER_API_KEY) return searchSerper(query); - if (env.YDC_API_KEY) return searchYouApi(query); - if (env.SERPAPI_KEY) return searchSerpApi(query); - if (env.SERPSTACK_API_KEY) return searchSerpStack(query); - if (env.SEARCHAPI_KEY) return searchSearchApi(query); - if (env.BING_SUBSCRIPTION_KEY) return searchBing(query); + if (config.USE_LOCAL_WEBSEARCH) return searchWebLocal(query); + if (config.SEARXNG_QUERY_URL) return searchSearxng(query); + if (config.SERPER_API_KEY) return searchSerper(query); + if (config.YDC_API_KEY) return searchYouApi(query); + if (config.SERPAPI_KEY) return searchSerpApi(query); + if (config.SERPSTACK_API_KEY) return searchSerpStack(query); + if (config.SEARCHAPI_KEY) return searchSearchApi(query); + if (config.BING_SUBSCRIPTION_KEY) return searchBing(query); throw new Error( "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." ); diff --git a/src/lib/server/websearch/search/endpoints/bing.ts b/src/lib/server/websearch/search/endpoints/bing.ts index 20b28ab4999cc23fb2b1dc1baf993de6d8a58e29..91b52220477b0571def556441a9b6cd4c2ef3dff 100644 --- a/src/lib/server/websearch/search/endpoints/bing.ts +++ b/src/lib/server/websearch/search/endpoints/bing.ts @@ -1,5 +1,5 @@ import type { WebSearchSource } from "$lib/types/WebSearch"; -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; export default async function search(query: string): Promise { // const params = { @@ -12,7 +12,7 @@ export default async function search(query: string): Promise { method: "GET", headers: { - "Ocp-Apim-Subscription-Key": env.BING_SUBSCRIPTION_KEY, + "Ocp-Apim-Subscription-Key": config.BING_SUBSCRIPTION_KEY, "Content-type": "application/json", }, } diff --git a/src/lib/server/websearch/search/endpoints/searchApi.ts b/src/lib/server/websearch/search/endpoints/searchApi.ts index 1108bb89a4f422fcc5fa5c7f6a0ff564cf549fcf..4784c9e5337676cd1afc1473e9e46eb3c8ad1a71 100644 --- a/src/lib/server/websearch/search/endpoints/searchApi.ts +++ b/src/lib/server/websearch/search/endpoints/searchApi.ts @@ -1,4 +1,4 @@ -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import type { WebSearchSource } from "$lib/types/WebSearch"; export default async function search(query: string): Promise { @@ -7,7 +7,7 @@ export default async function search(query: string): Promise { method: "GET", headers: { - Authorization: `Bearer ${env.SEARCHAPI_KEY}`, + Authorization: `Bearer ${config.SEARCHAPI_KEY}`, "Content-type": "application/json", }, } diff --git a/src/lib/server/websearch/search/endpoints/searxng.ts b/src/lib/server/websearch/search/endpoints/searxng.ts index 6ed1af379e1d73c20d6b4767f5898d0d6205ff17..029d2e666f6787df9d1ef77c1defec5420abd8a7 100644 --- a/src/lib/server/websearch/search/endpoints/searxng.ts +++ b/src/lib/server/websearch/search/endpoints/searxng.ts @@ -1,4 +1,4 @@ -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import { logger } from "$lib/server/logger"; import type { WebSearchSource } from "$lib/types/WebSearch"; import { isURL } from "$lib/utils/isUrl"; @@ -8,7 +8,7 @@ export default async function searchSearxng(query: string): Promise abortController.abort(), 10000); // Insert the query into the URL template - let url = env.SEARXNG_QUERY_URL.replace("", query); + let url = config.SEARXNG_QUERY_URL.replace("", query); // Check if "&format=json" already exists in the URL if (!url.includes("&format=json")) { diff --git a/src/lib/server/websearch/search/endpoints/serpApi.ts b/src/lib/server/websearch/search/endpoints/serpApi.ts index adef629d8717ec4f462d904134f7302cfd8f93d6..a30ae55dca435a789cd8437d4c215fd8e4984576 100644 --- a/src/lib/server/websearch/search/endpoints/serpApi.ts +++ b/src/lib/server/websearch/search/endpoints/serpApi.ts @@ -1,4 +1,4 @@ -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import { getJson, type GoogleParameters } from "serpapi"; import type { WebSearchSource } from "$lib/types/WebSearch"; import { isURL } from "$lib/utils/isUrl"; @@ -15,7 +15,7 @@ export default async function searchWebSerpApi(query: string): Promise { const response = await fetch( - `http://api.serpstack.com/search?access_key=${env.SERPSTACK_API_KEY}&query=${query}&hl=en&gl=us`, + `http://api.serpstack.com/search?access_key=${config.SERPSTACK_API_KEY}&query=${query}&hl=en&gl=us`, { headers: { "Content-type": "application/json; charset=UTF-8" } } ); diff --git a/src/lib/server/websearch/search/endpoints/serper.ts b/src/lib/server/websearch/search/endpoints/serper.ts index 1a3223990833a567f9a16eef2688d0046ab4ace0..ffa3733607907a31c7ef26d036fd6ef99e45aaa4 100644 --- a/src/lib/server/websearch/search/endpoints/serper.ts +++ b/src/lib/server/websearch/search/endpoints/serper.ts @@ -1,4 +1,4 @@ -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import type { WebSearchSource } from "$lib/types/WebSearch"; export default async function search(query: string): Promise { @@ -12,7 +12,7 @@ export default async function search(query: string): Promise method: "POST", body: JSON.stringify(params), headers: { - "x-api-key": env.SERPER_API_KEY, + "x-api-key": config.SERPER_API_KEY, "Content-type": "application/json", }, }); diff --git a/src/lib/server/websearch/search/endpoints/youApi.ts b/src/lib/server/websearch/search/endpoints/youApi.ts index 0173000d77d102cde876b5b59e779f88c98d6f69..ee11c872650bb55a792db4ae7eafef0ecd3b72b7 100644 --- a/src/lib/server/websearch/search/endpoints/youApi.ts +++ b/src/lib/server/websearch/search/endpoints/youApi.ts @@ -1,4 +1,4 @@ -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import { isURL } from "$lib/utils/isUrl"; import type { WebSearchSource } from "$lib/types/WebSearch"; @@ -18,7 +18,7 @@ export default async function searchWebYouApi(query: string): Promise { - if (env.ENABLE_LOCAL_FETCH !== "true") { + if (config.ENABLE_LOCAL_FETCH !== "true") { const localLinks = await Promise.all(links.map(isURLStringLocal)); links = links.filter((_, index) => !localLinks[index]); } diff --git a/src/lib/types/ConfigKey.ts b/src/lib/types/ConfigKey.ts new file mode 100644 index 0000000000000000000000000000000000000000..e76b142b2b22cca1e8da73c07018d8ab71b09da4 --- /dev/null +++ b/src/lib/types/ConfigKey.ts @@ -0,0 +1,4 @@ +export interface ConfigKey { + key: string; // unique + value: string; +} diff --git a/src/lib/types/Semaphore.ts b/src/lib/types/Semaphore.ts index 8ea0d8ccb10bbce15b5be1f6dc7c7eff158d7091..b4bb5d889e550d19180b0b2c51a5bd05de4763c9 100644 --- a/src/lib/types/Semaphore.ts +++ b/src/lib/types/Semaphore.ts @@ -2,4 +2,13 @@ import type { Timestamps } from "./Timestamps"; export interface Semaphore extends Timestamps { key: string; + deleteAt: Date; +} + +export enum Semaphores { + ASSISTANTS_COUNT = "assistants.count", + CONVERSATION_STATS = "conversation.stats", + CONFIG_UPDATE = "config.update", + MIGRATION = "migration", + TEST_MIGRATION = "test.migration", } diff --git a/src/lib/utils/PublicConfig.svelte.ts b/src/lib/utils/PublicConfig.svelte.ts new file mode 100644 index 0000000000000000000000000000000000000000..a080e38a9d543fe01df246afaf5e18166bff4dfc --- /dev/null +++ b/src/lib/utils/PublicConfig.svelte.ts @@ -0,0 +1,45 @@ +import type { env as publicEnv } from "$env/dynamic/public"; + +type PublicConfigKey = keyof typeof publicEnv; + +class PublicConfigManager { + #configStore = $state>({}); + + constructor() { + this.init = this.init.bind(this); + } + + init(publicConfig: Record) { + this.#configStore = publicConfig; + } + + get(key: PublicConfigKey) { + return this.#configStore[key]; + } + + get isHuggingChat() { + return this.#configStore.PUBLIC_APP_ASSETS === "huggingchat"; + } +} + +const publicConfigManager = new PublicConfigManager(); + +type ConfigProxy = PublicConfigManager & { [K in PublicConfigKey]: string }; + +export const publicConfig: ConfigProxy = new Proxy(publicConfigManager, { + get(target, prop) { + if (prop in target) { + return Reflect.get(target, prop); + } + if (typeof prop === "string") { + return target.get(prop as PublicConfigKey); + } + return undefined; + }, + set(target, prop, value, receiver) { + if (prop in target) { + return Reflect.set(target, prop, value, receiver); + } + return false; + }, +}) as ConfigProxy; diff --git a/src/lib/utils/getShareUrl.ts b/src/lib/utils/getShareUrl.ts index 5278ab6fd6ef21b1c34468a0c9b81a164f78b52b..8f4b5e1abb2ce593c6b42c89f9ff97835a9aac39 100644 --- a/src/lib/utils/getShareUrl.ts +++ b/src/lib/utils/getShareUrl.ts @@ -1,8 +1,8 @@ import { base } from "$app/paths"; -import { env as envPublic } from "$env/dynamic/public"; +import { publicConfig } from "$lib/utils/PublicConfig.svelte"; export function getShareUrl(url: URL, shareId: string): string { return `${ - envPublic.PUBLIC_SHARE_PREFIX || `${envPublic.PUBLIC_ORIGIN || url.origin}${base}` + publicConfig.PUBLIC_SHARE_PREFIX || `${publicConfig.PUBLIC_ORIGIN || url.origin}${base}` }/r/${shareId}`; } diff --git a/src/lib/utils/isHuggingChat.ts b/src/lib/utils/isHuggingChat.ts deleted file mode 100644 index df1ad80039eb147a5427cd5ca1980e92b5c2c22a..0000000000000000000000000000000000000000 --- a/src/lib/utils/isHuggingChat.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { env as envPublic } from "$env/dynamic/public"; - -export const isHuggingChat = envPublic.PUBLIC_APP_ASSETS === "huggingchat"; diff --git a/src/lib/utils/messageUpdates.ts b/src/lib/utils/messageUpdates.ts index 0b9baaec9b54a0a42f4d20db61ea858571977e3d..728683b6f0ca45c503706e39fb837fcb2fa83be5 100644 --- a/src/lib/utils/messageUpdates.ts +++ b/src/lib/utils/messageUpdates.ts @@ -14,8 +14,8 @@ import { type MessageToolErrorUpdate, type MessageToolResultUpdate, } from "$lib/types/MessageUpdate"; -import { env as envPublic } from "$env/dynamic/public"; +import { publicConfig } from "$lib/utils/PublicConfig.svelte"; export const isMessageWebSearchUpdate = (update: MessageUpdate): update is MessageWebSearchUpdate => update.type === MessageUpdateType.WebSearch; export const isMessageWebSearchGeneralUpdate = ( @@ -96,7 +96,7 @@ export async function fetchMessageUpdates( throw Error("Body not defined"); } - if (!(envPublic.PUBLIC_SMOOTH_UPDATES === "true")) { + if (!(publicConfig.PUBLIC_SMOOTH_UPDATES === "true")) { return endpointStreamToIterator(response, abortController); } diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index 159255283c6713dcba4c220d622298ee7bd84bb0..79e49915b68bc0ae62f4bc959466a8526f83dec8 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -5,7 +5,7 @@ import { UrlDependency } from "$lib/types/UrlDependency"; import { defaultModel, models, oldModels, validateModel } from "$lib/server/models"; import { authCondition, requiresUser } from "$lib/server/auth"; import { DEFAULT_SETTINGS } from "$lib/types/Settings"; -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import { ObjectId } from "mongodb"; import type { ConvSidebar } from "$lib/types/ConvSidebar"; import { toolFromConfigs } from "$lib/server/tools"; @@ -13,7 +13,6 @@ import { MetricsServer } from "$lib/server/metrics"; import type { ToolFront, ToolInputFile } from "$lib/types/Tool"; import { ReviewStatus } from "$lib/types/Review"; import { base } from "$app/paths"; - export const load: LayoutServerLoad = async ({ locals, depends, fetch }) => { depends(UrlDependency.ConversationList); @@ -42,7 +41,7 @@ export const load: LayoutServerLoad = async ({ locals, depends, fetch }) => { }); } - const enableAssistants = env.ENABLE_ASSISTANTS === "true"; + const enableAssistants = config.ENABLE_ASSISTANTS === "true"; const assistantActive = !models.map(({ id }) => id).includes(settings?.activeModel ?? ""); @@ -85,7 +84,9 @@ export const load: LayoutServerLoad = async ({ locals, depends, fetch }) => { .toArray() ); - const messagesBeforeLogin = env.MESSAGES_BEFORE_LOGIN ? parseInt(env.MESSAGES_BEFORE_LOGIN) : 0; + const messagesBeforeLogin = config.MESSAGES_BEFORE_LOGIN + ? parseInt(config.MESSAGES_BEFORE_LOGIN) + : 0; let loginRequired = false; @@ -177,14 +178,14 @@ export const load: LayoutServerLoad = async ({ locals, depends, fetch }) => { ), settings: { searchEnabled: !!( - env.SERPAPI_KEY || - env.SERPER_API_KEY || - env.SERPSTACK_API_KEY || - env.SEARCHAPI_KEY || - env.YDC_API_KEY || - env.USE_LOCAL_WEBSEARCH || - env.SEARXNG_QUERY_URL || - env.BING_SUBSCRIPTION_KEY + config.SERPAPI_KEY || + config.SERPER_API_KEY || + config.SERPSTACK_API_KEY || + config.SEARCHAPI_KEY || + config.YDC_API_KEY || + config.USE_LOCAL_WEBSEARCH || + config.SEARXNG_QUERY_URL || + config.BING_SUBSCRIPTION_KEY ), ethicsModalAccepted: !!settings?.ethicsModalAcceptedAt, ethicsModalAcceptedAt: settings?.ethicsModalAcceptedAt ?? null, @@ -275,10 +276,11 @@ export const load: LayoutServerLoad = async ({ locals, depends, fetch }) => { isAdmin: locals.isAdmin, assistant: assistant ? JSON.parse(JSON.stringify(assistant)) : null, enableAssistants, - enableAssistantsRAG: env.ENABLE_ASSISTANTS_RAG === "true", - enableCommunityTools: env.COMMUNITY_TOOLS === "true", + enableAssistantsRAG: config.ENABLE_ASSISTANTS_RAG === "true", + enableCommunityTools: config.COMMUNITY_TOOLS === "true", loginRequired, loginEnabled: requiresUser, guestMode: requiresUser && messagesBeforeLogin > 0, + publicConfig: config.getPublicConfig(), }; }; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index fcbd38463f1358b1c1a1b33b3a554af7a64e639e..57f0da2f1e7d5957f085986e83685c395f61dc7a 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -6,7 +6,7 @@ import { base } from "$app/paths"; import { page } from "$app/stores"; - import { env as envPublic } from "$env/dynamic/public"; + import { publicConfig } from "$lib/utils/PublicConfig.svelte"; import { error } from "$lib/stores/errors"; import { createSettingsStore } from "$lib/stores/settings"; @@ -22,7 +22,6 @@ import { loginModalOpen } from "$lib/stores/loginModal"; import LoginModal from "$lib/components/LoginModal.svelte"; import OverloadedModal from "$lib/components/OverloadedModal.svelte"; - import { isHuggingChat } from "$lib/utils/isHuggingChat"; let { data = $bindable(), children } = $props(); @@ -178,13 +177,21 @@ let showDisclaimer = $derived( !$settings.ethicsModalAccepted && $page.url.pathname !== `${base}/privacy` && - envPublic.PUBLIC_APP_DISCLAIMER === "1" && + publicConfig.PUBLIC_APP_DISCLAIMER === "1" && !($page.data.shared === true) ); + + $effect(() => { + () => publicConfig.init(data.publicConfig); + }); + + onMount(() => { + publicConfig.init(data.publicConfig); + }); - {envPublic.PUBLIC_APP_NAME} + {publicConfig.PUBLIC_APP_NAME} @@ -192,49 +199,49 @@ {#if !$page.url.pathname.includes("/assistant/") && $page.route.id !== "/assistants" && !$page.url.pathname.includes("/models/") && !$page.url.pathname.includes("/tools")} - + - + - + {/if} - {#if envPublic.PUBLIC_PLAUSIBLE_SCRIPT_URL && envPublic.PUBLIC_ORIGIN} + {#if publicConfig.PUBLIC_PLAUSIBLE_SCRIPT_URL && publicConfig.PUBLIC_ORIGIN} {/if} - {#if envPublic.PUBLIC_APPLE_APP_ID} - + {#if publicConfig.PUBLIC_APPLE_APP_ID} + {/if} @@ -250,7 +257,7 @@ /> {/if} -{#if overloadedModalOpen && isHuggingChat} +{#if overloadedModalOpen && publicConfig.isHuggingChat} (overloadedModalOpen = false)} /> {/if} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 2e1f106febd7bf78c9b18b02010a42ff07bae836..d9fd9c738408e7490843e638c267742a41bd323d 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -2,7 +2,8 @@ import { goto } from "$app/navigation"; import { base } from "$app/paths"; import { page } from "$app/state"; - import { env as envPublic } from "$env/dynamic/public"; + import { publicConfig } from "$lib/utils/PublicConfig.svelte"; + import ChatWindow from "$lib/components/chat/ChatWindow.svelte"; import { ERROR_MESSAGES, error } from "$lib/stores/errors"; import { pendingMessage } from "$lib/stores/pendingMessage"; @@ -90,7 +91,7 @@ - {envPublic.PUBLIC_APP_NAME} + {publicConfig.PUBLIC_APP_NAME} " -H "Content-Type: application/json" -d '{"model": "OpenAssistant/oasst-sft-6-llama-30b-xor"}' export async function POST({ request }) { - if (!env.PARQUET_EXPORT_DATASET || !env.PARQUET_EXPORT_HF_TOKEN) { + if (!config.PARQUET_EXPORT_DATASET || !config.PARQUET_EXPORT_HF_TOKEN) { error(500, "Parquet export is not configured."); } @@ -144,10 +144,10 @@ export async function POST({ request }) { await uploadFile({ file: pathToFileURL(fileName) as URL, - credentials: { accessToken: env.PARQUET_EXPORT_HF_TOKEN }, + credentials: { accessToken: config.PARQUET_EXPORT_HF_TOKEN }, repo: { type: "dataset", - name: env.PARQUET_EXPORT_DATASET, + name: config.PARQUET_EXPORT_DATASET, }, }); diff --git a/src/routes/api/assistant/[id]/report/+server.ts b/src/routes/api/assistant/[id]/report/+server.ts index e5372fbdaf1e9e5cc4cfcdd967c46ba6327c0a66..6756aeb32f9e29ee2824f16435ed46975fb8c75d 100644 --- a/src/routes/api/assistant/[id]/report/+server.ts +++ b/src/routes/api/assistant/[id]/report/+server.ts @@ -5,8 +5,7 @@ import { error } from "@sveltejs/kit"; import { ObjectId } from "mongodb"; import { z } from "zod"; -import { env } from "$env/dynamic/private"; -import { env as envPublic } from "$env/dynamic/public"; +import { config } from "$lib/server/config"; import { sendSlack } from "$lib/server/sendSlack"; import type { Assistant } from "$lib/types/Assistant"; @@ -42,9 +41,8 @@ export async function POST({ params, request, locals, url }) { return error(500, "Failed to report assistant"); } - if (env.WEBHOOK_URL_REPORT_ASSISTANT) { - const prefixUrl = - envPublic.PUBLIC_SHARE_PREFIX || `${envPublic.PUBLIC_ORIGIN || url.origin}${base}`; + if (config.WEBHOOK_URL_REPORT_ASSISTANT) { + const prefixUrl = config.PUBLIC_SHARE_PREFIX || `${config.PUBLIC_ORIGIN || url.origin}${base}`; const assistantUrl = `${prefixUrl}/assistant/${params.id}`; const assistant = await collections.assistants.findOne>( diff --git a/src/routes/api/assistant/[id]/review/+server.ts b/src/routes/api/assistant/[id]/review/+server.ts index 84b8b184e2c5925b472d3704deabf646bd292857..450263b1a56562c12fb2740d74bae0a8315597f3 100644 --- a/src/routes/api/assistant/[id]/review/+server.ts +++ b/src/routes/api/assistant/[id]/review/+server.ts @@ -2,7 +2,7 @@ import { collections } from "$lib/server/database"; import { error } from "@sveltejs/kit"; import { ObjectId } from "mongodb"; import { base } from "$app/paths"; -import { env as envPublic } from "$env/dynamic/public"; +import { config } from "$lib/server/config"; import { ReviewStatus } from "$lib/types/Review"; import { sendSlack } from "$lib/server/sendSlack"; import { z } from "zod"; @@ -58,8 +58,7 @@ export async function PATCH({ params, request, locals, url }) { } if (status === ReviewStatus.PENDING) { - const prefixUrl = - envPublic.PUBLIC_SHARE_PREFIX || `${envPublic.PUBLIC_ORIGIN || url.origin}${base}`; + const prefixUrl = config.PUBLIC_SHARE_PREFIX || `${config.PUBLIC_ORIGIN || url.origin}${base}`; const assistantUrl = `${prefixUrl}/assistant/${assistantId}`; const username = locals.user?.username; diff --git a/src/routes/api/assistants/+server.ts b/src/routes/api/assistants/+server.ts index 6176e3b2979a16ba26a3ea95d678c6fd8d403d3b..1e77b8b4238025c470facd38110699e5123ce78f 100644 --- a/src/routes/api/assistants/+server.ts +++ b/src/routes/api/assistants/+server.ts @@ -3,7 +3,7 @@ import type { Assistant } from "$lib/types/Assistant"; import type { User } from "$lib/types/User"; import { generateQueryTokens } from "$lib/utils/searchTokens.js"; import type { Filter } from "mongodb"; -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import { ReviewStatus } from "$lib/types/Review"; const NUM_PER_PAGE = 24; @@ -30,7 +30,7 @@ export async function GET({ url, locals }) { // if we require featured assistants, that we are not on a user page and we are not an admin who wants to see unfeatured assistants, we show featured assistants let shouldBeFeatured = {}; - if (env.REQUIRE_FEATURED_ASSISTANTS === "true" && !(locals.isAdmin && showUnfeatured)) { + if (config.REQUIRE_FEATURED_ASSISTANTS === "true" && !(locals.isAdmin && showUnfeatured)) { if (!user) { // only show featured assistants on the community page shouldBeFeatured = { review: ReviewStatus.APPROVED }; diff --git a/src/routes/api/spaces-config/+server.ts b/src/routes/api/spaces-config/+server.ts index 00b960bb4f51acde38f0319ad729688a59b6923b..82e0bb4341c79f95d0f2847bccc61bbc9ea42ce6 100644 --- a/src/routes/api/spaces-config/+server.ts +++ b/src/routes/api/spaces-config/+server.ts @@ -1,8 +1,8 @@ -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import { Client } from "@gradio/client"; export async function GET({ url }) { - if (env.COMMUNITY_TOOLS !== "true") { + if (config.COMMUNITY_TOOLS !== "true") { return new Response("Community tools are not enabled", { status: 403 }); } diff --git a/src/routes/api/tools/+server.ts b/src/routes/api/tools/+server.ts index ffc30aa7959c513fbde5305fe3f04b176853c71a..a8695146b08d5beae1e7e8afae02b3ac098ebbdf 100644 --- a/src/routes/api/tools/+server.ts +++ b/src/routes/api/tools/+server.ts @@ -1,4 +1,4 @@ -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import { authCondition, requiresUser } from "$lib/server/auth.js"; import { collections } from "$lib/server/database.js"; import { editableToolSchema } from "$lib/server/tools/index.js"; @@ -9,7 +9,7 @@ import { error } from "@sveltejs/kit"; import { usageLimits } from "$lib/server/usageLimits.js"; export async function POST({ request, locals }) { - if (env.COMMUNITY_TOOLS !== "true") { + if (config.COMMUNITY_TOOLS !== "true") { error(403, "Community tools are not enabled"); } const body = await request.json(); diff --git a/src/routes/api/tools/[toolId]/+server.ts b/src/routes/api/tools/[toolId]/+server.ts index ebc110fe970f40b7c7d3ee97016a42e027c583a7..76ffc3b9f62edf51606a6d77c11c924f3e5a2809 100644 --- a/src/routes/api/tools/[toolId]/+server.ts +++ b/src/routes/api/tools/[toolId]/+server.ts @@ -1,4 +1,4 @@ -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import { collections } from "$lib/server/database.js"; import { toolFromConfigs } from "$lib/server/tools/index.js"; import { ReviewStatus } from "$lib/types/Review"; @@ -10,7 +10,7 @@ import { error } from "@sveltejs/kit"; import { requiresUser } from "$lib/server/auth"; export async function GET({ params }) { - if (env.COMMUNITY_TOOLS !== "true") { + if (config.COMMUNITY_TOOLS !== "true") { return new Response("Community tools are not enabled", { status: 403 }); } diff --git a/src/routes/api/tools/[toolId]/report/+server.ts b/src/routes/api/tools/[toolId]/report/+server.ts index 80b23c942b600df53f31fb73113645e7f134c3de..8faf3223bd8aa6ff2fa94ad4645b0fbd0b273aa6 100644 --- a/src/routes/api/tools/[toolId]/report/+server.ts +++ b/src/routes/api/tools/[toolId]/report/+server.ts @@ -5,8 +5,7 @@ import { error } from "@sveltejs/kit"; import { ObjectId } from "mongodb"; import { z } from "zod"; -import { env } from "$env/dynamic/private"; -import { env as envPublic } from "$env/dynamic/public"; +import { config } from "$lib/server/config"; import { sendSlack } from "$lib/server/sendSlack"; import type { Tool } from "$lib/types/Tool"; @@ -42,9 +41,8 @@ export async function POST({ params, request, locals, url }) { return error(500, "Failed to report tool"); } - if (env.WEBHOOK_URL_REPORT_ASSISTANT) { - const prefixUrl = - envPublic.PUBLIC_SHARE_PREFIX || `${envPublic.PUBLIC_ORIGIN || url.origin}${base}`; + if (config.WEBHOOK_URL_REPORT_ASSISTANT) { + const prefixUrl = config.PUBLIC_SHARE_PREFIX || `${config.PUBLIC_ORIGIN || url.origin}${base}`; const toolUrl = `${prefixUrl}/tools/${params.toolId}`; const tool = await collections.tools.findOne>( diff --git a/src/routes/api/tools/[toolId]/review/+server.ts b/src/routes/api/tools/[toolId]/review/+server.ts index 7d580642ca44ca9ad9ab0722382acd3f6eeffea1..cfbcd01100ce7050ab0c68b92752235a377a1e78 100644 --- a/src/routes/api/tools/[toolId]/review/+server.ts +++ b/src/routes/api/tools/[toolId]/review/+server.ts @@ -2,7 +2,7 @@ import { collections } from "$lib/server/database"; import { error } from "@sveltejs/kit"; import { ObjectId } from "mongodb"; import { base } from "$app/paths"; -import { env as envPublic } from "$env/dynamic/public"; +import { config } from "$lib/server/config"; import { ReviewStatus } from "$lib/types/Review"; import { sendSlack } from "$lib/server/sendSlack"; import { z } from "zod"; @@ -55,8 +55,7 @@ export async function PATCH({ params, request, locals, url }) { } if (status === ReviewStatus.PENDING) { - const prefixUrl = - envPublic.PUBLIC_SHARE_PREFIX || `${envPublic.PUBLIC_ORIGIN || url.origin}${base}`; + const prefixUrl = config.PUBLIC_SHARE_PREFIX || `${config.PUBLIC_ORIGIN || url.origin}${base}`; const toolUrl = `${prefixUrl}/tools/${toolId}`; const username = locals.user?.username; diff --git a/src/routes/api/tools/search/+server.ts b/src/routes/api/tools/search/+server.ts index 3baf366cf5aff8e9edf6380a3202d0e76cf1d319..a3cba817a3ec2e29d7250c42fe0844989f7186e1 100644 --- a/src/routes/api/tools/search/+server.ts +++ b/src/routes/api/tools/search/+server.ts @@ -1,4 +1,4 @@ -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import { collections } from "$lib/server/database.js"; import { toolFromConfigs } from "$lib/server/tools/index.js"; import type { BaseTool, CommunityToolDB } from "$lib/types/Tool.js"; @@ -6,7 +6,7 @@ import { generateQueryTokens, generateSearchTokens } from "$lib/utils/searchToke import type { Filter } from "mongodb"; import { ReviewStatus } from "$lib/types/Review"; export async function GET({ url }) { - if (env.COMMUNITY_TOOLS !== "true") { + if (config.COMMUNITY_TOOLS !== "true") { return new Response("Community tools are not enabled", { status: 403 }); } diff --git a/src/routes/assistant/[assistantId]/+page.svelte b/src/routes/assistant/[assistantId]/+page.svelte index 1760c0849b1d0b9457937524df16b96220a7e02b..8a8051c489e14752b83b00a71aa6ead7cdb1277f 100644 --- a/src/routes/assistant/[assistantId]/+page.svelte +++ b/src/routes/assistant/[assistantId]/+page.svelte @@ -3,7 +3,8 @@ import { base } from "$app/paths"; import { goto } from "$app/navigation"; import { onMount } from "svelte"; - import { env as envPublic } from "$env/dynamic/public"; + import { publicConfig } from "$lib/utils/PublicConfig.svelte"; + import ChatWindow from "$lib/components/chat/ChatWindow.svelte"; import { findCurrentModel } from "$lib/utils/models"; import { useSettingsStore } from "$lib/stores/settings"; @@ -68,15 +69,15 @@ - + diff --git a/src/routes/assistants/+page.server.ts b/src/routes/assistants/+page.server.ts index e415bfb3b7fc79d1725de719fb6c56d6c167bfe7..6945534977e825166736ba3c646e37f75a11946d 100644 --- a/src/routes/assistants/+page.server.ts +++ b/src/routes/assistants/+page.server.ts @@ -1,5 +1,5 @@ import { base } from "$app/paths"; -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import { collections } from "$lib/server/database.js"; import { SortKey, type Assistant } from "$lib/types/Assistant"; import type { User } from "$lib/types/User"; @@ -10,7 +10,7 @@ import { ReviewStatus } from "$lib/types/Review"; const NUM_PER_PAGE = 24; export const load = async ({ url, locals }) => { - if (!env.ENABLE_ASSISTANTS) { + if (!config.ENABLE_ASSISTANTS) { redirect(302, `${base}/`); } @@ -36,7 +36,7 @@ export const load = async ({ url, locals }) => { // if we require featured assistants, that we are not on a user page and we are not an admin who wants to see unfeatured assistants, we show featured assistants let shouldBeFeatured = {}; - if (env.REQUIRE_FEATURED_ASSISTANTS === "true" && !(locals.isAdmin && showUnfeatured)) { + if (config.REQUIRE_FEATURED_ASSISTANTS === "true" && !(locals.isAdmin && showUnfeatured)) { if (!user) { // only show featured assistants on the community page shouldBeFeatured = { review: ReviewStatus.APPROVED }; diff --git a/src/routes/assistants/+page.svelte b/src/routes/assistants/+page.svelte index 14204ed15538cb3d89bd63d9ae19d8b6cf0fbb38..346c6b93307e505a1fa8df41304f6a5f3e3afd03 100644 --- a/src/routes/assistants/+page.svelte +++ b/src/routes/assistants/+page.svelte @@ -1,8 +1,7 @@ - {#if isHuggingChat} + {#if publicConfig.isHuggingChat} HuggingChat - Assistants @@ -113,8 +112,8 @@ /> {/if} @@ -124,7 +123,7 @@
- {#if isHuggingChat} + {#if publicConfig.isHuggingChat} org.sub === env.HF_ORG_ADMIN)) || false; + const isAdmin = + (config.HF_ORG_ADMIN && orgs?.some((org) => org.sub === config.HF_ORG_ADMIN)) || false; const isEarlyAccess = - (env.HF_ORG_EARLY_ACCESS && orgs?.some((org) => org.sub === env.HF_ORG_EARLY_ACCESS)) || false; + (config.HF_ORG_EARLY_ACCESS && orgs?.some((org) => org.sub === config.HF_ORG_EARLY_ACCESS)) || + false; logger.debug( { diff --git a/src/routes/logout/+page.server.ts b/src/routes/logout/+page.server.ts index 935846a5da6c575a64408b6d6accd71c602e7a91..be5f8c7f2c3e0c220f6419eb5d9d1d1b37dc22b2 100644 --- a/src/routes/logout/+page.server.ts +++ b/src/routes/logout/+page.server.ts @@ -1,6 +1,6 @@ import { dev } from "$app/environment"; import { base } from "$app/paths"; -import { env } from "$env/dynamic/private"; +import { config } from "$lib/server/config"; import { collections } from "$lib/server/database"; import { redirect } from "@sveltejs/kit"; @@ -8,11 +8,11 @@ export const actions = { async default({ cookies, locals }) { await collections.sessions.deleteOne({ sessionId: locals.sessionId }); - cookies.delete(env.COOKIE_NAME, { + cookies.delete(config.COOKIE_NAME, { path: "/", // So that it works inside the space's iframe - sameSite: dev || env.ALLOW_INSECURE_COOKIES === "true" ? "lax" : "none", - secure: !dev && !(env.ALLOW_INSECURE_COOKIES === "true"), + sameSite: dev || config.ALLOW_INSECURE_COOKIES === "true" ? "lax" : "none", + secure: !dev && !(config.ALLOW_INSECURE_COOKIES === "true"), httpOnly: true, }); redirect(303, `${base}/`); diff --git a/src/routes/models/+page.svelte b/src/routes/models/+page.svelte index f75843948f2a2d4c4f4c8e700d0b9064520deb48..cf4115f370dbb5dc209f4f7edd35611a5727ed96 100644 --- a/src/routes/models/+page.svelte +++ b/src/routes/models/+page.svelte @@ -1,8 +1,6 @@ - {#if isHuggingChat} + {#if publicConfig.isHuggingChat} HuggingChat - Models @@ -34,7 +32,7 @@
-

All models available on {envPublic.PUBLIC_APP_NAME}

+

All models available on {publicConfig.PUBLIC_APP_NAME}

{#each data.models.filter((el) => !el.unlisted) as model, index (model.id)}
- + - + diff --git a/src/routes/models/[...model]/thumbnail.png/ModelThumbnail.svelte b/src/routes/models/[...model]/thumbnail.png/ModelThumbnail.svelte index b67ac049162c70b1e6c0e4fa21776e8c83458bef..8ce936ec509f295332c2d914a1b27989496d0e9b 100644 --- a/src/routes/models/[...model]/thumbnail.png/ModelThumbnail.svelte +++ b/src/routes/models/[...model]/thumbnail.png/ModelThumbnail.svelte @@ -1,6 +1,5 @@

Application Settings

- {#if !!envPublic.PUBLIC_COMMIT_SHA} + {#if !!publicConfig.PUBLIC_COMMIT_SHA}
{/if} + {#if page.data.isAdmin} +

You are an admin.

+ {/if}
- {#if envPublic.PUBLIC_APP_DATA_SHARING === "1"} + {#if publicConfig.PUBLIC_APP_DATA_SHARING === "1"}