Compare commits

...
136 Commits
Author SHA1 Message Date
Timothy Jaeryang Baek 3c10c3b928 Merge pull request #910 from open-webui/dev
0.1.104
2024-02-25 16:05:21 -05:00
Timothy J. Baek 1d937ec2f2 fix: cleanup unused tags 2024-02-25 13:03:26 -08:00
Timothy J. Baek 7c1b8e396a Update CHANGELOG.md 2024-02-25 12:51:08 -08:00
Timothy J. Baek ce891dfd5a fix: styling 2024-02-25 12:45:23 -08:00
Timothy J. Baek c5009907ff Update CHANGELOG.md 2024-02-25 12:37:23 -08:00
Timothy J. Baek 6780b1b19e fix: styling 2024-02-25 12:36:33 -08:00
Timothy J. Baek 8dce6b4b49 feat: personalised greeting 2024-02-25 12:34:32 -08:00
Timothy J. Baek 0d9e38a0ce refac: styling 2024-02-25 12:31:31 -08:00
Timothy Jaeryang Baek 129bb21a0a Merge pull request #911 from open-webui/check-updates
fix: check version loading progress
2024-02-25 15:11:06 -05:00
Timothy J. Baek fb3804ac95 fix: check version loading progress 2024-02-25 12:10:14 -08:00
Timothy Jaeryang Baek 13db1d5063 Merge pull request #909 from open-webui/check-updates
Check updates
2024-02-25 14:55:56 -05:00
Timothy J. Baek 759883a4c8 feat: check for updates 2024-02-25 11:55:15 -08:00
Timothy J. Baek 6bfe2a6306 feat: latest release check api 2024-02-25 11:26:58 -08:00
Timothy Jaeryang Baek b82ac66c70 Merge pull request #908 from open-webui/main
dev
2024-02-25 14:12:48 -05:00
Timothy Jaeryang Baek 02fb517bbe Merge pull request #907 from open-webui/dev
0.1.103
2024-02-25 14:01:52 -05:00
Timothy J. Baek 0bd3ec9ef5 doc: update 2024-02-25 11:01:05 -08:00
Timothy J. Baek 07a59cd338 Update CHANGELOG.md 2024-02-25 10:56:56 -08:00
Timothy Jaeryang Baek 6676f0b469 Merge pull request #903 from bannert1337/patch-1
Replace OLLAMA_WEBUI_PORT with OPEN_WEBUI_PORT
2024-02-25 13:50:46 -05:00
bannert 0aad32373b Replace OLLAMA_WEBUI_PORT with OPEN_WEBUI_PORT 2024-02-25 13:49:04 +01:00
Timothy J. Baek c8631660c7 Update requirements.txt 2024-02-25 00:58:58 -08:00
Timothy J. Baek 86da5b21d6 doc: changelog 2024-02-24 23:17:04 -08:00
Timothy J. Baek d0c0d0d2b4 fix: litellm config issue 2024-02-24 22:35:11 -08:00
Timothy J. Baek 366679ff7c fix: 'dict' object issue 2024-02-24 22:10:43 -08:00
Timothy J. Baek 1d3fadb221 Update CHANGELOG.md 2024-02-24 20:31:13 -08:00
Timothy J. Baek b93bc925eb Update CHANGELOG.md 2024-02-24 20:29:34 -08:00
Timothy Jaeryang Baek 16265ee64f Merge pull request #899 from tracer123/main
Update app.html
2024-02-24 22:40:07 -05:00
tracer123 e7637da55b Update app.html
Indexing by our current overlords should be disabled by default.
2024-02-24 20:58:44 -06:00
Timothy J. Baek f467483753 fix: error handling 2024-02-24 18:51:13 -08:00
Timothy J. Baek 7aae0cab8d fix: error message 2024-02-24 18:47:04 -08:00
Timothy J. Baek 4bbcb58706 fix: styling 2024-02-24 18:13:51 -08:00
Timothy Jaeryang Baek 92ec0d90e1 Merge pull request #888 from jmfirth/feature/img-gen-steps-setting
Add steps setting for image generation
2024-02-24 21:11:16 -05:00
Timothy J. Baek 41a6bf113a fix: default image generation value 2024-02-24 18:11:03 -08:00
Timothy J. Baek 0f8a74d52d Update CHANGELOG.md 2024-02-24 18:10:13 -08:00
Timothy Jaeryang Baek 271e5bf44d Merge branch 'dev' into feature/img-gen-steps-setting 2024-02-24 21:09:06 -05:00
Timothy J. Baek 393531c9b0 fix 2024-02-24 18:08:35 -08:00
Timothy Jaeryang Baek 1a9a56d690 Merge pull request #844 from open-webui/litellm
feat: direct litellm integration
2024-02-24 21:03:03 -05:00
Timothy J. Baek ec6f53e298 feat: litellm proxy frontend integration 2024-02-24 17:56:53 -08:00
Timothy J. BaekandAlex Gartner a453be07e1 Update CHANGELOG.md
Co-Authored-By: Alex Gartner <2747955+gartnera@users.noreply.github.com>
2024-02-24 17:22:28 -08:00
Timothy Jaeryang Baek 437d7ff6f5 Merge pull request #897 from open-webui/main
dev
2024-02-24 18:59:16 -05:00
Timothy J. Baek e15e015db1 fix: images 2024-02-24 15:48:14 -08:00
Timothy Jaeryang Baek 81eceb48ef Merge pull request #894 from open-webui/doc
Update README.md
2024-02-24 16:57:18 -05:00
Timothy J. Baek 42384ea1a9 Update README.md 2024-02-24 13:56:56 -08:00
Timothy Jaeryang Baek a9cb7e1414 Merge pull request #893 from open-webui/doc
Update README.md
2024-02-24 16:52:47 -05:00
Timothy J. Baek e69dfd12e7 Update README.md 2024-02-24 13:52:21 -08:00
Timothy Jaeryang Baek 061460ee41 Merge pull request #892 from open-webui/doc
Update README.md
2024-02-24 16:48:30 -05:00
Timothy J. Baek 4669099da6 Update README.md 2024-02-24 13:48:12 -08:00
Timothy Jaeryang Baek f31989c120 Merge pull request #891 from open-webui/doc
doc: update
2024-02-24 16:41:47 -05:00
Timothy J. Baek c8a11dc75f Update README.md 2024-02-24 13:41:01 -08:00
Timothy Jaeryang Baek 384f79dcb2 Merge branch 'main' into doc 2024-02-24 16:39:18 -05:00
Timothy J. Baek 3bcfc7ca76 doc: update 2024-02-24 13:38:06 -08:00
Timothy J. Baek 32da2aa511 Update config.py 2024-02-24 13:11:56 -08:00
Justin Firth af1675813f Restore image steps setting when Settings modal is presented 2024-02-24 11:34:11 -05:00
Justin Firth 3fa79e59bf Allow configuration of steps, default to a1111 default 2024-02-24 10:44:08 -05:00
Timothy J. Baek fdb6499bfa fix: error handling 2024-02-24 01:49:19 -08:00
Timothy Jaeryang Baek 261a53c8b1 Merge pull request #885 from open-webui/styling
fix: styling
2024-02-24 04:39:33 -05:00
Timothy J. Baek 65d6d220d4 fix: styling 2024-02-24 01:39:14 -08:00
Timothy J. Baek a74d47b6fc Update requirements.txt 2024-02-24 01:27:44 -08:00
Timothy J. Baek bfba72d486 fix: pip dependency 2024-02-24 01:19:44 -08:00
Timothy J. Baek e8904ac3f7 feat: remove ollama dependency 2024-02-24 01:03:28 -08:00
Timothy J. Baek 43f9a827d3 fix: gitignore 2024-02-24 00:29:03 -08:00
Timothy J. Baek 14dd0d11fb feat: litellm yaml 2024-02-24 00:21:53 -08:00
Timothy Jaeryang Baek 8b38724bd2 Merge pull request #808 from duhow/kubernetes
feat(kubernetes)!: Major Helm Chart refactor
2024-02-24 01:45:54 -05:00
Timothy J. Baek b5bd07a06a feat: secure litellm api 2024-02-23 22:44:56 -08:00
Timothy J. Baek af388dfe62 fix 2024-02-23 22:27:28 -08:00
Timothy Jaeryang Baek ee22e641ff Merge branch 'main' into litellm 2024-02-24 01:10:04 -05:00
Timothy Jaeryang Baek f69c0d2e85 Merge pull request #883 from open-webui/custom-name
fix: error handling
2024-02-23 20:37:22 -05:00
Timothy J. Baek d1f170dc77 fix: error handling 2024-02-23 17:36:38 -08:00
Timothy Jaeryang Baek 1f6b348851 Merge pull request #882 from open-webui/custom-name
Update config.py
2024-02-23 20:25:05 -05:00
Timothy J. Baek decba05c7a Update config.py 2024-02-23 17:24:46 -08:00
Timothy Jaeryang Baek 93180ef001 Merge pull request #881 from open-webui/custom-name
fix
2024-02-23 20:18:14 -05:00
Timothy J. Baek 9e339fdb18 fix 2024-02-23 17:17:59 -08:00
Timothy Jaeryang Baek 9e287262f0 Merge pull request #880 from open-webui/custom-name
feat: sponsor custom name support
2024-02-23 20:16:47 -05:00
Timothy J. Baek 62f67bed29 feat: sponsor custom name support 2024-02-23 17:12:19 -08:00
Timothy Jaeryang Baek fc330a1e8b Merge pull request #870 from ching-kuo/fix_k8s_manifest
Fix Kubernetes Manifest for Web UI
2024-02-23 18:20:25 -05:00
duhow 8eb6a68fcf fix webui-service 2024-02-23 23:58:10 +01:00
Timothy Jaeryang Baek 4c9bb5b465 Merge pull request #874 from open-webui/doc
Update README.md
2024-02-23 14:30:43 -05:00
Timothy J. Baek 3327c6ca0a Update README.md 2024-02-23 11:30:19 -08:00
Timothy Jaeryang Baek d097fbfad8 Merge pull request #873 from open-webui/doc
chore: template
2024-02-23 14:20:11 -05:00
Timothy J. Baek 33cd0888ee fix: template 2024-02-23 11:19:46 -08:00
Timothy J. Baek 90df6e7388 Update pull_request_template.md 2024-02-23 11:18:28 -08:00
Timothy Jaeryang Baek 1198108033 Merge pull request #872 from open-webui/doc
Create pull_request_template.md
2024-02-23 14:17:13 -05:00
Timothy J. Baek 57f379b4ce Create pull_request_template.md 2024-02-23 11:16:53 -08:00
Timothy Jaeryang Baek a974d736d2 Merge pull request #866 from asedmammad/fix-rag-scan-unsupported-mime
fix: RAG scan unsupported mime type
2024-02-23 14:03:24 -05:00
Timothy Jaeryang Baek 53fce7cae8 Merge pull request #865 from jannikstdl/patch-2
development clarification README.md
2024-02-23 13:50:20 -05:00
Timothy Jaeryang Baek 507a92ece7 Merge pull request #863 from duhow/patch-1
config: allow custom default user role
2024-02-23 13:49:50 -05:00
Gene Kuo 035c2e0500 Fix Kubernetes Manifest for Web UI
There are currently 2 issues with the Kubernetes manifest:

- open-webui-deployment refers to ollama-webui-pvc but not included in
  kustomization.yaml, causing issue if using GPU deployment
- ollama-webui-pvc is using the wrong namespace

