From 5df474abb997f0fb50493a4a0620a6546828bee3 Mon Sep 17 00:00:00 2001 From: Tristan Morris Date: Sun, 2 Feb 2025 07:58:59 -0600 Subject: [PATCH 01/96] Add support for Deepgram STT --- backend/open_webui/config.py | 6 ++ backend/open_webui/main.py | 2 + backend/open_webui/routers/audio.py | 64 +++++++++++++++++++ .../components/admin/Settings/Audio.svelte | 37 ++++++++++- 4 files changed, 108 insertions(+), 1 deletion(-) diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index c37b831dec..4b85d51943 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1954,6 +1954,12 @@ WHISPER_MODEL_AUTO_UPDATE = ( and os.environ.get("WHISPER_MODEL_AUTO_UPDATE", "").lower() == "true" ) +# Add Deepgram configuration +DEEPGRAM_API_KEY = PersistentConfig( + "DEEPGRAM_API_KEY", + "audio.stt.deepgram.api_key", + os.getenv("DEEPGRAM_API_KEY", ""), +) AUDIO_STT_OPENAI_API_BASE_URL = PersistentConfig( "AUDIO_STT_OPENAI_API_BASE_URL", diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 00270aabc4..6323f34c33 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -130,6 +130,7 @@ from open_webui.config import ( AUDIO_TTS_AZURE_SPEECH_REGION, AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT, WHISPER_MODEL, + DEEPGRAM_API_KEY, WHISPER_MODEL_AUTO_UPDATE, WHISPER_MODEL_DIR, # Retrieval @@ -609,6 +610,7 @@ app.state.config.STT_ENGINE = AUDIO_STT_ENGINE app.state.config.STT_MODEL = AUDIO_STT_MODEL app.state.config.WHISPER_MODEL = WHISPER_MODEL +app.state.config.DEEPGRAM_API_KEY = DEEPGRAM_API_KEY app.state.config.TTS_OPENAI_API_BASE_URL = AUDIO_TTS_OPENAI_API_BASE_URL app.state.config.TTS_OPENAI_API_KEY = AUDIO_TTS_OPENAI_API_KEY diff --git a/backend/open_webui/routers/audio.py b/backend/open_webui/routers/audio.py index c1b15772bd..7242042e2a 100644 --- a/backend/open_webui/routers/audio.py +++ b/backend/open_webui/routers/audio.py @@ -11,6 +11,7 @@ from pydub.silence import split_on_silence import aiohttp import aiofiles import requests +import mimetypes from fastapi import ( Depends, @@ -138,6 +139,7 @@ class STTConfigForm(BaseModel): ENGINE: str MODEL: str WHISPER_MODEL: str + DEEPGRAM_API_KEY: str class AudioConfigUpdateForm(BaseModel): @@ -165,6 +167,7 @@ async def get_audio_config(request: Request, user=Depends(get_admin_user)): "ENGINE": request.app.state.config.STT_ENGINE, "MODEL": request.app.state.config.STT_MODEL, "WHISPER_MODEL": request.app.state.config.WHISPER_MODEL, + "DEEPGRAM_API_KEY": request.app.state.config.DEEPGRAM_API_KEY, }, } @@ -190,6 +193,7 @@ async def update_audio_config( request.app.state.config.STT_ENGINE = form_data.stt.ENGINE request.app.state.config.STT_MODEL = form_data.stt.MODEL request.app.state.config.WHISPER_MODEL = form_data.stt.WHISPER_MODEL + request.app.state.config.DEEPGRAM_API_KEY = form_data.stt.DEEPGRAM_API_KEY if request.app.state.config.STT_ENGINE == "": request.app.state.faster_whisper_model = set_faster_whisper_model( @@ -214,6 +218,7 @@ async def update_audio_config( "ENGINE": request.app.state.config.STT_ENGINE, "MODEL": request.app.state.config.STT_MODEL, "WHISPER_MODEL": request.app.state.config.WHISPER_MODEL, + "DEEPGRAM_API_KEY": request.app.state.config.DEEPGRAM_API_KEY, }, } @@ -521,6 +526,65 @@ def transcribe(request: Request, file_path): raise Exception(detail if detail else "Open WebUI: Server Connection Error") + elif request.app.state.config.STT_ENGINE == "deepgram": + try: + # Determine the MIME type of the file + mime, _ = mimetypes.guess_type(file_path) + if not mime: + mime = "audio/wav" # fallback to wav if undetectable + + # Read the audio file + with open(file_path, "rb") as f: + file_data = f.read() + + # Build headers and parameters + headers = { + "Authorization": f"Token {request.app.state.config.DEEPGRAM_API_KEY}", + "Content-Type": mime, + } + + # Add model if specified + params = {} + if request.app.state.config.STT_MODEL: + params["model"] = request.app.state.config.STT_MODEL + + # Make request to Deepgram API + r = requests.post( + "https://api.deepgram.com/v1/listen", + headers=headers, + params=params, + data=file_data, + ) + r.raise_for_status() + response_data = r.json() + + # Extract transcript from Deepgram response + try: + transcript = response_data["results"]["channels"][0]["alternatives"][0].get("transcript", "") + except (KeyError, IndexError) as e: + log.error(f"Malformed response from Deepgram: {str(e)}") + raise Exception("Failed to parse Deepgram response - unexpected response format") + data = {"text": transcript.strip()} + + # Save transcript + transcript_file = f"{file_dir}/{id}.json" + with open(transcript_file, "w") as f: + json.dump(data, f) + + return data + + except Exception as e: + log.exception(e) + detail = None + if r is not None: + try: + res = r.json() + if "error" in res: + detail = f"External: {res['error'].get('message', '')}" + except Exception: + detail = f"External: {e}" + raise Exception(detail if detail else "Open WebUI: Server Connection Error") + def compress_audio(file_path): if os.path.getsize(file_path) > MAX_FILE_SIZE: diff --git a/src/lib/components/admin/Settings/Audio.svelte b/src/lib/components/admin/Settings/Audio.svelte index a7a0300276..f3f3f3bb69 100644 --- a/src/lib/components/admin/Settings/Audio.svelte +++ b/src/lib/components/admin/Settings/Audio.svelte @@ -39,6 +39,7 @@ let STT_ENGINE = ''; let STT_MODEL = ''; let STT_WHISPER_MODEL = ''; + let STT_DEEPGRAM_API_KEY = ''; let STT_WHISPER_MODEL_LOADING = false; @@ -103,7 +104,8 @@ OPENAI_API_KEY: STT_OPENAI_API_KEY, ENGINE: STT_ENGINE, MODEL: STT_MODEL, - WHISPER_MODEL: STT_WHISPER_MODEL + WHISPER_MODEL: STT_WHISPER_MODEL, + DEEPGRAM_API_KEY: STT_DEEPGRAM_API_KEY } }); @@ -143,6 +145,7 @@ STT_ENGINE = res.stt.ENGINE; STT_MODEL = res.stt.MODEL; STT_WHISPER_MODEL = res.stt.WHISPER_MODEL; + STT_DEEPGRAM_API_KEY = res.stt.DEEPGRAM_API_KEY; } await getVoices(); @@ -173,6 +176,7 @@ + @@ -210,6 +214,37 @@ + {:else if STT_ENGINE === 'deepgram'} +
+
+ +
+
+ +
+ +
+
{$i18n.t('STT Model')}
+
+
+ +
+
+
+ {$i18n.t('Leave model field empty to use the default model.')} + + {$i18n.t('Click here to see available models.')} + +
+
{:else if STT_ENGINE === ''}
{$i18n.t('STT Model')}
From 98ba0c37b9862c1f893a4eb078a7f37588ebdcff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Feb 2025 09:25:26 +0000 Subject: [PATCH 02/96] build(deps-dev): bump vitest Bumps the npm_and_yarn group with 1 update in the / directory: [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest). Updates `vitest` from 1.6.0 to 1.6.1 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v1.6.1/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-type: direct:development dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- package-lock.json | 122 ++++++++++++++++++++++++++++------------------ package.json | 2 +- 2 files changed, 75 insertions(+), 49 deletions(-) diff --git a/package-lock.json b/package-lock.json index 074ff21c12..3483729283 100644 --- a/package-lock.json +++ b/package-lock.json @@ -91,7 +91,7 @@ "tslib": "^2.4.1", "typescript": "^5.5.4", "vite": "^5.4.14", - "vitest": "^1.6.0" + "vitest": "^1.6.1" }, "engines": { "node": ">=18.13.0 <=22.x.x", @@ -1558,6 +1558,7 @@ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, + "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" }, @@ -2210,7 +2211,8 @@ "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", @@ -3146,13 +3148,14 @@ "integrity": "sha512-g7f0IkJdPW2xhY7H4iE72DAsIyfuwEFc6JWc2tYFwKDMWWAF699vGjrM348cwQuOXgHpe1gWFe+Eiyjx/ewvvw==" }, "node_modules/@vitest/expect": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", - "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", + "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", "chai": "^4.3.10" }, "funding": { @@ -3160,12 +3163,13 @@ } }, "node_modules/@vitest/runner": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz", - "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", + "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/utils": "1.6.0", + "@vitest/utils": "1.6.1", "p-limit": "^5.0.0", "pathe": "^1.1.1" }, @@ -3178,6 +3182,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", "dev": true, + "license": "MIT", "dependencies": { "yocto-queue": "^1.0.0" }, @@ -3189,10 +3194,11 @@ } }, "node_modules/@vitest/runner/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", "dev": true, + "license": "MIT", "engines": { "node": ">=12.20" }, @@ -3201,10 +3207,11 @@ } }, "node_modules/@vitest/snapshot": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", - "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", + "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", "dev": true, + "license": "MIT", "dependencies": { "magic-string": "^0.30.5", "pathe": "^1.1.1", @@ -3215,10 +3222,11 @@ } }, "node_modules/@vitest/spy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", - "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", + "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", "dev": true, + "license": "MIT", "dependencies": { "tinyspy": "^2.2.0" }, @@ -3227,10 +3235,11 @@ } }, "node_modules/@vitest/utils": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", - "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", "dev": true, + "license": "MIT", "dependencies": { "diff-sequences": "^29.6.3", "estree-walker": "^3.0.3", @@ -3246,6 +3255,7 @@ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" } @@ -3496,6 +3506,7 @@ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true, + "license": "MIT", "engines": { "node": "*" } @@ -3895,6 +3906,7 @@ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -3972,10 +3984,11 @@ "dev": true }, "node_modules/chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", "dev": true, + "license": "MIT", "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", @@ -3983,7 +3996,7 @@ "get-func-name": "^2.0.2", "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.8" + "type-detect": "^4.1.0" }, "engines": { "node": ">=4" @@ -4019,6 +4032,7 @@ "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "dev": true, + "license": "MIT", "dependencies": { "get-func-name": "^2.0.2" }, @@ -5135,10 +5149,11 @@ } }, "node_modules/deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", "dev": true, + "license": "MIT", "dependencies": { "type-detect": "^4.0.0" }, @@ -5257,6 +5272,7 @@ "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "dev": true, + "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -6238,6 +6254,7 @@ "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, + "license": "MIT", "engines": { "node": "*" } @@ -7464,6 +7481,7 @@ "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", "dev": true, + "license": "MIT", "dependencies": { "get-func-name": "^2.0.1" } @@ -8686,6 +8704,7 @@ "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true, + "license": "MIT", "engines": { "node": "*" } @@ -9064,6 +9083,7 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, + "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -9078,6 +9098,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -9504,7 +9525,8 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/read-cache": { "version": "1.0.0", @@ -11247,6 +11269,7 @@ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -11406,10 +11429,11 @@ "integrity": "sha512-fLIydlJy7IG9XL4wjRwEcKhxx/ekLXiWiMvcGo01cOMF+TN+5ZqajM1mRNRz2bNNi1bzou2yofhjZEQi7kgl9A==" }, "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -11749,10 +11773,11 @@ } }, "node_modules/vite-node": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", - "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", + "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", "dev": true, + "license": "MIT", "dependencies": { "cac": "^6.7.14", "debug": "^4.3.4", @@ -12166,16 +12191,17 @@ } }, "node_modules/vitest": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", - "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", + "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/expect": "1.6.0", - "@vitest/runner": "1.6.0", - "@vitest/snapshot": "1.6.0", - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", + "@vitest/expect": "1.6.1", + "@vitest/runner": "1.6.1", + "@vitest/snapshot": "1.6.1", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", "acorn-walk": "^8.3.2", "chai": "^4.3.10", "debug": "^4.3.4", @@ -12189,7 +12215,7 @@ "tinybench": "^2.5.1", "tinypool": "^0.8.3", "vite": "^5.0.0", - "vite-node": "1.6.0", + "vite-node": "1.6.1", "why-is-node-running": "^2.2.2" }, "bin": { @@ -12204,8 +12230,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.6.0", - "@vitest/ui": "1.6.0", + "@vitest/browser": "1.6.1", + "@vitest/ui": "1.6.1", "happy-dom": "*", "jsdom": "*" }, diff --git a/package.json b/package.json index d2c4795c64..bfad6ef6ea 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "tslib": "^2.4.1", "typescript": "^5.5.4", "vite": "^5.4.14", - "vitest": "^1.6.0" + "vitest": "^1.6.1" }, "type": "module", "dependencies": { From 68703951e8abbdbed469953ad9beaa22806b5da2 Mon Sep 17 00:00:00 2001 From: "M.Abdulrahman Alnaseer" <20760062+abdalrohman@users.noreply.github.com> Date: Wed, 5 Feb 2025 19:14:40 +0300 Subject: [PATCH 03/96] feat(ui): implement domain filter list for web search settings --- backend/open_webui/routers/retrieval.py | 6 +++++ .../admin/Settings/WebSearch.svelte | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index 35cea62376..434f392c32 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -392,6 +392,7 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)): "exa_api_key": request.app.state.config.EXA_API_KEY, "result_count": request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, "concurrent_requests": request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS, + "domain_filter_list": request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST, }, }, } @@ -441,6 +442,7 @@ class WebSearchConfig(BaseModel): exa_api_key: Optional[str] = None result_count: Optional[int] = None concurrent_requests: Optional[int] = None + domain_filter_list: Optional[List[str]] = [] class WebConfig(BaseModel): @@ -553,6 +555,9 @@ async def update_rag_config( request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = ( form_data.web.search.concurrent_requests ) + request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = ( + form_data.web.search.domain_filter_list + ) return { "status": True, @@ -599,6 +604,7 @@ async def update_rag_config( "exa_api_key": request.app.state.config.EXA_API_KEY, "result_count": request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, "concurrent_requests": request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS, + "domain_filter_list": request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST, }, }, } diff --git a/src/lib/components/admin/Settings/WebSearch.svelte b/src/lib/components/admin/Settings/WebSearch.svelte index b2accbf1d7..927086d5d4 100644 --- a/src/lib/components/admin/Settings/WebSearch.svelte +++ b/src/lib/components/admin/Settings/WebSearch.svelte @@ -34,6 +34,14 @@ let youtubeProxyUrl = ''; const submitHandler = async () => { + // Convert domain filter string to array before sending + if (webConfig?.search?.domain_filter_list) { + webConfig.search.domain_filter_list = webConfig.search.domain_filter_list + .split(',') + .map(domain => domain.trim()) + .filter(domain => domain.length > 0); + } + const res = await updateRAGConfig(localStorage.token, { web: webConfig, youtube: { @@ -49,6 +57,10 @@ if (res) { webConfig = res.web; + // Convert array back to comma-separated string for display + if (webConfig?.search?.domain_filter_list) { + webConfig.search.domain_filter_list = webConfig.search.domain_filter_list.join(', '); + } youtubeLanguage = res.youtube.language.join(','); youtubeTranslation = res.youtube.translation; @@ -334,6 +346,18 @@ />
+ +
+
+ {$i18n.t('Domain Filter List')} +
+ + +
{/if} From 34b62e71cc1b0c3d98e7bc2b9d1091e2fbf1f0d2 Mon Sep 17 00:00:00 2001 From: "D. MacAlpine" Date: Wed, 5 Feb 2025 21:31:55 -0500 Subject: [PATCH 04/96] fix: check for email claim before skipping userinfo endpoint --- backend/open_webui/utils/oauth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 7c0c53c2d5..83e0ca1d6d 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -193,7 +193,7 @@ class OAuthManager: log.warning(f"OAuth callback error: {e}") raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) user_data: UserInfo = token.get("userinfo") - if not user_data: + if not user_data or "email" not in user_data: user_data: UserInfo = await client.userinfo(token=token) if not user_data: log.warning(f"OAuth callback failed, user data is missing: {token}") From c676303a55b9c78800efbd7fd70c84b4ccc356ed Mon Sep 17 00:00:00 2001 From: Rory <16675082+roryeckel@users.noreply.github.com> Date: Wed, 5 Feb 2025 23:26:13 -0600 Subject: [PATCH 05/96] enh: automatically remove incorrect backticks before code_interpreter tags --- backend/open_webui/utils/middleware.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 06763483cb..402b699c16 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -1188,6 +1188,23 @@ async def process_chat_response( output = block.get("output", None) lang = attributes.get("lang", "") + # Separate content from ending whitespace but preserve it + original_whitespace = '' + content_stripped = content.rstrip() + if len(content) > len(content_stripped): + original_whitespace = content[len(content_stripped):] + + # Count the number of backticks to identify if we are in an opening code block + backtick_segments = content_stripped.split('```') + # Odd number of ``` segments -> the last backticks are closing a block + # Even number -> the last backticks are opening a new block + if len(backtick_segments) > 1 and len(backtick_segments) % 2 == 0: + # The trailing backticks are opening a new block, they need to be removed or it will break the code interpreter markdown + content = content_stripped.rstrip('`').rstrip() + original_whitespace + else: + # The trailing backticks are closing a block (or there are no backticks), so it won't cause issues + content = content_stripped + original_whitespace + if output: output = html.escape(json.dumps(output)) From 74b971b88861b26c9be76cd11c068de4336187b0 Mon Sep 17 00:00:00 2001 From: Rory <16675082+roryeckel@users.noreply.github.com> Date: Wed, 5 Feb 2025 23:38:35 -0600 Subject: [PATCH 06/96] refac: clean up solution for correcting code_interpreter backticks --- backend/open_webui/utils/middleware.py | 27 +++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 402b699c16..331b850ff3 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -1122,6 +1122,16 @@ async def process_chat_response( }, ) + def split_content_and_whitespace(content): + content_stripped = content.rstrip() + original_whitespace = content[len(content_stripped):] if len(content) > len(content_stripped) else '' + return content_stripped, original_whitespace + + def is_opening_code_block(content): + backtick_segments = content.split('```') + # Even number of segments means the last backticks are opening a new block + return len(backtick_segments) > 1 and len(backtick_segments) % 2 == 0 + # Handle as a background task async def post_response_handler(response, events): def serialize_content_blocks(content_blocks, raw=False): @@ -1188,21 +1198,12 @@ async def process_chat_response( output = block.get("output", None) lang = attributes.get("lang", "") - # Separate content from ending whitespace but preserve it - original_whitespace = '' - content_stripped = content.rstrip() - if len(content) > len(content_stripped): - original_whitespace = content[len(content_stripped):] - - # Count the number of backticks to identify if we are in an opening code block - backtick_segments = content_stripped.split('```') - # Odd number of ``` segments -> the last backticks are closing a block - # Even number -> the last backticks are opening a new block - if len(backtick_segments) > 1 and len(backtick_segments) % 2 == 0: - # The trailing backticks are opening a new block, they need to be removed or it will break the code interpreter markdown + content_stripped, original_whitespace = split_content_and_whitespace(content) + if is_opening_code_block(content_stripped): + # Remove trailing backticks that would open a new block content = content_stripped.rstrip('`').rstrip() + original_whitespace else: - # The trailing backticks are closing a block (or there are no backticks), so it won't cause issues + # Keep content as is - either closing backticks or no backticks content = content_stripped + original_whitespace if output: From fd6b0398591aa59b31879bef1f701ec18cb93fd8 Mon Sep 17 00:00:00 2001 From: Vineeth B V <37930821+vinsdragonis@users.noreply.github.com> Date: Thu, 6 Feb 2025 12:04:14 +0530 Subject: [PATCH 07/96] Added a query method for OpenSearch vector db. - This PR aims to address the error 400: "**'OpenSearchClient' object has no attribute 'query'**". - With the implemented query() method, this issue should be resolved and allow uploaded documents to be vectorized and retrieved based on the given query. --- .../retrieval/vector/dbs/opensearch.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/backend/open_webui/retrieval/vector/dbs/opensearch.py b/backend/open_webui/retrieval/vector/dbs/opensearch.py index b3d8b5eb8a..c4732e1bc0 100644 --- a/backend/open_webui/retrieval/vector/dbs/opensearch.py +++ b/backend/open_webui/retrieval/vector/dbs/opensearch.py @@ -113,6 +113,40 @@ class OpenSearchClient: return self._result_to_search_result(result) + def query( + self, index_name: str, filter: dict, limit: Optional[int] = None + ) -> Optional[GetResult]: + if not self.has_collection(index_name): + return None + + query_body = { + "query": { + "bool": { + "filter": [] + } + }, + "_source": ["text", "metadata"], + } + + for field, value in filter.items(): + query_body["query"]["bool"]["filter"].append({ + "term": {field: value} + }) + + size = limit if limit else 10 + + try: + result = self.client.search( + index=f"{self.index_prefix}_{index_name}", + body=query_body, + size=size + ) + + return self._result_to_get_result(result) + + except Exception as e: + return None + def get_or_create_index(self, index_name: str, dimension: int): if not self.has_index(index_name): self._create_index(index_name, dimension) From 80e123f58f1c8c807aad29489e394be38a73f393 Mon Sep 17 00:00:00 2001 From: hurxxxx Date: Thu, 6 Feb 2025 16:41:24 +0900 Subject: [PATCH 08/96] fix : O3 also does not support the max_tokens parameter, so title generation is not possible when using the O3 model --- backend/open_webui/routers/openai.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py index d18f2a8ffc..96ed50ceb3 100644 --- a/backend/open_webui/routers/openai.py +++ b/backend/open_webui/routers/openai.py @@ -75,9 +75,9 @@ async def cleanup_response( await session.close() -def openai_o1_handler(payload): +def openai_o1_o3_handler(payload): """ - Handle O1 specific parameters + Handle o1, o3 specific parameters """ if "max_tokens" in payload: # Remove "max_tokens" from the payload @@ -621,10 +621,10 @@ async def generate_chat_completion( url = request.app.state.config.OPENAI_API_BASE_URLS[idx] key = request.app.state.config.OPENAI_API_KEYS[idx] - # Fix: O1 does not support the "max_tokens" parameter, Modify "max_tokens" to "max_completion_tokens" - is_o1 = payload["model"].lower().startswith("o1-") - if is_o1: - payload = openai_o1_handler(payload) + # Fix: o1,o3 does not support the "max_tokens" parameter, Modify "max_tokens" to "max_completion_tokens" + is_o1_o3 = payload["model"].lower().startswith(("o1-", "o3-")) + if is_o1_o3: + payload = openai_o1_o3_handler(payload) elif "api.openai.com" not in url: # Remove "max_completion_tokens" from the payload for backward compatibility if "max_completion_tokens" in payload: From b9480c0e8a16aee5ec0919aa973db75389ae5b54 Mon Sep 17 00:00:00 2001 From: hurxxxx Date: Thu, 6 Feb 2025 16:53:04 +0900 Subject: [PATCH 09/96] fix : o1 should also be applied --- backend/open_webui/routers/openai.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py index 96ed50ceb3..afda362373 100644 --- a/backend/open_webui/routers/openai.py +++ b/backend/open_webui/routers/openai.py @@ -622,7 +622,7 @@ async def generate_chat_completion( key = request.app.state.config.OPENAI_API_KEYS[idx] # Fix: o1,o3 does not support the "max_tokens" parameter, Modify "max_tokens" to "max_completion_tokens" - is_o1_o3 = payload["model"].lower().startswith(("o1-", "o3-")) + is_o1_o3 = payload["model"].lower().startswith(("o1", "o3-")) if is_o1_o3: payload = openai_o1_o3_handler(payload) elif "api.openai.com" not in url: From 7c78facfd9ae16c69df93dcf7e8ce3e2ea5744be Mon Sep 17 00:00:00 2001 From: Vineeth B V <37930821+vinsdragonis@users.noreply.github.com> Date: Thu, 6 Feb 2025 13:36:11 +0530 Subject: [PATCH 10/96] Update opensearch.py --- backend/open_webui/retrieval/vector/dbs/opensearch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/open_webui/retrieval/vector/dbs/opensearch.py b/backend/open_webui/retrieval/vector/dbs/opensearch.py index c4732e1bc0..41d6343912 100644 --- a/backend/open_webui/retrieval/vector/dbs/opensearch.py +++ b/backend/open_webui/retrieval/vector/dbs/opensearch.py @@ -114,9 +114,9 @@ class OpenSearchClient: return self._result_to_search_result(result) def query( - self, index_name: str, filter: dict, limit: Optional[int] = None + self, collection_name: str, filter: dict, limit: Optional[int] = None ) -> Optional[GetResult]: - if not self.has_collection(index_name): + if not self.has_collection(collection_name): return None query_body = { @@ -137,7 +137,7 @@ class OpenSearchClient: try: result = self.client.search( - index=f"{self.index_prefix}_{index_name}", + index=f"{self.index_prefix}_{collection_name}", body=query_body, size=size ) From a023667e1ee2e8ff00cc6aaa46f5106252937241 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 6 Feb 2025 00:37:10 -0800 Subject: [PATCH 11/96] fix: user params save issue --- src/lib/components/chat/Settings/General.svelte | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib/components/chat/Settings/General.svelte b/src/lib/components/chat/Settings/General.svelte index ce8ed80e22..cb7843c694 100644 --- a/src/lib/components/chat/Settings/General.svelte +++ b/src/lib/components/chat/Settings/General.svelte @@ -49,6 +49,7 @@ function_calling: null, seed: null, temperature: null, + reasoning_effort: null, frequency_penalty: null, repeat_last_n: null, mirostat: null, @@ -333,9 +334,13 @@ system: system !== '' ? system : undefined, params: { stream_response: params.stream_response !== null ? params.stream_response : undefined, + function_calling: + params.function_calling !== null ? params.function_calling : undefined, seed: (params.seed !== null ? params.seed : undefined) ?? undefined, stop: params.stop ? params.stop.split(',').filter((e) => e) : undefined, temperature: params.temperature !== null ? params.temperature : undefined, + reasoning_effort: + params.reasoning_effort !== null ? params.reasoning_effort : undefined, frequency_penalty: params.frequency_penalty !== null ? params.frequency_penalty : undefined, repeat_last_n: params.repeat_last_n !== null ? params.repeat_last_n : undefined, From feffdf197f91e1c03a60d88288a538edb1829fc6 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 6 Feb 2025 00:38:06 -0800 Subject: [PATCH 12/96] doc: readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0fb03537df..56ab09b05d 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ docker run --rm --volume /var/run/docker.sock:/var/run/docker.sock containrrr/wa In the last part of the command, replace `open-webui` with your container name if it is different. -Check our Migration Guide available in our [Open WebUI Documentation](https://docs.openwebui.com/tutorials/migration/). +Check our Updating Guide available in our [Open WebUI Documentation](https://docs.openwebui.com/getting-started/updating). ### Using the Dev Branch 🌙 From a1e26016bb288464e6d2518dee71570963f4a270 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 6 Feb 2025 00:52:22 -0800 Subject: [PATCH 13/96] fix: new connections config --- src/lib/components/admin/Settings/Connections.svelte | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/lib/components/admin/Settings/Connections.svelte b/src/lib/components/admin/Settings/Connections.svelte index 35e6e0293a..c7145464cf 100644 --- a/src/lib/components/admin/Settings/Connections.svelte +++ b/src/lib/components/admin/Settings/Connections.svelte @@ -101,14 +101,17 @@ const addOpenAIConnectionHandler = async (connection) => { OPENAI_API_BASE_URLS = [...OPENAI_API_BASE_URLS, connection.url]; OPENAI_API_KEYS = [...OPENAI_API_KEYS, connection.key]; - OPENAI_API_CONFIGS[OPENAI_API_BASE_URLS.length] = connection.config; + OPENAI_API_CONFIGS[OPENAI_API_BASE_URLS.length - 1] = connection.config; await updateOpenAIHandler(); }; const addOllamaConnectionHandler = async (connection) => { OLLAMA_BASE_URLS = [...OLLAMA_BASE_URLS, connection.url]; - OLLAMA_API_CONFIGS[OLLAMA_BASE_URLS.length] = connection.config; + OLLAMA_API_CONFIGS[OLLAMA_BASE_URLS.length - 1] = { + ...connection.config, + key: connection.key + }; await updateOllamaHandler(); }; From 159578dfd41a5fc68ac6c7cfdcf0db837600fe35 Mon Sep 17 00:00:00 2001 From: Rodrigo Agundez Date: Thu, 6 Feb 2025 17:59:59 +0900 Subject: [PATCH 14/96] Enable usage of the DB to store generated images --- backend/open_webui/routers/images.py | 63 ++++++++++------------------ 1 file changed, 22 insertions(+), 41 deletions(-) diff --git a/backend/open_webui/routers/images.py b/backend/open_webui/routers/images.py index 7afd9d106d..68465e191c 100644 --- a/backend/open_webui/routers/images.py +++ b/backend/open_webui/routers/images.py @@ -7,26 +7,21 @@ import re import uuid from pathlib import Path from typing import Optional +import io import requests - - -from fastapi import Depends, FastAPI, HTTPException, Request, APIRouter -from fastapi.middleware.cors import CORSMiddleware -from pydantic import BaseModel - - +from fastapi import APIRouter, Depends, UploadFile, HTTPException, Request from open_webui.config import CACHE_DIR from open_webui.constants import ERROR_MESSAGES -from open_webui.env import ENV, SRC_LOG_LEVELS, ENABLE_FORWARD_USER_INFO_HEADERS - +from open_webui.env import ENABLE_FORWARD_USER_INFO_HEADERS, SRC_LOG_LEVELS +from open_webui.routers.files import upload_file from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.images.comfyui import ( ComfyUIGenerateImageForm, ComfyUIWorkflow, comfyui_generate_image, ) - +from pydantic import BaseModel log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["IMAGES"]) @@ -39,7 +34,7 @@ router = APIRouter() @router.get("/config") -async def get_config(request: Request, user=Depends(get_admin_user)): +async def get_def(request: Request, user=Depends(get_admin_user)): return { "enabled": request.app.state.config.ENABLE_IMAGE_GENERATION, "engine": request.app.state.config.IMAGE_GENERATION_ENGINE, @@ -271,7 +266,6 @@ async def get_image_config(request: Request, user=Depends(get_admin_user)): async def update_image_config( request: Request, form_data: ImageConfigForm, user=Depends(get_admin_user) ): - set_image_model(request, form_data.MODEL) pattern = r"^\d+x\d+$" @@ -383,35 +377,18 @@ class GenerateImageForm(BaseModel): negative_prompt: Optional[str] = None -def save_b64_image(b64_str): +def load_b64_image_data(b64_str): try: - image_id = str(uuid.uuid4()) - if "," in b64_str: header, encoded = b64_str.split(",", 1) mime_type = header.split(";")[0] - img_data = base64.b64decode(encoded) - image_format = mimetypes.guess_extension(mime_type) - - image_filename = f"{image_id}{image_format}" - file_path = IMAGE_CACHE_DIR / f"{image_filename}" - with open(file_path, "wb") as f: - f.write(img_data) - return image_filename else: - image_filename = f"{image_id}.png" - file_path = IMAGE_CACHE_DIR.joinpath(image_filename) - + mime_type = "image/png" img_data = base64.b64decode(b64_str) - - # Write the image data to a file - with open(file_path, "wb") as f: - f.write(img_data) - return image_filename - + return img_data, mime_type except Exception as e: - log.exception(f"Error saving image: {e}") + log.exception(f"Error loading image data: {e}") return None @@ -500,13 +477,17 @@ async def image_generations( images = [] for image in res["data"]: - image_filename = save_b64_image(image["b64_json"]) - images.append({"url": f"/cache/image/generations/{image_filename}"}) - file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json") - - with open(file_body_path, "w") as f: - json.dump(data, f) - + image_data, content_type = load_b64_image_data(image["b64_json"]) + file = UploadFile( + file=io.BytesIO(image_data), + filename="image", # will be converted to a unique ID on upload_file + headers={ + "content-type": content_type, + }, + ) + file_item = upload_file(request, file, user) + url = request.app.url_path_for("get_file_content_by_id", id=file_item.id) + images.append({"url": url}) return images elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui": @@ -618,4 +599,4 @@ async def image_generations( data = r.json() if "error" in data: error = data["error"]["message"] - raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(error)) + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(error)) \ No newline at end of file From 8ca21ea83830107e5842a91487276aa414fddcab Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 6 Feb 2025 01:07:01 -0800 Subject: [PATCH 15/96] refac: styling --- src/routes/(app)/admin/+layout.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/(app)/admin/+layout.svelte b/src/routes/(app)/admin/+layout.svelte index 2d10ae64b7..bc3caa338b 100644 --- a/src/routes/(app)/admin/+layout.svelte +++ b/src/routes/(app)/admin/+layout.svelte @@ -26,7 +26,7 @@ {#if loaded}
From 8d43fdadc17c6db83df4f474a505fae1f509e6ea Mon Sep 17 00:00:00 2001 From: Rodrigo Agundez Date: Thu, 6 Feb 2025 18:24:57 +0900 Subject: [PATCH 16/96] Add functionality in other image generation types --- backend/open_webui/routers/images.py | 64 +++++++++++++++++----------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/backend/open_webui/routers/images.py b/backend/open_webui/routers/images.py index 68465e191c..a26c06c611 100644 --- a/backend/open_webui/routers/images.py +++ b/backend/open_webui/routers/images.py @@ -1,5 +1,6 @@ import asyncio import base64 +import io import json import logging import mimetypes @@ -7,10 +8,9 @@ import re import uuid from pathlib import Path from typing import Optional -import io import requests -from fastapi import APIRouter, Depends, UploadFile, HTTPException, Request +from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile from open_webui.config import CACHE_DIR from open_webui.constants import ERROR_MESSAGES from open_webui.env import ENABLE_FORWARD_USER_INFO_HEADERS, SRC_LOG_LEVELS @@ -392,8 +392,7 @@ def load_b64_image_data(b64_str): return None -def save_url_image(url, headers=None): - image_id = str(uuid.uuid4()) +def load_url_image_data(url, headers=None): try: if headers: r = requests.get(url, headers=headers) @@ -403,18 +402,7 @@ def save_url_image(url, headers=None): r.raise_for_status() if r.headers["content-type"].split("/")[0] == "image": mime_type = r.headers["content-type"] - image_format = mimetypes.guess_extension(mime_type) - - if not image_format: - raise ValueError("Could not determine image type from MIME type") - - image_filename = f"{image_id}{image_format}" - - file_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}") - with open(file_path, "wb") as image_file: - for chunk in r.iter_content(chunk_size=8192): - image_file.write(chunk) - return image_filename + return r.content, mime_type else: log.error("Url does not point to an image.") return None @@ -486,8 +474,14 @@ async def image_generations( }, ) file_item = upload_file(request, file, user) - url = request.app.url_path_for("get_file_content_by_id", id=file_item.id) + url = request.app.url_path_for( + "get_file_content_by_id", id=file_item.id + ) images.append({"url": url}) + file_body_path = IMAGE_CACHE_DIR.joinpath(f"{file_item.id}.json") + + with open(file_body_path, "w") as f: + json.dump(data, f) return images elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui": @@ -533,9 +527,20 @@ async def image_generations( "Authorization": f"Bearer {request.app.state.config.COMFYUI_API_KEY}" } - image_filename = save_url_image(image["url"], headers) - images.append({"url": f"/cache/image/generations/{image_filename}"}) - file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json") + image_data, content_type = load_url_image_data(image["url"], headers) + file = UploadFile( + file=io.BytesIO(image_data), + filename="image", # will be converted to a unique ID on upload_file + headers={ + "content-type": content_type, + }, + ) + file_item = upload_file(request, file, user) + url = request.app.url_path_for( + "get_file_content_by_id", id=file_item.id + ) + images.append({"url": url}) + file_body_path = IMAGE_CACHE_DIR.joinpath(f"{file_item.id}.json") with open(file_body_path, "w") as f: json.dump(form_data.model_dump(exclude_none=True), f) @@ -585,9 +590,20 @@ async def image_generations( images = [] for image in res["images"]: - image_filename = save_b64_image(image) - images.append({"url": f"/cache/image/generations/{image_filename}"}) - file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json") + image_data, content_type = load_b64_image_data(image) + file = UploadFile( + file=io.BytesIO(image_data), + filename="image", # will be converted to a unique ID on upload_file + headers={ + "content-type": content_type, + }, + ) + file_item = upload_file(request, file, user) + url = request.app.url_path_for( + "get_file_content_by_id", id=file_item.id + ) + images.append({"url": url}) + file_body_path = IMAGE_CACHE_DIR.joinpath(f"{file_item.id}.json") with open(file_body_path, "w") as f: json.dump({**data, "info": res["info"]}, f) @@ -599,4 +615,4 @@ async def image_generations( data = r.json() if "error" in data: error = data["error"]["message"] - raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(error)) \ No newline at end of file + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(error)) From 14398ab62847c1840a9413efa0a34cf12df89dad Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 6 Feb 2025 01:28:33 -0800 Subject: [PATCH 17/96] refac: styling --- .../components/chat/Messages/Citations.svelte | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/lib/components/chat/Messages/Citations.svelte b/src/lib/components/chat/Messages/Citations.svelte index b434b15db1..232d8abb1c 100644 --- a/src/lib/components/chat/Messages/Citations.svelte +++ b/src/lib/components/chat/Messages/Citations.svelte @@ -97,7 +97,7 @@ {#if citations.length > 0}
{#if citations.length <= 3} -
+
{#each citations as citation, idx}
{:else} - +
-
- -
+
+ +
{#each citations.slice(0, 2) as citation, idx} {/each}
-
+
{citations.length - 2} {$i18n.t('more')} @@ -167,7 +175,7 @@
-
+
{#each citations as citation, idx} From 88db4ca7babc232f9bbb2962bb078178deb038b6 Mon Sep 17 00:00:00 2001 From: binxn <78713335+binxn@users.noreply.github.com> Date: Thu, 6 Feb 2025 14:30:27 +0100 Subject: [PATCH 18/96] Update jina_search.py Updated Jina's search function in order to use POST and make use of the result count passed by the user Note: Jina supports a max of 10 result count --- .../open_webui/retrieval/web/jina_search.py | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/backend/open_webui/retrieval/web/jina_search.py b/backend/open_webui/retrieval/web/jina_search.py index 3de6c18077..02af42ea64 100644 --- a/backend/open_webui/retrieval/web/jina_search.py +++ b/backend/open_webui/retrieval/web/jina_search.py @@ -20,14 +20,26 @@ def search_jina(api_key: str, query: str, count: int) -> list[SearchResult]: list[SearchResult]: A list of search results """ jina_search_endpoint = "https://s.jina.ai/" - headers = {"Accept": "application/json", "Authorization": f"Bearer {api_key}"} - url = str(URL(jina_search_endpoint + query)) - response = requests.get(url, headers=headers) + + headers = { + "Accept": "application/json", + "Content-Type": "application/json", + "Authorization": api_key, + "X-Retain-Images": "none" + } + + payload = { + "q": query, + "count": count if count <= 10 else 10 + } + + url = str(URL(jina_search_endpoint)) + response = requests.post(url, headers=headers, json=payload) response.raise_for_status() data = response.json() results = [] - for result in data["data"][:count]: + for result in data["data"]: results.append( SearchResult( link=result["url"], From 8215aa36d00be0d003267a3e2f6d24a046dfc5f5 Mon Sep 17 00:00:00 2001 From: Girish Ramakrishnan Date: Thu, 6 Feb 2025 17:57:00 +0100 Subject: [PATCH 19/96] oidc: pick up username correctly --- backend/open_webui/utils/oauth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 83e0ca1d6d..b98e6e585b 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -281,7 +281,7 @@ class OAuthManager: username_claim = auth_manager_config.OAUTH_USERNAME_CLAIM name = user_data.get(username_claim) - if not isinstance(user, str): + if not isinstance(name, str): name = email role = self.get_user_role(None, user_data) From ac3338265d224bbff2ed80310a7bd1bd4d763bed Mon Sep 17 00:00:00 2001 From: Rodrigo Agundez Date: Fri, 7 Feb 2025 07:30:58 +0900 Subject: [PATCH 20/96] Set get_config as the name of the function --- backend/open_webui/routers/images.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/open_webui/routers/images.py b/backend/open_webui/routers/images.py index a26c06c611..a437149560 100644 --- a/backend/open_webui/routers/images.py +++ b/backend/open_webui/routers/images.py @@ -34,7 +34,7 @@ router = APIRouter() @router.get("/config") -async def get_def(request: Request, user=Depends(get_admin_user)): +async def get_config(request: Request, user=Depends(get_admin_user)): return { "enabled": request.app.state.config.ENABLE_IMAGE_GENERATION, "engine": request.app.state.config.IMAGE_GENERATION_ENGINE, @@ -456,7 +456,7 @@ async def image_generations( requests.post, url=f"{request.app.state.config.IMAGES_OPENAI_API_BASE_URL}/images/generations", json=data, - headers=headers, + headers=headers, ) r.raise_for_status() From 7e97e9dcc925dbe969190a510cf1b34bf9fdd1d7 Mon Sep 17 00:00:00 2001 From: Rodrigo Agundez Date: Fri, 7 Feb 2025 07:37:18 +0900 Subject: [PATCH 21/96] Improve style --- backend/open_webui/routers/images.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/open_webui/routers/images.py b/backend/open_webui/routers/images.py index a437149560..df69485c2e 100644 --- a/backend/open_webui/routers/images.py +++ b/backend/open_webui/routers/images.py @@ -3,9 +3,7 @@ import base64 import io import json import logging -import mimetypes import re -import uuid from pathlib import Path from typing import Optional @@ -456,7 +454,7 @@ async def image_generations( requests.post, url=f"{request.app.state.config.IMAGES_OPENAI_API_BASE_URL}/images/generations", json=data, - headers=headers, + headers=headers, ) r.raise_for_status() From 89669a21fc4859e1a5dfb30778a7e3ce3791d0aa Mon Sep 17 00:00:00 2001 From: Xingjian Xie Date: Thu, 6 Feb 2025 23:01:43 +0000 Subject: [PATCH 22/96] Refactor common code between inlet and outlet --- backend/open_webui/utils/chat.py | 141 ++++++------------------- backend/open_webui/utils/filter.py | 100 ++++++++++++++++++ backend/open_webui/utils/middleware.py | 105 ++---------------- 3 files changed, 143 insertions(+), 203 deletions(-) create mode 100644 backend/open_webui/utils/filter.py diff --git a/backend/open_webui/utils/chat.py b/backend/open_webui/utils/chat.py index 0719f6af5b..ebd5bb5e34 100644 --- a/backend/open_webui/utils/chat.py +++ b/backend/open_webui/utils/chat.py @@ -44,6 +44,10 @@ from open_webui.utils.response import ( convert_response_ollama_to_openai, convert_streaming_response_ollama_to_openai, ) +from open_webui.utils.filter import ( + get_sorted_filter_ids, + process_filter_functions, +) from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL, BYPASS_MODEL_ACCESS_CONTROL @@ -177,116 +181,37 @@ async def chat_completed(request: Request, form_data: dict, user: Any): except Exception as e: return Exception(f"Error: {e}") - __event_emitter__ = get_event_emitter( - { - "chat_id": data["chat_id"], - "message_id": data["id"], - "session_id": data["session_id"], - "user_id": user.id, - } - ) + metadata = { + "chat_id": data["chat_id"], + "message_id": data["id"], + "session_id": data["session_id"], + "user_id": user.id, + } - __event_call__ = get_event_call( - { - "chat_id": data["chat_id"], - "message_id": data["id"], - "session_id": data["session_id"], - "user_id": user.id, - } - ) + extra_params = { + "__event_emitter__": get_event_emitter(metadata), + "__event_call__": get_event_call(metadata), + "__user__": { + "id": user.id, + "email": user.email, + "name": user.name, + "role": user.role, + }, + "__metadata__": metadata, + "__request__": request, + } - def get_priority(function_id): - function = Functions.get_function_by_id(function_id) - if function is not None and hasattr(function, "valves"): - # TODO: Fix FunctionModel to include vavles - return (function.valves if function.valves else {}).get("priority", 0) - return 0 - - filter_ids = [function.id for function in Functions.get_global_filter_functions()] - if "info" in model and "meta" in model["info"]: - filter_ids.extend(model["info"]["meta"].get("filterIds", [])) - filter_ids = list(set(filter_ids)) - - enabled_filter_ids = [ - function.id - for function in Functions.get_functions_by_type("filter", active_only=True) - ] - filter_ids = [ - filter_id for filter_id in filter_ids if filter_id in enabled_filter_ids - ] - - # Sort filter_ids by priority, using the get_priority function - filter_ids.sort(key=get_priority) - - for filter_id in filter_ids: - filter = Functions.get_function_by_id(filter_id) - if not filter: - continue - - if filter_id in request.app.state.FUNCTIONS: - function_module = request.app.state.FUNCTIONS[filter_id] - else: - function_module, _, _ = load_function_module_by_id(filter_id) - request.app.state.FUNCTIONS[filter_id] = function_module - - if hasattr(function_module, "valves") and hasattr(function_module, "Valves"): - valves = Functions.get_function_valves_by_id(filter_id) - function_module.valves = function_module.Valves( - **(valves if valves else {}) - ) - - if not hasattr(function_module, "outlet"): - continue - try: - outlet = function_module.outlet - - # Get the signature of the function - sig = inspect.signature(outlet) - params = {"body": data} - - # Extra parameters to be passed to the function - extra_params = { - "__model__": model, - "__id__": filter_id, - "__event_emitter__": __event_emitter__, - "__event_call__": __event_call__, - "__request__": request, - } - - # Add extra params in contained in function signature - for key, value in extra_params.items(): - if key in sig.parameters: - params[key] = value - - if "__user__" in sig.parameters: - __user__ = { - "id": user.id, - "email": user.email, - "name": user.name, - "role": user.role, - } - - try: - if hasattr(function_module, "UserValves"): - __user__["valves"] = function_module.UserValves( - **Functions.get_user_valves_by_id_and_user_id( - filter_id, user.id - ) - ) - except Exception as e: - print(e) - - params = {**params, "__user__": __user__} - - if inspect.iscoroutinefunction(outlet): - data = await outlet(**params) - else: - data = outlet(**params) - - except Exception as e: - return Exception(f"Error: {e}") - - return data + try: + result, _ = await process_filter_functions( + handler_type="outlet", + filter_ids=get_sorted_filter_ids(model), + request=request, + data=data, + extra_params=extra_params, + ) + return result + except Exception as e: + return Exception(f"Error: {e}") async def chat_action(request: Request, action_id: str, form_data: dict, user: Any): diff --git a/backend/open_webui/utils/filter.py b/backend/open_webui/utils/filter.py new file mode 100644 index 0000000000..2ad0c025e6 --- /dev/null +++ b/backend/open_webui/utils/filter.py @@ -0,0 +1,100 @@ +import inspect +from open_webui.utils.plugin import load_function_module_by_id +from open_webui.models.functions import Functions + +def get_sorted_filter_ids(model): + def get_priority(function_id): + function = Functions.get_function_by_id(function_id) + if function is not None and hasattr(function, "valves"): + # TODO: Fix FunctionModel to include vavles + return (function.valves if function.valves else {}).get("priority", 0) + return 0 + + filter_ids = [function.id for function in Functions.get_global_filter_functions()] + if "info" in model and "meta" in model["info"]: + filter_ids.extend(model["info"]["meta"].get("filterIds", [])) + filter_ids = list(set(filter_ids)) + + enabled_filter_ids = [ + function.id + for function in Functions.get_functions_by_type("filter", active_only=True) + ] + + filter_ids = [fid for fid in filter_ids if fid in enabled_filter_ids] + filter_ids.sort(key=get_priority) + return filter_ids + +async def process_filter_functions( + handler_type, + filter_ids, + request, + data, + extra_params +): + skip_files = None + + for filter_id in filter_ids: + filter = Functions.get_function_by_id(filter_id) + if not filter: + continue + + if filter_id in request.app.state.FUNCTIONS: + function_module = request.app.state.FUNCTIONS[filter_id] + else: + function_module, _, _ = load_function_module_by_id(filter_id) + request.app.state.FUNCTIONS[filter_id] = function_module + + # Check if the function has a file_handler variable + if handler_type == "inlet" and hasattr(function_module, "file_handler"): + skip_files = function_module.file_handler + + # Apply valves to the function + if hasattr(function_module, "valves") and hasattr(function_module, "Valves"): + valves = Functions.get_function_valves_by_id(filter_id) + function_module.valves = function_module.Valves( + **(valves if valves else {}) + ) + + # Prepare handler function + handler = getattr(function_module, handler_type, None) + if not handler: + continue + + try: + # Prepare parameters + sig = inspect.signature(handler) + params = {"body": data} + + # Add extra parameters that exist in the handler's signature + for key in list(extra_params.keys()): + if key in sig.parameters: + params[key] = extra_params[key] + + # Handle user parameters + if "__user__" in sig.parameters: + if hasattr(function_module, "UserValves"): + try: + params["__user__"]["valves"] = function_module.UserValves( + **Functions.get_user_valves_by_id_and_user_id( + filter_id, params["__user__"]["id"] + ) + ) + except Exception as e: + print(e) + + + # Execute handler + if inspect.iscoroutinefunction(handler): + data = await handler(**params) + else: + data = handler(**params) + + except Exception as e: + print(f"Error in {handler_type} handler {filter_id}: {e}") + raise e + + # Handle file cleanup for inlet + if skip_files and "files" in data.get("metadata", {}): + del data["metadata"]["files"] + + return data, {} \ No newline at end of file diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 331b850ff3..c69d0c909d 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -68,6 +68,10 @@ from open_webui.utils.misc import ( ) from open_webui.utils.tools import get_tools from open_webui.utils.plugin import load_function_module_by_id +from open_webui.utils.filter import ( + get_sorted_filter_ids, + process_filter_functions, +) from open_webui.tasks import create_task @@ -91,99 +95,6 @@ log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["MAIN"]) -async def chat_completion_filter_functions_handler(request, body, model, extra_params): - skip_files = None - - def get_filter_function_ids(model): - def get_priority(function_id): - function = Functions.get_function_by_id(function_id) - if function is not None and hasattr(function, "valves"): - # TODO: Fix FunctionModel - return (function.valves if function.valves else {}).get("priority", 0) - return 0 - - filter_ids = [ - function.id for function in Functions.get_global_filter_functions() - ] - if "info" in model and "meta" in model["info"]: - filter_ids.extend(model["info"]["meta"].get("filterIds", [])) - filter_ids = list(set(filter_ids)) - - enabled_filter_ids = [ - function.id - for function in Functions.get_functions_by_type("filter", active_only=True) - ] - - filter_ids = [ - filter_id for filter_id in filter_ids if filter_id in enabled_filter_ids - ] - - filter_ids.sort(key=get_priority) - return filter_ids - - filter_ids = get_filter_function_ids(model) - for filter_id in filter_ids: - filter = Functions.get_function_by_id(filter_id) - if not filter: - continue - - if filter_id in request.app.state.FUNCTIONS: - function_module = request.app.state.FUNCTIONS[filter_id] - else: - function_module, _, _ = load_function_module_by_id(filter_id) - request.app.state.FUNCTIONS[filter_id] = function_module - - # Check if the function has a file_handler variable - if hasattr(function_module, "file_handler"): - skip_files = function_module.file_handler - - # Apply valves to the function - if hasattr(function_module, "valves") and hasattr(function_module, "Valves"): - valves = Functions.get_function_valves_by_id(filter_id) - function_module.valves = function_module.Valves( - **(valves if valves else {}) - ) - - if hasattr(function_module, "inlet"): - try: - inlet = function_module.inlet - - # Create a dictionary of parameters to be passed to the function - params = {"body": body} | { - k: v - for k, v in { - **extra_params, - "__model__": model, - "__id__": filter_id, - }.items() - if k in inspect.signature(inlet).parameters - } - - if "__user__" in params and hasattr(function_module, "UserValves"): - try: - params["__user__"]["valves"] = function_module.UserValves( - **Functions.get_user_valves_by_id_and_user_id( - filter_id, params["__user__"]["id"] - ) - ) - except Exception as e: - print(e) - - if inspect.iscoroutinefunction(inlet): - body = await inlet(**params) - else: - body = inlet(**params) - - except Exception as e: - print(f"Error: {e}") - raise e - - if skip_files and "files" in body.get("metadata", {}): - del body["metadata"]["files"] - - return body, {} - - async def chat_completion_tools_handler( request: Request, body: dict, user: UserModel, models, tools ) -> tuple[dict, dict]: @@ -782,8 +693,12 @@ async def process_chat_payload(request, form_data, metadata, user, model): ) try: - form_data, flags = await chat_completion_filter_functions_handler( - request, form_data, model, extra_params + form_data, flags = await process_filter_functions( + handler_type="inlet", + filter_ids=get_sorted_filter_ids(model), + request=request, + data=form_data, + extra_params=extra_params, ) except Exception as e: raise Exception(f"Error: {e}") From 4974c9cbb08eb9efd998c5eeb5ba75af827306b3 Mon Sep 17 00:00:00 2001 From: Rodrigo Agundez Date: Fri, 7 Feb 2025 08:12:04 +0900 Subject: [PATCH 23/96] Refactor upload function --- backend/open_webui/routers/images.py | 77 +++++++++++----------------- 1 file changed, 31 insertions(+), 46 deletions(-) diff --git a/backend/open_webui/routers/images.py b/backend/open_webui/routers/images.py index df69485c2e..2d608c1339 100644 --- a/backend/open_webui/routers/images.py +++ b/backend/open_webui/routers/images.py @@ -410,6 +410,24 @@ def load_url_image_data(url, headers=None): return None +def upload_image(request, data, image_data, content_type, user): + file = UploadFile( + file=io.BytesIO(image_data), + filename="image", # will be converted to a unique ID on upload_file + headers={ + "content-type": content_type, + }, + ) + file_item = upload_file(request, file, user) + file_body_path = IMAGE_CACHE_DIR.joinpath(f"{file_item.id}.json") + + with open(file_body_path, "w") as f: + json.dump(data, f) + + url = request.app.url_path_for("get_file_content_by_id", id=file_item.id) + return url + + @router.post("/generations") async def image_generations( request: Request, @@ -464,22 +482,8 @@ async def image_generations( for image in res["data"]: image_data, content_type = load_b64_image_data(image["b64_json"]) - file = UploadFile( - file=io.BytesIO(image_data), - filename="image", # will be converted to a unique ID on upload_file - headers={ - "content-type": content_type, - }, - ) - file_item = upload_file(request, file, user) - url = request.app.url_path_for( - "get_file_content_by_id", id=file_item.id - ) + url = upload_image(request, data, image_data, content_type, user) images.append({"url": url}) - file_body_path = IMAGE_CACHE_DIR.joinpath(f"{file_item.id}.json") - - with open(file_body_path, "w") as f: - json.dump(data, f) return images elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui": @@ -526,24 +530,14 @@ async def image_generations( } image_data, content_type = load_url_image_data(image["url"], headers) - file = UploadFile( - file=io.BytesIO(image_data), - filename="image", # will be converted to a unique ID on upload_file - headers={ - "content-type": content_type, - }, - ) - file_item = upload_file(request, file, user) - url = request.app.url_path_for( - "get_file_content_by_id", id=file_item.id + url = upload_image( + request, + form_data.model_dump(exclude_none=True), + image_data, + content_type, + user, ) images.append({"url": url}) - file_body_path = IMAGE_CACHE_DIR.joinpath(f"{file_item.id}.json") - - with open(file_body_path, "w") as f: - json.dump(form_data.model_dump(exclude_none=True), f) - - log.debug(f"images: {images}") return images elif ( request.app.state.config.IMAGE_GENERATION_ENGINE == "automatic1111" @@ -589,23 +583,14 @@ async def image_generations( for image in res["images"]: image_data, content_type = load_b64_image_data(image) - file = UploadFile( - file=io.BytesIO(image_data), - filename="image", # will be converted to a unique ID on upload_file - headers={ - "content-type": content_type, - }, - ) - file_item = upload_file(request, file, user) - url = request.app.url_path_for( - "get_file_content_by_id", id=file_item.id + url = upload_image( + request, + {**data, "info": res["info"]}, + image_data, + content_type, + user, ) images.append({"url": url}) - file_body_path = IMAGE_CACHE_DIR.joinpath(f"{file_item.id}.json") - - with open(file_body_path, "w") as f: - json.dump({**data, "info": res["info"]}, f) - return images except Exception as e: error = e From 312f273a1bf60f66089fbcd9f7ad225466419d30 Mon Sep 17 00:00:00 2001 From: Rodrigo Agundez Date: Fri, 7 Feb 2025 08:22:20 +0900 Subject: [PATCH 24/96] Add extension to image filename --- backend/open_webui/routers/images.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/open_webui/routers/images.py b/backend/open_webui/routers/images.py index 2d608c1339..da4bf8a17e 100644 --- a/backend/open_webui/routers/images.py +++ b/backend/open_webui/routers/images.py @@ -3,6 +3,7 @@ import base64 import io import json import logging +import mimetypes import re from pathlib import Path from typing import Optional @@ -411,15 +412,16 @@ def load_url_image_data(url, headers=None): def upload_image(request, data, image_data, content_type, user): + image_format = mimetypes.guess_extension(content_type) file = UploadFile( file=io.BytesIO(image_data), - filename="image", # will be converted to a unique ID on upload_file + filename=f"generated{image_format}", # will be converted to a unique ID on upload_file headers={ "content-type": content_type, }, ) file_item = upload_file(request, file, user) - file_body_path = IMAGE_CACHE_DIR.joinpath(f"{file_item.id}.json") + file_body_path = IMAGE_CACHE_DIR.joinpath(f"{file_item.filename}.json") with open(file_body_path, "w") as f: json.dump(data, f) From ffb9e739753f1d096531d5084fc524e642744266 Mon Sep 17 00:00:00 2001 From: Rodrigo Agundez Date: Fri, 7 Feb 2025 08:32:06 +0900 Subject: [PATCH 25/96] Save image metadata to DB --- backend/open_webui/routers/files.py | 36 +++++++++++++--------------- backend/open_webui/routers/images.py | 9 ++----- 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/backend/open_webui/routers/files.py b/backend/open_webui/routers/files.py index 7160c2e86e..7cf7a942f4 100644 --- a/backend/open_webui/routers/files.py +++ b/backend/open_webui/routers/files.py @@ -3,30 +3,22 @@ import os import uuid from pathlib import Path from typing import Optional -from pydantic import BaseModel -import mimetypes from urllib.parse import quote -from open_webui.storage.provider import Storage - +from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile, status +from fastapi.responses import FileResponse, StreamingResponse +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import SRC_LOG_LEVELS from open_webui.models.files import ( FileForm, FileModel, FileModelResponse, Files, ) -from open_webui.routers.retrieval import process_file, ProcessFileForm - -from open_webui.config import UPLOAD_DIR -from open_webui.env import SRC_LOG_LEVELS -from open_webui.constants import ERROR_MESSAGES - - -from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status, Request -from fastapi.responses import FileResponse, StreamingResponse - - +from open_webui.routers.retrieval import ProcessFileForm, process_file +from open_webui.storage.provider import Storage from open_webui.utils.auth import get_admin_user, get_verified_user +from pydantic import BaseModel log = logging.getLogger(__name__) log.setLevel(SRC_LOG_LEVELS["MODELS"]) @@ -41,7 +33,10 @@ router = APIRouter() @router.post("/", response_model=FileModelResponse) def upload_file( - request: Request, file: UploadFile = File(...), user=Depends(get_verified_user) + request: Request, + file: UploadFile = File(...), + user=Depends(get_verified_user), + file_metadata: dict = {}, ): log.info(f"file.content_type: {file.content_type}") try: @@ -61,6 +56,7 @@ def upload_file( "id": id, "filename": name, "path": file_path, + "data": file_metadata, "meta": { "name": name, "content_type": file.content_type, @@ -126,7 +122,7 @@ async def delete_all_files(user=Depends(get_admin_user)): Storage.delete_all_files() except Exception as e: log.exception(e) - log.error(f"Error deleting files") + log.error("Error deleting files") raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT("Error deleting files"), @@ -248,7 +244,7 @@ async def get_file_content_by_id(id: str, user=Depends(get_verified_user)): ) except Exception as e: log.exception(e) - log.error(f"Error getting file content") + log.error("Error getting file content") raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT("Error getting file content"), @@ -279,7 +275,7 @@ async def get_html_file_content_by_id(id: str, user=Depends(get_verified_user)): ) except Exception as e: log.exception(e) - log.error(f"Error getting file content") + log.error("Error getting file content") raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT("Error getting file content"), @@ -355,7 +351,7 @@ async def delete_file_by_id(id: str, user=Depends(get_verified_user)): Storage.delete_file(file.path) except Exception as e: log.exception(e) - log.error(f"Error deleting files") + log.error("Error deleting files") raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT("Error deleting files"), diff --git a/backend/open_webui/routers/images.py b/backend/open_webui/routers/images.py index da4bf8a17e..1dcaca866b 100644 --- a/backend/open_webui/routers/images.py +++ b/backend/open_webui/routers/images.py @@ -411,7 +411,7 @@ def load_url_image_data(url, headers=None): return None -def upload_image(request, data, image_data, content_type, user): +def upload_image(request, image_metadata, image_data, content_type, user): image_format = mimetypes.guess_extension(content_type) file = UploadFile( file=io.BytesIO(image_data), @@ -420,12 +420,7 @@ def upload_image(request, data, image_data, content_type, user): "content-type": content_type, }, ) - file_item = upload_file(request, file, user) - file_body_path = IMAGE_CACHE_DIR.joinpath(f"{file_item.filename}.json") - - with open(file_body_path, "w") as f: - json.dump(data, f) - + file_item = upload_file(request, file, user, file_metadata=image_metadata) url = request.app.url_path_for("get_file_content_by_id", id=file_item.id) return url From 3a2d964d393008d53fe10a2994b61a3d4879ad0d Mon Sep 17 00:00:00 2001 From: EntropyYue Date: Fri, 7 Feb 2025 07:43:39 +0800 Subject: [PATCH 26/96] enh: optimize time display --- src/lib/components/common/Collapsible.svelte | 12 +++++++++--- src/lib/i18n/locales/en-US/translation.json | 1 + src/lib/i18n/locales/zh-CN/translation.json | 3 ++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/lib/components/common/Collapsible.svelte b/src/lib/components/common/Collapsible.svelte index 4f6b1ea958..4ee4c1b11e 100644 --- a/src/lib/components/common/Collapsible.svelte +++ b/src/lib/components/common/Collapsible.svelte @@ -74,9 +74,15 @@
{#if attributes?.type === 'reasoning'} {#if attributes?.done === 'true' && attributes?.duration} - {$i18n.t('Thought for {{DURATION}}', { - DURATION: dayjs.duration(attributes.duration, 'seconds').humanize() - })} + {#if attributes.duration < 60} + {$i18n.t('Thought for {{DURATION}} seconds', { + DURATION: attributes.duration + })} + {:else} + {$i18n.t('Thought for {{DURATION}}', { + DURATION: dayjs.duration(attributes.duration, 'seconds').humanize() + })} + {/if} {:else} {$i18n.t('Thinking...')} {/if} diff --git a/src/lib/i18n/locales/en-US/translation.json b/src/lib/i18n/locales/en-US/translation.json index cca4e14687..2b46c3a6e3 100644 --- a/src/lib/i18n/locales/en-US/translation.json +++ b/src/lib/i18n/locales/en-US/translation.json @@ -944,6 +944,7 @@ "This will reset the knowledge base and sync all files. Do you wish to continue?": "", "Thorough explanation": "", "Thought for {{DURATION}}": "", + "Thought for {{DURATION}} seconds": "", "Tika": "", "Tika Server URL required.": "", "Tiktoken": "", diff --git a/src/lib/i18n/locales/zh-CN/translation.json b/src/lib/i18n/locales/zh-CN/translation.json index 2e06bc61fc..a114fd0120 100644 --- a/src/lib/i18n/locales/zh-CN/translation.json +++ b/src/lib/i18n/locales/zh-CN/translation.json @@ -943,7 +943,8 @@ "This will delete all models including custom models and cannot be undone.": "这将删除所有模型,包括自定义模型,且无法撤销。", "This will reset the knowledge base and sync all files. Do you wish to continue?": "这将重置知识库并替换所有文件为目录下文件。确认继续?", "Thorough explanation": "解释较为详细", - "Thought for {{DURATION}}": "思考时间 {{DURATION}}", + "Thought for {{DURATION}}": "已推理 持续 {{DURATION}}", + "Thought for {{DURATION}} seconds": "已推理 持续 {{DURATION}} 秒", "Tika": "Tika", "Tika Server URL required.": "请输入 Tika 服务器地址。", "Tiktoken": "Tiktoken", From 5ef00dece96d8ccea448e32193749a26dfa328b3 Mon Sep 17 00:00:00 2001 From: Tiancong Li Date: Fri, 7 Feb 2025 19:13:51 +0800 Subject: [PATCH 27/96] i18n: update zh-TW --- src/lib/i18n/locales/zh-TW/translation.json | 38 ++++++++++----------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/lib/i18n/locales/zh-TW/translation.json b/src/lib/i18n/locales/zh-TW/translation.json index aa505a2e73..f4503ca7c9 100644 --- a/src/lib/i18n/locales/zh-TW/translation.json +++ b/src/lib/i18n/locales/zh-TW/translation.json @@ -63,11 +63,11 @@ "Allowed Endpoints": "允許的端點", "Already have an account?": "已經有帳號了嗎?", "Alternative to the top_p, and aims to ensure a balance of quality and variety. The parameter p represents the minimum probability for a token to be considered, relative to the probability of the most likely token. For example, with p=0.05 and the most likely token having a probability of 0.9, logits with a value less than 0.045 are filtered out. (Default: 0.0)": "作為 top_p 的替代方案,旨在確保質量和多樣性的平衡。相對於最可能的 token 機率而言,參數 p 代表一個 token 被考慮在内的最低機率。例如,當 p=0.05 且最可能的 token 機率為 0.9 時,數值低於 0.045 的對數機率會被過濾掉。(預設值:0.0)", - "Always": "", + "Always": "總是", "Amazing": "很棒", "an assistant": "一位助手", - "Analyzed": "", - "Analyzing...": "", + "Analyzed": "分析完畢", + "Analyzing...": "分析中……", "and": "和", "and {{COUNT}} more": "和另外 {{COUNT}} 個", "and create a new shared link.": "並建立新的共用連結。", @@ -172,11 +172,11 @@ "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "剪貼簿寫入權限遭拒。請檢查您的瀏覽器設定,授予必要的存取權限。", "Clone": "複製", "Clone Chat": "複製對話", - "Clone of {{TITLE}}": "", + "Clone of {{TITLE}}": "{{TITLE}} 的副本", "Close": "關閉", "Code execution": "程式碼執行", "Code formatted successfully": "程式碼格式化成功", - "Code Interpreter": "", + "Code Interpreter": "程式碼解釋器", "Collection": "收藏", "Color": "顏色", "ComfyUI": "ComfyUI", @@ -238,7 +238,7 @@ "Default": "預設", "Default (Open AI)": "預設 (OpenAI)", "Default (SentenceTransformers)": "預設 (SentenceTransformers)", - "Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model’s built-in tool-calling capabilities, but requires the model to inherently support this feature.": "", + "Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model’s built-in tool-calling capabilities, but requires the model to inherently support this feature.": "預設模式透過在執行前呼叫工具一次,來與更廣泛的模型相容。原生模式則利用模型內建的工具呼叫能力,但需要模型本身就支援此功能。", "Default Model": "預設模型", "Default model updated": "預設模型已更新", "Default Models": "預設模型", @@ -349,7 +349,7 @@ "Enter Chunk Overlap": "輸入區塊重疊", "Enter Chunk Size": "輸入區塊大小", "Enter description": "輸入描述", - "Enter Exa API Key": "", + "Enter Exa API Key": "輸入 Exa API 金鑰", "Enter Github Raw URL": "輸入 GitHub Raw URL", "Enter Google PSE API Key": "輸入 Google PSE API 金鑰", "Enter Google PSE Engine Id": "輸入 Google PSE 引擎 ID", @@ -398,14 +398,14 @@ "Error accessing Google Drive: {{error}}": "存取 Google Drive 時發生錯誤:{{error}}", "Error uploading file: {{error}}": "上傳檔案時發生錯誤:{{error}}", "Evaluations": "評估", - "Exa API Key": "", + "Exa API Key": "Exa API 金鑰", "Example: (&(objectClass=inetOrgPerson)(uid=%s))": "範例:(&(objectClass=inetOrgPerson)(uid=%s))", "Example: ALL": "範例:ALL", "Example: mail": "範例:mail", "Example: ou=users,dc=foo,dc=example": "範例:ou=users,dc=foo,dc=example", "Example: sAMAccountName or uid or userPrincipalName": "範例:sAMAccountName 或 uid 或 userPrincipalName", "Exclude": "排除", - "Execute code for analysis": "", + "Execute code for analysis": "執行程式碼以進行分析", "Experimental": "實驗性功能", "Explore the cosmos": "探索宇宙", "Export": "匯出", @@ -458,7 +458,7 @@ "Format your variables using brackets like this:": "使用方括號格式化您的變數,如下所示:", "Frequency Penalty": "頻率懲罰", "Function": "函式", - "Function Calling": "", + "Function Calling": "函式呼叫", "Function created successfully": "成功建立函式", "Function deleted successfully": "成功刪除函式", "Function Description": "函式描述", @@ -473,7 +473,7 @@ "Functions imported successfully": "成功匯入函式", "General": "一般", "General Settings": "一般設定", - "Generate an image": "", + "Generate an image": "產生圖片", "Generate Image": "產生圖片", "Generating search query": "正在產生搜尋查詢", "Get started": "開始使用", @@ -630,7 +630,7 @@ "More": "更多", "Name": "名稱", "Name your knowledge base": "命名您的知識庫", - "Native": "", + "Native": "原生", "New Chat": "新增對話", "New Folder": "新增資料夾", "New Password": "新密碼", @@ -725,7 +725,7 @@ "Please enter a prompt": "請輸入提示詞", "Please fill in all fields.": "請填寫所有欄位。", "Please select a model first.": "請先選擇型號。", - "Please select a model.": "", + "Please select a model.": "請選擇一個模型。", "Please select a reason": "請選擇原因", "Port": "連接埠", "Positive attitude": "積極的態度", @@ -734,7 +734,7 @@ "Previous 30 days": "過去 30 天", "Previous 7 days": "過去 7 天", "Profile Image": "個人檔案圖片", - "Prompt": "", + "Prompt": "提示詞", "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "提示詞(例如:告訴我關於羅馬帝國的一些趣事)", "Prompt Content": "提示詞內容", "Prompt created successfully": "提示詞建立成功", @@ -810,7 +810,7 @@ "Search options": "搜尋選項", "Search Prompts": "搜尋提示詞", "Search Result Count": "搜尋結果數量", - "Search the internet": "", + "Search the internet": "搜尋網際網路", "Search Tools": "搜尋工具", "SearchApi API Key": "SearchApi API 金鑰", "SearchApi Engine": "SearchApi 引擎", @@ -980,7 +980,7 @@ "Tools": "工具", "Tools Access": "工具存取", "Tools are a function calling system with arbitrary code execution": "工具是一個具有任意程式碼執行功能的函式呼叫系統", - "Tools Function Calling Prompt": "", + "Tools Function Calling Prompt": "工具函式呼叫提示詞", "Tools have a function calling system that allows arbitrary code execution": "工具具有允許執行任意程式碼的函式呼叫系統", "Tools have a function calling system that allows arbitrary code execution.": "工具具有允許執行任意程式碼的函式呼叫系統。", "Top K": "Top K", @@ -1051,7 +1051,7 @@ "Web Loader Settings": "網頁載入器設定", "Web Search": "網頁搜尋", "Web Search Engine": "網頁搜尋引擎", - "Web Search in Chat": "", + "Web Search in Chat": "在對話中進行網路搜尋", "Web Search Query Generation": "網頁搜尋查詢生成", "Webhook URL": "Webhook URL", "WebUI Settings": "WebUI 設定", @@ -1081,8 +1081,8 @@ "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "您可以透過下方的「管理」按鈕新增記憶,將您與大型語言模型的互動個人化,讓它們更有幫助並更符合您的需求。", "You cannot upload an empty file.": "您無法上傳空檔案", "You do not have permission to access this feature.": "您沒有權限訪問此功能", - "You do not have permission to upload files": "", - "You do not have permission to upload files.": "您沒有權限上傳檔案", + "You do not have permission to upload files": "您沒有權限上傳檔案", + "You do not have permission to upload files.": "您沒有權限上傳檔案。", "You have no archived conversations.": "您沒有已封存的對話。", "You have shared this chat": "您已分享此對話", "You're a helpful assistant.": "您是一位樂於助人的助手。", From 5ca6afc0fc853411316e6db498498243e565ab81 Mon Sep 17 00:00:00 2001 From: Patrick Deniso Date: Fri, 7 Feb 2025 12:15:54 -0500 Subject: [PATCH 28/96] add s3 key prefix support --- backend/open_webui/config.py | 1 + backend/open_webui/storage/provider.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index bf6f1d0256..17f53be74b 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -660,6 +660,7 @@ S3_ACCESS_KEY_ID = os.environ.get("S3_ACCESS_KEY_ID", None) S3_SECRET_ACCESS_KEY = os.environ.get("S3_SECRET_ACCESS_KEY", None) S3_REGION_NAME = os.environ.get("S3_REGION_NAME", None) S3_BUCKET_NAME = os.environ.get("S3_BUCKET_NAME", None) +S3_KEY_PREFIX = os.environ.get("S3_KEY_PREFIX", None) S3_ENDPOINT_URL = os.environ.get("S3_ENDPOINT_URL", None) GCS_BUCKET_NAME = os.environ.get("GCS_BUCKET_NAME", None) diff --git a/backend/open_webui/storage/provider.py b/backend/open_webui/storage/provider.py index 0c0a8aacfc..60fdf77b5b 100644 --- a/backend/open_webui/storage/provider.py +++ b/backend/open_webui/storage/provider.py @@ -10,6 +10,7 @@ from open_webui.config import ( S3_ACCESS_KEY_ID, S3_BUCKET_NAME, S3_ENDPOINT_URL, + S3_KEY_PREFIX, S3_REGION_NAME, S3_SECRET_ACCESS_KEY, GCS_BUCKET_NAME, @@ -98,7 +99,8 @@ class S3StorageProvider(StorageProvider): """Handles uploading of the file to S3 storage.""" _, file_path = LocalStorageProvider.upload_file(file, filename) try: - self.s3_client.upload_file(file_path, self.bucket_name, filename) + s3_key = os.path.join(S3_KEY_PREFIX, filename) + self.s3_client.upload_file(file_path, self.bucket_name, s3_key) return ( open(file_path, "rb").read(), "s3://" + self.bucket_name + "/" + filename, From 94f56db5eeb6645d5b660bc24f9fe70355419362 Mon Sep 17 00:00:00 2001 From: Mistrick Date: Sat, 8 Feb 2025 01:10:18 +0700 Subject: [PATCH 29/96] fix max seed for comfyui --- backend/open_webui/utils/images/comfyui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/open_webui/utils/images/comfyui.py b/backend/open_webui/utils/images/comfyui.py index 679fff9f64..b86c257591 100644 --- a/backend/open_webui/utils/images/comfyui.py +++ b/backend/open_webui/utils/images/comfyui.py @@ -161,7 +161,7 @@ async def comfyui_generate_image( seed = ( payload.seed if payload.seed - else random.randint(0, 18446744073709551614) + else random.randint(0, 1125899906842624) ) for node_id in node.node_ids: workflow[node_id]["inputs"][node.key] = seed From 7f8247692685ef23e54bef781a83c96c9943876a Mon Sep 17 00:00:00 2001 From: Patrick Deniso Date: Fri, 7 Feb 2025 13:56:57 -0500 Subject: [PATCH 30/96] use key_prefix in rest of S3StorageProvider --- backend/open_webui/storage/provider.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/backend/open_webui/storage/provider.py b/backend/open_webui/storage/provider.py index 60fdf77b5b..f287daf2ff 100644 --- a/backend/open_webui/storage/provider.py +++ b/backend/open_webui/storage/provider.py @@ -94,35 +94,36 @@ class S3StorageProvider(StorageProvider): aws_secret_access_key=S3_SECRET_ACCESS_KEY, ) self.bucket_name = S3_BUCKET_NAME + self.key_prefix = S3_KEY_PREFIX def upload_file(self, file: BinaryIO, filename: str) -> Tuple[bytes, str]: """Handles uploading of the file to S3 storage.""" _, file_path = LocalStorageProvider.upload_file(file, filename) try: - s3_key = os.path.join(S3_KEY_PREFIX, filename) + s3_key = os.path.join(self.key_prefix, filename) self.s3_client.upload_file(file_path, self.bucket_name, s3_key) return ( open(file_path, "rb").read(), - "s3://" + self.bucket_name + "/" + filename, + "s3://" + self.bucket_name + "/" + s3_key, ) except ClientError as e: raise RuntimeError(f"Error uploading file to S3: {e}") - + def get_file(self, file_path: str) -> str: """Handles downloading of the file from S3 storage.""" try: - bucket_name, key = file_path.split("//")[1].split("/") - local_file_path = f"{UPLOAD_DIR}/{key}" - self.s3_client.download_file(bucket_name, key, local_file_path) + s3_key = self._extract_s3_key(file_path) + local_file_path = self._get_local_file_path(s3_key) + self.s3_client.download_file(self.bucket_name, s3_key, local_file_path) return local_file_path except ClientError as e: raise RuntimeError(f"Error downloading file from S3: {e}") def delete_file(self, file_path: str) -> None: """Handles deletion of the file from S3 storage.""" - filename = file_path.split("/")[-1] try: - self.s3_client.delete_object(Bucket=self.bucket_name, Key=filename) + s3_key = self._extract_s3_key(file_path) + self.s3_client.delete_object(Bucket=self.bucket_name, Key=s3_key) except ClientError as e: raise RuntimeError(f"Error deleting file from S3: {e}") @@ -135,6 +136,9 @@ class S3StorageProvider(StorageProvider): response = self.s3_client.list_objects_v2(Bucket=self.bucket_name) if "Contents" in response: for content in response["Contents"]: + # Skip objects that were not uploaded from open-webui in the first place + if not content["Key"].startswith(self.key_prefix): continue + self.s3_client.delete_object( Bucket=self.bucket_name, Key=content["Key"] ) @@ -144,6 +148,12 @@ class S3StorageProvider(StorageProvider): # Always delete from local storage LocalStorageProvider.delete_all_files() + # The s3 key is the name assigned to an object. It excludes the bucket name, but includes the internal path and the file name. + def _extract_s3_key(self, full_file_path: str) -> str: + return ''.join(full_file_path.split("//")[1].split("/")[1:]) + + def _get_local_file_path(self, s3_key: str) -> str: + return f"{UPLOAD_DIR}/{s3_key.split('/')[-1]}" class GCSStorageProvider(StorageProvider): def __init__(self): From c092db379e79964c15ecb8d744ce66d8f98886c9 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 7 Feb 2025 11:23:04 -0800 Subject: [PATCH 31/96] fix --- backend/open_webui/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index bf6f1d0256..01f91ec648 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1645,7 +1645,7 @@ RAG_WEB_SEARCH_ENGINE = PersistentConfig( # This ensures the highest level of safety and reliability of the information sources. RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = PersistentConfig( "RAG_WEB_SEARCH_DOMAIN_FILTER_LIST", - "rag.rag.web.search.domain.filter_list", + "rag.web.search.domain.filter_list", [ # "wikipedia.com", # "wikimedia.org", From f8a8218149d106e173cd152fd0d07479afcc17e8 Mon Sep 17 00:00:00 2001 From: Patrick Deniso Date: Fri, 7 Feb 2025 14:42:16 -0500 Subject: [PATCH 32/96] fix bug where '/' was not properly inserted in s3 key strings --- backend/open_webui/storage/provider.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/open_webui/storage/provider.py b/backend/open_webui/storage/provider.py index f287daf2ff..afc50b3973 100644 --- a/backend/open_webui/storage/provider.py +++ b/backend/open_webui/storage/provider.py @@ -94,7 +94,7 @@ class S3StorageProvider(StorageProvider): aws_secret_access_key=S3_SECRET_ACCESS_KEY, ) self.bucket_name = S3_BUCKET_NAME - self.key_prefix = S3_KEY_PREFIX + self.key_prefix = S3_KEY_PREFIX if S3_KEY_PREFIX else "" def upload_file(self, file: BinaryIO, filename: str) -> Tuple[bytes, str]: """Handles uploading of the file to S3 storage.""" @@ -150,7 +150,7 @@ class S3StorageProvider(StorageProvider): # The s3 key is the name assigned to an object. It excludes the bucket name, but includes the internal path and the file name. def _extract_s3_key(self, full_file_path: str) -> str: - return ''.join(full_file_path.split("//")[1].split("/")[1:]) + return '/'.join(full_file_path.split("//")[1].split("/")[1:]) def _get_local_file_path(self, s3_key: str) -> str: return f"{UPLOAD_DIR}/{s3_key.split('/')[-1]}" From 4b4a86d4e7a49149de3e6daeee7fa9b79e9148e5 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 7 Feb 2025 11:51:27 -0800 Subject: [PATCH 33/96] chore: dependencies --- backend/requirements.txt | 2 ++ pyproject.toml | 3 +++ 2 files changed, 5 insertions(+) diff --git a/backend/requirements.txt b/backend/requirements.txt index 14ad4b9cdf..92d9c7f22f 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -32,6 +32,8 @@ boto3==1.35.53 argon2-cffi==23.1.0 APScheduler==3.10.4 +RestrictedPython==8.0 + # AI libraries openai anthropic diff --git a/pyproject.toml b/pyproject.toml index f121089e8f..076e58b7c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,9 @@ dependencies = [ "argon2-cffi==23.1.0", "APScheduler==3.10.4", + + "RestrictedPython==8.0", + "openai", "anthropic", "google-generativeai==0.7.2", From 85912d726e759e83a42f03497175b08a2a421a1a Mon Sep 17 00:00:00 2001 From: tarmst Date: Fri, 7 Feb 2025 19:53:25 +0000 Subject: [PATCH 34/96] Adding debug logs for oauth role & group management --- backend/open_webui/env.py | 1 + backend/open_webui/utils/oauth.py | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index 00605e15dc..0be3887f82 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -92,6 +92,7 @@ log_sources = [ "RAG", "WEBHOOK", "SOCKET", + "OAUTH", ] SRC_LOG_LEVELS = {} diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 83e0ca1d6d..3adbac20ea 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -1,6 +1,7 @@ import base64 import logging import mimetypes +import sys import uuid import aiohttp @@ -40,7 +41,11 @@ from open_webui.utils.misc import parse_duration from open_webui.utils.auth import get_password_hash, create_token from open_webui.utils.webhook import post_webhook +from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL + +logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["OAUTH"]) auth_manager_config = AppConfig() auth_manager_config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE @@ -72,12 +77,15 @@ class OAuthManager: def get_user_role(self, user, user_data): if user and Users.get_num_users() == 1: # If the user is the only user, assign the role "admin" - actually repairs role for single user on login + log.debug("Assigning the only user the admin role") return "admin" if not user and Users.get_num_users() == 0: # If there are no users, assign the role "admin", as the first user will be an admin + log.debug("Assigning the first user the admin role") return "admin" if auth_manager_config.ENABLE_OAUTH_ROLE_MANAGEMENT: + log.debug("Running OAUTH Role management") oauth_claim = auth_manager_config.OAUTH_ROLES_CLAIM oauth_allowed_roles = auth_manager_config.OAUTH_ALLOWED_ROLES oauth_admin_roles = auth_manager_config.OAUTH_ADMIN_ROLES @@ -93,17 +101,24 @@ class OAuthManager: claim_data = claim_data.get(nested_claim, {}) oauth_roles = claim_data if isinstance(claim_data, list) else None + log.debug(f"Oauth Roles claim: {oauth_claim}") + log.debug(f"User roles from oauth: {oauth_roles}") + log.debug(f"Accepted user roles: {oauth_allowed_roles}") + log.debug(f"Accepted admin roles: {oauth_admin_roles}") + # If any roles are found, check if they match the allowed or admin roles if oauth_roles: # If role management is enabled, and matching roles are provided, use the roles for allowed_role in oauth_allowed_roles: # If the user has any of the allowed roles, assign the role "user" if allowed_role in oauth_roles: + log.debug("Assigned user the user role") role = "user" break for admin_role in oauth_admin_roles: # If the user has any of the admin roles, assign the role "admin" if admin_role in oauth_roles: + log.debug("Assigned user the admin role") role = "admin" break else: @@ -117,16 +132,23 @@ class OAuthManager: return role def update_user_groups(self, user, user_data, default_permissions): + log.debug("Running OAUTH Group management") oauth_claim = auth_manager_config.OAUTH_GROUPS_CLAIM user_oauth_groups: list[str] = user_data.get(oauth_claim, list()) user_current_groups: list[GroupModel] = Groups.get_groups_by_member_id(user.id) all_available_groups: list[GroupModel] = Groups.get_groups() + log.debug(f"Oauth Groups claim: {oauth_claim}") + log.debug(f"User oauth groups: {user_oauth_groups}") + log.debug(f"User's current groups: {[g.name for g in user_current_groups]}") + log.debug(f"All groups available in OpenWebUI: {[g.name for g in all_available_groups]}") + # Remove groups that user is no longer a part of for group_model in user_current_groups: if group_model.name not in user_oauth_groups: # Remove group from user + log.debug(f"Removing user from group {group_model.name} as it is no longer in their oauth groups") user_ids = group_model.user_ids user_ids = [i for i in user_ids if i != user.id] @@ -152,6 +174,7 @@ class OAuthManager: gm.name == group_model.name for gm in user_current_groups ): # Add user to group + log.debug(f"Adding user to group {group_model.name} as it was found in their oauth groups") user_ids = group_model.user_ids user_ids.append(user.id) From c56bedc5ffa81967270da44b2f76f5a7d51a7b16 Mon Sep 17 00:00:00 2001 From: Xingjian Xie Date: Fri, 7 Feb 2025 20:15:54 +0000 Subject: [PATCH 35/96] Fix tag_content_handler issue: after_tag should be remove from the current content_blocks --- backend/open_webui/utils/middleware.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 331b850ff3..79210030d1 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -1260,10 +1260,10 @@ async def process_chat_response( match.end() : ] # Content after opening tag - # Remove the start tag from the currently handling text block + # Remove the start tag and after from the currently handling text block content_blocks[-1]["content"] = content_blocks[-1][ "content" - ].replace(match.group(0), "") + ].replace(match.group(0) + after_tag, "") if before_tag: content_blocks[-1]["content"] = before_tag From 546ef6ab42c2fcc59aeae8e03bf9b67ec74f28e1 Mon Sep 17 00:00:00 2001 From: Rodrigo Agundez Date: Sat, 8 Feb 2025 09:49:16 +0900 Subject: [PATCH 36/96] Check is response is OK from retrieve the picture if not then default --- backend/open_webui/utils/oauth.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 83e0ca1d6d..5e52937f41 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -261,15 +261,18 @@ class OAuthManager: } async with aiohttp.ClientSession() as session: async with session.get(picture_url, **get_kwargs) as resp: - picture = await resp.read() - base64_encoded_picture = base64.b64encode( - picture - ).decode("utf-8") - guessed_mime_type = mimetypes.guess_type(picture_url)[0] - if guessed_mime_type is None: - # assume JPG, browsers are tolerant enough of image formats - guessed_mime_type = "image/jpeg" - picture_url = f"data:{guessed_mime_type};base64,{base64_encoded_picture}" + if resp.ok: + picture = await resp.read() + base64_encoded_picture = base64.b64encode( + picture + ).decode("utf-8") + guessed_mime_type = mimetypes.guess_type(picture_url)[0] + if guessed_mime_type is None: + # assume JPG, browsers are tolerant enough of image formats + guessed_mime_type = "image/jpeg" + picture_url = f"data:{guessed_mime_type};base64,{base64_encoded_picture}" + else: + picture_url = "/user.png" except Exception as e: log.error( f"Error downloading profile image '{picture_url}': {e}" From d39a274ef89eb8381fc85d1775d8c167077a3a32 Mon Sep 17 00:00:00 2001 From: zoupingshi Date: Sat, 8 Feb 2025 12:14:01 +0800 Subject: [PATCH 37/96] chore: fix some typos Signed-off-by: zoupingshi --- backend/open_webui/models/chats.py | 2 +- backend/open_webui/utils/middleware.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index 73ff6c102d..9e0a5865e9 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -470,7 +470,7 @@ class ChatTable: try: with get_db() as db: # it is possible that the shared link was deleted. hence, - # we check if the chat is still shared by checkng if a chat with the share_id exists + # we check if the chat is still shared by checking if a chat with the share_id exists chat = db.query(Chat).filter_by(share_id=id).first() if chat: diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 79210030d1..984359869b 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -572,13 +572,13 @@ async def chat_image_generation_handler( { "type": "status", "data": { - "description": f"An error occured while generating an image", + "description": f"An error occurred while generating an image", "done": True, }, } ) - system_message_content = "Unable to generate an image, tell the user that an error occured" + system_message_content = "Unable to generate an image, tell the user that an error occurred" if system_message_content: form_data["messages"] = add_or_update_system_message( From 95aaacfeb494a659be2b488a9237e20bec18d273 Mon Sep 17 00:00:00 2001 From: SentinalMax Date: Fri, 7 Feb 2025 22:52:24 -0600 Subject: [PATCH 38/96] fixed GGUF model upload instability --- backend/open_webui/routers/ollama.py | 119 +++++++++++++++++---------- backend/open_webui/utils/misc.py | 9 +- 2 files changed, 79 insertions(+), 49 deletions(-) diff --git a/backend/open_webui/routers/ollama.py b/backend/open_webui/routers/ollama.py index 2ab06eb95e..1c63656839 100644 --- a/backend/open_webui/routers/ollama.py +++ b/backend/open_webui/routers/ollama.py @@ -11,10 +11,8 @@ import re import time from typing import Optional, Union from urllib.parse import urlparse - import aiohttp from aiocache import cached - import requests from fastapi import ( @@ -990,6 +988,8 @@ async def generate_chat_completion( ) payload = {**form_data.model_dump(exclude_none=True)} + if "metadata" in payload: + del payload["metadata"] model_id = payload["model"] model_info = Models.get_model_by_id(model_id) @@ -1408,9 +1408,10 @@ async def download_model( return None +# TODO: Progress bar does not reflect size & duration of upload. @router.post("/models/upload") @router.post("/models/upload/{url_idx}") -def upload_model( +async def upload_model( request: Request, file: UploadFile = File(...), url_idx: Optional[int] = None, @@ -1419,62 +1420,90 @@ def upload_model( if url_idx is None: url_idx = 0 ollama_url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] + file_path = os.path.join(UPLOAD_DIR, file.filename) + os.makedirs(UPLOAD_DIR, exist_ok=True) - file_path = f"{UPLOAD_DIR}/{file.filename}" + # --- P1: save file locally --- + chunk_size = 1024 * 1024 * 2 # 2 MB chunks + with open(file_path, "wb") as out_f: + while True: + chunk = file.file.read(chunk_size) + #log.info(f"Chunk: {str(chunk)}") # DEBUG + if not chunk: + break + out_f.write(chunk) - # Save file in chunks - with open(file_path, "wb+") as f: - for chunk in file.file: - f.write(chunk) - - def file_process_stream(): + async def file_process_stream(): nonlocal ollama_url total_size = os.path.getsize(file_path) - chunk_size = 1024 * 1024 + log.info(f"Total Model Size: {str(total_size)}") # DEBUG + + # --- P2: SSE progress + calculate sha256 hash --- + file_hash = calculate_sha256(file_path, chunk_size) + log.info(f"Model Hash: {str(file_hash)}") # DEBUG try: with open(file_path, "rb") as f: - total = 0 - done = False - - while not done: - chunk = f.read(chunk_size) - if not chunk: - done = True - continue - - total += len(chunk) - progress = round((total / total_size) * 100, 2) - - res = { + bytes_read = 0 + while chunk := f.read(chunk_size): + bytes_read += len(chunk) + progress = round(bytes_read / total_size * 100, 2) + data_msg = { "progress": progress, "total": total_size, - "completed": total, + "completed": bytes_read, } - yield f"data: {json.dumps(res)}\n\n" + yield f"data: {json.dumps(data_msg)}\n\n" - if done: - f.seek(0) - hashed = calculate_sha256(f) - f.seek(0) + # --- P3: Upload to ollama /api/blobs --- + with open(file_path, "rb") as f: + url = f"{ollama_url}/api/blobs/sha256:{file_hash}" + response = requests.post(url, data=f) - url = f"{ollama_url}/api/blobs/sha256:{hashed}" - response = requests.post(url, data=f) + if response.ok: + log.info(f"Uploaded to /api/blobs") # DEBUG + # Remove local file + os.remove(file_path) - if response.ok: - res = { - "done": done, - "blob": f"sha256:{hashed}", - "name": file.filename, - } - os.remove(file_path) - yield f"data: {json.dumps(res)}\n\n" - else: - raise Exception( - "Ollama: Could not create blob, Please try again." - ) + # Create model in ollama + model_name, ext = os.path.splitext(file.filename) + log.info(f"Created Model: {model_name}") # DEBUG + + create_payload = { + "model": model_name, + # Reference the file by its original name => the uploaded blob's digest + "files": { + file.filename: f"sha256:{file_hash}" + }, + } + log.info(f"Model Payload: {create_payload}") # DEBUG + + # Call ollama /api/create + #https://github.com/ollama/ollama/blob/main/docs/api.md#create-a-model + create_resp = requests.post( + url=f"{ollama_url}/api/create", + headers={"Content-Type": "application/json"}, + data=json.dumps(create_payload), + ) + + if create_resp.ok: + log.info(f"API SUCCESS!") # DEBUG + done_msg = { + "done": True, + "blob": f"sha256:{file_hash}", + "name": file.filename, + "model_created": model_name, + } + yield f"data: {json.dumps(done_msg)}\n\n" + else: + raise Exception( + f"Failed to create model in Ollama. {create_resp.text}" + ) + + else: + raise Exception("Ollama: Could not create blob, Please try again.") except Exception as e: res = {"error": str(e)} yield f"data: {json.dumps(res)}\n\n" - return StreamingResponse(file_process_stream(), media_type="text/event-stream") + return StreamingResponse(file_process_stream(), media_type="text/event-stream") \ No newline at end of file diff --git a/backend/open_webui/utils/misc.py b/backend/open_webui/utils/misc.py index b073939219..eb90ea5ead 100644 --- a/backend/open_webui/utils/misc.py +++ b/backend/open_webui/utils/misc.py @@ -244,11 +244,12 @@ def get_gravatar_url(email): return f"https://www.gravatar.com/avatar/{hash_hex}?d=mp" -def calculate_sha256(file): +def calculate_sha256(file_path, chunk_size): + #Compute SHA-256 hash of a file efficiently in chunks sha256 = hashlib.sha256() - # Read the file in chunks to efficiently handle large files - for chunk in iter(lambda: file.read(8192), b""): - sha256.update(chunk) + with open(file_path, "rb") as f: + while chunk := f.read(chunk_size): + sha256.update(chunk) return sha256.hexdigest() From 3dde2f67cfaa938b9b25cf549deb9249793835d8 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 7 Feb 2025 22:57:39 -0800 Subject: [PATCH 39/96] refac --- backend/open_webui/utils/chat.py | 6 +++--- backend/open_webui/utils/filter.py | 29 ++++++++++++-------------- backend/open_webui/utils/middleware.py | 23 +++++++++++++------- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/backend/open_webui/utils/chat.py b/backend/open_webui/utils/chat.py index ebd5bb5e34..f0b52eca27 100644 --- a/backend/open_webui/utils/chat.py +++ b/backend/open_webui/utils/chat.py @@ -203,10 +203,10 @@ async def chat_completed(request: Request, form_data: dict, user: Any): try: result, _ = await process_filter_functions( - handler_type="outlet", - filter_ids=get_sorted_filter_ids(model), request=request, - data=data, + filter_ids=get_sorted_filter_ids(model), + filter_type="outlet", + form_data=data, extra_params=extra_params, ) return result diff --git a/backend/open_webui/utils/filter.py b/backend/open_webui/utils/filter.py index 2ad0c025e6..88fe703535 100644 --- a/backend/open_webui/utils/filter.py +++ b/backend/open_webui/utils/filter.py @@ -2,6 +2,7 @@ import inspect from open_webui.utils.plugin import load_function_module_by_id from open_webui.models.functions import Functions + def get_sorted_filter_ids(model): def get_priority(function_id): function = Functions.get_function_by_id(function_id) @@ -19,17 +20,14 @@ def get_sorted_filter_ids(model): function.id for function in Functions.get_functions_by_type("filter", active_only=True) ] - + filter_ids = [fid for fid in filter_ids if fid in enabled_filter_ids] filter_ids.sort(key=get_priority) return filter_ids + async def process_filter_functions( - handler_type, - filter_ids, - request, - data, - extra_params + request, filter_ids, filter_type, form_data, extra_params ): skip_files = None @@ -45,7 +43,7 @@ async def process_filter_functions( request.app.state.FUNCTIONS[filter_id] = function_module # Check if the function has a file_handler variable - if handler_type == "inlet" and hasattr(function_module, "file_handler"): + if filter_type == "inlet" and hasattr(function_module, "file_handler"): skip_files = function_module.file_handler # Apply valves to the function @@ -56,14 +54,14 @@ async def process_filter_functions( ) # Prepare handler function - handler = getattr(function_module, handler_type, None) + handler = getattr(function_module, filter_type, None) if not handler: continue try: # Prepare parameters sig = inspect.signature(handler) - params = {"body": data} + params = {"body": form_data} # Add extra parameters that exist in the handler's signature for key in list(extra_params.keys()): @@ -82,19 +80,18 @@ async def process_filter_functions( except Exception as e: print(e) - # Execute handler if inspect.iscoroutinefunction(handler): - data = await handler(**params) + form_data = await handler(**params) else: - data = handler(**params) + form_data = handler(**params) except Exception as e: - print(f"Error in {handler_type} handler {filter_id}: {e}") + print(f"Error in {filter_type} handler {filter_id}: {e}") raise e # Handle file cleanup for inlet - if skip_files and "files" in data.get("metadata", {}): - del data["metadata"]["files"] + if skip_files and "files" in form_data.get("metadata", {}): + del form_data["metadata"]["files"] - return data, {} \ No newline at end of file + return form_data, {} diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index c69d0c909d..14d01221c4 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -694,10 +694,10 @@ async def process_chat_payload(request, form_data, metadata, user, model): try: form_data, flags = await process_filter_functions( - handler_type="inlet", - filter_ids=get_sorted_filter_ids(model), request=request, - data=form_data, + filter_ids=get_sorted_filter_ids(model), + filter_type="inlet", + form_data=form_data, extra_params=extra_params, ) except Exception as e: @@ -1039,11 +1039,15 @@ async def process_chat_response( def split_content_and_whitespace(content): content_stripped = content.rstrip() - original_whitespace = content[len(content_stripped):] if len(content) > len(content_stripped) else '' + original_whitespace = ( + content[len(content_stripped) :] + if len(content) > len(content_stripped) + else "" + ) return content_stripped, original_whitespace def is_opening_code_block(content): - backtick_segments = content.split('```') + backtick_segments = content.split("```") # Even number of segments means the last backticks are opening a new block return len(backtick_segments) > 1 and len(backtick_segments) % 2 == 0 @@ -1113,10 +1117,15 @@ async def process_chat_response( output = block.get("output", None) lang = attributes.get("lang", "") - content_stripped, original_whitespace = split_content_and_whitespace(content) + content_stripped, original_whitespace = ( + split_content_and_whitespace(content) + ) if is_opening_code_block(content_stripped): # Remove trailing backticks that would open a new block - content = content_stripped.rstrip('`').rstrip() + original_whitespace + content = ( + content_stripped.rstrip("`").rstrip() + + original_whitespace + ) else: # Keep content as is - either closing backticks or no backticks content = content_stripped + original_whitespace From 9be8bea6f4046ec828686ec3dc0728363d3117fc Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 8 Feb 2025 01:07:05 -0800 Subject: [PATCH 40/96] fix: filter --- backend/open_webui/utils/chat.py | 1 + backend/open_webui/utils/filter.py | 14 ++++++++------ backend/open_webui/utils/middleware.py | 1 + 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/backend/open_webui/utils/chat.py b/backend/open_webui/utils/chat.py index f0b52eca27..3b6d5ea042 100644 --- a/backend/open_webui/utils/chat.py +++ b/backend/open_webui/utils/chat.py @@ -199,6 +199,7 @@ async def chat_completed(request: Request, form_data: dict, user: Any): }, "__metadata__": metadata, "__request__": request, + "__model__": model, } try: diff --git a/backend/open_webui/utils/filter.py b/backend/open_webui/utils/filter.py index 88fe703535..de51bd46e5 100644 --- a/backend/open_webui/utils/filter.py +++ b/backend/open_webui/utils/filter.py @@ -61,12 +61,14 @@ async def process_filter_functions( try: # Prepare parameters sig = inspect.signature(handler) - params = {"body": form_data} - - # Add extra parameters that exist in the handler's signature - for key in list(extra_params.keys()): - if key in sig.parameters: - params[key] = extra_params[key] + params = {"body": form_data} | { + k: v + for k, v in { + **extra_params, + "__id__": filter_id, + }.items() + if k in sig.parameters + } # Handle user parameters if "__user__" in sig.parameters: diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index 39033a92ab..29bfb2ba1b 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -617,6 +617,7 @@ async def process_chat_payload(request, form_data, metadata, user, model): }, "__metadata__": metadata, "__request__": request, + "__model__": model, } # Initialize events to store additional event to be sent to the client From 181fca4707dda3222b29b3d6de40dad7fe3b2260 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sat, 8 Feb 2025 02:34:30 -0800 Subject: [PATCH 41/96] refac: transformer.js --- package-lock.json | 184 +++++++++++------- package.json | 4 +- .../admin/Evaluations/Leaderboard.svelte | 4 +- vite.config.ts | 15 +- 4 files changed, 132 insertions(+), 75 deletions(-) diff --git a/package-lock.json b/package-lock.json index 47856e4c0a..e5c18101bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "idb": "^7.1.1", "js-sha256": "^0.10.1", "katex": "^0.16.21", + "kokoro-js": "^1.1.1", "marked": "^9.1.0", "mermaid": "^10.9.3", "paneforge": "^0.0.6", @@ -62,7 +63,8 @@ "svelte-sonner": "^0.3.19", "tippy.js": "^6.3.7", "turndown": "^7.2.0", - "uuid": "^9.0.1" + "uuid": "^9.0.1", + "vite-plugin-static-copy": "^2.2.0" }, "devDependencies": { "@sveltejs/adapter-auto": "3.2.2", @@ -1078,21 +1080,23 @@ } }, "node_modules/@huggingface/jinja": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.3.1.tgz", - "integrity": "sha512-SbcBWUKDQ76lzlVYOloscUk0SJjuL1LcbZsfQv/Bxxc7dwJMYuS+DAQ+HhVw6ZkTFXArejaX5HQRuCuleYwYdA==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.3.3.tgz", + "integrity": "sha512-vQQr2JyWvVFba3Lj9es4q9vCl1sAc74fdgnEMoX8qHrXtswap9ge9uO3ONDzQB0cQ0PUyaKY2N6HaVbTBvSXvw==", + "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@huggingface/transformers": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-3.0.0.tgz", - "integrity": "sha512-OWIPnTijAw4DQ+IFHBOrej2SDdYyykYlTtpTLCEt5MZq/e9Cb65RS2YVhdGcgbaW/6JAL3i8ZA5UhDeWGm4iRQ==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-3.3.3.tgz", + "integrity": "sha512-OcMubhBjW6u1xnp0zSt5SvCxdGHuhP2k+w2Vlm3i0vNcTJhJTZWxxYQmPBfcb7PX+Q6c43lGSzWD6tsJFwka4Q==", + "license": "Apache-2.0", "dependencies": { - "@huggingface/jinja": "^0.3.0", - "onnxruntime-node": "1.19.2", - "onnxruntime-web": "1.20.0-dev.20241016-2b8fc5529b", + "@huggingface/jinja": "^0.3.3", + "onnxruntime-node": "1.20.1", + "onnxruntime-web": "1.21.0-dev.20250206-d981b153d3", "sharp": "^0.33.5" } }, @@ -1546,6 +1550,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", "dependencies": { "minipass": "^7.0.4" }, @@ -1799,7 +1804,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -1812,7 +1816,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "engines": { "node": ">= 8" } @@ -1821,7 +1824,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -1857,27 +1859,32 @@ "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" @@ -1886,27 +1893,32 @@ "node_modules/@protobufjs/float": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/inquire": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" }, "node_modules/@pyscript/core": { "version": "0.4.32", @@ -3426,7 +3438,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -3655,7 +3666,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, "engines": { "node": ">=8" }, @@ -3731,7 +3741,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "dependencies": { "fill-range": "^7.1.1" }, @@ -4091,7 +4100,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -4115,7 +4123,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -4127,6 +4134,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=18" } @@ -5915,7 +5923,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -5931,7 +5938,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -5955,7 +5961,6 @@ "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, "dependencies": { "reusify": "^1.0.4" } @@ -6014,7 +6019,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -6053,9 +6057,10 @@ } }, "node_modules/flatbuffers": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-1.12.0.tgz", - "integrity": "sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ==" + "version": "25.1.24", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.1.24.tgz", + "integrity": "sha512-Ni+KCqYquU30UEgGkrrwpbYtUcUmNuLFcQ5Xdy9DK7WUaji+AAov+Bf12FEYmu0eI15y31oD38utnBexe0cAYA==", + "license": "Apache-2.0" }, "node_modules/flatted": { "version": "3.3.1", @@ -6126,7 +6131,6 @@ "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "dev": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -6446,8 +6450,7 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "node_modules/graphemer": { "version": "1.4.0", @@ -6458,7 +6461,8 @@ "node_modules/guid-typescript": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", - "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==" + "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", + "license": "ISC" }, "node_modules/gulp-sort": { "version": "2.0.0", @@ -6826,7 +6830,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -6875,7 +6878,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -6892,7 +6894,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -6934,7 +6935,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -7114,7 +7114,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, "dependencies": { "universalify": "^2.0.0" }, @@ -7189,6 +7188,16 @@ "integrity": "sha512-tBECoUqNFbyAY4RrbqsBQqDFpGXAEbdD5QKr8kACx3+rnArmuuR22nKQWKazvp07N9yjTyDZaw/20UIH8tL9DQ==", "dev": true }, + "node_modules/kokoro-js": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/kokoro-js/-/kokoro-js-1.1.1.tgz", + "integrity": "sha512-cyLO34iI8nBJXPnd3fI4fGeQGS+a6Uatg7eXNL6QS8TLSxaa30WD6Fj7/XoIZYaHg8q6d+TCrui/f74MTY2g1g==", + "license": "Apache-2.0", + "dependencies": { + "@huggingface/transformers": "^3.3.3", + "phonemizer": "^1.2.1" + } + }, "node_modules/layout-base": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", @@ -7472,9 +7481,10 @@ } }, "node_modules/long": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", - "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.4.tgz", + "integrity": "sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg==", + "license": "Apache-2.0" }, "node_modules/loupe": { "version": "2.3.7", @@ -7627,7 +7637,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "engines": { "node": ">= 8" } @@ -8084,7 +8093,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -8168,6 +8176,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz", "integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==", + "license": "MIT", "dependencies": { "minipass": "^7.0.4", "rimraf": "^5.0.5" @@ -8180,6 +8189,7 @@ "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -8199,6 +8209,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -8213,6 +8224,7 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -8227,6 +8239,7 @@ "version": "5.0.10", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", "dependencies": { "glob": "^10.3.7" }, @@ -8347,7 +8360,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -8451,42 +8463,46 @@ } }, "node_modules/onnxruntime-common": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.19.2.tgz", - "integrity": "sha512-a4R7wYEVFbZBlp0BfhpbFWqe4opCor3KM+5Wm22Az3NGDcQMiU2hfG/0MfnBs+1ZrlSGmlgWeMcXQkDk1UFb8Q==" + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.20.1.tgz", + "integrity": "sha512-YiU0s0IzYYC+gWvqD1HzLc46Du1sXpSiwzKb63PACIJr6LfL27VsXSXQvt68EzD3V0D5Bc0vyJTjmMxp0ylQiw==", + "license": "MIT" }, "node_modules/onnxruntime-node": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.19.2.tgz", - "integrity": "sha512-9eHMP/HKbbeUcqte1JYzaaRC8JPn7ojWeCeoyShO86TOR97OCyIyAIOGX3V95ErjslVhJRXY8Em/caIUc0hm1Q==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.20.1.tgz", + "integrity": "sha512-di/I4HDXRw+FLgq+TyHmQEDd3cEp9iFFZm0r4uJ1Wd7b/WE1VXtKWo8yemex347c6GNF/3Pv86ZfPhIWxORr0w==", "hasInstallScript": true, + "license": "MIT", "os": [ "win32", "darwin", "linux" ], "dependencies": { - "onnxruntime-common": "1.19.2", + "onnxruntime-common": "1.20.1", "tar": "^7.0.1" } }, "node_modules/onnxruntime-web": { - "version": "1.20.0-dev.20241016-2b8fc5529b", - "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.20.0-dev.20241016-2b8fc5529b.tgz", - "integrity": "sha512-1XovqtgqeEFtupuyzdDQo7Tqj4GRyNHzOoXjapCEo4rfH3JrXok5VtqucWfRXHPsOI5qoNxMQ9VE+drDIp6woQ==", + "version": "1.21.0-dev.20250206-d981b153d3", + "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.21.0-dev.20250206-d981b153d3.tgz", + "integrity": "sha512-esDVQdRic6J44VBMFLumYvcGfioMh80ceLmzF1yheJyuLKq/Th8VT2aj42XWQst+2bcWnAhw4IKmRQaqzU8ugg==", + "license": "MIT", "dependencies": { - "flatbuffers": "^1.12.0", + "flatbuffers": "^25.1.24", "guid-typescript": "^1.0.9", "long": "^5.2.3", - "onnxruntime-common": "1.20.0-dev.20241016-2b8fc5529b", + "onnxruntime-common": "1.21.0-dev.20250206-d981b153d3", "platform": "^1.3.6", "protobufjs": "^7.2.4" } }, "node_modules/onnxruntime-web/node_modules/onnxruntime-common": { - "version": "1.20.0-dev.20241016-2b8fc5529b", - "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.20.0-dev.20241016-2b8fc5529b.tgz", - "integrity": "sha512-KZK8b6zCYGZFjd4ANze0pqBnqnFTS3GIVeclQpa2qseDpXrCQJfkWBixRcrZShNhm3LpFOZ8qJYFC5/qsJK9WQ==" + "version": "1.21.0-dev.20250206-d981b153d3", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.21.0-dev.20250206-d981b153d3.tgz", + "integrity": "sha512-TwaE51xV9q2y8pM61q73rbywJnusw9ivTEHAJ39GVWNZqxCoDBpe/tQkh/w9S+o/g+zS7YeeL0I/2mEWd+dgyA==", + "license": "MIT" }, "node_modules/optionator": { "version": "0.9.3", @@ -8564,7 +8580,8 @@ "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" }, "node_modules/paneforge": { "version": "0.0.6", @@ -8747,6 +8764,12 @@ "@types/estree": "*" } }, + "node_modules/phonemizer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/phonemizer/-/phonemizer-1.2.1.tgz", + "integrity": "sha512-v0KJ4mi2T4Q7eJQ0W15Xd4G9k4kICSXE8bpDeJ8jisL4RyJhNWsweKTOi88QXFc4r4LZlz5jVL5lCHhkpdT71A==", + "license": "Apache-2.0" + }, "node_modules/picocolors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", @@ -8800,7 +8823,8 @@ "node_modules/platform": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", - "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==" + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "license": "MIT" }, "node_modules/polyscript": { "version": "0.12.8", @@ -9335,6 +9359,7 @@ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", "hasInstallScript": true, + "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -9434,7 +9459,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -9556,7 +9580,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -9659,7 +9682,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -9785,7 +9807,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -11153,6 +11174,7 @@ "version": "7.4.3", "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "license": "ISC", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", @@ -11169,6 +11191,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", "bin": { "mkdirp": "dist/cjs/src/bin.js" }, @@ -11300,7 +11323,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -11508,7 +11530,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, "engines": { "node": ">= 10.0.0" } @@ -11795,6 +11816,24 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite-plugin-static-copy": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-2.2.0.tgz", + "integrity": "sha512-ytMrKdR9iWEYHbUxs6x53m+MRl4SJsOSoMu1U1+Pfg0DjPeMlsRVx3RR5jvoonineDquIue83Oq69JvNsFSU5w==", + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.3", + "fast-glob": "^3.2.11", + "fs-extra": "^11.1.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0" + } + }, "node_modules/vite/node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -12593,6 +12632,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=18" } diff --git a/package.json b/package.json index 7754eacce8..aa43f6a754 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "idb": "^7.1.1", "js-sha256": "^0.10.1", "katex": "^0.16.21", + "kokoro-js": "^1.1.1", "marked": "^9.1.0", "mermaid": "^10.9.3", "paneforge": "^0.0.6", @@ -105,7 +106,8 @@ "svelte-sonner": "^0.3.19", "tippy.js": "^6.3.7", "turndown": "^7.2.0", - "uuid": "^9.0.1" + "uuid": "^9.0.1", + "vite-plugin-static-copy": "^2.2.0" }, "engines": { "node": ">=18.13.0 <=22.x.x", diff --git a/src/lib/components/admin/Evaluations/Leaderboard.svelte b/src/lib/components/admin/Evaluations/Leaderboard.svelte index 59f6df916a..07e19e9792 100644 --- a/src/lib/components/admin/Evaluations/Leaderboard.svelte +++ b/src/lib/components/admin/Evaluations/Leaderboard.svelte @@ -1,6 +1,8 @@
From 205ce635f6f678e8cd5f5aa3ab1dac6325f784fb Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sun, 9 Feb 2025 23:42:27 -0800 Subject: [PATCH 60/96] feat: Kokoro-js TTS support --- .../chat/Messages/ResponseMessage.svelte | 165 +++++++++------- src/lib/components/chat/Settings/Audio.svelte | 176 ++++++++++++++++-- src/lib/stores/index.ts | 2 + src/lib/workers/KokoroWorker.ts | 70 +++++++ src/lib/workers/kokoro.worker.ts | 53 ++++++ 5 files changed, 388 insertions(+), 78 deletions(-) create mode 100644 src/lib/workers/KokoroWorker.ts create mode 100644 src/lib/workers/kokoro.worker.ts diff --git a/src/lib/components/chat/Messages/ResponseMessage.svelte b/src/lib/components/chat/Messages/ResponseMessage.svelte index f6a4b0bc0a..c839804823 100644 --- a/src/lib/components/chat/Messages/ResponseMessage.svelte +++ b/src/lib/components/chat/Messages/ResponseMessage.svelte @@ -4,12 +4,18 @@ import { createEventDispatcher } from 'svelte'; import { onMount, tick, getContext } from 'svelte'; + import type { Writable } from 'svelte/store'; + import type { i18n as i18nType } from 'i18next'; const i18n = getContext>('i18n'); const dispatch = createEventDispatcher(); - import { config, models, settings, user } from '$lib/stores'; + import { createNewFeedback, getFeedbackById, updateFeedbackById } from '$lib/apis/evaluations'; + import { getChatById } from '$lib/apis/chats'; + import { generateTags } from '$lib/apis'; + + import { config, models, settings, TTSWorker, user } from '$lib/stores'; import { synthesizeOpenAISpeech } from '$lib/apis/audio'; import { imageGenerations } from '$lib/apis/images'; import { @@ -34,13 +40,8 @@ import Error from './Error.svelte'; import Citations from './Citations.svelte'; import CodeExecutions from './CodeExecutions.svelte'; - - import type { Writable } from 'svelte/store'; - import type { i18n as i18nType } from 'i18next'; import ContentRenderer from './ContentRenderer.svelte'; - import { createNewFeedback, getFeedbackById, updateFeedbackById } from '$lib/apis/evaluations'; - import { getChatById } from '$lib/apis/chats'; - import { generateTags } from '$lib/apis'; + import { KokoroWorker } from '$lib/workers/KokoroWorker'; interface MessageType { id: string; @@ -193,62 +194,7 @@ speaking = true; - if ($config.audio.tts.engine !== '') { - loadingSpeech = true; - - const messageContentParts: string[] = getMessageContentParts( - message.content, - $config?.audio?.tts?.split_on ?? 'punctuation' - ); - - if (!messageContentParts.length) { - console.log('No content to speak'); - toast.info($i18n.t('No content to speak')); - - speaking = false; - loadingSpeech = false; - return; - } - - console.debug('Prepared message content for TTS', messageContentParts); - - audioParts = messageContentParts.reduce( - (acc, _sentence, idx) => { - acc[idx] = null; - return acc; - }, - {} as typeof audioParts - ); - - let lastPlayedAudioPromise = Promise.resolve(); // Initialize a promise that resolves immediately - - for (const [idx, sentence] of messageContentParts.entries()) { - const res = await synthesizeOpenAISpeech( - localStorage.token, - $settings?.audio?.tts?.defaultVoice === $config.audio.tts.voice - ? ($settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice) - : $config?.audio?.tts?.voice, - sentence - ).catch((error) => { - console.error(error); - toast.error(`${error}`); - - speaking = false; - loadingSpeech = false; - }); - - if (res) { - const blob = await res.blob(); - const blobUrl = URL.createObjectURL(blob); - const audio = new Audio(blobUrl); - audio.playbackRate = $settings.audio?.tts?.playbackRate ?? 1; - - audioParts[idx] = audio; - loadingSpeech = false; - lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx)); - } - } - } else { + if ($config.audio.tts.engine === '') { let voices = []; const getVoicesLoop = setInterval(() => { voices = speechSynthesis.getVoices(); @@ -283,6 +229,99 @@ speechSynthesis.speak(speak); } }, 100); + } else { + loadingSpeech = true; + + const messageContentParts: string[] = getMessageContentParts( + message.content, + $config?.audio?.tts?.split_on ?? 'punctuation' + ); + + if (!messageContentParts.length) { + console.log('No content to speak'); + toast.info($i18n.t('No content to speak')); + + speaking = false; + loadingSpeech = false; + return; + } + + console.debug('Prepared message content for TTS', messageContentParts); + + audioParts = messageContentParts.reduce( + (acc, _sentence, idx) => { + acc[idx] = null; + return acc; + }, + {} as typeof audioParts + ); + + let lastPlayedAudioPromise = Promise.resolve(); // Initialize a promise that resolves immediately + + if ($settings.audio?.tts?.engine === 'browser-kokoro') { + if (!$TTSWorker) { + await TTSWorker.set( + new KokoroWorker({ + dtype: $settings.audio?.tts?.engineConfig?.dtype ?? 'fp32' + }) + ); + + await $TTSWorker.init(); + } + + console.log($TTSWorker); + + for (const [idx, sentence] of messageContentParts.entries()) { + const blob = await $TTSWorker + .generate({ + text: sentence, + voice: $settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice + }) + .catch((error) => { + console.error(error); + toast.error(`${error}`); + + speaking = false; + loadingSpeech = false; + }); + + if (blob) { + const audio = new Audio(blob); + audio.playbackRate = $settings.audio?.tts?.playbackRate ?? 1; + + audioParts[idx] = audio; + loadingSpeech = false; + lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx)); + } + } + } else { + for (const [idx, sentence] of messageContentParts.entries()) { + const res = await synthesizeOpenAISpeech( + localStorage.token, + $settings?.audio?.tts?.defaultVoice === $config.audio.tts.voice + ? ($settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice) + : $config?.audio?.tts?.voice, + sentence + ).catch((error) => { + console.error(error); + toast.error(`${error}`); + + speaking = false; + loadingSpeech = false; + }); + + if (res) { + const blob = await res.blob(); + const blobUrl = URL.createObjectURL(blob); + const audio = new Audio(blobUrl); + audio.playbackRate = $settings.audio?.tts?.playbackRate ?? 1; + + audioParts[idx] = audio; + loadingSpeech = false; + lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx)); + } + } + } } }; diff --git a/src/lib/components/chat/Settings/Audio.svelte b/src/lib/components/chat/Settings/Audio.svelte index 3f9fa9335b..ce9cae7717 100644 --- a/src/lib/components/chat/Settings/Audio.svelte +++ b/src/lib/components/chat/Settings/Audio.svelte @@ -1,11 +1,14 @@
{$i18n.t('TTS Settings')}
+
+
{$i18n.t('Text-to-Speech Engine')}
+
+ +
+
+ + {#if TTSEngine === 'browser-kokoro'} +
+
{$i18n.t('Kokoro.js Dtype')}
+
+ +
+
+ {/if} +
{$i18n.t('Auto-playback response')}
@@ -178,7 +285,46 @@
- {#if $config.audio.tts.engine === ''} + {#if TTSEngine === 'browser-kokoro'} + {#if TTSModel} +
+
{$i18n.t('Set Voice')}
+
+
+ + + + {#each voices as voice} + + {/each} + +
+
+
+ {:else} +
+
+ + +
+ {$i18n.t('Loading Kokoro.js...')} + {TTSModelProgress && TTSModelProgress.status === 'progress' + ? `(${Math.round(TTSModelProgress.progress * 10) / 10}%)` + : ''} +
+
+ +
+ {$i18n.t('Please do not close the settings page while loading the model.')} +
+
+ {/if} + {:else if $config.audio.tts.engine === ''}
{$i18n.t('Set Voice')}
diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index 1b88395567..f96670cb62 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -41,6 +41,8 @@ export const shortCodesToEmojis = writable( }, {}) ); +export const TTSWorker = writable(null); + export const chatId = writable(''); export const chatTitle = writable(''); diff --git a/src/lib/workers/KokoroWorker.ts b/src/lib/workers/KokoroWorker.ts new file mode 100644 index 0000000000..e5cc4b930f --- /dev/null +++ b/src/lib/workers/KokoroWorker.ts @@ -0,0 +1,70 @@ +import WorkerInstance from '$lib/workers/kokoro.worker?worker'; + +export class KokoroWorker { + private worker: Worker | null = null; + private initialized: boolean = false; + private dtype: string; + + constructor(dtype: string = 'fp32') { + this.dtype = dtype; + } + + public async init() { + if (this.worker) { + console.warn('KokoroWorker is already initialized.'); + return; + } + + this.worker = new WorkerInstance(); + + return new Promise((resolve, reject) => { + this.worker!.onmessage = (event) => { + const { status, error } = event.data; + + if (status === 'init:complete') { + this.initialized = true; + resolve(); + } else if (status === 'init:error') { + console.error(error); + this.initialized = false; + reject(new Error(error)); + } + }; + + this.worker!.postMessage({ + type: 'init', + payload: { dtype: this.dtype } + }); + }); + } + + public async generate({ text, voice }: { text: string; voice: string }): Promise { + if (!this.initialized || !this.worker) { + throw new Error('KokoroTTS Worker is not initialized yet.'); + } + + return new Promise((resolve, reject) => { + this.worker.postMessage({ type: 'generate', payload: { text, voice } }); + + const handleMessage = (event: MessageEvent) => { + if (event.data.status === 'generate:complete') { + this.worker!.removeEventListener('message', handleMessage); + resolve(event.data.audioUrl); + } else if (event.data.status === 'generate:error') { + this.worker!.removeEventListener('message', handleMessage); + reject(new Error(event.data.error)); + } + }; + + this.worker.addEventListener('message', handleMessage); + }); + } + + public terminate() { + if (this.worker) { + this.worker.terminate(); + this.worker = null; + this.initialized = false; + } + } +} diff --git a/src/lib/workers/kokoro.worker.ts b/src/lib/workers/kokoro.worker.ts new file mode 100644 index 0000000000..39277330fd --- /dev/null +++ b/src/lib/workers/kokoro.worker.ts @@ -0,0 +1,53 @@ +import { KokoroTTS } from 'kokoro-js'; + +let tts; +let isInitialized = false; // Flag to track initialization status +const DEFAULT_MODEL_ID = 'onnx-community/Kokoro-82M-v1.0-ONNX'; // Default model + +self.onmessage = async (event) => { + const { type, payload } = event.data; + + if (type === 'init') { + let { model_id, dtype } = payload; + model_id = model_id || DEFAULT_MODEL_ID; // Use default model if none provided + + self.postMessage({ status: 'init:start' }); + + try { + tts = await KokoroTTS.from_pretrained(model_id, { + dtype, + device: !!navigator?.gpu ? 'webgpu' : 'wasm' // Detect WebGPU + }); + isInitialized = true; // Mark as initialized after successful loading + self.postMessage({ status: 'init:complete' }); + } catch (error) { + isInitialized = false; // Ensure it's marked as false on failure + self.postMessage({ status: 'init:error', error: error.message }); + } + } + + if (type === 'generate') { + if (!isInitialized || !tts) { + // Ensure model is initialized + self.postMessage({ status: 'generate:error', error: 'TTS model not initialized' }); + return; + } + + const { text, voice } = payload; + self.postMessage({ status: 'generate:start' }); + + try { + const rawAudio = await tts.generate(text, { voice }); + const blob = await rawAudio.toBlob(); + const blobUrl = URL.createObjectURL(blob); + self.postMessage({ status: 'generate:complete', audioUrl: blobUrl }); + } catch (error) { + self.postMessage({ status: 'generate:error', error: error.message }); + } + } + + if (type === 'status') { + // Respond with the current initialization status + self.postMessage({ status: 'status:check', initialized: isInitialized }); + } +}; From d95e5e0ba5dd8ce9b2b94866ca6ee4fcb720e57a Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Sun, 9 Feb 2025 23:54:24 -0800 Subject: [PATCH 61/96] enh: kokorojs call support --- src/lib/components/chat/MessageInput.svelte | 14 +++- .../chat/MessageInput/CallOverlay.svelte | 19 ++++- .../chat/Messages/ResponseMessage.svelte | 2 - src/lib/workers/KokoroWorker.ts | 84 +++++++++++++------ 4 files changed, 90 insertions(+), 29 deletions(-) diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index 261be72ede..f9c6786f6c 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -16,7 +16,8 @@ showCallOverlay, tools, user as _user, - showControls + showControls, + TTSWorker } from '$lib/stores'; import { blobToFile, compressImage, createMessagesList, findWordIndices } from '$lib/utils'; @@ -43,6 +44,7 @@ import PhotoSolid from '../icons/PhotoSolid.svelte'; import Photo from '../icons/Photo.svelte'; import CommandLine from '../icons/CommandLine.svelte'; + import { KokoroWorker } from '$lib/workers/KokoroWorker'; const i18n = getContext('i18n'); @@ -1281,6 +1283,16 @@ stream = null; + if (!$TTSWorker) { + await TTSWorker.set( + new KokoroWorker({ + dtype: $settings.audio?.tts?.engineConfig?.dtype ?? 'fp32' + }) + ); + + await $TTSWorker.init(); + } + showCallOverlay.set(true); showControls.set(true); } catch (err) { diff --git a/src/lib/components/chat/MessageInput/CallOverlay.svelte b/src/lib/components/chat/MessageInput/CallOverlay.svelte index 466fe7950d..c7a2d4d1e4 100644 --- a/src/lib/components/chat/MessageInput/CallOverlay.svelte +++ b/src/lib/components/chat/MessageInput/CallOverlay.svelte @@ -1,5 +1,5 @@ + + { + await submitHandler(); + saveHandler(); + }} +> +
+ {#if config} +
+
+ {$i18n.t('Code Interpreter')} +
+ +
+
+
+ {$i18n.t('Enable Code Interpreter')} +
+ + +
+
+ +
+
{$i18n.t('Code Interpreter Engine')}
+
+ +
+
+ + {#if config.CODE_INTERPRETER_ENGINE === 'jupyter'} +
+
+ {$i18n.t('Jupyter Kernel Gateway URL')} +
+ +
+
+ +
+
+
+ +
+
+ {$i18n.t('Jupyter Kernel Gateway Auth')} +
+ +
+ +
+
+ + {#if config.CODE_INTERPRETER_JUPYTER_AUTH} +
+
+ {#if config.CODE_INTERPRETER_JUPYTER_AUTH === 'password'} + + {:else} + + {/if} +
+
+ {/if} + {/if} +
+ + + {/if} +
+
+ +
+ From abfe8687327d2f0e1558fe46af0e335f0a1ae546 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 10 Feb 2025 02:28:01 -0800 Subject: [PATCH 66/96] enh: code interpreter global toggle --- backend/open_webui/main.py | 5 +++-- src/lib/components/chat/Chat.svelte | 3 ++- src/lib/components/chat/MessageInput.svelte | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index d9f1408ba4..2b741a2bce 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -1040,13 +1040,14 @@ async def get_app_config(request: Request): { "enable_channels": app.state.config.ENABLE_CHANNELS, "enable_web_search": app.state.config.ENABLE_RAG_WEB_SEARCH, - "enable_google_drive_integration": app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, + "enable_code_interpreter": app.state.config.ENABLE_CODE_INTERPRETER, "enable_image_generation": app.state.config.ENABLE_IMAGE_GENERATION, + "enable_autocomplete_generation": app.state.config.ENABLE_AUTOCOMPLETE_GENERATION, "enable_community_sharing": app.state.config.ENABLE_COMMUNITY_SHARING, "enable_message_rating": app.state.config.ENABLE_MESSAGE_RATING, - "enable_autocomplete_generation": app.state.config.ENABLE_AUTOCOMPLETE_GENERATION, "enable_admin_export": ENABLE_ADMIN_EXPORT, "enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS, + "enable_google_drive_integration": app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION, } if user is not None else {} diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index cc14f2c6fe..2348d42fe4 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -1556,7 +1556,8 @@ ? imageGenerationEnabled : false, code_interpreter: - $user.role === 'admin' || $user?.permissions?.features?.code_interpreter + $config?.features?.enable_code_interpreter && + ($user.role === 'admin' || $user?.permissions?.features?.code_interpreter) ? codeInterpreterEnabled : false, web_search: diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index f9c6786f6c..3ab5d05fed 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -1171,7 +1171,7 @@ {/if} - {#if $_user.role === 'admin' || $_user?.permissions?.features?.code_interpreter} + {#if $config?.features?.enable_code_interpreter && ($_user.role === 'admin' || $_user?.permissions?.features?.code_interpreter)}
- {:else if $mobile && ($user.role === 'admin' || $user?.permissions.chat?.controls)} + {:else if $mobile && ($user.role === 'admin' || $user?.permissions?.chat?.controls)} + +
+ +
+ {#each config?.OPENAI_API_BASE_URLS ?? [] as url, idx} + { + updateHandler(); + }} + onDelete={() => { + config.OPENAI_API_BASE_URLS = config.OPENAI_API_BASE_URLS.filter( + (url, urlIdx) => idx !== urlIdx + ); + config.OPENAI_API_KEYS = config.OPENAI_API_KEYS.filter( + (key, keyIdx) => idx !== keyIdx + ); + + let newConfig = {}; + config.OPENAI_API_BASE_URLS.forEach((url, newIdx) => { + newConfig[newIdx] = + config.OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1]; + }); + config.OPENAI_API_CONFIGS = newConfig; + }} + /> + {/each} +
+
+ {/if} +
+
+ +
+
+ +
+ +
+ diff --git a/src/lib/components/chat/Settings/Connections/Connection.svelte b/src/lib/components/chat/Settings/Connections/Connection.svelte new file mode 100644 index 0000000000..6c8475b4b2 --- /dev/null +++ b/src/lib/components/chat/Settings/Connections/Connection.svelte @@ -0,0 +1,106 @@ + + + { + url = connection.url; + key = connection.key; + config = connection.config; + onSubmit(connection); + }} +/> + +
+ + {#if !(config?.enable ?? true)} +
+ {/if} +
+
+ + + {#if pipeline} +
+ + + + + + + +
+ {/if} +
+ + +
+
+ +
+ + + +
+
diff --git a/src/lib/components/chat/SettingsModal.svelte b/src/lib/components/chat/SettingsModal.svelte index 93ed327c1e..fdf6d7ace6 100644 --- a/src/lib/components/chat/SettingsModal.svelte +++ b/src/lib/components/chat/SettingsModal.svelte @@ -1,7 +1,7 @@ - +
-
-
-
{$i18n.t('Direct Connections')}
+
+
+
+
{$i18n.t('Manage Direct Connections')}
+ + + + +
+ +
+ {#each config?.OPENAI_API_BASE_URLS ?? [] as url, idx} + { + updateHandler(); + }} + onDelete={() => { + config.OPENAI_API_BASE_URLS = config.OPENAI_API_BASE_URLS.filter( + (url, urlIdx) => idx !== urlIdx + ); + config.OPENAI_API_KEYS = config.OPENAI_API_KEYS.filter( + (key, keyIdx) => idx !== keyIdx + ); + + let newConfig = {}; + config.OPENAI_API_BASE_URLS.forEach((url, newIdx) => { + newConfig[newIdx] = + config.OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1]; + }); + config.OPENAI_API_CONFIGS = newConfig; + }} + /> + {/each} +
+
+
{$i18n.t('Connect to your own OpenAI compatible API endpoints.')}
- - {#if false} -
- -
-
-
{$i18n.t('Manage Connections')}
- - - - -
- -
- {#each config?.OPENAI_API_BASE_URLS ?? [] as url, idx} - { - updateHandler(); - }} - onDelete={() => { - config.OPENAI_API_BASE_URLS = config.OPENAI_API_BASE_URLS.filter( - (url, urlIdx) => idx !== urlIdx - ); - config.OPENAI_API_KEYS = config.OPENAI_API_KEYS.filter( - (key, keyIdx) => idx !== keyIdx - ); - - let newConfig = {}; - config.OPENAI_API_BASE_URLS.forEach((url, newIdx) => { - newConfig[newIdx] = - config.OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1]; - }); - config.OPENAI_API_CONFIGS = newConfig; - }} - /> - {/each} -
-
- {/if}
- -
From cd2f4142d55709e5b09fae0c0ff11f49ac1643ae Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 11 Feb 2025 23:42:31 -0800 Subject: [PATCH 91/96] fix: user settings save issue --- backend/open_webui/models/users.py | 18 ++++++++++++++++++ backend/open_webui/routers/users.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/backend/open_webui/models/users.py b/backend/open_webui/models/users.py index 5c196281f7..605299528d 100644 --- a/backend/open_webui/models/users.py +++ b/backend/open_webui/models/users.py @@ -271,6 +271,24 @@ class UsersTable: except Exception: return None + def update_user_settings_by_id(self, id: str, updated: dict) -> Optional[UserModel]: + try: + with get_db() as db: + user_settings = db.query(User).filter_by(id=id).first().settings + + if user_settings is None: + user_settings = {} + + user_settings.update(updated) + + db.query(User).filter_by(id=id).update({"settings": user_settings}) + db.commit() + + user = db.query(User).filter_by(id=id).first() + return UserModel.model_validate(user) + except Exception: + return None + def delete_user_by_id(self, id: str) -> bool: try: # Remove User from Groups diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index ddcaef7674..872212d3ce 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -153,7 +153,7 @@ async def get_user_settings_by_session_user(user=Depends(get_verified_user)): async def update_user_settings_by_session_user( form_data: UserSettings, user=Depends(get_verified_user) ): - user = Users.update_user_by_id(user.id, {"settings": form_data.model_dump()}) + user = Users.update_user_settings_by_id(user.id, form_data.model_dump()) if user: return user.settings else: From 982b1fb7e205d145e940aaae5aca87b4811c9f6b Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Wed, 12 Feb 2025 00:08:51 -0800 Subject: [PATCH 92/96] wip: direct connections --- .../chat/Settings/Connections.svelte | 168 +++++++++++------- .../Settings/Connections/Connection.svelte | 23 --- 2 files changed, 108 insertions(+), 83 deletions(-) diff --git a/src/lib/components/chat/Settings/Connections.svelte b/src/lib/components/chat/Settings/Connections.svelte index 3700f90e4c..4c0d54595b 100644 --- a/src/lib/components/chat/Settings/Connections.svelte +++ b/src/lib/components/chat/Settings/Connections.svelte @@ -6,7 +6,7 @@ const dispatch = createEventDispatcher(); const i18n = getContext('i18n'); - import { models, user } from '$lib/stores'; + import { models, settings, user } from '$lib/stores'; import Switch from '$lib/components/common/Switch.svelte'; import Spinner from '$lib/components/common/Spinner.svelte'; @@ -16,84 +16,132 @@ import AddConnectionModal from '$lib/components/AddConnectionModal.svelte'; - const getModels = async () => { - const models = await _getModels(localStorage.token); - return models; - }; + export let saveSettings: Function; let config = null; let showConnectionModal = false; - onMount(async () => {}); + const addConnectionHandler = async (connection) => { + config.OPENAI_API_BASE_URLS.push(connection.url); + config.OPENAI_API_KEYS.push(connection.key); + config.OPENAI_API_CONFIGS[config.OPENAI_API_BASE_URLS.length - 1] = connection.config; - const addConnectionHandler = async (connection) => {}; + await updateHandler(); + }; - const submitHandler = async () => {}; - const updateHandler = async () => {}; + const updateHandler = async () => { + // Remove trailing slashes + config.OPENAI_API_BASE_URLS = config.OPENAI_API_BASE_URLS.map((url) => url.replace(/\/$/, '')); + + // Check if API KEYS length is same than API URLS length + if (config.OPENAI_API_KEYS.length !== config.OPENAI_API_BASE_URLS.length) { + // if there are more keys than urls, remove the extra keys + if (config.OPENAI_API_KEYS.length > config.OPENAI_API_BASE_URLS.length) { + config.OPENAI_API_KEYS = config.OPENAI_API_KEYS.slice( + 0, + config.OPENAI_API_BASE_URLS.length + ); + } + + // if there are more urls than keys, add empty keys + if (config.OPENAI_API_KEYS.length < config.OPENAI_API_BASE_URLS.length) { + const diff = config.OPENAI_API_BASE_URLS.length - config.OPENAI_API_KEYS.length; + for (let i = 0; i < diff; i++) { + config.OPENAI_API_KEYS.push(''); + } + } + } + + await saveSettings({ + directConnections: config + }); + }; + + const submitHandler = async () => { + await updateHandler(); + + await saveSettings({ + directConnections: config + }); + }; + + onMount(async () => { + config = $settings?.directConnections ?? { + OPENAI_API_BASE_URLS: [], + OPENAI_API_KEYS: [], + OPENAI_API_CONFIGS: {} + }; + });
-
-
-
-
-
{$i18n.t('Manage Direct Connections')}
+ {#if config !== null} +
+
+
+
+
{$i18n.t('Manage Direct Connections')}
- - - + + + +
+ +
+ {#each config?.OPENAI_API_BASE_URLS ?? [] as url, idx} + { + updateHandler(); + }} + onDelete={() => { + config.OPENAI_API_BASE_URLS = config.OPENAI_API_BASE_URLS.filter( + (url, urlIdx) => idx !== urlIdx + ); + config.OPENAI_API_KEYS = config.OPENAI_API_KEYS.filter( + (key, keyIdx) => idx !== keyIdx + ); + + let newConfig = {}; + config.OPENAI_API_BASE_URLS.forEach((url, newIdx) => { + newConfig[newIdx] = + config.OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1]; + }); + config.OPENAI_API_CONFIGS = newConfig; + }} + /> + {/each} +
-
- {#each config?.OPENAI_API_BASE_URLS ?? [] as url, idx} - { - updateHandler(); - }} - onDelete={() => { - config.OPENAI_API_BASE_URLS = config.OPENAI_API_BASE_URLS.filter( - (url, urlIdx) => idx !== urlIdx - ); - config.OPENAI_API_KEYS = config.OPENAI_API_KEYS.filter( - (key, keyIdx) => idx !== keyIdx - ); - - let newConfig = {}; - config.OPENAI_API_BASE_URLS.forEach((url, newIdx) => { - newConfig[newIdx] = - config.OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1]; - }); - config.OPENAI_API_CONFIGS = newConfig; - }} - /> - {/each} -
-
- -
- -
-
- {$i18n.t('Connect to your own OpenAI compatible API endpoints.')} +
+
+ {$i18n.t('Connect to your own OpenAI compatible API endpoints.')} +
-
+ {:else} +
+
+ +
+
+ {/if}
diff --git a/src/lib/components/chat/Settings/Connections/Connection.svelte b/src/lib/components/chat/Settings/Connections/Connection.svelte index 6c8475b4b2..ef605876ec 100644 --- a/src/lib/components/chat/Settings/Connections/Connection.svelte +++ b/src/lib/components/chat/Settings/Connections/Connection.svelte @@ -57,29 +57,6 @@ bind:value={url} autocomplete="off" /> - - {#if pipeline} -
- - - - - - - -
- {/if}
Date: Wed, 12 Feb 2025 01:17:30 -0800 Subject: [PATCH 93/96] wip: direct models --- src/lib/apis/index.ts | 77 ++++++++++++++++++- src/lib/apis/openai/index.ts | 27 +++++++ src/lib/components/admin/Functions.svelte | 14 ++-- .../components/admin/Settings/Audio.svelte | 10 ++- .../admin/Settings/Connections.svelte | 4 +- .../admin/Settings/Evaluations.svelte | 10 +-- .../components/admin/Settings/Models.svelte | 10 ++- .../Models/Manage/ManageOllama.svelte | 10 +-- .../admin/Settings/Pipelines.svelte | 10 +-- .../chat/ModelSelector/Selector.svelte | 30 +++++++- .../chat/Settings/Connections.svelte | 15 ++-- src/lib/components/chat/SettingsModal.svelte | 6 +- src/lib/components/workspace/Models.svelte | 12 ++- src/routes/(app)/+layout.svelte | 2 +- .../(app)/admin/functions/create/+page.svelte | 4 +- .../(app)/admin/functions/edit/+page.svelte | 4 +- .../(app)/workspace/models/+page.svelte | 4 +- .../workspace/models/create/+page.svelte | 2 +- .../(app)/workspace/models/edit/+page.svelte | 2 +- src/routes/s/[id]/+page.svelte | 2 +- 20 files changed, 194 insertions(+), 61 deletions(-) diff --git a/src/lib/apis/index.ts b/src/lib/apis/index.ts index c7fd78819c..d1be9c2105 100644 --- a/src/lib/apis/index.ts +++ b/src/lib/apis/index.ts @@ -1,6 +1,11 @@ import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; +import { getOpenAIModelsDirect } from './openai'; -export const getModels = async (token: string = '', base: boolean = false) => { +export const getModels = async ( + token: string = '', + connections: object | null = null, + base: boolean = false +) => { let error = null; const res = await fetch(`${WEBUI_BASE_URL}/api/models${base ? '/base' : ''}`, { method: 'GET', @@ -25,6 +30,76 @@ export const getModels = async (token: string = '', base: boolean = false) => { } let models = res?.data ?? []; + + if (connections && !base) { + let localModels = []; + + if (connections) { + const OPENAI_API_BASE_URLS = connections.OPENAI_API_BASE_URLS; + const OPENAI_API_KEYS = connections.OPENAI_API_KEYS; + const OPENAI_API_CONFIGS = connections.OPENAI_API_CONFIGS; + + const requests = []; + for (const idx in OPENAI_API_BASE_URLS) { + const url = OPENAI_API_BASE_URLS[idx]; + + if (idx.toString() in OPENAI_API_CONFIGS) { + const apiConfig = OPENAI_API_CONFIGS[idx.toString()] ?? {}; + + const enable = apiConfig?.enable ?? true; + const modelIds = apiConfig?.model_ids ?? []; + + if (enable) { + if (modelIds.length > 0) { + const modelList = { + object: 'list', + data: modelIds.map((modelId) => ({ + id: modelId, + name: modelId, + owned_by: 'openai', + openai: { id: modelId }, + urlIdx: idx + })) + }; + + requests.push(() => modelList); + } else { + requests.push(getOpenAIModelsDirect(url, OPENAI_API_KEYS[idx])); + } + } else { + requests.push(() => {}); + } + } + } + const responses = await Promise.all(requests); + + for (const idx in responses) { + const response = responses[idx]; + const apiConfig = OPENAI_API_CONFIGS[idx.toString()] ?? {}; + + let models = Array.isArray(response) ? response : (response?.data ?? []); + models = models.map((model) => ({ ...model, openai: { id: model.id }, urlIdx: idx })); + + const prefixId = apiConfig.prefix_id; + if (prefixId) { + for (const model of models) { + model.id = `${prefixId}.${model.id}`; + } + } + + localModels = localModels.concat(models); + } + } + + models = models.concat( + localModels.map((model) => ({ + ...model, + name: model?.name ?? model?.id, + direct: true + })) + ); + } + return models; }; diff --git a/src/lib/apis/openai/index.ts b/src/lib/apis/openai/index.ts index 53f369e017..bab2d6e36a 100644 --- a/src/lib/apis/openai/index.ts +++ b/src/lib/apis/openai/index.ts @@ -208,6 +208,33 @@ export const updateOpenAIKeys = async (token: string = '', keys: string[]) => { return res.OPENAI_API_KEYS; }; +export const getOpenAIModelsDirect = async (url: string, key: string) => { + let error = null; + + const res = await fetch(`${url}/models`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(key && { authorization: `Bearer ${key}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = `OpenAI: ${err?.error?.message ?? 'Network Problem'}`; + return []; + }); + + if (error) { + throw error; + } + + return res; +}; + export const getOpenAIModels = async (token: string, urlIdx?: number) => { let error = null; diff --git a/src/lib/components/admin/Functions.svelte b/src/lib/components/admin/Functions.svelte index f7814ce91a..67bfc0499e 100644 --- a/src/lib/components/admin/Functions.svelte +++ b/src/lib/components/admin/Functions.svelte @@ -3,7 +3,7 @@ import fileSaver from 'file-saver'; const { saveAs } = fileSaver; - import { WEBUI_NAME, config, functions, models } from '$lib/stores'; + import { WEBUI_NAME, config, functions, models, settings } from '$lib/stores'; import { onMount, getContext, tick } from 'svelte'; import { goto } from '$app/navigation'; @@ -126,7 +126,7 @@ toast.success($i18n.t('Function deleted successfully')); functions.set(await getFunctions(localStorage.token)); - models.set(await getModels(localStorage.token)); + models.set(await getModels(localStorage.token, $settings?.directConnections ?? null)); } }; @@ -147,7 +147,7 @@ } functions.set(await getFunctions(localStorage.token)); - models.set(await getModels(localStorage.token)); + models.set(await getModels(localStorage.token, $settings?.directConnections ?? null)); } }; @@ -359,7 +359,9 @@ bind:state={func.is_active} on:change={async (e) => { toggleFunctionById(localStorage.token, func.id); - models.set(await getModels(localStorage.token)); + models.set( + await getModels(localStorage.token, $settings?.directConnections ?? null) + ); }} /> @@ -496,7 +498,7 @@ id={selectedFunction?.id ?? null} on:save={async () => { await tick(); - models.set(await getModels(localStorage.token)); + models.set(await getModels(localStorage.token, $settings?.directConnections ?? null)); }} /> @@ -517,7 +519,7 @@ toast.success($i18n.t('Functions imported successfully')); functions.set(await getFunctions(localStorage.token)); - models.set(await getModels(localStorage.token)); + models.set(await getModels(localStorage.token, $settings?.directConnections ?? null)); }; reader.readAsText(importFiles[0]); diff --git a/src/lib/components/admin/Settings/Audio.svelte b/src/lib/components/admin/Settings/Audio.svelte index 69dcb55fad..ca4401029e 100644 --- a/src/lib/components/admin/Settings/Audio.svelte +++ b/src/lib/components/admin/Settings/Audio.svelte @@ -10,7 +10,7 @@ getModels as _getModels, getVoices as _getVoices } from '$lib/apis/audio'; - import { config } from '$lib/stores'; + import { config, settings } from '$lib/stores'; import SensitiveInput from '$lib/components/common/SensitiveInput.svelte'; @@ -51,9 +51,11 @@ if (TTS_ENGINE === '') { models = []; } else { - const res = await _getModels(localStorage.token).catch((e) => { - toast.error(`${e}`); - }); + const res = await _getModels(localStorage.token, $settings?.directConnections ?? null).catch( + (e) => { + toast.error(`${e}`); + } + ); if (res) { console.log(res); diff --git a/src/lib/components/admin/Settings/Connections.svelte b/src/lib/components/admin/Settings/Connections.svelte index 893f45602b..a4254ae4cf 100644 --- a/src/lib/components/admin/Settings/Connections.svelte +++ b/src/lib/components/admin/Settings/Connections.svelte @@ -9,7 +9,7 @@ import { getModels as _getModels } from '$lib/apis'; import { getDirectConnectionsConfig, setDirectConnectionsConfig } from '$lib/apis/configs'; - import { models, user } from '$lib/stores'; + import { models, settings, user } from '$lib/stores'; import Switch from '$lib/components/common/Switch.svelte'; import Spinner from '$lib/components/common/Spinner.svelte'; @@ -23,7 +23,7 @@ const i18n = getContext('i18n'); const getModels = async () => { - const models = await _getModels(localStorage.token); + const models = await _getModels(localStorage.token, $settings?.directConnections ?? null); return models; }; diff --git a/src/lib/components/admin/Settings/Evaluations.svelte b/src/lib/components/admin/Settings/Evaluations.svelte index c0d1b4f32f..41c76d94ae 100644 --- a/src/lib/components/admin/Settings/Evaluations.svelte +++ b/src/lib/components/admin/Settings/Evaluations.svelte @@ -1,6 +1,6 @@