This commit fixes both issue.
2024-02-24 03:20:05 +09:00
duhow 9ee64d88c6 add missing existingClaim name 2024-02-23 17:41:17 +01:00
Ased Mammad b473ad574f fix: RAG scan unsupported mimetype
This fixes an issue with RAG that stops loading documents as soon
as it reaches a file with unsupported mimetype.
2024-02-23 14:27:31 +03:30
Jannik S 4500d2fe13 development clarification README.md 2024-02-23 11:45:28 +01:00
duhow dd64b1e693 use "latest" as appVersion 2024-02-23 11:24:19 +01:00
duhow 7d947c2988 fix types 2024-02-23 11:09:25 +01:00
David Girón 214881451d allow custom default user role 2024-02-23 11:03:06 +01:00
Timothy Jaeryang Baek b1fa32c5d6 Merge pull request #862 from open-webui/doc
fix: typo
2024-02-23 04:38:04 -05:00
Timothy J. Baek 8837918750 fix: typo 2024-02-23 01:37:22 -08:00
Timothy Jaeryang Baek 7bbb6bdabe Merge pull request #842 from jannikstdl/release-notes-modal
Release Notes Modal
2024-02-23 04:33:49 -05:00
Timothy J. Baek 934af898fc fix: styling 2024-02-23 01:32:32 -08:00
Timothy J. Baek 476e8f002f fix: styling 2024-02-23 01:31:28 -08:00
Timothy J. Baek 9523ecb87f fix: styling 2024-02-23 01:26:07 -08:00
Timothy J. Baek 21d3557f1c fix: styling 2024-02-23 01:25:28 -08:00
Timothy J. Baek c42507e0c7 fix: transform css param 2024-02-23 01:23:06 -08:00
Timothy J. Baek 42925025b7 revert: click outside model behaviour 2024-02-23 01:21:22 -08:00
Timothy J. Baek 0ae8d17931 refac: styling 2024-02-23 01:18:30 -08:00
Timothy J. Baek 66ff4fea07 Update vite.config.ts 2024-02-23 01:08:34 -08:00
Timothy Jaeryang Baek 8fac9de269 Merge branch 'main' into release-notes-modal 2024-02-23 04:07:16 -05:00
Timothy J. Baek 1875a03e85 feat: see what's new button added to about 2024-02-23 01:06:46 -08:00
Timothy J. Baek 5076700b3f feat: colour code sections 2024-02-23 00:59:48 -08:00
Timothy J. Baek d5005e39de fix: docker import file issue 2024-02-23 00:54:22 -08:00
Timothy J. Baek 3ab2c384f2 fix: changelog popup default behaviour 2024-02-23 00:50:54 -08:00
Timothy J. Baek 05aea9edf9 feat: changelog modal auto popup 2024-02-23 00:47:54 -08:00
Timothy J. Baek a84070c6d3 refac: changelog api 2024-02-23 00:36:53 -08:00
Timothy J. Baek ea0253828d Update Modal.svelte 2024-02-23 00:32:43 -08:00
Timothy J. Baek 9f950aea9c feat: changelog.md 2024-02-23 00:30:26 -08:00
Timothy Jaeryang Baek 5c3bb8a395 Merge pull request #858 from open-webui/images
Update Images.svelte
2024-02-22 22:41:25 -05:00
Timothy J. Baek 2e7efde4b7 Update Images.svelte 2024-02-22 19:40:51 -08:00
Jannik Streidl 9b27d952f8 better error handling 2024-02-22 15:42:35 +01:00
Jannik Streidl 06f53a6882 settings fix -> updates value directly 2024-02-22 15:35:31 +01:00
Jannik Streidl 4bb5b3db0a added animations 2024-02-22 15:20:48 +01:00
Timothy J. Baek 9b6dca3d7f feat: litellm frontend integration 2024-02-22 04:12:26 -08:00
Timothy J. Baek de0084c8df feat: litellm integration 2024-02-22 03:22:23 -08:00
Jannik Streidl e1e1cb9fd4 removed comments 2024-02-22 12:16:10 +01:00
Jannik Streidl 56c4ee0fff styling fix 2024-02-22 12:03:45 +01:00
Jannik Streidl 230f787da1 first draft 2024-02-22 11:54:55 +01:00
duhow f5ac3a1ebf update webui size 2024-02-20 00:06:53 +01:00
duhow a91a22567d add values-minikube 2024-02-19 23:39:09 +01:00
duhow 2f0ed3becb add ingress tls and settings 2024-02-19 23:37:05 +01:00
duhow 18463d935e pod optimization and advanced settings 2024-02-19 23:32:50 +01:00
duhow f82347ead6 add annotations 2024-02-19 23:22:10 +01:00
duhow 34b7fd18c7 update storage definition 2024-02-19 23:18:06 +01:00
duhow 4608afa5b9 fix values and update docker tag 2024-02-19 22:55:22 +01:00
duhow 62e0096817 add chart details 2024-02-19 22:51:57 +01:00
duhow 75a52df889 remove default resources allocation 2024-02-19 22:45:48 +01:00
duhow 0d803aa0fd update image repository and storage 2024-02-19 22:43:03 +01:00
duhow 05e31494c2 rename ports, labels and service settings 2024-02-19 22:33:22 +01:00
duhow 0b7f183a03 add labels and selectors 2024-02-19 22:23:36 +01:00
duhow cbaada1410 add define names 2024-02-19 21:58:25 +01:00
duhow 770c6f210c remove namespace 2024-02-19 21:49:53 +01:00
67 changed files with 2312 additions and 1014 deletions
+32
View File
@@ -0,0 +1,32 @@
## Pull Request Checklist
- [ ] **Description:** Briefly describe the changes in this pull request.
- [ ] **Changelog:** Ensure a changelog entry following the format of [Keep a Changelog](https://keepachangelog.com/) is added at the bottom of the PR description.
- [ ] **Documentation:** Have you updated relevant documentation?
- [ ] **Dependencies:** Are there any new dependencies? Have you updated the dependency versions in the documentation?
---
## Description
[Insert a brief description of the changes made in this pull request]
---
### Changelog Entry
### Added
- [List any new features or additions]
### Fixed
- [List any fixes or corrections]
### Changed
- [List any changes or updates]
### Removed
- [List any removed features or files]
+57
View File
@@ -0,0 +1,57 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.1.104] - 2024-02-25
### Added
- **🔄 Check for Updates**: Keep your system current by checking for updates conveniently located in Settings > About.
- **🗑️ Automatic Tag Deletion**: Unused tags on the sidebar will now be deleted automatically with just a click.
### Changed
- **🎨 Modernized Styling**: Enjoy a refreshed look with updated styling for a more contemporary experience.
## [0.1.103] - 2024-02-25
### Added
- **🔗 Built-in LiteLLM Proxy**: Now includes LiteLLM proxy within Open WebUI for enhanced functionality.
- Easily integrate existing LiteLLM configurations using `-v /path/to/config.yaml:/app/backend/data/litellm/config.yaml` flag.
- When utilizing Docker container to run Open WebUI, ensure connections to localhost use `host.docker.internal`.
- **🖼️ Image Generation Enhancements**: Introducing Advanced Settings with Image Preview Feature.
- Customize image generation by setting the number of steps; defaults to A1111 value.
### Fixed
- Resolved issue with RAG scan halting document loading upon encountering unsupported MIME types or exceptions (Issue #866).
### Changed
- Ollama is no longer required to run Open WebUI.
- Access our comprehensive documentation at [Open WebUI Documentation](https://docs.openwebui.com/).
## [0.1.102] - 2024-02-22
### Added
- **🖼️ Image Generation**: Generate Images using the AUTOMATIC1111/stable-diffusion-webui API. You can set this up in Settings > Images.
- **📝 Change title generation prompt**: Change the prompt used to generate titles for your chats. You can set this up in the Settings > Interface.
- **🤖 Change embedding model**: Change the embedding model used to generate embeddings for your chats in the Dockerfile. Use any sentence transformer model from huggingface.co.
- **📢 CHANGELOG.md/Popup**: This popup will show you the latest changes.
## [0.1.101] - 2024-02-22
### Fixed
- LaTex output formatting issue (#828)
### Changed
- Instead of having the previous 1.0.0-alpha.101, we switched to semantic versioning as a way to respect global conventions.
+2
View File
@@ -73,6 +73,8 @@ COPY --from=build /app/onnx /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2/onn
# copy built frontend files # copy built frontend files
COPY --from=build /app/build /app/build COPY --from=build /app/build /app/build
COPY --from=build /app/CHANGELOG.md /app/CHANGELOG.md
COPY --from=build /app/package.json /app/package.json
# copy backend files # copy backend files
COPY ./backend . COPY ./backend .
+23 -205
View File
@@ -11,12 +11,10 @@
[![Discord](https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white)](https://discord.gg/5rJgQTnV4s) [![Discord](https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white)](https://discord.gg/5rJgQTnV4s)
[![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/tjbck) [![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/tjbck)
User-friendly WebUI for LLMs, Inspired by ChatGPT User-friendly WebUI for LLMs, supported LLM runners include Ollama and OpenAI-compatible APIs. For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/).
![Open WebUI Demo](./demo.gif) ![Open WebUI Demo](./demo.gif)
Also check our sibling project, [Open WebUI Community](https://openwebui.com/), where you can discover, download, and explore customized Modelfiles for Ollama! 🦙🔍
## Features ⭐ ## Features ⭐
- 🖥️ **Intuitive Interface**: Our chat interface takes inspiration from ChatGPT, ensuring a user-friendly experience. - 🖥️ **Intuitive Interface**: Our chat interface takes inspiration from ChatGPT, ensuring a user-friendly experience.
@@ -77,56 +75,17 @@ Also check our sibling project, [Open WebUI Community](https://openwebui.com/),
## 🔗 Also Check Out Open WebUI Community! ## 🔗 Also Check Out Open WebUI Community!
Don't forget to explore our sibling project, [Open WebUI Community](https://openwebui.com/), where you can discover, download, and explore customized Modelfiles. Open WebUI Community offers a wide range of exciting possibilities for enhancing your chat interactions with Ollama! 🚀 Don't forget to explore our sibling project, [Open WebUI Community](https://openwebui.com/), where you can discover, download, and explore customized Modelfiles. Open WebUI Community offers a wide range of exciting possibilities for enhancing your chat interactions with Open WebUI! 🚀
## How to Install 🚀 ## How to Install 🚀
🌟 **Important Note on User Roles and Privacy:** > [!NOTE]
> Please note that for certain Docker environments, additional configurations might be needed. If you encounter any connection issues, our detailed guide on [Open WebUI Documentation](https://docs.openwebui.com/) is ready to assist you.
- **Admin Creation:** The very first account to sign up on Open WebUI will be granted **Administrator privileges**. This account will have comprehensive control over the platform, including user management and system settings. ### Quick Start with Docker 🐳
- **User Registrations:** All subsequent users signing up will initially have their accounts set to **Pending** status by default. These accounts will require approval from the Administrator to gain access to the platform functionalities. > [!IMPORTANT]
> When using Docker to install Open WebUI, make sure to include the `-v open-webui:/app/backend/data` in your Docker command. This step is crucial as it ensures your database is properly mounted and prevents any loss of data.
- **Privacy and Data Security:** We prioritize your privacy and data security above all. Please be reassured that all data entered into Open WebUI is stored locally on your device. Our system is designed to be privacy-first, ensuring that no external requests are made, and your data does not leave your local environment. We are committed to maintaining the highest standards of data privacy and security, ensuring that your information remains confidential and under your control.
### Steps to Install Open WebUI
#### Before You Begin
1. **Installing Docker:**
- **For Windows and Mac Users:**
- Download Docker Desktop from [Docker's official website](https://www.docker.com/products/docker-desktop).
- Follow the installation instructions provided on the website. After installation, open Docker Desktop to ensure it's running properly.
- **For Ubuntu and Other Linux Users:**
- Open your terminal.
- Set up your Docker apt repository according to the [Docker documentation](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository)
- Update your package index:
```bash
sudo apt-get update
```
- Install Docker using the following command:
```bash
sudo apt-get install docker-ce docker-ce-cli containerd.io
```
- Verify the Docker installation with:
```bash
sudo docker run hello-world
```
This command downloads a test image and runs it in a container, which prints an informational message.
2. **Ensure You Have the Latest Version of Ollama:**
- Download the latest version from [https://ollama.com/](https://ollama.com/).
3. **Verify Ollama Installation:**
- After installing Ollama, check if it's working by visiting [http://127.0.0.1:11434/](http://127.0.0.1:11434/) in your web browser. Remember, the port number might be different for you.
#### Installing with Docker 🐳
- **Important:** When using Docker to install Open WebUI, make sure to include the `-v open-webui:/app/backend/data` in your Docker command. This step is crucial as it ensures your database is properly mounted and prevents any loss of data.
- **If Ollama is on your computer**, use this command: - **If Ollama is on your computer**, use this command:
@@ -134,16 +93,7 @@ Don't forget to explore our sibling project, [Open WebUI Community](https://open
docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
``` ```
- **To build the container yourself**, follow these steps: - **If Ollama is on a Different Server**, use this command:
```bash
docker build -t open-webui .
docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always open-webui
```
- After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000).
#### Using Ollama on a Different Server
- To connect to Ollama on another server, change the `OLLAMA_API_BASE_URL` to the server's URL: - To connect to Ollama on another server, change the `OLLAMA_API_BASE_URL` to the server's URL:
@@ -151,66 +101,27 @@ Don't forget to explore our sibling project, [Open WebUI Community](https://open
docker run -d -p 3000:8080 -e OLLAMA_API_BASE_URL=https://example.com/api -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main docker run -d -p 3000:8080 -e OLLAMA_API_BASE_URL=https://example.com/api -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
``` ```
Or for a self-built container: - After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! 😄
```bash #### Open WebUI: Server Connection Error
docker build -t open-webui .
docker run -d -p 3000:8080 -e OLLAMA_API_BASE_URL=https://example.com/api -v open-webui:/app/backend/data --name open-webui --restart always open-webui
```
### Installing Ollama and Open WebUI Together If you're experiencing connection issues, it’s often due to the WebUI docker container not being able to reach the Ollama server at 127.0.0.1:11434 (host.docker.internal:11434) inside the container . Use the `--network=host` flag in your docker command to resolve this. Note that the port changes from 3000 to 8080, resulting in the link: `http://localhost:8080`.
#### Using Docker Compose **Example Docker Command**:
- If you don't have Ollama yet, use Docker Compose for easy installation. Run this command: ```bash
docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_API_BASE_URL=http://127.0.0.1:11434/api --name open-webui --restart always ghcr.io/open-webui/open-webui:main
```
```bash ### Other Installation Methods
docker compose up -d --build
```
- **For GPU Support:** Use an additional Docker Compose file: We offer various installation alternatives, including non-Docker methods, Docker Compose, Kustomize, and Helm. Visit our [Open WebUI Documentation](https://docs.openwebui.com/getting-started/) or join our [Discord community](https://discord.gg/5rJgQTnV4s) for comprehensive guidance.
```bash ### Troubleshooting
docker compose -f docker-compose.yaml -f docker-compose.gpu.yaml up -d --build
```
- **To Expose Ollama API:** Use another Docker Compose file: Encountering connection issues? Our [Open WebUI Documentation](https://docs.openwebui.com/getting-started/troubleshooting/) has got you covered. For further assistance and to join our vibrant community, visit the [Open WebUI Discord](https://discord.gg/5rJgQTnV4s).
```bash ### Keeping Your Docker Installation Up-to-Date
docker compose -f docker-compose.yaml -f docker-compose.api.yaml up -d --build
```
#### Using `run-compose.sh` Script (Linux or Docker-Enabled WSL2 on Windows)
- Give execute permission to the script:
```bash
chmod +x run-compose.sh
```
- For CPU-only container:
```bash
./run-compose.sh
```
- For GPU support (read the note about GPU compatibility):
```bash
./run-compose.sh --enable-gpu
```
- To build the latest local version, add `--build`:
```bash
./run-compose.sh --enable-gpu --build
```
### Alternative Installation Methods
For other ways to install, like using Kustomize or Helm, check out [INSTALLATION.md](/INSTALLATION.md). Join our [Open WebUI Discord community](https://discord.gg/5rJgQTnV4s) for more help and information.
### Updating your Docker Installation
In case you want to update your local Docker installation to the latest version, you can do it with [Watchtower](https://containrrr.dev/watchtower/): In case you want to update your local Docker installation to the latest version, you can do it with [Watchtower](https://containrrr.dev/watchtower/):
@@ -222,104 +133,11 @@ In the last part of the command, replace `open-webui` with your container name i
### Moving from Ollama WebUI to Open WebUI ### Moving from Ollama WebUI to Open WebUI
Given recent name changes, the docker image has been renamed. Additional steps are required to update for those people that used Ollama WebUI previously and want to start using the new images. Check our Migration Guide available in our [Open WebUI Documentation](https://docs.openwebui.com/migration/).
#### Updating to Open WebUI without keeping your data ## What's Next? 🌟
If you want to update to the new image but don't want to keep any previous data like conversations, prompts, documents, etc. you can perform the following steps: Discover upcoming features on our roadmap in the [Open WebUI Documentation](https://docs.openwebui.com/roadmap/).
```bash
docker rm -f ollama-webui
docker pull ghcr.io/open-webui/open-webui:main
[insert the equivalent command that you used to install with the new Docker image name]
docker volume rm ollama-webui
```
For example, for local installation it would be `docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main`. For other installation commands, check the relevant parts of this README document.
#### Migrating your contents from Ollama WebUI to Open WebUI
If you want to update to the new image migrating all your previous settings like conversations, prompts, documents, etc. you can perform the following steps:
```bash
docker rm -f ollama-webui
docker pull ghcr.io/open-webui/open-webui:main
# Creates a new volume and uses a temporary container to copy from one volume to another as per https://github.com/moby/moby/issues/31154#issuecomment-360531460
docker volume create --name open-webui
docker run --rm -v ollama-webui:/from -v open-webui:/to alpine ash -c "cd /from ; cp -av . /to"
[insert the equivalent command that you used to install with the new Docker image name]
```
Once you verify that all the data has been migrated you can erase the old volumen using the following command:
```bash
docker volume rm ollama-webui
```
## How to Install Without Docker
While we strongly recommend using our convenient Docker container installation for optimal support, we understand that some situations may require a non-Docker setup, especially for development purposes. Please note that non-Docker installations are not officially supported, and you might need to troubleshoot on your own.
### Project Components
Open WebUI consists of two primary components: the frontend and the backend (which serves as a reverse proxy, handling static frontend files, and additional features). Both need to be running concurrently for the development environment.
> [!IMPORTANT]
> The backend is required for proper functionality
### Requirements 📦
- 🐰 [Bun](https://bun.sh) >= 1.0.21 or 🐢 [Node.js](https://nodejs.org/en) >= 20.10
- 🐍 [Python](https://python.org) >= 3.11
### Build and Install 🛠️
Run the following commands to install:
```sh
git clone https://github.com/open-webui/open-webui.git
cd open-webui/
# Copying required .env file
cp -RPp .env.example .env
# Building Frontend Using Node
npm i
npm run build
# or Building Frontend Using Bun
# bun install
# bun run build
# Serving Frontend with the Backend
cd ./backend
pip install -r requirements.txt -U
sh start.sh
```
You should have Open WebUI up and running at http://localhost:8080/. Enjoy! 😄
## Troubleshooting
See [TROUBLESHOOTING.md](/TROUBLESHOOTING.md) for information on how to troubleshoot and/or join our [Open WebUI Discord community](https://discord.gg/5rJgQTnV4s).
## What's Next? 🚀
### Roadmap 📝
Here are some exciting tasks on our roadmap:
- 🔊 **Local Text-to-Speech Integration**: Seamlessly incorporate text-to-speech functionality directly within the platform, allowing for a smoother and more immersive user experience.
- 🛡️ **Granular Permissions and User Groups**: Empower administrators to finely control access levels and group users according to their roles and responsibilities. This feature ensures robust security measures and streamlined management of user privileges, enhancing overall platform functionality.
- 🔄 **Function Calling**: Empower your interactions by running code directly within the chat. Execute functions and commands effortlessly, enhancing the functionality of your conversations.
- ⚙️ **Custom Python Backend Actions**: Empower your Open WebUI by creating or downloading custom Python backend actions. Unleash the full potential of your web interface with tailored actions that suit your specific needs, enhancing functionality and versatility.
- 🔧 **Fine-tune Model (LoRA)**: Fine-tune your model directly from the user interface. This feature allows for precise customization and optimization of the chat experience to better suit your needs and preferences.
- 🧠 **Long-Term Memory**: Witness the power of persistent memory in our agents. Enjoy conversations that feel continuous as agents remember and reference past interactions, creating a more cohesive and personalized user experience.
- 🧪 **Research-Centric Features**: Empower researchers in the fields of LLM and HCI with a comprehensive web UI for conducting user studies. Stay tuned for ongoing feature enhancements (e.g., surveys, analytics, and participant tracking) to facilitate their research.
- 📈 **User Study Tools**: Providing specialized tools, like heat maps and behavior tracking modules, to empower researchers in capturing and analyzing user behavior patterns with precision and accuracy.
- 📚 **Enhanced Documentation**: Elevate your setup and customization experience with improved, comprehensive documentation.
Feel free to contribute and help us make Open WebUI even better! 🙌
## Supporters ✨ ## Supporters ✨
+7
View File
@@ -5,3 +5,10 @@ uploads
.ipynb_checkpoints .ipynb_checkpoints
*.db *.db
_test _test
!/data
/data/*
!/data/litellm
/data/litellm/*
!data/litellm/config.yaml
!data/config.json
+6 -1
View File
@@ -6,6 +6,11 @@ uploads
*.db *.db
_test _test
Pipfile Pipfile
data/* !/data
/data/*
!/data/litellm
/data/litellm/*
!data/litellm/config.yaml
!data/config.json !data/config.json
.webui_secret_key .webui_secret_key
+36 -4
View File
@@ -35,6 +35,7 @@ app.add_middleware(
app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
app.state.ENABLED = app.state.AUTOMATIC1111_BASE_URL != "" app.state.ENABLED = app.state.AUTOMATIC1111_BASE_URL != ""
app.state.IMAGE_SIZE = "512x512" app.state.IMAGE_SIZE = "512x512"
app.state.IMAGE_STEPS = 50
@app.get("/enabled", response_model=bool) @app.get("/enabled", response_model=bool)
@@ -49,7 +50,7 @@ async def toggle_enabled(request: Request, user=Depends(get_admin_user)):
app.state.ENABLED = not app.state.ENABLED app.state.ENABLED = not app.state.ENABLED
return app.state.ENABLED return app.state.ENABLED
except Exception as e: except Exception as e:
raise HTTPException(status_code=r.status_code, detail=ERROR_MESSAGES.DEFAULT(e)) raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
class UrlUpdateForm(BaseModel): class UrlUpdateForm(BaseModel):
@@ -102,6 +103,32 @@ async def update_image_size(
) )
class ImageStepsUpdateForm(BaseModel):
steps: int
@app.get("/steps")
async def get_image_size(user=Depends(get_admin_user)):
return {"IMAGE_STEPS": app.state.IMAGE_STEPS}
@app.post("/steps/update")
async def update_image_size(
form_data: ImageStepsUpdateForm, user=Depends(get_admin_user)
):
if form_data.steps >= 0:
app.state.IMAGE_STEPS = form_data.steps
return {
"IMAGE_STEPS": app.state.IMAGE_STEPS,
"status": True,
}
else:
raise HTTPException(
status_code=400,
detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (e.g., 50)."),
)
@app.get("/models") @app.get("/models")
def get_models(user=Depends(get_current_user)): def get_models(user=Depends(get_current_user)):
try: try:
@@ -109,7 +136,8 @@ def get_models(user=Depends(get_current_user)):
models = r.json() models = r.json()
return models return models
except Exception as e: except Exception as e:
raise HTTPException(status_code=r.status_code, detail=ERROR_MESSAGES.DEFAULT(e)) app.state.ENABLED = False
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
@app.get("/models/default") @app.get("/models/default")
@@ -120,7 +148,8 @@ async def get_default_model(user=Depends(get_admin_user)):
return {"model": options["sd_model_checkpoint"]} return {"model": options["sd_model_checkpoint"]}
except Exception as e: except Exception as e:
raise HTTPException(status_code=r.status_code, detail=ERROR_MESSAGES.DEFAULT(e)) app.state.ENABLED = False
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
class UpdateModelForm(BaseModel): class UpdateModelForm(BaseModel):
@@ -177,6 +206,9 @@ def generate_image(
"height": height, "height": height,
} }
if app.state.IMAGE_STEPS != None:
data["steps"] = app.state.IMAGE_STEPS
if form_data.negative_prompt != None: if form_data.negative_prompt != None:
data["negative_prompt"] = form_data.negative_prompt data["negative_prompt"] = form_data.negative_prompt
@@ -190,4 +222,4 @@ def generate_image(
return r.json() return r.json()
except Exception as e: except Exception as e:
print(e) print(e)
raise HTTPException(status_code=r.status_code, detail=ERROR_MESSAGES.DEFAULT(e)) raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
+5 -5
View File
@@ -423,7 +423,7 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
] or file_ext in ["xls", "xlsx"]: ] or file_ext in ["xls", "xlsx"]:
loader = UnstructuredExcelLoader(file_path) loader = UnstructuredExcelLoader(file_path)
elif file_ext in known_source_ext or file_content_type.find("text/") >= 0: elif file_ext in known_source_ext or (file_content_type and file_content_type.find("text/") >= 0):
loader = TextLoader(file_path) loader = TextLoader(file_path)
else: else:
loader = TextLoader(file_path) loader = TextLoader(file_path)
@@ -486,8 +486,8 @@ def store_doc(
@app.get("/scan") @app.get("/scan")
def scan_docs_dir(user=Depends(get_admin_user)): def scan_docs_dir(user=Depends(get_admin_user)):
try: for path in Path(DOCS_DIR).rglob("./**/*"):
for path in Path(DOCS_DIR).rglob("./**/*"): try:
if path.is_file() and not path.name.startswith("."): if path.is_file() and not path.name.startswith("."):
tags = extract_folders_after_data_docs(path) tags = extract_folders_after_data_docs(path)
filename = path.name filename = path.name
@@ -535,8 +535,8 @@ def scan_docs_dir(user=Depends(get_admin_user)):
), ),
) )
except Exception as e: except Exception as e:
print(e) print(e)
return True return True
+21
View File
@@ -167,6 +167,27 @@ class TagTable:
.count() .count()
) )
def delete_tag_by_tag_name_and_user_id(self, tag_name: str, user_id: str) -> bool:
try:
query = ChatIdTag.delete().where(
(ChatIdTag.tag_name == tag_name) & (ChatIdTag.user_id == user_id)
)
res = query.execute() # Remove the rows, return number of rows removed.
print(res)
tag_count = self.count_chat_ids_by_tag_name_and_user_id(tag_name, user_id)
if tag_count == 0:
# Remove tag item from Tag col as well
query = Tag.delete().where(
(Tag.name == tag_name) & (Tag.user_id == user_id)
)
query.execute() # Remove the rows, return number of rows removed.
return True
except Exception as e:
print("delete_tag", e)
return False
def delete_tag_by_tag_name_and_chat_id_and_user_id( def delete_tag_by_tag_name_and_chat_id_and_user_id(
self, tag_name: str, chat_id: str, user_id: str self, tag_name: str, chat_id: str, user_id: str
) -> bool: ) -> bool:
+5 -2
View File
@@ -115,9 +115,12 @@ async def get_user_chats_by_tag_name(
for chat_id_tag in Tags.get_chat_ids_by_tag_name_and_user_id(tag_name, user.id) for chat_id_tag in Tags.get_chat_ids_by_tag_name_and_user_id(tag_name, user.id)
] ]
print(chat_ids) chats = Chats.get_chat_lists_by_chat_ids(chat_ids, skip, limit)
return Chats.get_chat_lists_by_chat_ids(chat_ids, skip, limit) if len(chats) == 0:
Tags.delete_tag_by_tag_name_and_user_id(tag_name, user.id)
return chats
############################ ############################
+143 -3
View File
@@ -1,11 +1,19 @@
import os import os
import chromadb import chromadb
from chromadb import Settings from chromadb import Settings
from secrets import token_bytes
from base64 import b64encode from base64 import b64encode
from constants import ERROR_MESSAGES from bs4 import BeautifulSoup
from pathlib import Path from pathlib import Path
import json import json
import yaml
import markdown
import requests
import shutil
from secrets import token_bytes
from constants import ERROR_MESSAGES
try: try:
@@ -15,6 +23,8 @@ try:
except ImportError: except ImportError:
print("dotenv not installed, skipping...") print("dotenv not installed, skipping...")
WEBUI_NAME = "Open WebUI"
shutil.copyfile("../build/favicon.png", "./static/favicon.png")
#################################### ####################################
# ENV (dev,test,prod) # ENV (dev,test,prod)
@@ -22,6 +32,102 @@ except ImportError:
ENV = os.environ.get("ENV", "dev") ENV = os.environ.get("ENV", "dev")
try:
with open(f"../package.json", "r") as f:
PACKAGE_DATA = json.load(f)
except:
PACKAGE_DATA = {"version": "0.0.0"}
VERSION = PACKAGE_DATA["version"]
# Function to parse each section
def parse_section(section):
items = []
for li in section.find_all("li"):
# Extract raw HTML string
raw_html = str(li)
# Extract text without HTML tags
text = li.get_text(separator=" ", strip=True)
# Split into title and content
parts = text.split(": ", 1)
title = parts[0].strip() if len(parts) > 1 else ""
content = parts[1].strip() if len(parts) > 1 else text
items.append({"title": title, "content": content, "raw": raw_html})
return items
try:
with open("../CHANGELOG.md", "r") as file:
changelog_content = file.read()
except:
changelog_content = ""
# Convert markdown content to HTML
html_content = markdown.markdown(changelog_content)
# Parse the HTML content
soup = BeautifulSoup(html_content, "html.parser")
# Initialize JSON structure
changelog_json = {}
# Iterate over each version
for version in soup.find_all("h2"):
version_number = version.get_text().strip().split(" - ")[0][1:-1] # Remove brackets
date = version.get_text().strip().split(" - ")[1]
version_data = {"date": date}
# Find the next sibling that is a h3 tag (section title)
current = version.find_next_sibling()
while current and current.name != "h2":
if current.name == "h3":
section_title = current.get_text().lower() # e.g., "added", "fixed"
section_items = parse_section(current.find_next_sibling("ul"))
version_data[section_title] = section_items
# Move to the next element
current = current.find_next_sibling()
changelog_json[version_number] = version_data
CHANGELOG = changelog_json
####################################
# CUSTOM_NAME
####################################
CUSTOM_NAME = os.environ.get("CUSTOM_NAME", "")
if CUSTOM_NAME:
try:
r = requests.get(f"https://api.openwebui.com/api/v1/custom/{CUSTOM_NAME}")
data = r.json()
if r.ok:
if "logo" in data:
url = (
f"https://api.openwebui.com{data['logo']}"
if data["logo"][0] == "/"
else data["logo"]
)
r = requests.get(url, stream=True)
if r.status_code == 200:
with open("./static/favicon.png", "wb") as f:
r.raw.decode_content = True
shutil.copyfileobj(r.raw, f)
WEBUI_NAME = data["name"]
except Exception as e:
print(e)
pass
#################################### ####################################
# DATA/FRONTEND BUILD DIR # DATA/FRONTEND BUILD DIR
@@ -59,6 +165,40 @@ Path(CACHE_DIR).mkdir(parents=True, exist_ok=True)
DOCS_DIR = f"{DATA_DIR}/docs" DOCS_DIR = f"{DATA_DIR}/docs"
Path(DOCS_DIR).mkdir(parents=True, exist_ok=True) Path(DOCS_DIR).mkdir(parents=True, exist_ok=True)
####################################
# LITELLM_CONFIG
####################################
def create_config_file(file_path):
directory = os.path.dirname(file_path)
# Check if directory exists, if not, create it
if not os.path.exists(directory):
os.makedirs(directory)
# Data to write into the YAML file
config_data = {
"general_settings": {},
"litellm_settings": {},
"model_list": [],
"router_settings": {},
}
# Write data to YAML file
with open(file_path, "w") as file:
yaml.dump(config_data, file)
LITELLM_CONFIG_PATH = f"{DATA_DIR}/litellm/config.yaml"
if not os.path.exists(LITELLM_CONFIG_PATH):
print("Config file doesn't exist. Creating...")
create_config_file(LITELLM_CONFIG_PATH)
print("Config file created successfully.")
#################################### ####################################
# OLLAMA_API_BASE_URL # OLLAMA_API_BASE_URL
#################################### ####################################
@@ -116,7 +256,7 @@ DEFAULT_PROMPT_SUGGESTIONS = (
) )
DEFAULT_USER_ROLE = "pending" DEFAULT_USER_ROLE = os.getenv("DEFAULT_USER_ROLE", "pending")
USER_PERMISSIONS = {"chat": {"deletion": True}} USER_PERMISSIONS = {"chat": {"deletion": True}}
+1
View File
@@ -47,3 +47,4 @@ class ERROR_MESSAGES(str, Enum):
INCORRECT_FORMAT = ( INCORRECT_FORMAT = (
lambda err="": f"Invalid format. Please use the correct format{err if err else ''}" lambda err="": f"Invalid format. Please use the correct format{err if err else ''}"
) )
RATE_LIMIT_EXCEEDED = "API rate limit exceeded"
+4
View File
@@ -0,0 +1,4 @@
general_settings: {}
litellm_settings: {}
model_list: []
router_settings: {}
+80 -3
View File
@@ -1,22 +1,35 @@
from bs4 import BeautifulSoup
import json
import markdown
import time import time
import os
import sys
import requests
from fastapi import FastAPI, Request from fastapi import FastAPI, Request, Depends, status
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi import HTTPException from fastapi import HTTPException
from fastapi.responses import JSONResponse
from fastapi.middleware.wsgi import WSGIMiddleware from fastapi.middleware.wsgi import WSGIMiddleware
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from starlette.exceptions import HTTPException as StarletteHTTPException from starlette.exceptions import HTTPException as StarletteHTTPException
from litellm.proxy.proxy_server import ProxyConfig, initialize
from litellm.proxy.proxy_server import app as litellm_app
from apps.ollama.main import app as ollama_app from apps.ollama.main import app as ollama_app
from apps.openai.main import app as openai_app from apps.openai.main import app as openai_app
from apps.audio.main import app as audio_app from apps.audio.main import app as audio_app
from apps.images.main import app as images_app from apps.images.main import app as images_app
from apps.rag.main import app as rag_app from apps.rag.main import app as rag_app
from apps.web.main import app as webui_app from apps.web.main import app as webui_app
from config import ENV, FRONTEND_BUILD_DIR
from config import WEBUI_NAME, ENV, VERSION, CHANGELOG, FRONTEND_BUILD_DIR
from constants import ERROR_MESSAGES
from utils.utils import get_http_authorization_cred, get_current_user
class SPAStaticFiles(StaticFiles): class SPAStaticFiles(StaticFiles):
@@ -30,6 +43,21 @@ class SPAStaticFiles(StaticFiles):
raise ex raise ex
proxy_config = ProxyConfig()
async def config():
router, model_list, general_settings = await proxy_config.load_config(
router=None, config_file_path="./data/litellm/config.yaml"
)
await initialize(config="./data/litellm/config.yaml", telemetry=False)
async def startup():
await config()
app = FastAPI(docs_url="/docs" if ENV == "dev" else None, redoc_url=None) app = FastAPI(docs_url="/docs" if ENV == "dev" else None, redoc_url=None)
origins = ["*"] origins = ["*"]
@@ -43,6 +71,11 @@ app.add_middleware(
) )
@app.on_event("startup")
async def on_startup():
await startup()
@app.middleware("http") @app.middleware("http")
async def check_url(request: Request, call_next): async def check_url(request: Request, call_next):
start_time = int(time.time()) start_time = int(time.time())
@@ -53,7 +86,23 @@ async def check_url(request: Request, call_next):
return response return response
@litellm_app.middleware("http")
async def auth_middleware(request: Request, call_next):
auth_header = request.headers.get("Authorization", "")
if ENV != "dev":
try:
user = get_current_user(get_http_authorization_cred(auth_header))
print(user)
except Exception as e:
return JSONResponse(status_code=400, content={"detail": str(e)})
response = await call_next(request)
return response
app.mount("/api/v1", webui_app) app.mount("/api/v1", webui_app)
app.mount("/litellm/api", litellm_app)
app.mount("/ollama/api", ollama_app) app.mount("/ollama/api", ollama_app)
app.mount("/openai/api", openai_app) app.mount("/openai/api", openai_app)
@@ -65,14 +114,42 @@ app.mount("/rag/api/v1", rag_app)
@app.get("/api/config") @app.get("/api/config")
async def get_app_config(): async def get_app_config():
return { return {
"status": True, "status": True,
"name": WEBUI_NAME,
"version": VERSION,
"images": images_app.state.ENABLED, "images": images_app.state.ENABLED,
"default_models": webui_app.state.DEFAULT_MODELS, "default_models": webui_app.state.DEFAULT_MODELS,
"default_prompt_suggestions": webui_app.state.DEFAULT_PROMPT_SUGGESTIONS, "default_prompt_suggestions": webui_app.state.DEFAULT_PROMPT_SUGGESTIONS,
} }
@app.get("/api/changelog")
async def get_app_changelog():
return CHANGELOG
@app.get("/api/version/updates")
async def get_app_latest_release_version():
try:
response = requests.get(
f"https://api.github.com/repos/open-webui/open-webui/releases/latest"
)
response.raise_for_status()
latest_version = response.json()["tag_name"]
return {"current": VERSION, "latest": latest_version[1:]}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=ERROR_MESSAGES.RATE_LIMIT_EXCEEDED,
)
app.mount("/static", StaticFiles(directory="static"), name="static")
app.mount( app.mount(
"/", "/",
SPAStaticFiles(directory=FRONTEND_BUILD_DIR, html=True), SPAStaticFiles(directory=FRONTEND_BUILD_DIR, html=True),
+4
View File
@@ -16,6 +16,10 @@ aiohttp
peewee peewee
bcrypt bcrypt
litellm
apscheduler
google-generativeai
langchain langchain
langchain-community langchain-community
chromadb chromadb
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

+8
View File
@@ -58,6 +58,14 @@ def extract_token_from_auth_header(auth_header: str):
return auth_header[len("Bearer ") :] return auth_header[len("Bearer ") :]
def get_http_authorization_cred(auth_header: str):
try:
scheme, credentials = auth_header.split(" ")
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
except:
raise ValueError(ERROR_MESSAGES.INVALID_TOKEN)
def get_current_user( def get_current_user(
auth_token: HTTPAuthorizationCredentials = Depends(bearer_security), auth_token: HTTPAuthorizationCredentials = Depends(bearer_security),
): ):
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -23,7 +23,7 @@ services:
depends_on: depends_on:
- ollama - ollama
ports: ports:
- ${OLLAMA_WEBUI_PORT-3000}:8080 - ${OPEN_WEBUI_PORT-3000}:8080
environment: environment:
- 'OLLAMA_API_BASE_URL=http://ollama:11434/api' - 'OLLAMA_API_BASE_URL=http://ollama:11434/api'
- 'WEBUI_SECRET_KEY=' - 'WEBUI_SECRET_KEY='
+1
View File
@@ -0,0 +1 @@
values-minikube.yaml
+17 -1
View File
@@ -1,5 +1,21 @@
apiVersion: v2 apiVersion: v2
name: open-webui name: open-webui
description: "Open WebUI: A User-Friendly Web Interface for Chat Interactions 👋"
version: 1.0.0 version: 1.0.0
appVersion: "latest"
home: https://www.openwebui.com/
icon: https://raw.githubusercontent.com/open-webui/open-webui/main/static/favicon.png icon: https://raw.githubusercontent.com/open-webui/open-webui/main/static/favicon.png
description: "Open WebUI: A User-Friendly Web Interface for Chat Interactions 👋"
keywords:
- llm
- chat
- web-ui
sources:
- https://github.com/open-webui/open-webui/tree/main/kubernetes/helm
- https://hub.docker.com/r/ollama/ollama
- https://github.com/open-webui/open-webui/pkgs/container/open-webui
annotations:
licenses: MIT
+47
View File
@@ -0,0 +1,47 @@
{{- define "open-webui.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end -}}
{{- define "ollama.name" -}}
ollama
{{- end -}}
{{- define "ollama.url" -}}
{{- printf "http://%s.%s.svc.cluster.local:%d/api" (include "ollama.name" .) (.Release.Namespace) (.Values.ollama.service.port | int) }}
{{- end }}
{{- define "chart.name" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "base.labels" -}}
helm.sh/chart: {{ include "chart.name" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{- define "base.selectorLabels" -}}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
{{- define "open-webui.selectorLabels" -}}
{{ include "base.selectorLabels" . }}
app.kubernetes.io/component: {{ .Chart.Name }}
{{- end }}
{{- define "open-webui.labels" -}}
{{ include "base.labels" . }}
{{ include "open-webui.selectorLabels" . }}
{{- end }}
{{- define "ollama.selectorLabels" -}}
{{ include "base.selectorLabels" . }}
app.kubernetes.io/component: {{ include "ollama.name" . }}
{{- end }}
{{- define "ollama.labels" -}}
{{ include "base.labels" . }}
{{ include "ollama.selectorLabels" . }}
{{- end }}
@@ -1,4 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: {{ .Values.namespace }}
+14 -6
View File
@@ -1,13 +1,21 @@
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: ollama-service name: {{ include "ollama.name" . }}
namespace: {{ .Values.namespace }} labels:
{{- include "ollama.labels" . | nindent 4 }}
{{- with .Values.ollama.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec: spec:
type: {{ .Values.ollama.service.type }}
selector: selector:
app: ollama {{- include "ollama.selectorLabels" . | nindent 4 }}
{{- with .Values.ollama.service }}
type: {{ .type }}
ports: ports:
- protocol: TCP - protocol: TCP
port: {{ .Values.ollama.servicePort }} name: http
targetPort: {{ .Values.ollama.servicePort }} port: {{ .port }}
targetPort: http
{{- end }}
@@ -1,24 +1,43 @@
apiVersion: apps/v1 apiVersion: apps/v1
kind: StatefulSet kind: StatefulSet
metadata: metadata:
name: ollama name: {{ include "ollama.name" . }}
namespace: {{ .Values.namespace }} labels:
{{- include "ollama.labels" . | nindent 4 }}
{{- with .Values.ollama.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec: spec:
serviceName: "ollama" serviceName: {{ include "ollama.name" . }}
replicas: {{ .Values.ollama.replicaCount }} replicas: {{ .Values.ollama.replicaCount }}
selector: selector:
matchLabels: matchLabels:
app: ollama {{- include "ollama.selectorLabels" . | nindent 6 }}
template: template:
metadata: metadata:
labels: labels:
app: ollama {{- include "ollama.labels" . | nindent 8 }}
{{- with .Values.ollama.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
spec: spec:
enableServiceLinks: false
automountServiceAccountToken: false
{{- with .Values.ollama.runtimeClassName }}
runtimeClassName: {{ . }}
{{- end }}
containers: containers:
- name: ollama - name: {{ include "ollama.name" . }}
image: {{ .Values.ollama.image }} {{- with .Values.ollama.image }}
image: {{ .repository }}:{{ .tag }}
imagePullPolicy: {{ .pullPolicy }}
{{- end }}
tty: true
ports: ports:
- containerPort: {{ .Values.ollama.servicePort }} - name: http
containerPort: {{ .Values.ollama.service.containerPort }}
env: env:
{{- if .Values.ollama.gpu.enabled }} {{- if .Values.ollama.gpu.enabled }}
- name: PATH - name: PATH
@@ -27,29 +46,51 @@ spec:
value: /usr/local/nvidia/lib:/usr/local/nvidia/lib64 value: /usr/local/nvidia/lib:/usr/local/nvidia/lib64
- name: NVIDIA_DRIVER_CAPABILITIES - name: NVIDIA_DRIVER_CAPABILITIES
value: compute,utility value: compute,utility
{{- end}} {{- end }}
{{- if .Values.ollama.resources }} {{- with .Values.ollama.resources }}
resources: {{- toYaml .Values.ollama.resources | nindent 10 }} resources: {{- toYaml . | nindent 10 }}
{{- end }} {{- end }}
volumeMounts: volumeMounts:
- name: ollama-volume - name: data
mountPath: /root/.ollama mountPath: /root/.ollama
tty: true
{{- with .Values.ollama.nodeSelector }} {{- with .Values.ollama.nodeSelector }}
nodeSelector: nodeSelector:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}
{{- end }} {{- end }}
{{- with .Values.ollama.tolerations }}
tolerations: tolerations:
{{- if .Values.ollama.gpu.enabled }} {{- toYaml . | nindent 8 }}
- key: nvidia.com/gpu {{- end }}
operator: Exists volumes:
effect: NoSchedule {{- if and .Values.ollama.persistence.enabled .Values.ollama.persistence.existingClaim }}
{{- end }} - name: data
persistentVolumeClaim:
claimName: {{ .Values.ollama.persistence.existingClaim }}
{{- else if not .Values.ollama.persistence.enabled }}
- name: data
emptyDir: {}
{{- else if and .Values.ollama.persistence.enabled (not .Values.ollama.persistence.existingClaim) }}
[]
volumeClaimTemplates: volumeClaimTemplates:
- metadata: - metadata:
name: ollama-volume name: data
labels:
{{- include "ollama.selectorLabels" . | nindent 8 }}
{{- with .Values.ollama.persistence.annotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
spec: spec:
accessModes: [ "ReadWriteOnce" ] accessModes:
{{- range .Values.ollama.persistence.accessModes }}
- {{ . | quote }}
{{- end }}
resources: resources:
requests: requests:
storage: {{ .Values.ollama.volumeSize }} storage: {{ .Values.ollama.persistence.size | quote }}
storageClass: {{ .Values.ollama.persistence.storageClass }}
{{- with .Values.ollama.persistence.selector }}
selector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
+38 -14
View File
@@ -1,38 +1,62 @@
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: open-webui-deployment name: {{ include "open-webui.name" . }}
namespace: {{ .Values.namespace }} labels:
{{- include "open-webui.labels" . | nindent 4 }}
{{- with .Values.webui.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec: spec:
replicas: 1 replicas: {{ .Values.webui.replicaCount }}
selector: selector:
matchLabels: matchLabels:
app: open-webui {{- include "open-webui.selectorLabels" . | nindent 6 }}
template: template:
metadata: metadata:
labels: labels:
app: open-webui {{- include "open-webui.labels" . | nindent 8 }}
{{- with .Values.webui.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
spec: spec:
enableServiceLinks: false
automountServiceAccountToken: false
containers: containers:
- name: open-webui - name: {{ .Chart.Name }}
image: {{ .Values.webui.image }} {{- with .Values.webui.image }}
image: {{ .repository }}:{{ .tag | default $.Chart.AppVersion }}
imagePullPolicy: {{ .pullPolicy }}
{{- end }}
ports: ports:
- containerPort: 8080 - name: http
{{- if .Values.webui.resources }} containerPort: {{ .Values.webui.service.containerPort }}
resources: {{- toYaml .Values.webui.resources | nindent 10 }} {{- with .Values.webui.resources }}
resources: {{- toYaml . | nindent 10 }}
{{- end }} {{- end }}
volumeMounts: volumeMounts:
- name: webui-volume - name: data
mountPath: /app/backend/data mountPath: /app/backend/data
env: env:
- name: OLLAMA_API_BASE_URL - name: OLLAMA_API_BASE_URL
value: "http://ollama-service.{{ .Values.namespace }}.svc.cluster.local:{{ .Values.ollama.servicePort }}/api" value: {{ include "ollama.url" . | quote }}
tty: true tty: true
{{- with .Values.webui.nodeSelector }} {{- with .Values.webui.nodeSelector }}
nodeSelector: nodeSelector:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}
{{- end }} {{- end }}
volumes: volumes:
- name: webui-volume {{- if and .Values.webui.persistence.enabled .Values.webui.persistence.existingClaim }}
- name: data
persistentVolumeClaim: persistentVolumeClaim:
claimName: open-webui-pvc claimName: {{ .Values.webui.persistence.existingClaim }}
{{- else if not .Values.webui.persistence.enabled }}
- name: data
emptyDir: {}
{{- else if and .Values.webui.persistence.enabled (not .Values.webui.persistence.existingClaim) }}
- name: data
persistentVolumeClaim:
claimName: {{ include "open-webui.name" . }}
{{- end }}
+17 -7
View File
@@ -2,13 +2,23 @@
apiVersion: networking.k8s.io/v1 apiVersion: networking.k8s.io/v1
kind: Ingress kind: Ingress
metadata: metadata:
name: open-webui-ingress name: {{ include "open-webui.name" . }}
namespace: {{ .Values.namespace }} labels:
{{- if .Values.webui.ingress.annotations }} {{- include "open-webui.labels" . | nindent 4 }}
{{- with .Values.webui.ingress.annotations }}
annotations: annotations:
{{ toYaml .Values.webui.ingress.annotations | trimSuffix "\n" | indent 4 }} {{- toYaml . | nindent 4 }}
{{- end }} {{- end }}
spec: spec:
{{- with .Values.webui.ingress.class }}
ingressClassName: {{ . }}
{{- end }}
{{- if .Values.webui.ingress.tls }}
tls:
- hosts:
- {{ .Values.webui.ingress.host | quote }}
secretName: {{ default (printf "%s-tls" .Release.Name) .Values.webui.ingress.existingSecret }}
{{- end }}
rules: rules:
- host: {{ .Values.webui.ingress.host }} - host: {{ .Values.webui.ingress.host }}
http: http:
@@ -17,7 +27,7 @@ spec:
pathType: Prefix pathType: Prefix
backend: backend:
service: service:
name: open-webui-service name: {{ include "open-webui.name" . }}
port: port:
number: {{ .Values.webui.servicePort }} name: http
{{- end }} {{- end }}
+18 -5
View File
@@ -1,12 +1,25 @@
{{- if and .Values.webui.persistence.enabled (not .Values.webui.persistence.existingClaim) }}
apiVersion: v1 apiVersion: v1
kind: PersistentVolumeClaim kind: PersistentVolumeClaim
metadata: metadata:
name: {{ include "open-webui.name" . }}
labels: labels:
app: open-webui {{- include "open-webui.selectorLabels" . | nindent 4 }}
name: open-webui-pvc {{- with .Values.webui.persistence.annotations }}
namespace: {{ .Values.namespace }} annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
spec: spec:
accessModes: [ "ReadWriteOnce" ] accessModes:
{{- range .Values.webui.persistence.accessModes }}
- {{ . | quote }}
{{- end }}
resources: resources:
requests: requests:
storage: {{ .Values.webui.volumeSize }} storage: {{ .Values.webui.persistence.size }}
storageClass: {{ .Values.webui.persistence.storageClass }}
{{- with .Values.webui.persistence.selector }}
selector:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
+18 -9
View File
@@ -1,15 +1,24 @@
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: open-webui-service name: {{ include "open-webui.name" . }}
namespace: {{ .Values.namespace }} labels:
{{- include "open-webui.labels" . | nindent 4 }}
{{- with .Values.webui.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec: spec:
type: {{ .Values.webui.service.type }} # Default: NodePort # Use LoadBalancer if you're on a cloud that supports it
selector: selector:
app: open-webui {{- include "open-webui.selectorLabels" . | nindent 4 }}
{{- with .Values.webui.service }}
type: {{ .type }}
ports: ports:
- protocol: TCP - protocol: TCP
port: {{ .Values.webui.servicePort }} name: http
targetPort: {{ .Values.webui.servicePort }} port: {{ .port }}
# If using NodePort, you can optionally specify the nodePort: targetPort: http
# nodePort: 30000 {{- if .nodePort }}
nodePort: {{ .nodePort | int }}
{{- end }}
{{- end }}
+27
View File
@@ -0,0 +1,27 @@
ollama:
resources:
requests:
cpu: "2000m"
memory: "2Gi"
limits:
cpu: "4000m"
memory: "4Gi"
nvidia.com/gpu: "0"
service:
type: ClusterIP
gpu:
enabled: false
webui:
resources:
requests:
cpu: "500m"
memory: "500Mi"
limits:
cpu: "1000m"
memory: "1Gi"
ingress:
enabled: true
host: open-webui.minikube.local
service:
type: NodePort
+65 -37
View File
@@ -1,44 +1,72 @@
namespace: open-webui nameOverride: ""
ollama: ollama:
annotations: {}
podAnnotations: {}
replicaCount: 1 replicaCount: 1
image: ollama/ollama:latest image:
servicePort: 11434 repository: ollama/ollama
resources: tag: latest
requests: pullPolicy: Always
cpu: "2000m" resources: {}
memory: "2Gi" persistence:
limits: enabled: true
cpu: "4000m" size: 30Gi
memory: "4Gi" existingClaim: ""
nvidia.com/gpu: "0" accessModes:
volumeSize: 30Gi - ReadWriteOnce
storageClass: ""
selector: {}
annotations: {}
nodeSelector: {}
# -- If using a special runtime container such as nvidia, set it here.
runtimeClassName: ""
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
service:
type: ClusterIP
annotations: {}
port: 80
containerPort: 11434
gpu:
# -- Enable additional ENV values to help Ollama discover GPU usage
enabled: false
webui:
annotations: {}
podAnnotations: {}
replicaCount: 1
image:
repository: ghcr.io/open-webui/open-webui
tag: ""
pullPolicy: Always
resources: {}
ingress:
enabled: false
class: ""
# -- Use appropriate annotations for your Ingress controller, e.g., for NGINX:
# nginx.ingress.kubernetes.io/rewrite-target: /
annotations: {}
host: ""
tls: false
existingSecret: ""
persistence:
enabled: true
size: 2Gi
existingClaim: ""
# -- If using multiple replicas, you must update accessModes to ReadWriteMany
accessModes:
- ReadWriteOnce
storageClass: ""
selector: {}
annotations: {}
nodeSelector: {} nodeSelector: {}
tolerations: [] tolerations: []
service: service:
type: ClusterIP type: ClusterIP
gpu: annotations: {}
enabled: false port: 80
containerPort: 8080
webui: nodePort: ""
replicaCount: 1
image: ghcr.io/open-webui/open-webui:main
servicePort: 8080
resources:
requests:
cpu: "500m"
memory: "500Mi"
limits:
cpu: "1000m"
memory: "1Gi"
ingress:
enabled: true
annotations:
# Use appropriate annotations for your Ingress controller, e.g., for NGINX:
# nginx.ingress.kubernetes.io/rewrite-target: /
host: open-webui.minikube.local
volumeSize: 2Gi
nodeSelector: {}
tolerations: []
service:
type: NodePort
+1 -1
View File
@@ -4,7 +4,7 @@ metadata:
labels: labels:
app: ollama-webui app: ollama-webui
name: ollama-webui-pvc name: ollama-webui-pvc
namespace: ollama-namespace namespace: open-webui
spec: spec:
accessModes: ["ReadWriteOnce"] accessModes: ["ReadWriteOnce"]
resources: resources:
+1
View File
@@ -5,6 +5,7 @@ resources:
- base/webui-deployment.yaml - base/webui-deployment.yaml
- base/webui-service.yaml - base/webui-service.yaml
- base/webui-ingress.yaml - base/webui-ingress.yaml
- base/webui-pvc.yaml
apiVersion: kustomize.config.k8s.io/v1beta1 apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization kind: Kustomization
+19 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "open-webui", "name": "open-webui",
"version": "0.0.1", "version": "v1.0.0-alpha.101",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "open-webui", "name": "open-webui",
"version": "0.0.1", "version": "v1.0.0-alpha.101",
"dependencies": { "dependencies": {
"@sveltejs/adapter-node": "^1.3.1", "@sveltejs/adapter-node": "^1.3.1",
"async": "^3.2.5", "async": "^3.2.5",
@@ -38,6 +38,7 @@
"prettier-plugin-svelte": "^2.10.1", "prettier-plugin-svelte": "^2.10.1",
"svelte": "^4.0.5", "svelte": "^4.0.5",
"svelte-check": "^3.4.3", "svelte-check": "^3.4.3",
"svelte-confetti": "^1.3.2",
"tailwindcss": "^3.3.3", "tailwindcss": "^3.3.3",
"tslib": "^2.4.1", "tslib": "^2.4.1",
"typescript": "^5.0.0", "typescript": "^5.0.0",
@@ -3174,6 +3175,15 @@
"svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0" "svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0"
} }
}, },
"node_modules/svelte-confetti": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/svelte-confetti/-/svelte-confetti-1.3.2.tgz",
"integrity": "sha512-R+JwFTC7hIgWVA/OuXrkj384B7CMoceb0t9VacyW6dORTQg0pWojVBB8Bo3tM30cLEQE48Fekzqgx+XSzHESMA==",
"dev": true,
"peerDependencies": {
"svelte": "^4.0.0"
}
},
"node_modules/svelte-eslint-parser": { "node_modules/svelte-eslint-parser": {
"version": "0.33.1", "version": "0.33.1",
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.33.1.tgz", "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.33.1.tgz",
@@ -5852,6 +5862,13 @@
"typescript": "^5.0.3" "typescript": "^5.0.3"
} }
}, },
"svelte-confetti": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/svelte-confetti/-/svelte-confetti-1.3.2.tgz",
"integrity": "sha512-R+JwFTC7hIgWVA/OuXrkj384B7CMoceb0t9VacyW6dORTQg0pWojVBB8Bo3tM30cLEQE48Fekzqgx+XSzHESMA==",
"dev": true,
"requires": {}
},
"svelte-eslint-parser": { "svelte-eslint-parser": {
"version": "0.33.1", "version": "0.33.1",
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.33.1.tgz", "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.33.1.tgz",
+2 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "open-webui", "name": "open-webui",
"version": "0.1.102", "version": "0.1.104",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite dev --host", "dev": "vite dev --host",
@@ -32,6 +32,7 @@
"prettier-plugin-svelte": "^2.10.1", "prettier-plugin-svelte": "^2.10.1",
"svelte": "^4.0.5", "svelte": "^4.0.5",
"svelte-check": "^3.4.3", "svelte-check": "^3.4.3",
"svelte-confetti": "^1.3.2",
"tailwindcss": "^3.3.3", "tailwindcss": "^3.3.3",
"tslib": "^2.4.1", "tslib": "^2.4.1",
"typescript": "^5.0.0", "typescript": "^5.0.0",
+2 -2
View File
@@ -37,8 +37,8 @@ math {
} }
::-webkit-scrollbar { ::-webkit-scrollbar {
height: 0.45rem; height: 0.4rem;
width: 0.35rem; width: 0.4rem;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
+1
View File
@@ -5,6 +5,7 @@
<link rel="icon" href="%sveltekit.assets%/favicon.png" /> <link rel="icon" href="%sveltekit.assets%/favicon.png" />
<link rel="manifest" href="%sveltekit.assets%/manifest.json" /> <link rel="manifest" href="%sveltekit.assets%/manifest.json" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<meta name="robots" content="noindex,nofollow" />
<script> <script>
// On page load or when changing themes, best to add inline in `head` to avoid FOUC // On page load or when changing themes, best to add inline in `head` to avoid FOUC
if ( if (
+65
View File
@@ -198,6 +198,71 @@ export const updateImageSize = async (token: string = '', size: string) => {
return res.IMAGE_SIZE; return res.IMAGE_SIZE;
}; };
export const getImageSteps = async (token: string = '') => {
let error = null;
const res = await fetch(`${IMAGES_API_BASE_URL}/steps`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
} else {
error = 'Server connection failed';
}
return null;
});
if (error) {
throw error;
}
return res.IMAGE_STEPS;
};
export const updateImageSteps = async (token: string = '', steps: number) => {
let error = null;
const res = await fetch(`${IMAGES_API_BASE_URL}/steps/update`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
},
body: JSON.stringify({ steps })
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
} else {
error = 'Server connection failed';
}
return null;
});
if (error) {
throw error;
}
return res.IMAGE_STEPS;
};
export const getDiffusionModels = async (token: string = '') => { export const getDiffusionModels = async (token: string = '') => {
let error = null; let error = null;
+56
View File
@@ -19,5 +19,61 @@ export const getBackendConfig = async () => {
return null; return null;
}); });
if (error) {
throw error;
}
return res;
};
export const getChangelog = async () => {
let error = null;
const res = await fetch(`${WEBUI_BASE_URL}/api/changelog`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err;
return null;
});
if (error) {
throw error;
}
return res;
};
export const getVersionUpdates = async () => {
let error = null;
const res = await fetch(`${WEBUI_BASE_URL}/api/version/updates`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err;
return null;
});
if (error) {
throw error;
}
return res; return res;
}; };
+148
View File
@@ -0,0 +1,148 @@
import { LITELLM_API_BASE_URL } from '$lib/constants';
export const getLiteLLMModels = async (token: string = '') => {
let error = null;
const res = await fetch(`${LITELLM_API_BASE_URL}/v1/models`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = `LiteLLM: ${err?.error?.message ?? 'Network Problem'}`;
return [];
});
if (error) {
throw error;
}
const models = Array.isArray(res) ? res : res?.data ?? null;
return models
? models
.map((model) => ({
id: model.id,
name: model.name ?? model.id,
external: true,
source: 'litellm'
}))
.sort((a, b) => {
return a.name.localeCompare(b.name);
})
: models;
};
export const getLiteLLMModelInfo = async (token: string = '') => {
let error = null;
const res = await fetch(`${LITELLM_API_BASE_URL}/model/info`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = `LiteLLM: ${err?.error?.message ?? 'Network Problem'}`;
return [];
});
if (error) {
throw error;
}
const models = Array.isArray(res) ? res : res?.data ?? null;
return models;
};
type AddLiteLLMModelForm = {
name: string;
model: string;
api_base: string;
api_key: string;
rpm: string;
};
export const addLiteLLMModel = async (token: string = '', payload: AddLiteLLMModelForm) => {
let error = null;
const res = await fetch(`${LITELLM_API_BASE_URL}/model/new`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
},
body: JSON.stringify({
model_name: payload.name,
litellm_params: {
model: payload.model,
...(payload.api_base === '' ? {} : { api_base: payload.api_base }),
...(payload.api_key === '' ? {} : { api_key: payload.api_key }),
...(isNaN(parseInt(payload.rpm)) ? {} : { rpm: parseInt(payload.rpm) })
}
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = `LiteLLM: ${err?.error?.message ?? 'Network Problem'}`;
return [];
});
if (error) {
throw error;
}
return res;
};
export const deleteLiteLLMModel = async (token: string = '', id: string) => {
let error = null;
const res = await fetch(`${LITELLM_API_BASE_URL}/model/delete`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
},
body: JSON.stringify({
id: id
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = `LiteLLM: ${err?.error?.message ?? 'Network Problem'}`;
return [];
});
if (error) {
throw error;
}
return res;
};
+5 -3
View File
@@ -128,9 +128,11 @@ export const getOllamaModels = async (token: string = '') => {
throw error; throw error;
} }
return (res?.models ?? []).sort((a, b) => { return (res?.models ?? [])
return a.name.localeCompare(b.name); .map((model) => ({ id: model.model, name: model.name ?? model.model, ...model }))
}); .sort((a, b) => {
return a.name.localeCompare(b.name);
});
}; };
// TODO: migrate to backend // TODO: migrate to backend
+8 -4
View File
@@ -163,7 +163,7 @@ export const getOpenAIModels = async (token: string = '') => {
return models return models
? models ? models
.map((model) => ({ name: model.id, external: true })) .map((model) => ({ id: model.id, name: model.name ?? model.id, external: true }))
.sort((a, b) => { .sort((a, b) => {
return a.name.localeCompare(b.name); return a.name.localeCompare(b.name);
}) })
@@ -200,17 +200,21 @@ export const getOpenAIModelsDirect = async (
const models = Array.isArray(res) ? res : res?.data ?? null; const models = Array.isArray(res) ? res : res?.data ?? null;
return models return models
.map((model) => ({ name: model.id, external: true })) .map((model) => ({ id: model.id, name: model.name ?? model.id, external: true }))
.filter((model) => (base_url.includes('openai') ? model.name.includes('gpt') : true)) .filter((model) => (base_url.includes('openai') ? model.name.includes('gpt') : true))
.sort((a, b) => { .sort((a, b) => {
return a.name.localeCompare(b.name); return a.name.localeCompare(b.name);
}); });
}; };
export const generateOpenAIChatCompletion = async (token: string = '', body: object) => { export const generateOpenAIChatCompletion = async (
token: string = '',
body: object,
url: string = OPENAI_API_BASE_URL
) => {
let error = null; let error = null;
const res = await fetch(`${OPENAI_API_BASE_URL}/chat/completions`, { const res = await fetch(`${url}/chat/completions`, {
method: 'POST', method: 'POST',
headers: { headers: {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
+115
View File
@@ -0,0 +1,115 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Confetti } from 'svelte-confetti';
import { WEBUI_NAME, config } from '$lib/stores';
import { WEBUI_VERSION } from '$lib/constants';
import { getChangelog } from '$lib/apis';
import Modal from './common/Modal.svelte';
export let show = false;
let changelog = null;
onMount(async () => {
const res = await getChangelog();
changelog = res;
});
</script>
<Modal bind:show>
<div class="px-5 py-4 dark:text-gray-300">
<div class="flex justify-between items-start">
<div class="text-xl font-bold">
What’s New in {$WEBUI_NAME}
<Confetti x={[-1, -0.25]} y={[0, 0.5]} />
</div>
<button
class="self-center"
on:click={() => {
show = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
<div class="flex items-center mt-1">
<div class="text-sm dark:text-gray-200">Release Notes</div>
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
<div class="text-sm dark:text-gray-200">
v{WEBUI_VERSION}
</div>
</div>
</div>
<hr class=" dark:border-gray-800" />
<div class=" w-full p-4 px-5">
<div class=" overflow-y-scroll max-h-80">
<div class="mb-3">
{#if changelog}
{#each Object.keys(changelog) as version}
<div class=" mb-3 pr-2">
<div class="font-bold text-xl mb-1 dark:text-white">
v{version} - {changelog[version].date}
</div>
<hr class=" dark:border-gray-800 my-2" />
{#each Object.keys(changelog[version]).filter((section) => section !== 'date') as section}
<div class="">
<div
class="font-bold uppercase text-xs {section === 'added'
? 'text-white bg-blue-600'
: section === 'fixed'
? 'text-white bg-green-600'
: section === 'changed'
? 'text-white bg-yellow-600'
: section === 'removed'
? 'text-white bg-red-600'
: ''} w-fit px-3 rounded-full my-2.5"
>
{section}
</div>
<div class="my-2.5 px-1.5">
{#each Object.keys(changelog[version][section]) as item}
<div class="text-sm mb-2">
<div class="font-semibold uppercase">
{changelog[version][section][item].title}
</div>
<div class="mb-2 mt-1">{changelog[version][section][item].content}</div>
</div>
{/each}
</div>
</div>
{/each}
</div>
{/each}
{/if}
</div>
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<button
on:click={() => {
localStorage.version = $config.version;
show = false;
}}
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
>
<span class="relative">Okay, Let's Go!</span>
</button>
</div>
</div>
</Modal>
+12 -14
View File
@@ -490,7 +490,7 @@
}} }}
/> />
<form <form
class=" flex flex-col relative w-full rounded-xl border dark:border-gray-700 bg-white dark:bg-gray-900 dark:text-gray-100" class=" flex flex-col relative w-full rounded-3xl px-1.5 border border-gray-100 dark:border-gray-850 bg-white dark:bg-gray-900 dark:text-gray-100"
on:submit|preventDefault={() => { on:submit|preventDefault={() => {
submitPrompt(prompt, user); submitPrompt(prompt, user);
}} }}
@@ -633,9 +633,9 @@
<div class=" flex"> <div class=" flex">
{#if fileUploadEnabled} {#if fileUploadEnabled}
<div class=" self-end mb-2 ml-1.5"> <div class=" self-end mb-2 ml-1">
<button <button
class=" text-gray-600 dark:text-gray-200 transition rounded-lg p-1 ml-1" class="bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
type="button" type="button"
on:click={() => { on:click={() => {
filesInputElement.click(); filesInputElement.click();
@@ -643,14 +643,12 @@
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" viewBox="0 0 16 16"
fill="currentColor" fill="currentColor"
class="w-5 h-5" class="w-[1.2rem] h-[1.2rem]"
> >
<path <path
fill-rule="evenodd" d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
d="M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z"
clip-rule="evenodd"
/> />
</svg> </svg>
</button> </button>
@@ -659,7 +657,7 @@
<textarea <textarea
id="chat-textarea" id="chat-textarea"
class=" dark:bg-gray-900 dark:text-gray-100 outline-none w-full py-3 px-2 {fileUploadEnabled class=" dark:bg-gray-900 dark:text-gray-100 outline-none w-full py-3 px-3 {fileUploadEnabled
? '' ? ''
: ' pl-4'} rounded-xl resize-none h-[48px]" : ' pl-4'} rounded-xl resize-none h-[48px]"
placeholder={chatInputPlaceholder !== '' placeholder={chatInputPlaceholder !== ''
@@ -803,12 +801,12 @@
}} }}
/> />
<div class="self-end mb-2 flex space-x-0.5 mr-2"> <div class="self-end mb-2 flex space-x-1 mr-1">
{#if messages.length == 0 || messages.at(-1).done == true} {#if messages.length == 0 || messages.at(-1).done == true}
{#if speechRecognitionEnabled} {#if speechRecognitionEnabled}
<button <button
id="voice-input-button" id="voice-input-button"
class=" text-gray-600 dark:text-gray-300 transition rounded-lg p-1.5 mr-0.5 self-center" class=" text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-850 transition rounded-full p-1.5 mr-0.5 self-center"
type="button" type="button"
on:click={() => { on:click={() => {
speechRecognitionHandler(); speechRecognitionHandler();
@@ -869,7 +867,7 @@
<button <button
class="{prompt !== '' class="{prompt !== ''
? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 ' ? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 '
: 'text-white bg-gray-100 dark:text-gray-900 dark:bg-gray-800 disabled'} transition rounded-lg p-1 mr-0.5 w-7 h-7 self-center" : 'text-white bg-gray-100 dark:text-gray-900 dark:bg-gray-800 disabled'} transition rounded-full p-1.5 self-center"
type="submit" type="submit"
disabled={prompt === ''} disabled={prompt === ''}
> >
@@ -877,7 +875,7 @@
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16" viewBox="0 0 16 16"
fill="currentColor" fill="currentColor"
class="w-4.5 h-4.5 mx-auto" class="w-5 h-5"
> >
<path <path
fill-rule="evenodd" fill-rule="evenodd"
@@ -888,7 +886,7 @@
</button> </button>
{:else} {:else}
<button <button
class="bg-white hover:bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800 transition rounded-lg p-1.5" class="bg-white hover:bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5"
on:click={stopResponse} on:click={stopResponse}
> >
<svg <svg
@@ -10,45 +10,47 @@
: suggestionPrompts.sort(() => Math.random() - 0.5).slice(0, 4); : suggestionPrompts.sort(() => Math.random() - 0.5).slice(0, 4);
</script> </script>
<div class=" flex flex-wrap-reverse mb-3 md:p-1 text-left w-full"> <div class=" mb-3 md:p-1 text-left w-full">
{#each prompts as prompt, promptIdx} <div class=" flex flex-wrap-reverse px-2 text-left">
<div {#each prompts as prompt, promptIdx}
class="{promptIdx > 1 ? 'hidden sm:inline-flex' : ''} basis-full sm:basis-1/2 p-[5px] px-2" <div
> class="{promptIdx > 1 ? 'hidden sm:inline-flex' : ''} basis-full sm:basis-1/2 p-[5px] px-1"
<button
class=" flex-1 flex justify-between w-full h-full px-4 py-2.5 bg-white hover:bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-700 outline outline-1 outline-gray-200 dark:outline-gray-800 rounded-lg transition group"
on:click={() => {
submitPrompt(prompt.content);
}}
> >
<div class="flex flex-col text-left self-center"> <button
{#if prompt.title && prompt.title[0] !== ''} class=" flex-1 flex justify-between w-full h-full px-4 py-2.5 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 rounded-2xl transition group"
<div class="text-sm font-medium dark:text-gray-300">{prompt.title[0]}</div> on:click={() => {
<div class="text-sm text-gray-500 line-clamp-1">{prompt.title[1]}</div> submitPrompt(prompt.content);
{:else} }}
<div class=" self-center text-sm font-medium dark:text-gray-300 line-clamp-2">
{prompt.content}
</div>
{/if}
</div>
<div
class="self-center p-1 rounded-lg text-white group-hover:bg-gray-100 group-hover:text-gray-800 dark:group-hover:bg-gray-800 dark:group-hover:text-gray-100 dark:text-gray-900 transition"
> >
<svg <div class="flex flex-col text-left self-center">
xmlns="http://www.w3.org/2000/svg" {#if prompt.title && prompt.title[0] !== ''}
viewBox="0 0 16 16" <div class="text-sm font-medium dark:text-gray-300">{prompt.title[0]}</div>
fill="currentColor" <div class="text-sm text-gray-500 line-clamp-1">{prompt.title[1]}</div>
class="w-4 h-4" {:else}
<div class=" self-center text-sm font-medium dark:text-gray-300 line-clamp-2">
{prompt.content}
</div>
{/if}
</div>
<div
class="self-center p-1 rounded-lg text-gray-50 group-hover:text-gray-800 dark:text-gray-850 dark:group-hover:text-gray-100 transition"
> >
<path <svg
fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg"
d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z" viewBox="0 0 16 16"
clip-rule="evenodd" fill="currentColor"
/> class="w-4 h-4"
</svg> >
</div> <path
</button> fill-rule="evenodd"
</div> d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z"
{/each} clip-rule="evenodd"
/>
</svg>
</div>
</button>
</div>
{/each}
</div>
</div> </div>
@@ -1,4 +1,6 @@
<script lang="ts"> <script lang="ts">
import { WEBUI_BASE_URL } from '$lib/constants';
import { user } from '$lib/stores';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
export let models = []; export let models = [];
@@ -27,14 +29,16 @@
> >
{#if model in modelfiles} {#if model in modelfiles}
<img <img
src={modelfiles[model]?.imageUrl ?? './favicon.png'} src={modelfiles[model]?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`}
alt="modelfile" alt="modelfile"
class=" w-14 rounded-full border-[1px] border-gray-200 dark:border-none" class=" w-14 rounded-full border-[1px] border-gray-200 dark:border-none"
draggable="false" draggable="false"
/> />
{:else} {:else}
<img <img
src={models.length === 1 ? '/favicon.png' : '/favicon.png'} src={models.length === 1
? `${WEBUI_BASE_URL}/static/favicon.png`
: `${WEBUI_BASE_URL}/static/favicon.png`}
class=" w-14 rounded-full border-[1px] border-gray-200 dark:border-none" class=" w-14 rounded-full border-[1px] border-gray-200 dark:border-none"
alt="logo" alt="logo"
draggable="false" draggable="false"
@@ -60,7 +64,9 @@
</div> </div>
{/if} {/if}
{:else} {:else}
How can I help you today? <div class=" line-clamp-1">Hello, {$user.name}</div>
<div>How can I help you today?</div>
{/if} {/if}
</div> </div>
</div> </div>
@@ -21,6 +21,7 @@
import Skeleton from './Skeleton.svelte'; import Skeleton from './Skeleton.svelte';
import CodeBlock from './CodeBlock.svelte'; import CodeBlock from './CodeBlock.svelte';
import Image from '$lib/components/common/Image.svelte'; import Image from '$lib/components/common/Image.svelte';
import { WEBUI_BASE_URL } from '$lib/constants';
export let modelfiles = []; export let modelfiles = [];
export let message; export let message;
@@ -298,7 +299,9 @@
{#key message.id} {#key message.id}
<div class=" flex w-full message-{message.id}"> <div class=" flex w-full message-{message.id}">
<ProfileImage src={modelfiles[message.model]?.imageUrl ?? '/favicon.png'} /> <ProfileImage
src={modelfiles[message.model]?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`}
/>
<div class="w-full overflow-hidden"> <div class="w-full overflow-hidden">
<Name> <Name>
+2 -2
View File
@@ -25,7 +25,7 @@
$: if (selectedModels.length > 0 && $models.length > 0) { $: if (selectedModels.length > 0 && $models.length > 0) {
selectedModels = selectedModels.map((model) => selectedModels = selectedModels.map((model) =>
$models.map((m) => m.name).includes(model) ? model : '' $models.map((m) => m.id).includes(model) ? model : ''
); );
} }
</script> </script>
@@ -45,7 +45,7 @@
{#if model.name === 'hr'} {#if model.name === 'hr'}
<hr /> <hr />
{:else} {:else}
<option value={model.name} class="text-gray-700 text-lg" <option value={model.id} class="text-gray-700 text-lg"
>{model.name + >{model.name +
`${model.size ? ` (${(model.size / 1024 ** 3).toFixed(1)}GB)` : ''}`}</option `${model.size ? ` (${(model.size / 1024 ** 3).toFixed(1)}GB)` : ''}`}</option
> >
+76 -13
View File
@@ -1,38 +1,101 @@
<script lang="ts"> <script lang="ts">
import { getVersionUpdates } from '$lib/apis';
import { getOllamaVersion } from '$lib/apis/ollama'; import { getOllamaVersion } from '$lib/apis/ollama';
import { WEBUI_NAME, WEB_UI_VERSION } from '$lib/constants'; import { WEBUI_VERSION } from '$lib/constants';
import { config } from '$lib/stores'; import { WEBUI_NAME, config, showChangelog } from '$lib/stores';
import { compareVersion } from '$lib/utils';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
let ollamaVersion = ''; let ollamaVersion = '';
let updateAvailable = null;
let version = {
current: '',
latest: ''
};
const checkForVersionUpdates = async () => {
updateAvailable = null;
version = await getVersionUpdates(localStorage.token).catch((error) => {
return {
current: WEBUI_VERSION,
latest: WEBUI_VERSION
};
});
console.log(version);
updateAvailable = compareVersion(version.latest, version.current);
console.log(updateAvailable);
};
onMount(async () => { onMount(async () => {
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => { ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => {
return ''; return '';
}); });
checkForVersionUpdates();
}); });
</script> </script>
<div class="flex flex-col h-full justify-between space-y-3 text-sm mb-6"> <div class="flex flex-col h-full justify-between space-y-3 text-sm mb-6">
<div class=" space-y-3"> <div class=" space-y-3">
<div> <div>
<div class=" mb-2.5 text-sm font-medium">{WEBUI_NAME} Version</div> <div class=" mb-2.5 text-sm font-medium flex space-x-2 items-center">
<div class="flex w-full"> <div>
<div class="flex-1 text-xs text-gray-700 dark:text-gray-200"> {$WEBUI_NAME} Version
v{WEB_UI_VERSION}
</div> </div>
</div> </div>
<div class="flex w-full justify-between items-center">
<div class="flex flex-col text-xs text-gray-700 dark:text-gray-200">
<div>
v{WEBUI_VERSION}
<a
href="https://github.com/open-webui/open-webui/releases/tag/v{version.latest}"
target="_blank"
>
{updateAvailable === null
? 'Checking for updates...'
: updateAvailable
? `(v${version.latest} available!)`
: '(latest)'}
</a>
</div>
<button
class=" underline flex items-center space-x-1 text-xs text-gray-500 dark:text-gray-500"
on:click={() => {
showChangelog.set(true);
}}
>
<div>See what's new</div>
</button>
</div>
<button
class=" text-xs px-3 py-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-lg font-medium"
on:click={() => {
checkForVersionUpdates();
}}
>
Check for updates
</button>
</div>
</div> </div>
<hr class=" dark:border-gray-700" /> {#if ollamaVersion}
<hr class=" dark:border-gray-700" />
<div> <div>
<div class=" mb-2.5 text-sm font-medium">Ollama Version</div> <div class=" mb-2.5 text-sm font-medium">Ollama Version</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 text-xs text-gray-700 dark:text-gray-200"> <div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
{ollamaVersion ?? 'N/A'} {ollamaVersion ?? 'N/A'}
</div>
</div> </div>
</div> </div>
</div> {/if}
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-700" />
@@ -3,7 +3,7 @@
import { createEventDispatcher, onMount } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
import { getOllamaAPIUrl, updateOllamaAPIUrl } from '$lib/apis/ollama'; import { getOllamaAPIUrl, getOllamaVersion, updateOllamaAPIUrl } from '$lib/apis/ollama';
import { getOpenAIKey, getOpenAIUrl, updateOpenAIKey, updateOpenAIUrl } from '$lib/apis/openai'; import { getOpenAIKey, getOpenAIUrl, updateOpenAIKey, updateOpenAIUrl } from '$lib/apis/openai';
import toast from 'svelte-french-toast'; import toast from 'svelte-french-toast';
@@ -15,6 +15,9 @@
let OPENAI_API_KEY = ''; let OPENAI_API_KEY = '';
let OPENAI_API_BASE_URL = ''; let OPENAI_API_BASE_URL = '';
let showOpenAI = false;
let showLiteLLM = false;
const updateOpenAIHandler = async () => { const updateOpenAIHandler = async () => {
OPENAI_API_BASE_URL = await updateOpenAIUrl(localStorage.token, OPENAI_API_BASE_URL); OPENAI_API_BASE_URL = await updateOpenAIUrl(localStorage.token, OPENAI_API_BASE_URL);
OPENAI_API_KEY = await updateOpenAIKey(localStorage.token, OPENAI_API_KEY); OPENAI_API_KEY = await updateOpenAIKey(localStorage.token, OPENAI_API_KEY);
@@ -24,11 +27,15 @@
const updateOllamaAPIUrlHandler = async () => { const updateOllamaAPIUrlHandler = async () => {
API_BASE_URL = await updateOllamaAPIUrl(localStorage.token, API_BASE_URL); API_BASE_URL = await updateOllamaAPIUrl(localStorage.token, API_BASE_URL);
const _models = await getModels('ollama');
if (_models.length > 0) { const ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => {
toast.error(error);
return null;
});
if (ollamaVersion) {
toast.success('Server connection verified'); toast.success('Server connection verified');
await models.set(_models); await models.set(await getModels());
} }
}; };
@@ -42,7 +49,7 @@
</script> </script>
<form <form
class="flex flex-col h-full space-y-3 text-sm" class="flex flex-col h-full justify-between text-sm"
on:submit|preventDefault={() => { on:submit|preventDefault={() => {
updateOpenAIHandler(); updateOpenAIHandler();
dispatch('save'); dispatch('save');
@@ -53,81 +60,100 @@
// }); // });
}} }}
> >
<div> <div class=" pr-1.5 overflow-y-scroll max-h-[20.5rem] space-y-3">
<div class=" mb-2.5 text-sm font-medium">Ollama API URL</div> <div class=" space-y-3">
<div class="flex w-full"> <div class="mt-2 space-y-2 pr-1.5">
<div class="flex-1 mr-2"> <div class="flex justify-between items-center text-sm">
<input <div class=" font-medium">OpenAI API</div>
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none" <button
placeholder="Enter URL (e.g. http://localhost:11434/api)" class=" text-xs font-medium text-gray-500"
bind:value={API_BASE_URL} type="button"
/> on:click={() => {
showOpenAI = !showOpenAI;
}}>{showOpenAI ? 'Hide' : 'Show'}</button
>
</div>
{#if showOpenAI}
<div>
<div class=" mb-2.5 text-sm font-medium">API Key</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
placeholder="Enter OpenAI API Key"
bind:value={OPENAI_API_KEY}
autocomplete="off"
/>
</div>
</div>
</div>
<div>
<div class=" mb-2.5 text-sm font-medium">API Base URL</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
placeholder="Enter OpenAI API Base URL"
bind:value={OPENAI_API_BASE_URL}
autocomplete="off"
/>
</div>
</div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
WebUI will make requests to <span class=" text-gray-200"
>'{OPENAI_API_BASE_URL}/chat'</span
>
</div>
</div>
{/if}
</div> </div>
<button </div>
class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded transition"
on:click={() => { <hr class=" dark:border-gray-700" />
updateOllamaAPIUrlHandler();
}} <div>
type="button" <div class=" mb-2.5 text-sm font-medium">Ollama API URL</div>
> <div class="flex w-full">
<svg <div class="flex-1 mr-2">
xmlns="http://www.w3.org/2000/svg" <input
viewBox="0 0 20 20" class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
fill="currentColor" placeholder="Enter URL (e.g. http://localhost:11434/api)"
class="w-4 h-4" bind:value={API_BASE_URL}
/>
</div>
<button
class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded transition"
on:click={() => {
updateOllamaAPIUrlHandler();
}}
type="button"
> >
<path <svg
fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg"
d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z" viewBox="0 0 20 20"
clip-rule="evenodd" fill="currentColor"
/> class="w-4 h-4"
</svg> >
</button> <path
</div> fill-rule="evenodd"
d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> clip-rule="evenodd"
Trouble accessing Ollama? />
<a </svg>
class=" text-gray-300 font-medium" </button>
href="https://github.com/open-webui/open-webui#troubleshooting"
target="_blank"
>
Click here for help.
</a>
</div>
</div>
<hr class=" dark:border-gray-700" />
<div class=" space-y-3">
<div>
<div class=" mb-2.5 text-sm font-medium">OpenAI API Key</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
placeholder="Enter OpenAI API Key"
bind:value={OPENAI_API_KEY}
autocomplete="off"
/>
</div>
</div> </div>
</div>
<div>
<div class=" mb-2.5 text-sm font-medium">OpenAI API Base URL</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
placeholder="Enter OpenAI API Base URL"
bind:value={OPENAI_API_BASE_URL}
autocomplete="off"
/>
</div>
</div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
WebUI will make requests to <span class=" text-gray-200">'{OPENAI_API_BASE_URL}/chat'</span> Trouble accessing Ollama?
<a
class=" text-gray-300 font-medium"
href="https://github.com/open-webui/open-webui#troubleshooting"
target="_blank"
>
Click here for help.
</a>
</div> </div>
</div> </div>
</div> </div>
@@ -84,7 +84,7 @@
</script> </script>
<div class="flex flex-col h-full justify-between text-sm"> <div class="flex flex-col h-full justify-between text-sm">
<div class=" pr-1.5 overflow-y-scroll max-h-[21rem]"> <div class=" pr-1.5 overflow-y-scroll max-h-[20.5rem]">
<div class=""> <div class="">
<div class=" mb-1 text-sm font-medium">WebUI Settings</div> <div class=" mb-1 text-sm font-medium">WebUI Settings</div>
+42 -19
View File
@@ -12,7 +12,9 @@
toggleImageGenerationEnabledStatus, toggleImageGenerationEnabledStatus,
updateAUTOMATIC1111Url, updateAUTOMATIC1111Url,
updateDefaultDiffusionModel, updateDefaultDiffusionModel,
updateImageSize updateImageSize,
getImageSteps,
updateImageSteps
} from '$lib/apis/images'; } from '$lib/apis/images';
import { getBackendConfig } from '$lib/apis'; import { getBackendConfig } from '$lib/apis';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@@ -21,20 +23,23 @@
let loading = false; let loading = false;
let enableImageGeneration = true; let enableImageGeneration = false;
let AUTOMATIC1111_BASE_URL = ''; let AUTOMATIC1111_BASE_URL = '';
let selectedModel = ''; let selectedModel = '';
let models = []; let models = null;
let imageSize = ''; let imageSize = '';
let steps = 50;
const getModels = async () => { const getModels = async () => {
models = await getDiffusionModels(localStorage.token).catch((error) => { models = await getDiffusionModels(localStorage.token).catch((error) => {
toast.error(error); toast.error(error);
return null; return null;
}); });
selectedModel = await getDefaultDiffusionModel(localStorage.token); selectedModel = await getDefaultDiffusionModel(localStorage.token).catch((error) => {
return '';
});
}; };
const updateAUTOMATIC1111UrlHandler = async () => { const updateAUTOMATIC1111UrlHandler = async () => {
@@ -83,6 +88,7 @@
if (enableImageGeneration && AUTOMATIC1111_BASE_URL) { if (enableImageGeneration && AUTOMATIC1111_BASE_URL) {
imageSize = await getImageSize(localStorage.token); imageSize = await getImageSize(localStorage.token);
steps = await getImageSteps(localStorage.token);
getModels(); getModels();
} }
} }
@@ -98,12 +104,16 @@
toast.error(error); toast.error(error);
return null; return null;
}); });
await updateImageSteps(localStorage.token, steps).catch((error) => {
toast.error(error);
return null;
});
dispatch('save'); dispatch('save');
loading = false; loading = false;
}} }}
> >
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[21rem]"> <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[20.5rem]">
<div> <div>
<div class=" mb-1 text-sm font-medium">Image Settings</div> <div class=" mb-1 text-sm font-medium">Image Settings</div>
@@ -176,6 +186,28 @@
{#if enableImageGeneration} {#if enableImageGeneration}
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-700" />
<div>
<div class=" mb-2.5 text-sm font-medium">Set Default Model</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<select
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
bind:value={selectedModel}
placeholder="Select a model"
>
{#if !selectedModel}
<option value="" disabled selected>Select a model</option>
{/if}
{#each models ?? [] as model}
<option value={model.title} class="bg-gray-100 dark:bg-gray-700"
>{model.model_name}</option
>
{/each}
</select>
</div>
</div>
</div>
<div> <div>
<div class=" mb-2.5 text-sm font-medium">Set Image Size</div> <div class=" mb-2.5 text-sm font-medium">Set Image Size</div>
<div class="flex w-full"> <div class="flex w-full">
@@ -190,23 +222,14 @@
</div> </div>
<div> <div>
<div class=" mb-2.5 text-sm font-medium">Set default model</div> <div class=" mb-2.5 text-sm font-medium">Set Steps</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<select <input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none" class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
bind:value={selectedModel} placeholder="Enter Number of Steps (e.g. 50)"
placeholder="Select a model" bind:value={steps}
> />
{#if !selectedModel}
<option value="" disabled selected>Select a model</option>
{/if}
{#each models as model}
<option value={model.title} class="bg-gray-100 dark:bg-gray-700"
>{model.model_name}</option
>
{/each}
</select>
</div> </div>
</div> </div>
</div> </div>
+615 -284
View File
@@ -2,14 +2,33 @@
import queue from 'async/queue'; import queue from 'async/queue';
import toast from 'svelte-french-toast'; import toast from 'svelte-french-toast';
import { createModel, deleteModel, pullModel } from '$lib/apis/ollama'; import { createModel, deleteModel, getOllamaVersion, pullModel } from '$lib/apis/ollama';
import { WEBUI_API_BASE_URL, WEBUI_NAME } from '$lib/constants'; import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
import { models, user } from '$lib/stores'; import { WEBUI_NAME, models, user } from '$lib/stores';
import { splitStream } from '$lib/utils'; import { splitStream } from '$lib/utils';
import { onMount } from 'svelte';
import { addLiteLLMModel, deleteLiteLLMModel, getLiteLLMModelInfo } from '$lib/apis/litellm';
export let getModels: Function; export let getModels: Function;
let showLiteLLM = false;
let showLiteLLMParams = false;
let liteLLMModelInfo = [];
let liteLLMModel = '';
let liteLLMModelName = '';
let liteLLMAPIBase = '';
let liteLLMAPIKey = '';
let liteLLMRPM = '';
let deleteLiteLLMModelId = '';
$: liteLLMModelName = liteLLMModel;
// Models // Models
let showExperimentalOllama = false;
let ollamaVersion = '';
const MAX_PARALLEL_DOWNLOADS = 3; const MAX_PARALLEL_DOWNLOADS = 3;
const modelDownloadQueue = queue( const modelDownloadQueue = queue(
(task: { modelName: string }, cb) => (task: { modelName: string }, cb) =>
@@ -59,9 +78,9 @@
} else { } else {
toast.success(`Model '${modelName}' has been successfully downloaded.`); toast.success(`Model '${modelName}' has been successfully downloaded.`);
const notification = new Notification(WEBUI_NAME, { const notification = new Notification($WEBUI_NAME, {
body: `Model '${modelName}' has been successfully downloaded.`, body: `Model '${modelName}' has been successfully downloaded.`,
icon: '/favicon.png' icon: `${WEBUI_BASE_URL}/static/favicon.png`
}); });
models.set(await getModels()); models.set(await getModels());
@@ -286,256 +305,184 @@
opts.callback({ success: true, modelName: opts.modelName }); opts.callback({ success: true, modelName: opts.modelName });
} }
}; };
const addLiteLLMModelHandler = async () => {
if (!liteLLMModelInfo.find((info) => info.model_name === liteLLMModelName)) {
const res = await addLiteLLMModel(localStorage.token, {
name: liteLLMModelName,
model: liteLLMModel,
api_base: liteLLMAPIBase,
api_key: liteLLMAPIKey,
rpm: liteLLMRPM
}).catch((error) => {
toast.error(error);
return null;
});
if (res) {
if (res.message) {
toast.success(res.message);
}
}
} else {
toast.error(`Model ${liteLLMModelName} already exists.`);
}
liteLLMModelName = '';
liteLLMModel = '';
liteLLMAPIBase = '';
liteLLMAPIKey = '';
liteLLMRPM = '';
liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
models.set(await getModels());
};
const deleteLiteLLMModelHandler = async () => {
const res = await deleteLiteLLMModel(localStorage.token, deleteLiteLLMModelId).catch(
(error) => {
toast.error(error);
return null;
}
);
if (res) {
if (res.message) {
toast.success(res.message);
}
}
deleteLiteLLMModelId = '';
liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
models.set(await getModels());
};
onMount(async () => {
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
});
</script> </script>
<div class="flex flex-col h-full justify-between text-sm"> <div class="flex flex-col h-full justify-between text-sm">
<div class=" space-y-3 pr-1.5 overflow-y-scroll h-80"> <div class=" space-y-3 pr-1.5 overflow-y-scroll h-[23rem]">
<div> {#if ollamaVersion}
<div class=" mb-2.5 text-sm font-medium">Pull a model from Ollama.com</div> <div class="space-y-2 pr-1.5">
<div class="flex w-full"> <div>
<div class="flex-1 mr-2"> <div class=" mb-2 text-sm font-medium">Manage Ollama Models</div>
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none" <div class=" mb-2 text-sm font-medium">Pull a model from Ollama.com</div>
placeholder="Enter model tag (e.g. mistral:7b)" <div class="flex w-full">
bind:value={modelTag} <div class="flex-1 mr-2">
/> <input
</div> class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
<button placeholder="Enter model tag (e.g. mistral:7b)"
class="px-3 text-gray-100 bg-emerald-600 hover:bg-emerald-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded transition" bind:value={modelTag}
on:click={() => { />
pullModelHandler();
}}
disabled={modelTransferring}
>
{#if modelTransferring}
<div class="self-center">
<svg
class=" w-4 h-4"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
</div> </div>
{:else} <button
<svg class="px-3 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded transition"
xmlns="http://www.w3.org/2000/svg" on:click={() => {
viewBox="0 0 16 16" pullModelHandler();
fill="currentColor" }}
class="w-4 h-4" disabled={modelTransferring}
> >
<path {#if modelTransferring}
d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z" <div class="self-center">
/> <svg
<path class=" w-4 h-4"
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z" viewBox="0 0 24 24"
/> fill="currentColor"
</svg> xmlns="http://www.w3.org/2000/svg"
{/if} ><style>
</button> .spinner_ajPY {
</div> transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
To access the available model names for downloading, <a
class=" text-gray-500 dark:text-gray-300 font-medium"
href="https://ollama.com/library"
target="_blank">click here.</a
>
</div>
{#if Object.keys(modelDownloadStatus).length > 0}
{#each Object.keys(modelDownloadStatus) as model}
<div class="flex flex-col">
<div class="font-medium mb-1">{model}</div>
<div class="">
<div
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
style="width: {Math.max(15, modelDownloadStatus[model].pullProgress ?? 0)}%"
>
{modelDownloadStatus[model].pullProgress ?? 0}%
</div>
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
{modelDownloadStatus[model].digest}
</div>
</div>
</div>
{/each}
{/if}
</div>
<hr class=" dark:border-gray-700" />
<div>
<div class=" mb-2.5 text-sm font-medium">Delete a model</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<select
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
bind:value={deleteModelTag}
placeholder="Select a model"
>
{#if !deleteModelTag}
<option value="" disabled selected>Select a model</option>
{/if}
{#each $models.filter((m) => m.size != null) as model}
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
>{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option
>
{/each}
</select>
</div>
<button
class="px-3 bg-red-700 hover:bg-red-800 text-gray-100 rounded transition"
on:click={() => {
deleteModelHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
</div>
<hr class=" dark:border-gray-700" />
<form
on:submit|preventDefault={() => {
uploadModelHandler();
}}
>
<div class=" mb-2 flex w-full justify-between">
<div class=" text-sm font-medium">
Upload a GGUF model <a
class=" text-xs font-medium text-gray-500 underline"
href="https://github.com/jmorganca/ollama/blob/main/README.md#import-from-gguf"
target="_blank">(Experimental)</a
>
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
if (modelUploadMode === 'file') {
modelUploadMode = 'url';
} else {
modelUploadMode = 'file';
}
}}
type="button"
>
{#if modelUploadMode === 'file'}
<span class="ml-2 self-center">File Mode</span>
{:else}
<span class="ml-2 self-center">URL Mode</span>
{/if}
</button>
</div>
<div class="flex w-full mb-1.5">
<div class="flex flex-col w-full">
{#if modelUploadMode === 'file'}
<div class="flex-1 {modelInputFile && modelInputFile.length > 0 ? 'mr-2' : ''}">
<input
id="model-upload-input"
type="file"
bind:files={modelInputFile}
on:change={() => {
console.log(modelInputFile);
}}
accept=".gguf"
required
hidden
/>
<button
type="button"
class="w-full rounded text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-800"
on:click={() => {
document.getElementById('model-upload-input').click();
}}
>
{#if modelInputFile && modelInputFile.length > 0}
{modelInputFile[0].name}
{:else}
Click here to select
{/if}
</button>
</div>
{:else}
<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
<input
class="w-full rounded text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-800 outline-none {modelFileUrl !==
''
? 'mr-2'
: ''}"
type="url"
required
bind:value={modelFileUrl}
placeholder="Type HuggingFace Resolve (Download) URL"
/>
</div>
{/if}
</div>
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
<button
class="px-3 text-gray-100 bg-emerald-600 hover:bg-emerald-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded transition"
type="submit"
disabled={modelTransferring}
>
{#if modelTransferring}
<div class="self-center">
<svg
class=" w-4 h-4"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
} }
} @keyframes spinner_AtaB {
</style><path 100% {
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" transform: rotate(360deg);
opacity=".25" }
/><path }
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" </style><path
class="spinner_ajPY" d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
/></svg opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
</div>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
> >
<path
d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
/>
<path
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
/>
</svg>
{/if}
</button>
</div>
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
To access the available model names for downloading, <a
class=" text-gray-500 dark:text-gray-300 font-medium"
href="https://ollama.com/library"
target="_blank">click here.</a
>
</div>
{#if Object.keys(modelDownloadStatus).length > 0}
{#each Object.keys(modelDownloadStatus) as model}
<div class="flex flex-col">
<div class="font-medium mb-1">{model}</div>
<div class="">
<div
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
style="width: {Math.max(15, modelDownloadStatus[model].pullProgress ?? 0)}%"
>
{modelDownloadStatus[model].pullProgress ?? 0}%
</div>
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
{modelDownloadStatus[model].digest}
</div>
</div>
</div> </div>
{:else} {/each}
{/if}
</div>
<div>
<div class=" mb-2 text-sm font-medium">Delete a model</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<select
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={deleteModelTag}
placeholder="Select a model"
>
{#if !deleteModelTag}
<option value="" disabled selected>Select a model</option>
{/if}
{#each $models.filter((m) => m.size != null) as model}
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
>{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option
>
{/each}
</select>
</div>
<button
class="px-3 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded transition"
on:click={() => {
deleteModelHandler();
}}
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16" viewBox="0 0 16 16"
@@ -543,54 +490,438 @@
class="w-4 h-4" class="w-4 h-4"
> >
<path <path
d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z" fill-rule="evenodd"
/> d="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z"
<path clip-rule="evenodd"
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
/> />
</svg> </svg>
</button>
</div>
</div>
<div>
<div class="flex justify-between items-center text-xs">
<div class=" text-sm font-medium">Experimental</div>
<button
class=" text-xs font-medium text-gray-500"
type="button"
on:click={() => {
showExperimentalOllama = !showExperimentalOllama;
}}>{showExperimentalOllama ? 'Show' : 'Hide'}</button
>
</div>
</div>
{#if showExperimentalOllama}
<form
on:submit|preventDefault={() => {
uploadModelHandler();
}}
>
<div class=" mb-2 flex w-full justify-between">
<div class=" text-sm font-medium">Upload a GGUF model</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
if (modelUploadMode === 'file') {
modelUploadMode = 'url';
} else {
modelUploadMode = 'file';
}
}}
type="button"
>
{#if modelUploadMode === 'file'}
<span class="ml-2 self-center">File Mode</span>
{:else}
<span class="ml-2 self-center">URL Mode</span>
{/if}
</button>
</div>
<div class="flex w-full mb-1.5">
<div class="flex flex-col w-full">
{#if modelUploadMode === 'file'}
<div class="flex-1 {modelInputFile && modelInputFile.length > 0 ? 'mr-2' : ''}">
<input
id="model-upload-input"
type="file"
bind:files={modelInputFile}
on:change={() => {
console.log(modelInputFile);
}}
accept=".gguf"
required
hidden
/>
<button
type="button"
class="w-full rounded text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-850"
on:click={() => {
document.getElementById('model-upload-input').click();
}}
>
{#if modelInputFile && modelInputFile.length > 0}
{modelInputFile[0].name}
{:else}
Click here to select
{/if}
</button>
</div>
{:else}
<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
<input
class="w-full rounded text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
''
? 'mr-2'
: ''}"
type="url"
required
bind:value={modelFileUrl}
placeholder="Type HuggingFace Resolve (Download) URL"
/>
</div>
{/if}
</div>
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
<button
class="px-3 text-gray-100 bg-emerald-600 hover:bg-emerald-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded transition"
type="submit"
disabled={modelTransferring}
>
{#if modelTransferring}
<div class="self-center">
<svg
class=" w-4 h-4"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
</div>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z"
/>
<path
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
/>
</svg>
{/if}
</button>
{/if}
</div>
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
<div>
<div>
<div class=" my-2.5 text-sm font-medium">Modelfile Content</div>
<textarea
bind:value={modelFileContent}
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
rows="6"
/>
</div>
</div>
{/if} {/if}
</button> <div class=" mt-1 text-xs text-gray-400 dark:text-gray-500">
To access the GGUF models available for downloading, <a
class=" text-gray-500 dark:text-gray-300 font-medium"
href="https://huggingface.co/models?search=gguf"
target="_blank">click here.</a
>
</div>
{#if uploadProgress !== null}
<div class="mt-2">
<div class=" mb-2 text-xs">Upload Progress</div>
<div class="w-full rounded-full dark:bg-gray-800">
<div
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
style="width: {Math.max(15, uploadProgress ?? 0)}%"
>
{uploadProgress ?? 0}%
</div>
</div>
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
{modelFileDigest}
</div>
</div>
{/if}
</form>
{/if} {/if}
</div> </div>
<hr class=" dark:border-gray-700 my-2" />
{/if}
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')} <div class=" space-y-3">
<div class="mt-2 space-y-3 pr-1.5">
<div> <div>
<div class=" mb-2 text-sm font-medium">Manage LiteLLM Models</div>
<div> <div>
<div class=" my-2.5 text-sm font-medium">Modelfile Content</div> <div class="flex justify-between items-center text-xs">
<textarea <div class=" text-sm font-medium">Add a model</div>
bind:value={modelFileContent} <button
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none" class=" text-xs font-medium text-gray-500"
rows="6" type="button"
/> on:click={() => {
</div> showLiteLLMParams = !showLiteLLMParams;
</div> }}>{showLiteLLMParams ? 'Advanced' : 'Default'}</button
{/if} >
<div class=" mt-1 text-xs text-gray-400 dark:text-gray-500">
To access the GGUF models available for downloading, <a
class=" text-gray-500 dark:text-gray-300 font-medium"
href="https://huggingface.co/models?search=gguf"
target="_blank">click here.</a
>
</div>
{#if uploadProgress !== null}
<div class="mt-2">
<div class=" mb-2 text-xs">Upload Progress</div>
<div class="w-full rounded-full dark:bg-gray-800">
<div
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
style="width: {Math.max(15, uploadProgress ?? 0)}%"
>
{uploadProgress ?? 0}%
</div> </div>
</div> </div>
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
{modelFileDigest} <div class="my-2 space-y-2">
<div class="flex w-full mb-1.5">
<div class="flex-1 mr-2">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder="Enter LiteLLM Model (litellm_params.model)"
bind:value={liteLLMModel}
autocomplete="off"
/>
</div>
<button
class="px-3 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded transition"
on:click={() => {
addLiteLLMModelHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
/>
</svg>
</button>
</div>
{#if showLiteLLMParams}
<div>
<div class=" mb-1.5 text-sm font-medium">Model Name</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder="Enter Model Name (model_name)"
bind:value={liteLLMModelName}
autocomplete="off"
/>
</div>
</div>
</div>
<div>
<div class=" mb-1.5 text-sm font-medium">API Base URL</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder="Enter LiteLLM API Base URL (litellm_params.api_base)"
bind:value={liteLLMAPIBase}
autocomplete="off"
/>
</div>
</div>
</div>
<div>
<div class=" mb-1.5 text-sm font-medium">API Key</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder="Enter LiteLLM API Key (litellm_params.api_key)"
bind:value={liteLLMAPIKey}
autocomplete="off"
/>
</div>
</div>
</div>
<div>
<div class="mb-1.5 text-sm font-medium">API RPM</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder="Enter LiteLLM API RPM (litellm_params.rpm)"
bind:value={liteLLMRPM}
autocomplete="off"
/>
</div>
</div>
</div>
{/if}
</div>
<div class="mb-2 text-xs text-gray-400 dark:text-gray-500">
Not sure what to add?
<a
class=" text-gray-300 font-medium"
href="https://litellm.vercel.app/docs/proxy/configs#quick-start"
target="_blank"
>
Click here for help.
</a>
</div>
<div>
<div class=" mb-2.5 text-sm font-medium">Delete a model</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<select
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={deleteLiteLLMModelId}
placeholder="Select a model"
>
{#if !deleteLiteLLMModelId}
<option value="" disabled selected>Select a model</option>
{/if}
{#each liteLLMModelInfo as model}
<option value={model.model_info.id} class="bg-gray-100 dark:bg-gray-700"
>{model.model_name}</option
>
{/each}
</select>
</div>
<button
class="px-3 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded transition"
on:click={() => {
deleteLiteLLMModelHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
</div> </div>
</div> </div>
{/if} </div>
</form>
<!-- <div class="mt-2 space-y-3 pr-1.5">
<div>
<div class=" mb-2.5 text-sm font-medium">Add LiteLLM Model</div>
<div class="flex w-full mb-2">
<div class="flex-1">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
placeholder="Enter LiteLLM Model (e.g. ollama/mistral)"
bind:value={liteLLMModel}
autocomplete="off"
/>
</div>
</div>
<div class="flex justify-between items-center text-sm">
<div class=" font-medium">Advanced Model Params</div>
<button
class=" text-xs font-medium text-gray-500"
type="button"
on:click={() => {
showLiteLLMParams = !showLiteLLMParams;
}}>{showLiteLLMParams ? 'Hide' : 'Show'}</button
>
</div>
{#if showLiteLLMParams}
<div>
<div class=" mb-2.5 text-sm font-medium">LiteLLM API Key</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
placeholder="Enter LiteLLM API Key (e.g. os.environ/AZURE_API_KEY_CA)"
bind:value={liteLLMAPIKey}
autocomplete="off"
/>
</div>
</div>
</div>
<div>
<div class=" mb-2.5 text-sm font-medium">LiteLLM API Base URL</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
placeholder="Enter LiteLLM API Base URL"
bind:value={liteLLMAPIBase}
autocomplete="off"
/>
</div>
</div>
</div>
<div>
<div class=" mb-2.5 text-sm font-medium">LiteLLM API RPM</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
placeholder="Enter LiteLLM API RPM"
bind:value={liteLLMRPM}
autocomplete="off"
/>
</div>
</div>
</div>
{/if}
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
Not sure what to add?
<a
class=" text-gray-300 font-medium"
href="https://litellm.vercel.app/docs/proxy/configs#quick-start"
target="_blank"
>
Click here for help.
</a>
</div>
</div>
</div> -->
</div>
</div> </div>
</div> </div>
+21 -14
View File
@@ -4,6 +4,7 @@
import { getOllamaModels } from '$lib/apis/ollama'; import { getOllamaModels } from '$lib/apis/ollama';
import { getOpenAIModels } from '$lib/apis/openai'; import { getOpenAIModels } from '$lib/apis/openai';
import { getLiteLLMModels } from '$lib/apis/litellm';
import Modal from '../common/Modal.svelte'; import Modal from '../common/Modal.svelte';
import Account from './Settings/Account.svelte'; import Account from './Settings/Account.svelte';
@@ -27,23 +28,29 @@
let selectedTab = 'general'; let selectedTab = 'general';
const getModels = async (type = 'all') => { const getModels = async () => {
const models = []; let models = await Promise.all([
models.push( await getOllamaModels(localStorage.token).catch((error) => {
...(await getOllamaModels(localStorage.token).catch((error) => {
toast.error(error);
return [];
}))
);
if (type === 'all') {
const openAIModels = await getOpenAIModels(localStorage.token).catch((error) => {
console.log(error); console.log(error);
return null; return null;
}); }),
models.push(...(openAIModels ? [{ name: 'hr' }, ...openAIModels] : [])); await getOpenAIModels(localStorage.token).catch((error) => {
} console.log(error);
return null;
}),
await getLiteLLMModels(localStorage.token).catch((error) => {
console.log(error);
return null;
})
]);
models = models
.filter((models) => models)
.reduce((a, e, i, arr) => a.concat(e, ...(i < arr.length - 1 ? [{ name: 'hr' }] : [])), []);
// models.push(...(ollamaModels ? [{ name: 'hr' }, ...ollamaModels] : []));
// models.push(...(openAIModels ? [{ name: 'hr' }, ...openAIModels] : []));
// models.push(...(liteLLMModels ? [{ name: 'hr' }, ...liteLLMModels] : []));
return models; return models;
}; };
</script> </script>
+22 -4
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { fade, blur } from 'svelte/transition'; import { fade } from 'svelte/transition';
export let show = true; export let show = true;
export let size = 'md'; export let size = 'md';
@@ -34,16 +34,17 @@
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div <div
class="fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center z-50 overflow-hidden overscroll-contain" class=" fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center z-50 overflow-hidden overscroll-contain"
in:fade={{ duration: 10 }}
on:click={() => { on:click={() => {
show = false; show = false;
}} }}
> >
<div <div
class="m-auto rounded-xl max-w-full {sizeToWidth( class=" modal-content m-auto rounded-xl max-w-full {sizeToWidth(
size size
)} mx-2 bg-gray-50 dark:bg-gray-900 shadow-3xl" )} mx-2 bg-gray-50 dark:bg-gray-900 shadow-3xl"
transition:fade={{ delay: 100, duration: 200 }} in:fade={{ duration: 10 }}
on:click={(e) => { on:click={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
@@ -52,3 +53,20 @@
</div> </div>
</div> </div>
{/if} {/if}
<style>
.modal-content {
animation: scaleUp 0.1s ease-out forwards;
}
@keyframes scaleUp {
from {
transform: scale(0.985);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
</style>
+3 -4
View File
@@ -4,14 +4,13 @@
const { saveAs } = fileSaver; const { saveAs } = fileSaver;
import { getChatById } from '$lib/apis/chats'; import { getChatById } from '$lib/apis/chats';
import { chatId, modelfiles, settings } from '$lib/stores'; import { WEBUI_NAME, chatId, modelfiles, settings } from '$lib/stores';
import ShareChatModal from '../chat/ShareChatModal.svelte'; import ShareChatModal from '../chat/ShareChatModal.svelte';
import TagInput from '../common/Tags/TagInput.svelte'; import TagInput from '../common/Tags/TagInput.svelte';
import Tags from '../common/Tags.svelte'; import Tags from '../common/Tags.svelte';
import { WEBUI_NAME } from '$lib/constants';
export let initNewChat: Function; export let initNewChat: Function;
export let title: string = WEBUI_NAME; export let title: string = $WEBUI_NAME;
export let shareEnabled: boolean = false; export let shareEnabled: boolean = false;
export let tags = []; export let tags = [];
@@ -102,7 +101,7 @@
</div> </div>
<div class=" flex-1 self-center font-medium line-clamp-1"> <div class=" flex-1 self-center font-medium line-clamp-1">
<div> <div>
{title != '' ? title : WEBUI_NAME} {title != '' ? title : $WEBUI_NAME}
</div> </div>
</div> </div>
+19 -22
View File
@@ -13,9 +13,12 @@
getChatList, getChatList,
getChatById, getChatById,
getChatListByTagName, getChatListByTagName,
updateChatById updateChatById,
getAllChatTags
} from '$lib/apis/chats'; } from '$lib/apis/chats';
import toast from 'svelte-french-toast'; import toast from 'svelte-french-toast';
import { slide } from 'svelte/transition';
import { WEBUI_BASE_URL } from '$lib/constants';
let show = false; let show = false;
let navElement; let navElement;
@@ -113,7 +116,11 @@
> >
<div class="flex self-center"> <div class="flex self-center">
<div class="self-center mr-1.5"> <div class="self-center mr-1.5">
<img src="/favicon.png" class=" w-7 -translate-x-1.5 rounded-full" alt="logo" /> <img
src="{WEBUI_BASE_URL}/static/favicon.png"
class=" w-7 -translate-x-1.5 rounded-full"
alt="logo"
/>
</div> </div>
<div class=" self-center font-medium text-sm">New Chat</div> <div class=" self-center font-medium text-sm">New Chat</div>
@@ -324,7 +331,12 @@
<button <button
class="px-2.5 text-xs font-medium bg-gray-900 hover:bg-gray-800 transition rounded-full" class="px-2.5 text-xs font-medium bg-gray-900 hover:bg-gray-800 transition rounded-full"
on:click={async () => { on:click={async () => {
await chats.set(await getChatListByTagName(localStorage.token, tag.name)); let chatIds = await getChatListByTagName(localStorage.token, tag.name);
if (chatIds.length === 0) {
await tags.set(await getAllChatTags(localStorage.token));
chatIds = await getChatList(localStorage.token);
}
await chats.set(chatIds);
}} }}
> >
{tag.name} {tag.name}
@@ -361,27 +373,11 @@
: ''} transition whitespace-nowrap text-ellipsis" : ''} transition whitespace-nowrap text-ellipsis"
href="/c/{chat.id}" href="/c/{chat.id}"
> >
<div class=" flex self-center flex-1"> <div class=" flex self-center flex-1 w-full">
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.25 12.76c0 1.6 1.123 2.994 2.707 3.227 1.087.16 2.185.283 3.293.369V21l4.076-4.076a1.526 1.526 0 011.037-.443 48.282 48.282 0 005.68-.494c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z"
/>
</svg>
</div>
<div <div
class=" text-left self-center overflow-hidden {chat.id === $chatId class=" text-left self-center overflow-hidden {chat.id === $chatId
? 'w-[120px]' ? 'w-[160px]'
: 'w-[180px]'} " : 'w-full'} "
> >
{#if chatTitleEditId === chat.id} {#if chatTitleEditId === chat.id}
<input bind:value={chatTitle} class=" bg-transparent w-full" /> <input bind:value={chatTitle} class=" bg-transparent w-full" />
@@ -562,6 +558,7 @@
<div <div
id="dropdownDots" id="dropdownDots"
class="absolute z-40 bottom-[70px] 4.5rem rounded-lg shadow w-[240px] bg-gray-900" class="absolute z-40 bottom-[70px] 4.5rem rounded-lg shadow w-[240px] bg-gray-900"
in:slide={{ duration: 150 }}
> >
<div class="py-2 w-full"> <div class="py-2 w-full">
{#if $user.role === 'admin'} {#if $user.role === 'admin'}
+4 -3
View File
@@ -1,18 +1,19 @@
import { dev } from '$app/environment'; import { dev } from '$app/environment';
// import { version } from '../../package.json'; // import { version } from '../../package.json';
export const WEBUI_NAME = 'Open WebUI'; export const APP_NAME = 'Open WebUI';
export const WEBUI_BASE_URL = dev ? `http://${location.hostname}:8080` : ``; export const WEBUI_BASE_URL = dev ? `http://${location.hostname}:8080` : ``;
export const WEBUI_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1`; export const WEBUI_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1`;
export const LITELLM_API_BASE_URL = `${WEBUI_BASE_URL}/litellm/api`;
export const OLLAMA_API_BASE_URL = `${WEBUI_BASE_URL}/ollama/api`; export const OLLAMA_API_BASE_URL = `${WEBUI_BASE_URL}/ollama/api`;
export const OPENAI_API_BASE_URL = `${WEBUI_BASE_URL}/openai/api`; export const OPENAI_API_BASE_URL = `${WEBUI_BASE_URL}/openai/api`;
export const AUDIO_API_BASE_URL = `${WEBUI_BASE_URL}/audio/api/v1`; export const AUDIO_API_BASE_URL = `${WEBUI_BASE_URL}/audio/api/v1`;
export const IMAGES_API_BASE_URL = `${WEBUI_BASE_URL}/images/api/v1`; export const IMAGES_API_BASE_URL = `${WEBUI_BASE_URL}/images/api/v1`;
export const RAG_API_BASE_URL = `${WEBUI_BASE_URL}/rag/api/v1`; export const RAG_API_BASE_URL = `${WEBUI_BASE_URL}/rag/api/v1`;
export const WEB_UI_VERSION = APP_VERSION; export const WEBUI_VERSION = APP_VERSION;
export const REQUIRED_OLLAMA_VERSION = '0.1.16'; export const REQUIRED_OLLAMA_VERSION = '0.1.16';
export const SUPPORTED_FILE_TYPE = [ export const SUPPORTED_FILE_TYPE = [
+3
View File
@@ -1,6 +1,8 @@
import { APP_NAME } from '$lib/constants';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
// Backend // Backend
export const WEBUI_NAME = writable(APP_NAME);
export const config = writable(undefined); export const config = writable(undefined);
export const user = writable(undefined); export const user = writable(undefined);
@@ -32,3 +34,4 @@ export const documents = writable([
export const settings = writable({}); export const settings = writable({});
export const showSettings = writable(false); export const showSettings = writable(false);
export const showChangelog = writable(false);
+2 -3
View File
@@ -101,11 +101,10 @@ export const copyToClipboard = (text) => {
); );
}; };
export const checkVersion = (required, current) => { export const compareVersion = (latest, current) => {
// Returns true when current version is below required
return current === '0.0.0' return current === '0.0.0'
? false ? false
: current.localeCompare(required, undefined, { : current.localeCompare(latest, undefined, {
numeric: true, numeric: true,
sensitivity: 'case', sensitivity: 'case',
caseFirst: 'upper' caseFirst: 'upper'
+37 -81
View File
@@ -1,17 +1,19 @@
<script lang="ts"> <script lang="ts">
import toast from 'svelte-french-toast'; import toast from 'svelte-french-toast';
import { openDB, deleteDB } from 'idb'; import { openDB, deleteDB } from 'idb';
import { onMount, tick } from 'svelte';
import { goto } from '$app/navigation';
import fileSaver from 'file-saver'; import fileSaver from 'file-saver';
const { saveAs } = fileSaver; const { saveAs } = fileSaver;
import { onMount, tick } from 'svelte';
import { goto } from '$app/navigation';
import { getOllamaModels, getOllamaVersion } from '$lib/apis/ollama'; import { getOllamaModels, getOllamaVersion } from '$lib/apis/ollama';
import { getModelfiles } from '$lib/apis/modelfiles'; import { getModelfiles } from '$lib/apis/modelfiles';
import { getPrompts } from '$lib/apis/prompts'; import { getPrompts } from '$lib/apis/prompts';
import { getOpenAIModels } from '$lib/apis/openai'; import { getOpenAIModels } from '$lib/apis/openai';
import { getLiteLLMModels } from '$lib/apis/litellm';
import { getDocs } from '$lib/apis/documents';
import { getAllChatTags } from '$lib/apis/chats';
import { import {
user, user,
@@ -21,16 +23,17 @@
modelfiles, modelfiles,
prompts, prompts,
documents, documents,
tags tags,
showChangelog,
config
} from '$lib/stores'; } from '$lib/stores';
import { REQUIRED_OLLAMA_VERSION, WEBUI_API_BASE_URL } from '$lib/constants'; import { REQUIRED_OLLAMA_VERSION, WEBUI_API_BASE_URL } from '$lib/constants';
import { compareVersion } from '$lib/utils';
import SettingsModal from '$lib/components/chat/SettingsModal.svelte'; import SettingsModal from '$lib/components/chat/SettingsModal.svelte';
import Sidebar from '$lib/components/layout/Sidebar.svelte'; import Sidebar from '$lib/components/layout/Sidebar.svelte';
import { checkVersion } from '$lib/utils';
import ShortcutsModal from '$lib/components/chat/ShortcutsModal.svelte'; import ShortcutsModal from '$lib/components/chat/ShortcutsModal.svelte';
import { getDocs } from '$lib/apis/documents'; import ChangelogModal from '$lib/components/ChangelogModal.svelte';
import { getAllChatTags } from '$lib/apis/chats';
let ollamaVersion = ''; let ollamaVersion = '';
let loaded = false; let loaded = false;
@@ -41,24 +44,28 @@
let showShortcuts = false; let showShortcuts = false;
const getModels = async () => { const getModels = async () => {
let models = []; let models = await Promise.all([
models.push( await getOllamaModels(localStorage.token).catch((error) => {
...(await getOllamaModels(localStorage.token).catch((error) => { console.log(error);
toast.error(error); return null;
return []; }),
})) await getOpenAIModels(localStorage.token).catch((error) => {
); console.log(error);
return null;
}),
await getLiteLLMModels(localStorage.token).catch((error) => {
console.log(error);
return null;
})
]);
// $settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1', models = models
// $settings.OPENAI_API_KEY .filter((models) => models)
.reduce((a, e, i, arr) => a.concat(e, ...(i < arr.length - 1 ? [{ name: 'hr' }] : [])), []);
const openAIModels = await getOpenAIModels(localStorage.token).catch((error) => {
console.log(error);
return null;
});
models.push(...(openAIModels ? [{ name: 'hr' }, ...openAIModels] : []));
// models.push(...(ollamaModels ? [{ name: 'hr' }, ...ollamaModels] : []));
// models.push(...(openAIModels ? [{ name: 'hr' }, ...openAIModels] : []));
// models.push(...(liteLLMModels ? [{ name: 'hr' }, ...liteLLMModels] : []));
return models; return models;
}; };
@@ -72,7 +79,7 @@
ollamaVersion = version; ollamaVersion = version;
console.log(ollamaVersion); console.log(ollamaVersion);
if (checkVersion(REQUIRED_OLLAMA_VERSION, ollamaVersion)) { if (compareVersion(REQUIRED_OLLAMA_VERSION, ollamaVersion)) {
toast.error(`Ollama Version: ${ollamaVersion !== '' ? ollamaVersion : 'Not Detected'}`); toast.error(`Ollama Version: ${ollamaVersion !== '' ? ollamaVersion : 'Not Detected'}`);
} }
}; };
@@ -115,8 +122,6 @@
await models.set(await getModels()); await models.set(await getModels());
}); });
await setOllamaVersion();
document.addEventListener('keydown', function (event) { document.addEventListener('keydown', function (event) {
const isCtrlPressed = event.ctrlKey || event.metaKey; // metaKey is for Cmd key on Mac const isCtrlPressed = event.ctrlKey || event.metaKey; // metaKey is for Cmd key on Mac
// Check if the Shift key is pressed // Check if the Shift key is pressed
@@ -182,6 +187,10 @@
} }
}); });
if ($user.role === 'admin') {
showChangelog.set(localStorage.version !== $config.version);
}
await tick(); await tick();
} }
@@ -244,60 +253,6 @@
</div> </div>
</div> </div>
</div> </div>
{:else if checkVersion(REQUIRED_OLLAMA_VERSION, ollamaVersion ?? '0')}
<div class="fixed w-full h-full flex z-50">
<div
class="absolute w-full h-full backdrop-blur-md bg-white/20 dark:bg-gray-900/50 flex justify-center"
>
<div class="m-auto pb-44 flex flex-col justify-center">
<div class="max-w-md">
<div class="text-center dark:text-white text-2xl font-medium z-50">
Connection Issue or Update Needed
</div>
<div class=" mt-4 text-center text-sm dark:text-gray-200 w-full">
Oops! It seems like your Ollama needs a little attention. <br
class=" hidden sm:flex"
/>We've detected either a connection hiccup or observed that you're using an older
version. Ensure you're on the latest Ollama version
<br class=" hidden sm:flex" />(version
<span class=" dark:text-white font-medium">{REQUIRED_OLLAMA_VERSION} or higher</span
>) or check your connection.
<div class="mt-1 text-sm">
Trouble accessing Ollama?
<a
class=" text-black dark:text-white font-semibold underline"
href="https://github.com/open-webui/open-webui#troubleshooting"
target="_blank"
>
Click here for help.
</a>
</div>
</div>
<div class=" mt-6 mx-auto relative group w-fit">
<button
class="relative z-20 flex px-5 py-2 rounded-full bg-white border border-gray-100 dark:border-none hover:bg-gray-100 transition font-medium text-sm"
on:click={async () => {
location.href = '/';
// await setOllamaVersion();
}}
>
Check Again
</button>
<button
class="text-xs text-center w-full mt-2 text-gray-400 underline"
on:click={async () => {
await setOllamaVersion(REQUIRED_OLLAMA_VERSION);
}}>Close</button
>
</div>
</div>
</div>
</div>
</div>
{:else if localDBChats.length > 0} {:else if localDBChats.length > 0}
<div class="fixed w-full h-full flex z-50"> <div class="fixed w-full h-full flex z-50">
<div <div
@@ -355,6 +310,7 @@
> >
<Sidebar /> <Sidebar />
<SettingsModal bind:show={$showSettings} /> <SettingsModal bind:show={$showSettings} />
<ChangelogModal bind:show={$showChangelog} />
<slot /> <slot />
</div> </div>
</div> </div>
+94 -78
View File
@@ -36,6 +36,8 @@
import ModelSelector from '$lib/components/chat/ModelSelector.svelte'; import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
import Navbar from '$lib/components/layout/Navbar.svelte'; import Navbar from '$lib/components/layout/Navbar.svelte';
import { RAGTemplate } from '$lib/utils/rag'; import { RAGTemplate } from '$lib/utils/rag';
import { LITELLM_API_BASE_URL, OPENAI_API_BASE_URL } from '$lib/constants';
import { WEBUI_BASE_URL } from '$lib/constants';
let stopResponseFlag = false; let stopResponseFlag = false;
let autoScroll = true; let autoScroll = true;
@@ -131,6 +133,10 @@
selectedModels = ['']; selectedModels = [''];
} }
selectedModels = selectedModels.map((modelId) =>
$models.map((m) => m.id).includes(modelId) ? modelId : ''
);
let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
settings.set({ settings.set({
..._settings ..._settings
@@ -149,6 +155,10 @@
const submitPrompt = async (userPrompt, _user = null) => { const submitPrompt = async (userPrompt, _user = null) => {
console.log('submitPrompt', $chatId); console.log('submitPrompt', $chatId);
selectedModels = selectedModels.map((modelId) =>
$models.map((m) => m.id).includes(modelId) ? modelId : ''
);
if (selectedModels.includes('')) { if (selectedModels.includes('')) {
toast.error('Model not selected'); toast.error('Model not selected');
} else if (messages.length != 0 && messages.at(-1).done != true) { } else if (messages.length != 0 && messages.at(-1).done != true) {
@@ -277,40 +287,41 @@
} }
await Promise.all( await Promise.all(
selectedModels.map(async (model) => { selectedModels.map(async (modelId) => {
console.log(model); const model = $models.filter((m) => m.id === modelId).at(0);
const modelTag = $models.filter((m) => m.name === model).at(0);
// Create response message if (model) {
let responseMessageId = uuidv4(); // Create response message
let responseMessage = { let responseMessageId = uuidv4();
parentId: parentId, let responseMessage = {
id: responseMessageId, parentId: parentId,
childrenIds: [], id: responseMessageId,
role: 'assistant', childrenIds: [],
content: '', role: 'assistant',
model: model, content: '',
timestamp: Math.floor(Date.now() / 1000) // Unix epoch model: model.id,
}; timestamp: Math.floor(Date.now() / 1000) // Unix epoch
};
// Add message to history and Set currentId to messageId // Add message to history and Set currentId to messageId
history.messages[responseMessageId] = responseMessage; history.messages[responseMessageId] = responseMessage;
history.currentId = responseMessageId; history.currentId = responseMessageId;
// Append messageId to childrenIds of parent message // Append messageId to childrenIds of parent message
if (parentId !== null) { if (parentId !== null) {
history.messages[parentId].childrenIds = [ history.messages[parentId].childrenIds = [
...history.messages[parentId].childrenIds, ...history.messages[parentId].childrenIds,
responseMessageId responseMessageId
]; ];
} }
if (modelTag?.external) { if (model?.external) {
await sendPromptOpenAI(model, prompt, responseMessageId, _chatId); await sendPromptOpenAI(model, prompt, responseMessageId, _chatId);
} else if (modelTag) { } else if (model) {
await sendPromptOllama(model, prompt, responseMessageId, _chatId); await sendPromptOllama(model, prompt, responseMessageId, _chatId);
}
} else { } else {
toast.error(`Model ${model} not found`); toast.error(`Model ${modelId} not found`);
} }
}) })
); );
@@ -319,6 +330,7 @@
}; };
const sendPromptOllama = async (model, userPrompt, responseMessageId, _chatId) => { const sendPromptOllama = async (model, userPrompt, responseMessageId, _chatId) => {
model = model.id;
const responseMessage = history.messages[responseMessageId]; const responseMessage = history.messages[responseMessageId];
// Wait until history/message have been updated // Wait until history/message have been updated
@@ -334,7 +346,7 @@
content: $settings.system content: $settings.system
} }
: undefined, : undefined,
...messages.filter(message => !message.deleted) ...messages.filter((message) => !message.deleted)
] ]
.filter((message) => message) .filter((message) => message)
.map((message, idx, arr) => ({ .map((message, idx, arr) => ({
@@ -452,7 +464,7 @@
: `${model}`, : `${model}`,
{ {
body: responseMessage.content, body: responseMessage.content,
icon: selectedModelfile?.imageUrl ?? '/favicon.png' icon: selectedModelfile?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`
} }
); );
} }
@@ -530,54 +542,58 @@
const responseMessage = history.messages[responseMessageId]; const responseMessage = history.messages[responseMessageId];
scrollToBottom(); scrollToBottom();
const res = await generateOpenAIChatCompletion(localStorage.token, { const res = await generateOpenAIChatCompletion(
model: model, localStorage.token,
stream: true, {
messages: [ model: model.id,
$settings.system stream: true,
? { messages: [
role: 'system', $settings.system
content: $settings.system
}
: undefined,
...messages.filter(message => !message.deleted)
]
.filter((message) => message)
.map((message, idx, arr) => ({
role: message.role,
...(message.files?.filter((file) => file.type === 'image').length > 0 ?? false
? { ? {
content: [ role: 'system',
{ content: $settings.system
type: 'text',
text:
arr.length - 1 !== idx
? message.content
: message?.raContent ?? message.content
},
...message.files
.filter((file) => file.type === 'image')
.map((file) => ({
type: 'image_url',
image_url: {
url: file.url
}
}))
]
} }
: { : undefined,
content: ...messages.filter((message) => !message.deleted)
arr.length - 1 !== idx ? message.content : message?.raContent ?? message.content ]
}) .filter((message) => message)
})), .map((message, idx, arr) => ({
seed: $settings?.options?.seed ?? undefined, role: message.role,
stop: $settings?.options?.stop ?? undefined, ...(message.files?.filter((file) => file.type === 'image').length > 0 ?? false
temperature: $settings?.options?.temperature ?? undefined, ? {
top_p: $settings?.options?.top_p ?? undefined, content: [
num_ctx: $settings?.options?.num_ctx ?? undefined, {
frequency_penalty: $settings?.options?.repeat_penalty ?? undefined, type: 'text',
max_tokens: $settings?.options?.num_predict ?? undefined text:
}); arr.length - 1 !== idx
? message.content
: message?.raContent ?? message.content
},
...message.files
.filter((file) => file.type === 'image')
.map((file) => ({
type: 'image_url',
image_url: {
url: file.url
}
}))
]
}
: {
content:
arr.length - 1 !== idx ? message.content : message?.raContent ?? message.content
})
})),
seed: $settings?.options?.seed ?? undefined,
stop: $settings?.options?.stop ?? undefined,
temperature: $settings?.options?.temperature ?? undefined,
top_p: $settings?.options?.top_p ?? undefined,
num_ctx: $settings?.options?.num_ctx ?? undefined,
frequency_penalty: $settings?.options?.repeat_penalty ?? undefined,
max_tokens: $settings?.options?.num_predict ?? undefined
},
model.source === 'litellm' ? `${LITELLM_API_BASE_URL}/v1` : `${OPENAI_API_BASE_URL}`
);
if (res && res.ok) { if (res && res.ok) {
const reader = res.body const reader = res.body
@@ -622,7 +638,7 @@
if ($settings.notificationEnabled && !document.hasFocus()) { if ($settings.notificationEnabled && !document.hasFocus()) {
const notification = new Notification(`OpenAI ${model}`, { const notification = new Notification(`OpenAI ${model}`, {
body: responseMessage.content, body: responseMessage.content,
icon: '/favicon.png' icon: `${WEBUI_BASE_URL}/static/favicon.png`
}); });
} }
+3 -2
View File
@@ -37,6 +37,7 @@
import ModelSelector from '$lib/components/chat/ModelSelector.svelte'; import ModelSelector from '$lib/components/chat/ModelSelector.svelte';
import Navbar from '$lib/components/layout/Navbar.svelte'; import Navbar from '$lib/components/layout/Navbar.svelte';
import { RAGTemplate } from '$lib/utils/rag'; import { RAGTemplate } from '$lib/utils/rag';
import { WEBUI_BASE_URL } from '$lib/constants';
let loaded = false; let loaded = false;
@@ -466,7 +467,7 @@
: `${model}`, : `${model}`,
{ {
body: responseMessage.content, body: responseMessage.content,
icon: selectedModelfile?.imageUrl ?? '/favicon.png' icon: selectedModelfile?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`
} }
); );
} }
@@ -637,7 +638,7 @@
if ($settings.notificationEnabled && !document.hasFocus()) { if ($settings.notificationEnabled && !document.hasFocus()) {
const notification = new Notification(`OpenAI ${model}`, { const notification = new Notification(`OpenAI ${model}`, {
body: responseMessage.content, body: responseMessage.content,
icon: '/favicon.png' icon: `${WEBUI_BASE_URL}/static/favicon.png`
}); });
} }
+6 -3
View File
@@ -1,6 +1,6 @@
<script> <script>
import { onMount, tick } from 'svelte'; import { onMount, tick } from 'svelte';
import { config, user, theme } from '$lib/stores'; import { config, user, theme, WEBUI_NAME } from '$lib/stores';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import toast, { Toaster } from 'svelte-french-toast'; import toast, { Toaster } from 'svelte-french-toast';
@@ -10,7 +10,7 @@
import '../app.css'; import '../app.css';
import '../tailwind.css'; import '../tailwind.css';
import 'tippy.js/dist/tippy.css'; import 'tippy.js/dist/tippy.css';
import { WEBUI_NAME } from '$lib/constants'; import { WEBUI_BASE_URL } from '$lib/constants';
let loaded = false; let loaded = false;
@@ -22,6 +22,8 @@
if (backendConfig) { if (backendConfig) {
// Save Backend Status to Store // Save Backend Status to Store
await config.set(backendConfig); await config.set(backendConfig);
await WEBUI_NAME.set(backendConfig.name);
console.log(backendConfig); console.log(backendConfig);
if ($config) { if ($config) {
@@ -55,7 +57,8 @@
</script> </script>
<svelte:head> <svelte:head>
<title>{WEBUI_NAME}</title> <title>{$WEBUI_NAME}</title>
<link rel="icon" href="{WEBUI_BASE_URL}/static/favicon.png" />
<link rel="stylesheet" type="text/css" href="/themes/rosepine.css" /> <link rel="stylesheet" type="text/css" href="/themes/rosepine.css" />
<link rel="stylesheet" type="text/css" href="/themes/rosepine-dawn.css" /> <link rel="stylesheet" type="text/css" href="/themes/rosepine-dawn.css" />
+5 -5
View File
@@ -1,8 +1,8 @@
<script> <script>
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { userSignIn, userSignUp } from '$lib/apis/auths'; import { userSignIn, userSignUp } from '$lib/apis/auths';
import { WEBUI_API_BASE_URL, WEBUI_NAME } from '$lib/constants'; import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
import { config, user } from '$lib/stores'; import { WEBUI_NAME, config, user } from '$lib/stores';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import toast from 'svelte-french-toast'; import toast from 'svelte-french-toast';
@@ -61,7 +61,7 @@
<div class="fixed m-10 z-50"> <div class="fixed m-10 z-50">
<div class="flex space-x-2"> <div class="flex space-x-2">
<div class=" self-center"> <div class=" self-center">
<img src="/favicon.png" class=" w-8 rounded-full" alt="logo" /> <img src="{WEBUI_BASE_URL}/static/favicon.png" class=" w-8 rounded-full" alt="logo" />
</div> </div>
</div> </div>
</div> </div>
@@ -90,12 +90,12 @@
}} }}
> >
<div class=" text-xl md:text-2xl font-bold"> <div class=" text-xl md:text-2xl font-bold">
{mode === 'signin' ? 'Sign in' : 'Sign up'} to {WEBUI_NAME} {mode === 'signin' ? 'Sign in' : 'Sign up'} to {$WEBUI_NAME}
</div> </div>
{#if mode === 'signup'} {#if mode === 'signup'}
<div class=" mt-1 text-xs font-medium text-gray-500"> <div class=" mt-1 text-xs font-medium text-gray-500">
ⓘ {WEBUI_NAME} does not make any external connections, and your data stays securely on ⓘ {$WEBUI_NAME} does not make any external connections, and your data stays securely on
your locally hosted server. your locally hosted server.
</div> </div>
{/if} {/if}
+2 -3
View File
@@ -1,7 +1,6 @@
<script> <script>
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { WEBUI_NAME } from '$lib/constants'; import { WEBUI_NAME, config } from '$lib/stores';
import { config } from '$lib/stores';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
let loaded = false; let loaded = false;
@@ -20,7 +19,7 @@
<div class="absolute rounded-xl w-full h-full backdrop-blur flex justify-center"> <div class="absolute rounded-xl w-full h-full backdrop-blur flex justify-center">
<div class="m-auto pb-44 flex flex-col justify-center"> <div class="m-auto pb-44 flex flex-col justify-center">
<div class="max-w-md"> <div class="max-w-md">
<div class="text-center text-2xl font-medium z-50">{WEBUI_NAME} Backend Required</div> <div class="text-center text-2xl font-medium z-50">{$WEBUI_NAME} Backend Required</div>
<div class=" mt-4 text-center text-sm w-full"> <div class=" mt-4 text-center text-sm w-full">
Oops! You're using an unsupported method (frontend only). Please serve the WebUI from Oops! You're using an unsupported method (frontend only). Please serve the WebUI from
+6
View File
@@ -0,0 +1,6 @@
{
"model_name": "string",
"litellm_params": {
"model": "ollama/mistral"
}
}