forked from open-webui/open-webui
		
	main #2
					 30 changed files with 849 additions and 569 deletions
				
			
		|  | @ -7,7 +7,6 @@ node_modules | ||||||
| /package | /package | ||||||
| .env | .env | ||||||
| .env.* | .env.* | ||||||
| !.env.example |  | ||||||
| vite.config.js.timestamp-* | vite.config.js.timestamp-* | ||||||
| vite.config.ts.timestamp-* | vite.config.ts.timestamp-* | ||||||
| __pycache__ | __pycache__ | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| # Ollama URL for the backend to connect | # Ollama URL for the backend to connect | ||||||
| # The path '/ollama/api' will be redirected to the specified backend URL | # The path '/ollama' will be redirected to the specified backend URL | ||||||
| OLLAMA_API_BASE_URL='http://localhost:11434/api' | OLLAMA_BASE_URL='http://localhost:11434' | ||||||
| 
 | 
 | ||||||
| OPENAI_API_BASE_URL='' | OPENAI_API_BASE_URL='' | ||||||
| OPENAI_API_KEY='' | OPENAI_API_KEY='' | ||||||
|  |  | ||||||
							
								
								
									
										12
									
								
								.github/workflows/build-release.yml
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								.github/workflows/build-release.yml
									
										
									
									
										vendored
									
									
								
							|  | @ -26,17 +26,27 @@ jobs: | ||||||
|         VERSION=$(jq -r '.version' package.json) |         VERSION=$(jq -r '.version' package.json) | ||||||
|         echo "::set-output name=version::$VERSION" |         echo "::set-output name=version::$VERSION" | ||||||
| 
 | 
 | ||||||
|  |     - name: Extract latest CHANGELOG entry | ||||||
|  |       id: changelog | ||||||
|  |       run: | | ||||||
|  |         CHANGELOG_CONTENT=$(awk '/^## \[/{n++} n==1' CHANGELOG.md) | ||||||
|  |         echo "CHANGELOG_CONTENT<<EOF"  | ||||||
|  |         echo "$CHANGELOG_CONTENT" | ||||||
|  |         echo "EOF"  | ||||||
|  |         echo "::set-output name=content::${CHANGELOG_CONTENT}" | ||||||
|  | 
 | ||||||
|     - name: Create GitHub release |     - name: Create GitHub release | ||||||
|       uses: actions/github-script@v5 |       uses: actions/github-script@v5 | ||||||
|       with: |       with: | ||||||
|         github-token: ${{ secrets.GITHUB_TOKEN }} |         github-token: ${{ secrets.GITHUB_TOKEN }} | ||||||
|         script: | |         script: | | ||||||
|  |           const changelog = `${{ steps.changelog.outputs.content }}`; | ||||||
|           const release = await github.rest.repos.createRelease({ |           const release = await github.rest.repos.createRelease({ | ||||||
|             owner: context.repo.owner, |             owner: context.repo.owner, | ||||||
|             repo: context.repo.repo, |             repo: context.repo.repo, | ||||||
|             tag_name: `v${{ steps.get_version.outputs.version }}`, |             tag_name: `v${{ steps.get_version.outputs.version }}`, | ||||||
|             name: `v${{ steps.get_version.outputs.version }}`, |             name: `v${{ steps.get_version.outputs.version }}`, | ||||||
|             body: 'Automatically created new release', |             body: changelog, | ||||||
|           }) |           }) | ||||||
|           console.log(`Created release ${release.data.html_url}`) |           console.log(`Created release ${release.data.html_url}`) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										32
									
								
								CHANGELOG.md
									
										
									
									
									
								
							
							
						
						
									
										32
									
								
								CHANGELOG.md
									
										
									
									
									
								
							|  | @ -5,6 +5,38 @@ 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/), | 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). | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||||||
| 
 | 
 | ||||||
|  | ## [0.1.110] - 2024-03-06 | ||||||
|  | 
 | ||||||
|  | ### Added | ||||||
|  | 
 | ||||||
|  | - **🌐 Multiple OpenAI Servers Support**: Enjoy seamless integration with multiple OpenAI-compatible APIs, now supported natively. | ||||||
|  | 
 | ||||||
|  | ### Fixed | ||||||
|  | 
 | ||||||
|  | - **🔍 OCR Issue**: Resolved PDF parsing issue caused by OCR malfunction. | ||||||
|  | - **🚫 RAG Issue**: Fixed the RAG functionality, ensuring it operates smoothly. | ||||||
|  | - **📄 "Add Docs" Model Button**: Addressed the non-functional behavior of the "Add Docs" model button. | ||||||
|  | 
 | ||||||
|  | ## [0.1.109] - 2024-03-06 | ||||||
|  | 
 | ||||||
|  | ### Added | ||||||
|  | 
 | ||||||
|  | - **🔄 Multiple Ollama Servers Support**: Enjoy enhanced scalability and performance with support for multiple Ollama servers in a single WebUI. Load balancing features are now available, providing improved efficiency (#788, #278). | ||||||
|  | - **🔧 Support for Claude 3 and Gemini**: Responding to user requests, we've expanded our toolset to include Claude 3 and Gemini, offering a wider range of functionalities within our platform (#1064). | ||||||
|  | - **🔍 OCR Functionality for PDF Loader**: We've augmented our PDF loader with Optical Character Recognition (OCR) capabilities. Now, extract text from scanned documents and images within PDFs, broadening the scope of content processing (#1050). | ||||||
|  | 
 | ||||||
|  | ### Fixed | ||||||
|  | 
 | ||||||
|  | - **🛠️ RAG Collection**: Implemented a dynamic mechanism to recreate RAG collections, ensuring users have up-to-date and accurate data (#1031). | ||||||
|  | - **📝 User Agent Headers**: Fixed issue of RAG web requests being sent with empty user_agent headers, reducing rejections from certain websites. Realistic headers are now utilized for these requests (#1024). | ||||||
|  | - **⏹️ Playground Cancel Functionality**: Introducing a new "Cancel" option for stopping Ollama generation in the Playground, enhancing user control and usability (#1006). | ||||||
|  | - **🔤 Typographical Error in 'ASSISTANT' Field**: Corrected a typographical error in the 'ASSISTANT' field within the GGUF model upload template for accuracy and consistency (#1061). | ||||||
|  | 
 | ||||||
|  | ### Changed | ||||||
|  | 
 | ||||||
|  | - **🔄 Refactored Message Deletion Logic**: Streamlined message deletion process for improved efficiency and user experience, simplifying interactions within the platform (#1004). | ||||||
|  | - **⚠️ Deprecation of `OLLAMA_API_BASE_URL`**: Deprecated `OLLAMA_API_BASE_URL` environment variable; recommend using `OLLAMA_BASE_URL` instead. Refer to our documentation for further details. | ||||||
|  | 
 | ||||||
| ## [0.1.108] - 2024-03-02 | ## [0.1.108] - 2024-03-02 | ||||||
| 
 | 
 | ||||||
| ### Added | ### Added | ||||||
|  |  | ||||||
|  | @ -20,7 +20,7 @@ FROM python:3.11-slim-bookworm as base | ||||||
| ENV ENV=prod | ENV ENV=prod | ||||||
| ENV PORT "" | ENV PORT "" | ||||||
| 
 | 
 | ||||||
| ENV OLLAMA_API_BASE_URL "/ollama/api" | ENV OLLAMA_BASE_URL "/ollama" | ||||||
| 
 | 
 | ||||||
| ENV OPENAI_API_BASE_URL "" | ENV OPENAI_API_BASE_URL "" | ||||||
| ENV OPENAI_API_KEY "" | ENV OPENAI_API_KEY "" | ||||||
|  | @ -53,6 +53,8 @@ WORKDIR /app/backend | ||||||
| # install python dependencies | # install python dependencies | ||||||
| COPY ./backend/requirements.txt ./requirements.txt | COPY ./backend/requirements.txt ./requirements.txt | ||||||
| 
 | 
 | ||||||
|  | RUN apt-get update && apt-get install ffmpeg libsm6 libxext6  -y | ||||||
|  | 
 | ||||||
| RUN pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir | RUN pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir | ||||||
| RUN pip3 install -r requirements.txt --no-cache-dir | RUN pip3 install -r requirements.txt --no-cache-dir | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -95,10 +95,10 @@ Don't forget to explore our sibling project, [Open WebUI Community](https://open | ||||||
| 
 | 
 | ||||||
| - **If Ollama is on a Different Server**, use this command: | - **If Ollama is on a Different Server**, use this command: | ||||||
| 
 | 
 | ||||||
| - 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_BASE_URL` to the server's URL: | ||||||
| 
 | 
 | ||||||
|   ```bash |   ```bash | ||||||
|   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_BASE_URL=https://example.com -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main | ||||||
|   ``` |   ``` | ||||||
| 
 | 
 | ||||||
| - After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! 😄 | - After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! 😄 | ||||||
|  | @ -110,7 +110,7 @@ If you're experiencing connection issues, it’s often due to the WebUI docker c | ||||||
| **Example Docker Command**: | **Example Docker Command**: | ||||||
| 
 | 
 | ||||||
| ```bash | ```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 | docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ### Other Installation Methods | ### Other Installation Methods | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ | ||||||
| 
 | 
 | ||||||
| The Open WebUI system is designed to streamline interactions between the client (your browser) and the Ollama API. At the heart of this design is a backend reverse proxy, enhancing security and resolving CORS issues. | The Open WebUI system is designed to streamline interactions between the client (your browser) and the Ollama API. At the heart of this design is a backend reverse proxy, enhancing security and resolving CORS issues. | ||||||
| 
 | 
 | ||||||
| - **How it Works**: The Open WebUI is designed to interact with the Ollama API through a specific route. When a request is made from the WebUI to Ollama, it is not directly sent to the Ollama API. Initially, the request is sent to the Open WebUI backend via `/ollama/api` route. From there, the backend is responsible for forwarding the request to the Ollama API. This forwarding is accomplished by using the route specified in the `OLLAMA_API_BASE_URL` environment variable. Therefore, a request made to `/ollama/api` in the WebUI is effectively the same as making a request to `OLLAMA_API_BASE_URL` in the backend. For instance, a request to `/ollama/api/tags` in the WebUI is equivalent to `OLLAMA_API_BASE_URL/tags` in the backend. | - **How it Works**: The Open WebUI is designed to interact with the Ollama API through a specific route. When a request is made from the WebUI to Ollama, it is not directly sent to the Ollama API. Initially, the request is sent to the Open WebUI backend via `/ollama` route. From there, the backend is responsible for forwarding the request to the Ollama API. This forwarding is accomplished by using the route specified in the `OLLAMA_BASE_URL` environment variable. Therefore, a request made to `/ollama` in the WebUI is effectively the same as making a request to `OLLAMA_BASE_URL` in the backend. For instance, a request to `/ollama/api/tags` in the WebUI is equivalent to `OLLAMA_BASE_URL/api/tags` in the backend. | ||||||
| 
 | 
 | ||||||
| - **Security Benefits**: This design prevents direct exposure of the Ollama API to the frontend, safeguarding against potential CORS (Cross-Origin Resource Sharing) issues and unauthorized access. Requiring authentication to access the Ollama API further enhances this security layer. | - **Security Benefits**: This design prevents direct exposure of the Ollama API to the frontend, safeguarding against potential CORS (Cross-Origin Resource Sharing) issues and unauthorized access. Requiring authentication to access the Ollama API further enhances this security layer. | ||||||
| 
 | 
 | ||||||
|  | @ -15,7 +15,7 @@ If you're experiencing connection issues, it’s often due to the WebUI docker c | ||||||
| **Example Docker Command**: | **Example Docker Command**: | ||||||
| 
 | 
 | ||||||
| ```bash | ```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 | docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ### General Connection Errors | ### General Connection Errors | ||||||
|  | @ -25,8 +25,8 @@ docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_API_BASE_ | ||||||
| **Troubleshooting Steps**: | **Troubleshooting Steps**: | ||||||
| 
 | 
 | ||||||
| 1. **Verify Ollama URL Format**: | 1. **Verify Ollama URL Format**: | ||||||
|    - When running the Web UI container, ensure the `OLLAMA_API_BASE_URL` is correctly set, including the `/api` suffix. (e.g., `http://192.168.1.1:11434/api` for different host setups). |    - When running the Web UI container, ensure the `OLLAMA_BASE_URL` is correctly set. (e.g., `http://192.168.1.1:11434` for different host setups). | ||||||
|    - In the Open WebUI, navigate to "Settings" > "General". |    - In the Open WebUI, navigate to "Settings" > "General". | ||||||
|    - Confirm that the Ollama Server URL is correctly set to `[OLLAMA URL]/api` (e.g., `http://localhost:11434/api`), including the `/api` suffix. |    - Confirm that the Ollama Server URL is correctly set to `[OLLAMA URL]` (e.g., `http://localhost:11434`). | ||||||
| 
 | 
 | ||||||
| By following these enhanced troubleshooting steps, connection issues should be effectively resolved. For further assistance or queries, feel free to reach out to us on our community Discord. | By following these enhanced troubleshooting steps, connection issues should be effectively resolved. For further assistance or queries, feel free to reach out to us on our community Discord. | ||||||
|  |  | ||||||
|  | @ -15,7 +15,7 @@ import asyncio | ||||||
| from apps.web.models.users import Users | from apps.web.models.users import Users | ||||||
| from constants import ERROR_MESSAGES | from constants import ERROR_MESSAGES | ||||||
| from utils.utils import decode_token, get_current_user, get_admin_user | from utils.utils import decode_token, get_current_user, get_admin_user | ||||||
| from config import OLLAMA_BASE_URL, WEBUI_AUTH | from config import OLLAMA_BASE_URLS | ||||||
| 
 | 
 | ||||||
| from typing import Optional, List, Union | from typing import Optional, List, Union | ||||||
| 
 | 
 | ||||||
|  | @ -29,8 +29,7 @@ app.add_middleware( | ||||||
|     allow_headers=["*"], |     allow_headers=["*"], | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| app.state.OLLAMA_BASE_URL = OLLAMA_BASE_URL | app.state.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS | ||||||
| app.state.OLLAMA_BASE_URLS = [OLLAMA_BASE_URL] |  | ||||||
| app.state.MODELS = {} | app.state.MODELS = {} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -223,7 +222,7 @@ async def pull_model( | ||||||
|             r = requests.request( |             r = requests.request( | ||||||
|                 method="POST", |                 method="POST", | ||||||
|                 url=f"{url}/api/pull", |                 url=f"{url}/api/pull", | ||||||
|                 data=form_data.model_dump_json(exclude_none=True), |                 data=form_data.model_dump_json(exclude_none=True).encode(), | ||||||
|                 stream=True, |                 stream=True, | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|  | @ -295,7 +294,7 @@ async def push_model( | ||||||
|             r = requests.request( |             r = requests.request( | ||||||
|                 method="POST", |                 method="POST", | ||||||
|                 url=f"{url}/api/push", |                 url=f"{url}/api/push", | ||||||
|                 data=form_data.model_dump_json(exclude_none=True), |                 data=form_data.model_dump_json(exclude_none=True).encode(), | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|             r.raise_for_status() |             r.raise_for_status() | ||||||
|  | @ -357,7 +356,7 @@ async def create_model( | ||||||
|             r = requests.request( |             r = requests.request( | ||||||
|                 method="POST", |                 method="POST", | ||||||
|                 url=f"{url}/api/create", |                 url=f"{url}/api/create", | ||||||
|                 data=form_data.model_dump_json(exclude_none=True), |                 data=form_data.model_dump_json(exclude_none=True).encode(), | ||||||
|                 stream=True, |                 stream=True, | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|  | @ -420,7 +419,7 @@ async def copy_model( | ||||||
|         r = requests.request( |         r = requests.request( | ||||||
|             method="POST", |             method="POST", | ||||||
|             url=f"{url}/api/copy", |             url=f"{url}/api/copy", | ||||||
|             data=form_data.model_dump_json(exclude_none=True), |             data=form_data.model_dump_json(exclude_none=True).encode(), | ||||||
|         ) |         ) | ||||||
|         r.raise_for_status() |         r.raise_for_status() | ||||||
| 
 | 
 | ||||||
|  | @ -467,7 +466,7 @@ async def delete_model( | ||||||
|         r = requests.request( |         r = requests.request( | ||||||
|             method="DELETE", |             method="DELETE", | ||||||
|             url=f"{url}/api/delete", |             url=f"{url}/api/delete", | ||||||
|             data=form_data.model_dump_json(exclude_none=True), |             data=form_data.model_dump_json(exclude_none=True).encode(), | ||||||
|         ) |         ) | ||||||
|         r.raise_for_status() |         r.raise_for_status() | ||||||
| 
 | 
 | ||||||
|  | @ -507,7 +506,7 @@ async def show_model_info(form_data: ModelNameForm, user=Depends(get_current_use | ||||||
|         r = requests.request( |         r = requests.request( | ||||||
|             method="POST", |             method="POST", | ||||||
|             url=f"{url}/api/show", |             url=f"{url}/api/show", | ||||||
|             data=form_data.model_dump_json(exclude_none=True), |             data=form_data.model_dump_json(exclude_none=True).encode(), | ||||||
|         ) |         ) | ||||||
|         r.raise_for_status() |         r.raise_for_status() | ||||||
| 
 | 
 | ||||||
|  | @ -559,7 +558,7 @@ async def generate_embeddings( | ||||||
|         r = requests.request( |         r = requests.request( | ||||||
|             method="POST", |             method="POST", | ||||||
|             url=f"{url}/api/embeddings", |             url=f"{url}/api/embeddings", | ||||||
|             data=form_data.model_dump_json(exclude_none=True), |             data=form_data.model_dump_json(exclude_none=True).encode(), | ||||||
|         ) |         ) | ||||||
|         r.raise_for_status() |         r.raise_for_status() | ||||||
| 
 | 
 | ||||||
|  | @ -645,7 +644,7 @@ async def generate_completion( | ||||||
|             r = requests.request( |             r = requests.request( | ||||||
|                 method="POST", |                 method="POST", | ||||||
|                 url=f"{url}/api/generate", |                 url=f"{url}/api/generate", | ||||||
|                 data=form_data.model_dump_json(exclude_none=True), |                 data=form_data.model_dump_json(exclude_none=True).encode(), | ||||||
|                 stream=True, |                 stream=True, | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|  | @ -715,7 +714,7 @@ async def generate_chat_completion( | ||||||
| 
 | 
 | ||||||
|     r = None |     r = None | ||||||
| 
 | 
 | ||||||
|     print(form_data.model_dump_json(exclude_none=True)) |     print(form_data.model_dump_json(exclude_none=True).encode()) | ||||||
| 
 | 
 | ||||||
|     def get_request(): |     def get_request(): | ||||||
|         nonlocal form_data |         nonlocal form_data | ||||||
|  | @ -745,7 +744,7 @@ async def generate_chat_completion( | ||||||
|             r = requests.request( |             r = requests.request( | ||||||
|                 method="POST", |                 method="POST", | ||||||
|                 url=f"{url}/api/chat", |                 url=f"{url}/api/chat", | ||||||
|                 data=form_data.model_dump_json(exclude_none=True), |                 data=form_data.model_dump_json(exclude_none=True).encode(), | ||||||
|                 stream=True, |                 stream=True, | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|  | @ -757,6 +756,7 @@ async def generate_chat_completion( | ||||||
|                 headers=dict(r.headers), |                 headers=dict(r.headers), | ||||||
|             ) |             ) | ||||||
|         except Exception as e: |         except Exception as e: | ||||||
|  |             print(e) | ||||||
|             raise e |             raise e | ||||||
| 
 | 
 | ||||||
|     try: |     try: | ||||||
|  | @ -844,7 +844,7 @@ async def generate_openai_chat_completion( | ||||||
|             r = requests.request( |             r = requests.request( | ||||||
|                 method="POST", |                 method="POST", | ||||||
|                 url=f"{url}/v1/chat/completions", |                 url=f"{url}/v1/chat/completions", | ||||||
|                 data=form_data.model_dump_json(exclude_none=True), |                 data=form_data.model_dump_json(exclude_none=True).encode(), | ||||||
|                 stream=True, |                 stream=True, | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -3,7 +3,10 @@ from fastapi.middleware.cors import CORSMiddleware | ||||||
| from fastapi.responses import StreamingResponse, JSONResponse, FileResponse | from fastapi.responses import StreamingResponse, JSONResponse, FileResponse | ||||||
| 
 | 
 | ||||||
| import requests | import requests | ||||||
|  | import aiohttp | ||||||
|  | import asyncio | ||||||
| import json | import json | ||||||
|  | 
 | ||||||
| from pydantic import BaseModel | from pydantic import BaseModel | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -15,7 +18,9 @@ from utils.utils import ( | ||||||
|     get_verified_user, |     get_verified_user, | ||||||
|     get_admin_user, |     get_admin_user, | ||||||
| ) | ) | ||||||
| from config import OPENAI_API_BASE_URL, OPENAI_API_KEY, CACHE_DIR | from config import OPENAI_API_BASE_URLS, OPENAI_API_KEYS, CACHE_DIR | ||||||
|  | from typing import List, Optional | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| import hashlib | import hashlib | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
|  | @ -29,49 +34,59 @@ app.add_middleware( | ||||||
|     allow_headers=["*"], |     allow_headers=["*"], | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| app.state.OPENAI_API_BASE_URL = OPENAI_API_BASE_URL | app.state.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS | ||||||
| app.state.OPENAI_API_KEY = OPENAI_API_KEY | app.state.OPENAI_API_KEYS = OPENAI_API_KEYS | ||||||
|  | 
 | ||||||
|  | app.state.MODELS = {} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class UrlUpdateForm(BaseModel): | @app.middleware("http") | ||||||
|     url: str | async def check_url(request: Request, call_next): | ||||||
|  |     if len(app.state.MODELS) == 0: | ||||||
|  |         await get_all_models() | ||||||
|  |     else: | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  |     response = await call_next(request) | ||||||
|  |     return response | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class KeyUpdateForm(BaseModel): | class UrlsUpdateForm(BaseModel): | ||||||
|     key: str |     urls: List[str] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.get("/url") | class KeysUpdateForm(BaseModel): | ||||||
| async def get_openai_url(user=Depends(get_admin_user)): |     keys: List[str] | ||||||
|     return {"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL} |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.post("/url/update") | @app.get("/urls") | ||||||
| async def update_openai_url(form_data: UrlUpdateForm, user=Depends(get_admin_user)): | async def get_openai_urls(user=Depends(get_admin_user)): | ||||||
|     app.state.OPENAI_API_BASE_URL = form_data.url |     return {"OPENAI_API_BASE_URLS": app.state.OPENAI_API_BASE_URLS} | ||||||
|     return {"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL} |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.get("/key") | @app.post("/urls/update") | ||||||
| async def get_openai_key(user=Depends(get_admin_user)): | async def update_openai_urls(form_data: UrlsUpdateForm, user=Depends(get_admin_user)): | ||||||
|     return {"OPENAI_API_KEY": app.state.OPENAI_API_KEY} |     app.state.OPENAI_API_BASE_URLS = form_data.urls | ||||||
|  |     return {"OPENAI_API_BASE_URLS": app.state.OPENAI_API_BASE_URLS} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.post("/key/update") | @app.get("/keys") | ||||||
| async def update_openai_key(form_data: KeyUpdateForm, user=Depends(get_admin_user)): | async def get_openai_keys(user=Depends(get_admin_user)): | ||||||
|     app.state.OPENAI_API_KEY = form_data.key |     return {"OPENAI_API_KEYS": app.state.OPENAI_API_KEYS} | ||||||
|     return {"OPENAI_API_KEY": app.state.OPENAI_API_KEY} | 
 | ||||||
|  | 
 | ||||||
|  | @app.post("/keys/update") | ||||||
|  | async def update_openai_key(form_data: KeysUpdateForm, user=Depends(get_admin_user)): | ||||||
|  |     app.state.OPENAI_API_KEYS = form_data.keys | ||||||
|  |     return {"OPENAI_API_KEYS": app.state.OPENAI_API_KEYS} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.post("/audio/speech") | @app.post("/audio/speech") | ||||||
| async def speech(request: Request, user=Depends(get_verified_user)): | async def speech(request: Request, user=Depends(get_verified_user)): | ||||||
|     target_url = f"{app.state.OPENAI_API_BASE_URL}/audio/speech" |     idx = None | ||||||
| 
 |     try: | ||||||
|     if app.state.OPENAI_API_KEY == "": |         idx = app.state.OPENAI_API_BASE_URLS.index("https://api.openai.com/v1") | ||||||
|         raise HTTPException(status_code=401, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND) |  | ||||||
| 
 |  | ||||||
|         body = await request.body() |         body = await request.body() | ||||||
| 
 |  | ||||||
|         name = hashlib.sha256(body).hexdigest() |         name = hashlib.sha256(body).hexdigest() | ||||||
| 
 | 
 | ||||||
|         SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/") |         SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/") | ||||||
|  | @ -84,13 +99,12 @@ async def speech(request: Request, user=Depends(get_verified_user)): | ||||||
|             return FileResponse(file_path) |             return FileResponse(file_path) | ||||||
| 
 | 
 | ||||||
|         headers = {} |         headers = {} | ||||||
|     headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}" |         headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEYS[idx]}" | ||||||
|         headers["Content-Type"] = "application/json" |         headers["Content-Type"] = "application/json" | ||||||
| 
 | 
 | ||||||
|         try: |         try: | ||||||
|         print("openai") |  | ||||||
|             r = requests.post( |             r = requests.post( | ||||||
|             url=target_url, |                 url=f"{app.state.OPENAI_API_BASE_URLS[idx]}/audio/speech", | ||||||
|                 data=body, |                 data=body, | ||||||
|                 headers=headers, |                 headers=headers, | ||||||
|                 stream=True, |                 stream=True, | ||||||
|  | @ -122,23 +136,105 @@ async def speech(request: Request, user=Depends(get_verified_user)): | ||||||
| 
 | 
 | ||||||
|             raise HTTPException(status_code=r.status_code, detail=error_detail) |             raise HTTPException(status_code=r.status_code, detail=error_detail) | ||||||
| 
 | 
 | ||||||
|  |     except ValueError: | ||||||
|  |         raise HTTPException(status_code=401, detail=ERROR_MESSAGES.OPENAI_NOT_FOUND) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | async def fetch_url(url, key): | ||||||
|  |     try: | ||||||
|  |         headers = {"Authorization": f"Bearer {key}"} | ||||||
|  |         async with aiohttp.ClientSession() as session: | ||||||
|  |             async with session.get(url, headers=headers) as response: | ||||||
|  |                 return await response.json() | ||||||
|  |     except Exception as e: | ||||||
|  |         # Handle connection error here | ||||||
|  |         print(f"Connection error: {e}") | ||||||
|  |         return None | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def merge_models_lists(model_lists): | ||||||
|  |     merged_list = [] | ||||||
|  | 
 | ||||||
|  |     for idx, models in enumerate(model_lists): | ||||||
|  |         merged_list.extend( | ||||||
|  |             [ | ||||||
|  |                 {**model, "urlIdx": idx} | ||||||
|  |                 for model in models | ||||||
|  |                 if "api.openai.com" not in app.state.OPENAI_API_BASE_URLS[idx] | ||||||
|  |                 or "gpt" in model["id"] | ||||||
|  |             ] | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     return merged_list | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | async def get_all_models(): | ||||||
|  |     print("get_all_models") | ||||||
|  |     tasks = [ | ||||||
|  |         fetch_url(f"{url}/models", app.state.OPENAI_API_KEYS[idx]) | ||||||
|  |         for idx, url in enumerate(app.state.OPENAI_API_BASE_URLS) | ||||||
|  |     ] | ||||||
|  |     responses = await asyncio.gather(*tasks) | ||||||
|  |     responses = list(filter(lambda x: x is not None and "error" not in x, responses)) | ||||||
|  |     models = { | ||||||
|  |         "data": merge_models_lists( | ||||||
|  |             list(map(lambda response: response["data"], responses)) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |     app.state.MODELS = {model["id"]: model for model in models["data"]} | ||||||
|  | 
 | ||||||
|  |     return models | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # , user=Depends(get_current_user) | ||||||
|  | @app.get("/models") | ||||||
|  | @app.get("/models/{url_idx}") | ||||||
|  | async def get_models(url_idx: Optional[int] = None): | ||||||
|  |     if url_idx == None: | ||||||
|  |         return await get_all_models() | ||||||
|  |     else: | ||||||
|  |         url = app.state.OPENAI_API_BASE_URLS[url_idx] | ||||||
|  |         try: | ||||||
|  |             r = requests.request(method="GET", url=f"{url}/models") | ||||||
|  |             r.raise_for_status() | ||||||
|  | 
 | ||||||
|  |             response_data = r.json() | ||||||
|  |             if "api.openai.com" in url: | ||||||
|  |                 response_data["data"] = list( | ||||||
|  |                     filter(lambda model: "gpt" in model["id"], response_data["data"]) | ||||||
|  |                 ) | ||||||
|  | 
 | ||||||
|  |             return response_data | ||||||
|  |         except Exception as e: | ||||||
|  |             print(e) | ||||||
|  |             error_detail = "Open WebUI: Server Connection Error" | ||||||
|  |             if r is not None: | ||||||
|  |                 try: | ||||||
|  |                     res = r.json() | ||||||
|  |                     if "error" in res: | ||||||
|  |                         error_detail = f"External: {res['error']}" | ||||||
|  |                 except: | ||||||
|  |                     error_detail = f"External: {e}" | ||||||
|  | 
 | ||||||
|  |             raise HTTPException( | ||||||
|  |                 status_code=r.status_code if r else 500, | ||||||
|  |                 detail=error_detail, | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"]) | @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"]) | ||||||
| async def proxy(path: str, request: Request, user=Depends(get_verified_user)): | async def proxy(path: str, request: Request, user=Depends(get_verified_user)): | ||||||
|     target_url = f"{app.state.OPENAI_API_BASE_URL}/{path}" |     idx = 0 | ||||||
|     print(target_url, app.state.OPENAI_API_KEY) |  | ||||||
| 
 |  | ||||||
|     if app.state.OPENAI_API_KEY == "": |  | ||||||
|         raise HTTPException(status_code=401, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND) |  | ||||||
| 
 | 
 | ||||||
|     body = await request.body() |     body = await request.body() | ||||||
| 
 |  | ||||||
|     # TODO: Remove below after gpt-4-vision fix from Open AI |     # TODO: Remove below after gpt-4-vision fix from Open AI | ||||||
|     # Try to decode the body of the request from bytes to a UTF-8 string (Require add max_token to fix gpt-4-vision) |     # Try to decode the body of the request from bytes to a UTF-8 string (Require add max_token to fix gpt-4-vision) | ||||||
|     try: |     try: | ||||||
|         body = body.decode("utf-8") |         body = body.decode("utf-8") | ||||||
|         body = json.loads(body) |         body = json.loads(body) | ||||||
| 
 | 
 | ||||||
|  |         idx = app.state.MODELS[body.get("model")]["urlIdx"] | ||||||
|  | 
 | ||||||
|         # Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000 |         # Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000 | ||||||
|         # This is a workaround until OpenAI fixes the issue with this model |         # This is a workaround until OpenAI fixes the issue with this model | ||||||
|         if body.get("model") == "gpt-4-vision-preview": |         if body.get("model") == "gpt-4-vision-preview": | ||||||
|  | @ -158,8 +254,16 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): | ||||||
|     except json.JSONDecodeError as e: |     except json.JSONDecodeError as e: | ||||||
|         print("Error loading request body into a dictionary:", e) |         print("Error loading request body into a dictionary:", e) | ||||||
| 
 | 
 | ||||||
|  |     url = app.state.OPENAI_API_BASE_URLS[idx] | ||||||
|  |     key = app.state.OPENAI_API_KEYS[idx] | ||||||
|  | 
 | ||||||
|  |     target_url = f"{url}/{path}" | ||||||
|  | 
 | ||||||
|  |     if key == "": | ||||||
|  |         raise HTTPException(status_code=401, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND) | ||||||
|  | 
 | ||||||
|     headers = {} |     headers = {} | ||||||
|     headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}" |     headers["Authorization"] = f"Bearer {key}" | ||||||
|     headers["Content-Type"] = "application/json" |     headers["Content-Type"] = "application/json" | ||||||
| 
 | 
 | ||||||
|     try: |     try: | ||||||
|  | @ -181,21 +285,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): | ||||||
|                 headers=dict(r.headers), |                 headers=dict(r.headers), | ||||||
|             ) |             ) | ||||||
|         else: |         else: | ||||||
|             # For non-SSE, read the response and return it |  | ||||||
|             # response_data = ( |  | ||||||
|             #     r.json() |  | ||||||
|             #     if r.headers.get("Content-Type", "") |  | ||||||
|             #     == "application/json" |  | ||||||
|             #     else r.text |  | ||||||
|             # ) |  | ||||||
| 
 |  | ||||||
|             response_data = r.json() |             response_data = r.json() | ||||||
| 
 |  | ||||||
|             if "api.openai.com" in app.state.OPENAI_API_BASE_URL and path == "models": |  | ||||||
|                 response_data["data"] = list( |  | ||||||
|                     filter(lambda model: "gpt" in model["id"], response_data["data"]) |  | ||||||
|                 ) |  | ||||||
| 
 |  | ||||||
|             return response_data |             return response_data | ||||||
|     except Exception as e: |     except Exception as e: | ||||||
|         print(e) |         print(e) | ||||||
|  |  | ||||||
|  | @ -425,7 +425,7 @@ def get_loader(filename: str, file_content_type: str, file_path: str): | ||||||
|     ] |     ] | ||||||
| 
 | 
 | ||||||
|     if file_ext == "pdf": |     if file_ext == "pdf": | ||||||
|         loader = PyPDFLoader(file_path) |         loader = PyPDFLoader(file_path, extract_images=True) | ||||||
|     elif file_ext == "csv": |     elif file_ext == "csv": | ||||||
|         loader = CSVLoader(file_path) |         loader = CSVLoader(file_path) | ||||||
|     elif file_ext == "rst": |     elif file_ext == "rst": | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ import json | ||||||
| from utils.utils import get_admin_user | from utils.utils import get_admin_user | ||||||
| from utils.misc import calculate_sha256, get_gravatar_url | from utils.misc import calculate_sha256, get_gravatar_url | ||||||
| 
 | 
 | ||||||
| from config import OLLAMA_API_BASE_URL, DATA_DIR, UPLOAD_DIR | from config import OLLAMA_BASE_URLS, DATA_DIR, UPLOAD_DIR | ||||||
| from constants import ERROR_MESSAGES | from constants import ERROR_MESSAGES | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -75,7 +75,7 @@ async def download_file_stream(url, file_path, file_name, chunk_size=1024 * 1024 | ||||||
|                     hashed = calculate_sha256(file) |                     hashed = calculate_sha256(file) | ||||||
|                     file.seek(0) |                     file.seek(0) | ||||||
| 
 | 
 | ||||||
|                     url = f"{OLLAMA_API_BASE_URL}/blobs/sha256:{hashed}" |                     url = f"{OLLAMA_BASE_URLS[0]}/blobs/sha256:{hashed}" | ||||||
|                     response = requests.post(url, data=file) |                     response = requests.post(url, data=file) | ||||||
| 
 | 
 | ||||||
|                     if response.ok: |                     if response.ok: | ||||||
|  | @ -147,7 +147,7 @@ def upload(file: UploadFile = File(...)): | ||||||
|                     hashed = calculate_sha256(f) |                     hashed = calculate_sha256(f) | ||||||
|                     f.seek(0) |                     f.seek(0) | ||||||
| 
 | 
 | ||||||
|                     url = f"{OLLAMA_API_BASE_URL}/blobs/sha256:{hashed}" |                     url = f"{OLLAMA_BASE_URLS[0]}/blobs/sha256:{hashed}" | ||||||
|                     response = requests.post(url, data=f) |                     response = requests.post(url, data=f) | ||||||
| 
 | 
 | ||||||
|                     if response.ok: |                     if response.ok: | ||||||
|  |  | ||||||
|  | @ -200,27 +200,32 @@ if not os.path.exists(LITELLM_CONFIG_PATH): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| #################################### | #################################### | ||||||
| # OLLAMA_API_BASE_URL | # OLLAMA_BASE_URL | ||||||
| #################################### | #################################### | ||||||
| 
 | 
 | ||||||
| OLLAMA_API_BASE_URL = os.environ.get( | OLLAMA_API_BASE_URL = os.environ.get( | ||||||
|     "OLLAMA_API_BASE_URL", "http://localhost:11434/api" |     "OLLAMA_API_BASE_URL", "http://localhost:11434/api" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| if ENV == "prod": |  | ||||||
|     if OLLAMA_API_BASE_URL == "/ollama/api": |  | ||||||
|         OLLAMA_API_BASE_URL = "http://host.docker.internal:11434/api" |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "") | OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "") | ||||||
| 
 | 
 | ||||||
| if OLLAMA_BASE_URL == "": | if ENV == "prod": | ||||||
|  |     if OLLAMA_BASE_URL == "/ollama": | ||||||
|  |         OLLAMA_BASE_URL = "http://host.docker.internal:11434" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "": | ||||||
|     OLLAMA_BASE_URL = ( |     OLLAMA_BASE_URL = ( | ||||||
|         OLLAMA_API_BASE_URL[:-4] |         OLLAMA_API_BASE_URL[:-4] | ||||||
|         if OLLAMA_API_BASE_URL.endswith("/api") |         if OLLAMA_API_BASE_URL.endswith("/api") | ||||||
|         else OLLAMA_API_BASE_URL |         else OLLAMA_API_BASE_URL | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|  | OLLAMA_BASE_URLS = os.environ.get("OLLAMA_BASE_URLS", "") | ||||||
|  | OLLAMA_BASE_URLS = OLLAMA_BASE_URLS if OLLAMA_BASE_URLS != "" else OLLAMA_BASE_URL | ||||||
|  | 
 | ||||||
|  | OLLAMA_BASE_URLS = [url.strip() for url in OLLAMA_BASE_URLS.split(";")] | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| #################################### | #################################### | ||||||
| # OPENAI_API | # OPENAI_API | ||||||
|  | @ -229,9 +234,25 @@ if OLLAMA_BASE_URL == "": | ||||||
| OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "") | OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "") | ||||||
| OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "") | OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "") | ||||||
| 
 | 
 | ||||||
|  | if OPENAI_API_KEY == "": | ||||||
|  |     OPENAI_API_KEY = "none" | ||||||
|  | 
 | ||||||
| if OPENAI_API_BASE_URL == "": | if OPENAI_API_BASE_URL == "": | ||||||
|     OPENAI_API_BASE_URL = "https://api.openai.com/v1" |     OPENAI_API_BASE_URL = "https://api.openai.com/v1" | ||||||
| 
 | 
 | ||||||
|  | OPENAI_API_KEYS = os.environ.get("OPENAI_API_KEYS", "") | ||||||
|  | OPENAI_API_KEYS = OPENAI_API_KEYS if OPENAI_API_KEYS != "" else OPENAI_API_KEY | ||||||
|  | 
 | ||||||
|  | OPENAI_API_KEYS = [url.strip() for url in OPENAI_API_KEYS.split(";")] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | OPENAI_API_BASE_URLS = os.environ.get("OPENAI_API_BASE_URLS", "") | ||||||
|  | OPENAI_API_BASE_URLS = ( | ||||||
|  |     OPENAI_API_BASE_URLS if OPENAI_API_BASE_URLS != "" else OPENAI_API_BASE_URL | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | OPENAI_API_BASE_URLS = [url.strip() for url in OPENAI_API_BASE_URL.split(";")] | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| #################################### | #################################### | ||||||
| # WEBUI | # WEBUI | ||||||
|  |  | ||||||
|  | @ -41,6 +41,7 @@ class ERROR_MESSAGES(str, Enum): | ||||||
|     NOT_FOUND = "We could not find what you're looking for :/" |     NOT_FOUND = "We could not find what you're looking for :/" | ||||||
|     USER_NOT_FOUND = "We could not find what you're looking for :/" |     USER_NOT_FOUND = "We could not find what you're looking for :/" | ||||||
|     API_KEY_NOT_FOUND = "Oops! It looks like there's a hiccup. The API key is missing. Please make sure to provide a valid API key to access this feature." |     API_KEY_NOT_FOUND = "Oops! It looks like there's a hiccup. The API key is missing. Please make sure to provide a valid API key to access this feature." | ||||||
|  | 
 | ||||||
|     MALICIOUS = "Unusual activities detected, please try again in a few minutes." |     MALICIOUS = "Unusual activities detected, please try again in a few minutes." | ||||||
| 
 | 
 | ||||||
|     PANDOC_NOT_INSTALLED = "Pandoc is not installed on the server. Please contact your administrator for assistance." |     PANDOC_NOT_INSTALLED = "Pandoc is not installed on the server. Please contact your administrator for assistance." | ||||||
|  | @ -50,3 +51,4 @@ class ERROR_MESSAGES(str, Enum): | ||||||
|     RATE_LIMIT_EXCEEDED = "API rate limit exceeded" |     RATE_LIMIT_EXCEEDED = "API rate limit exceeded" | ||||||
| 
 | 
 | ||||||
|     MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found" |     MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found" | ||||||
|  |     OPENAI_NOT_FOUND = lambda name="": f"OpenAI API was not found" | ||||||
|  |  | ||||||
|  | @ -35,6 +35,9 @@ openpyxl | ||||||
| pyxlsb | pyxlsb | ||||||
| xlrd | xlrd | ||||||
| 
 | 
 | ||||||
|  | opencv-python-headless | ||||||
|  | rapidocr-onnxruntime | ||||||
|  | 
 | ||||||
| faster-whisper | faster-whisper | ||||||
| 
 | 
 | ||||||
| PyJWT | PyJWT | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ services: | ||||||
|     build: |     build: | ||||||
|       context: . |       context: . | ||||||
|       args: |       args: | ||||||
|         OLLAMA_API_BASE_URL: '/ollama/api' |         OLLAMA_BASE_URL: '/ollama' | ||||||
|       dockerfile: Dockerfile |       dockerfile: Dockerfile | ||||||
|     image: ghcr.io/open-webui/open-webui:main |     image: ghcr.io/open-webui/open-webui:main | ||||||
|     container_name: open-webui |     container_name: open-webui | ||||||
|  | @ -25,7 +25,7 @@ services: | ||||||
|     ports: |     ports: | ||||||
|       - ${OPEN_WEBUI_PORT-3000}:8080 |       - ${OPEN_WEBUI_PORT-3000}:8080 | ||||||
|     environment: |     environment: | ||||||
|       - 'OLLAMA_API_BASE_URL=http://ollama:11434/api' |       - 'OLLAMA_BASE_URL=http://ollama:11434' | ||||||
|       - 'WEBUI_SECRET_KEY=' |       - 'WEBUI_SECRET_KEY=' | ||||||
|     extra_hosts: |     extra_hosts: | ||||||
|       - host.docker.internal:host-gateway |       - host.docker.internal:host-gateway | ||||||
|  |  | ||||||
|  | @ -40,7 +40,7 @@ spec: | ||||||
|         - name: data |         - name: data | ||||||
|           mountPath: /app/backend/data |           mountPath: /app/backend/data | ||||||
|         env: |         env: | ||||||
|         - name: OLLAMA_API_BASE_URL |         - name: OLLAMA_BASE_URL | ||||||
|           value: {{ include "ollama.url" . | quote }} |           value: {{ include "ollama.url" . | quote }} | ||||||
|         tty: true |         tty: true | ||||||
|       {{- with .Values.webui.nodeSelector }} |       {{- with .Values.webui.nodeSelector }} | ||||||
|  |  | ||||||
|  | @ -26,8 +26,8 @@ spec: | ||||||
|             cpu: "1000m" |             cpu: "1000m" | ||||||
|             memory: "1Gi" |             memory: "1Gi" | ||||||
|         env: |         env: | ||||||
|         - name: OLLAMA_API_BASE_URL |         - name: OLLAMA_BASE_URL | ||||||
|           value: "http://ollama-service.open-webui.svc.cluster.local:11434/api" |           value: "http://ollama-service.open-webui.svc.cluster.local:11434" | ||||||
|         tty: true |         tty: true | ||||||
|         volumeMounts: |         volumeMounts: | ||||||
|         - name: webui-volume |         - name: webui-volume | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| { | { | ||||||
| 	"name": "open-webui", | 	"name": "open-webui", | ||||||
| 	"version": "0.1.108", | 	"version": "0.1.110", | ||||||
| 	"private": true, | 	"private": true, | ||||||
| 	"scripts": { | 	"scripts": { | ||||||
| 		"dev": "vite dev --host", | 		"dev": "vite dev --host", | ||||||
|  |  | ||||||
|  | @ -43,6 +43,10 @@ ol > li { | ||||||
| 	font-weight: 400; | 	font-weight: 400; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | li p { | ||||||
|  | 	display: inline; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| ::-webkit-scrollbar-thumb { | ::-webkit-scrollbar-thumb { | ||||||
| 	--tw-border-opacity: 1; | 	--tw-border-opacity: 1; | ||||||
| 	background-color: rgba(217, 217, 227, 0.8); | 	background-color: rgba(217, 217, 227, 0.8); | ||||||
|  |  | ||||||
|  | @ -1,9 +1,9 @@ | ||||||
| import { OPENAI_API_BASE_URL } from '$lib/constants'; | import { OPENAI_API_BASE_URL } from '$lib/constants'; | ||||||
| 
 | 
 | ||||||
| export const getOpenAIUrl = async (token: string = '') => { | export const getOpenAIUrls = async (token: string = '') => { | ||||||
| 	let error = null; | 	let error = null; | ||||||
| 
 | 
 | ||||||
| 	const res = await fetch(`${OPENAI_API_BASE_URL}/url`, { | 	const res = await fetch(`${OPENAI_API_BASE_URL}/urls`, { | ||||||
| 		method: 'GET', | 		method: 'GET', | ||||||
| 		headers: { | 		headers: { | ||||||
| 			Accept: 'application/json', | 			Accept: 'application/json', | ||||||
|  | @ -29,13 +29,13 @@ export const getOpenAIUrl = async (token: string = '') => { | ||||||
| 		throw error; | 		throw error; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return res.OPENAI_API_BASE_URL; | 	return res.OPENAI_API_BASE_URLS; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const updateOpenAIUrl = async (token: string = '', url: string) => { | export const updateOpenAIUrls = async (token: string = '', urls: string[]) => { | ||||||
| 	let error = null; | 	let error = null; | ||||||
| 
 | 
 | ||||||
| 	const res = await fetch(`${OPENAI_API_BASE_URL}/url/update`, { | 	const res = await fetch(`${OPENAI_API_BASE_URL}/urls/update`, { | ||||||
| 		method: 'POST', | 		method: 'POST', | ||||||
| 		headers: { | 		headers: { | ||||||
| 			Accept: 'application/json', | 			Accept: 'application/json', | ||||||
|  | @ -43,7 +43,7 @@ export const updateOpenAIUrl = async (token: string = '', url: string) => { | ||||||
| 			...(token && { authorization: `Bearer ${token}` }) | 			...(token && { authorization: `Bearer ${token}` }) | ||||||
| 		}, | 		}, | ||||||
| 		body: JSON.stringify({ | 		body: JSON.stringify({ | ||||||
| 			url: url | 			urls: urls | ||||||
| 		}) | 		}) | ||||||
| 	}) | 	}) | ||||||
| 		.then(async (res) => { | 		.then(async (res) => { | ||||||
|  | @ -64,13 +64,13 @@ export const updateOpenAIUrl = async (token: string = '', url: string) => { | ||||||
| 		throw error; | 		throw error; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return res.OPENAI_API_BASE_URL; | 	return res.OPENAI_API_BASE_URLS; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const getOpenAIKey = async (token: string = '') => { | export const getOpenAIKeys = async (token: string = '') => { | ||||||
| 	let error = null; | 	let error = null; | ||||||
| 
 | 
 | ||||||
| 	const res = await fetch(`${OPENAI_API_BASE_URL}/key`, { | 	const res = await fetch(`${OPENAI_API_BASE_URL}/keys`, { | ||||||
| 		method: 'GET', | 		method: 'GET', | ||||||
| 		headers: { | 		headers: { | ||||||
| 			Accept: 'application/json', | 			Accept: 'application/json', | ||||||
|  | @ -96,13 +96,13 @@ export const getOpenAIKey = async (token: string = '') => { | ||||||
| 		throw error; | 		throw error; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return res.OPENAI_API_KEY; | 	return res.OPENAI_API_KEYS; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const updateOpenAIKey = async (token: string = '', key: string) => { | export const updateOpenAIKeys = async (token: string = '', keys: string[]) => { | ||||||
| 	let error = null; | 	let error = null; | ||||||
| 
 | 
 | ||||||
| 	const res = await fetch(`${OPENAI_API_BASE_URL}/key/update`, { | 	const res = await fetch(`${OPENAI_API_BASE_URL}/keys/update`, { | ||||||
| 		method: 'POST', | 		method: 'POST', | ||||||
| 		headers: { | 		headers: { | ||||||
| 			Accept: 'application/json', | 			Accept: 'application/json', | ||||||
|  | @ -110,7 +110,7 @@ export const updateOpenAIKey = async (token: string = '', key: string) => { | ||||||
| 			...(token && { authorization: `Bearer ${token}` }) | 			...(token && { authorization: `Bearer ${token}` }) | ||||||
| 		}, | 		}, | ||||||
| 		body: JSON.stringify({ | 		body: JSON.stringify({ | ||||||
| 			key: key | 			keys: keys | ||||||
| 		}) | 		}) | ||||||
| 	}) | 	}) | ||||||
| 		.then(async (res) => { | 		.then(async (res) => { | ||||||
|  | @ -131,7 +131,7 @@ export const updateOpenAIKey = async (token: string = '', key: string) => { | ||||||
| 		throw error; | 		throw error; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return res.OPENAI_API_KEY; | 	return res.OPENAI_API_KEYS; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const getOpenAIModels = async (token: string = '') => { | export const getOpenAIModels = async (token: string = '') => { | ||||||
|  |  | ||||||
|  | @ -225,33 +225,80 @@ | ||||||
| 		}, 100); | 		}, 100); | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	// TODO: change delete behaviour |  | ||||||
| 	// const deleteMessageAndDescendants = async (messageId: string) => { |  | ||||||
| 	// 	if (history.messages[messageId]) { |  | ||||||
| 	// 		history.messages[messageId].deleted = true; |  | ||||||
| 
 |  | ||||||
| 	// 		for (const childId of history.messages[messageId].childrenIds) { |  | ||||||
| 	// 			await deleteMessageAndDescendants(childId); |  | ||||||
| 	// 		} |  | ||||||
| 	// 	} |  | ||||||
| 	// }; |  | ||||||
| 
 |  | ||||||
| 	// const triggerDeleteMessageRecursive = async (messageId: string) => { |  | ||||||
| 	// 	await deleteMessageAndDescendants(messageId); |  | ||||||
| 	// 	await updateChatById(localStorage.token, chatId, { history }); |  | ||||||
| 	// 	await chats.set(await getChatList(localStorage.token)); |  | ||||||
| 	// }; |  | ||||||
| 
 |  | ||||||
| 	const messageDeleteHandler = async (messageId) => { | 	const messageDeleteHandler = async (messageId) => { | ||||||
| 		if (history.messages[messageId]) { | 		const messageToDelete = history.messages[messageId]; | ||||||
| 			history.messages[messageId].deleted = true; | 		const messageParentId = messageToDelete.parentId; | ||||||
| 
 | 		const messageChildrenIds = messageToDelete.childrenIds ?? []; | ||||||
| 			for (const childId of history.messages[messageId].childrenIds) { | 		const hasSibling = messageChildrenIds.some( | ||||||
| 				history.messages[childId].deleted = true; | 			(childId) => history.messages[childId]?.childrenIds?.length > 0 | ||||||
|  | 		); | ||||||
|  | 		messageChildrenIds.forEach((childId) => { | ||||||
|  | 			const child = history.messages[childId]; | ||||||
|  | 			if (child && child.childrenIds) { | ||||||
|  | 				if (child.childrenIds.length === 0 && !hasSibling) { | ||||||
|  | 					// if last prompt/response pair | ||||||
|  | 					history.messages[messageParentId].childrenIds = []; | ||||||
|  | 					history.currentId = messageParentId; | ||||||
|  | 				} else { | ||||||
|  | 					child.childrenIds.forEach((grandChildId) => { | ||||||
|  | 						if (history.messages[grandChildId]) { | ||||||
|  | 							history.messages[grandChildId].parentId = messageParentId; | ||||||
|  | 							history.messages[messageParentId].childrenIds.push(grandChildId); | ||||||
|  | 						} | ||||||
|  | 					}); | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		await updateChatById(localStorage.token, chatId, { history }); | 			// remove response | ||||||
|  | 			history.messages[messageParentId].childrenIds = history.messages[ | ||||||
|  | 				messageParentId | ||||||
|  | 			].childrenIds.filter((id) => id !== childId); | ||||||
|  | 		}); | ||||||
|  | 		// remove prompt | ||||||
|  | 		history.messages[messageParentId].childrenIds = history.messages[ | ||||||
|  | 			messageParentId | ||||||
|  | 		].childrenIds.filter((id) => id !== messageId); | ||||||
|  | 		await updateChatById(localStorage.token, chatId, { | ||||||
|  | 			messages: messages, | ||||||
|  | 			history: history | ||||||
|  | 		}); | ||||||
| 	}; | 	}; | ||||||
|  | 
 | ||||||
|  | 	// const messageDeleteHandler = async (messageId) => { | ||||||
|  | 	// 	const message = history.messages[messageId]; | ||||||
|  | 	// 	const parentId = message.parentId; | ||||||
|  | 	// 	const childrenIds = message.childrenIds ?? []; | ||||||
|  | 	// 	const grandchildrenIds = []; | ||||||
|  | 
 | ||||||
|  | 	// 	// Iterate through childrenIds to find grandchildrenIds | ||||||
|  | 	// 	for (const childId of childrenIds) { | ||||||
|  | 	// 		const childMessage = history.messages[childId]; | ||||||
|  | 	// 		const grandChildrenIds = childMessage.childrenIds ?? []; | ||||||
|  | 
 | ||||||
|  | 	// 		for (const grandchildId of grandchildrenIds) { | ||||||
|  | 	// 			const childMessage = history.messages[grandchildId]; | ||||||
|  | 	// 			childMessage.parentId = parentId; | ||||||
|  | 	// 		} | ||||||
|  | 	// 		grandchildrenIds.push(...grandChildrenIds); | ||||||
|  | 	// 	} | ||||||
|  | 
 | ||||||
|  | 	// 	history.messages[parentId].childrenIds.push(...grandchildrenIds); | ||||||
|  | 	// 	history.messages[parentId].childrenIds = history.messages[parentId].childrenIds.filter( | ||||||
|  | 	// 		(id) => id !== messageId | ||||||
|  | 	// 	); | ||||||
|  | 
 | ||||||
|  | 	// 	// Select latest message | ||||||
|  | 	// 	let currentMessageId = grandchildrenIds.at(-1); | ||||||
|  | 	// 	if (currentMessageId) { | ||||||
|  | 	// 		let messageChildrenIds = history.messages[currentMessageId].childrenIds; | ||||||
|  | 	// 		while (messageChildrenIds.length !== 0) { | ||||||
|  | 	// 			currentMessageId = messageChildrenIds.at(-1); | ||||||
|  | 	// 			messageChildrenIds = history.messages[currentMessageId].childrenIds; | ||||||
|  | 	// 		} | ||||||
|  | 	// 		history.currentId = currentMessageId; | ||||||
|  | 	// 	} | ||||||
|  | 
 | ||||||
|  | 	// 	await updateChatById(localStorage.token, chatId, { messages, history }); | ||||||
|  | 	// }; | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| {#if messages.length == 0} | {#if messages.length == 0} | ||||||
|  | @ -260,7 +307,6 @@ | ||||||
| 	<div class=" pb-10"> | 	<div class=" pb-10"> | ||||||
| 		{#key chatId} | 		{#key chatId} | ||||||
| 			{#each messages as message, messageIdx} | 			{#each messages as message, messageIdx} | ||||||
| 				{#if !message.deleted} |  | ||||||
| 				<div class=" w-full"> | 				<div class=" w-full"> | ||||||
| 					<div | 					<div | ||||||
| 						class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null | 						class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null | ||||||
|  | @ -310,7 +356,6 @@ | ||||||
| 						{/if} | 						{/if} | ||||||
| 					</div> | 					</div> | ||||||
| 				</div> | 				</div> | ||||||
| 				{/if} |  | ||||||
| 			{/each} | 			{/each} | ||||||
| 
 | 
 | ||||||
| 			{#if bottomPadding} | 			{#if bottomPadding} | ||||||
|  |  | ||||||
|  | @ -24,6 +24,7 @@ | ||||||
| 	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'; | 	import { WEBUI_BASE_URL } from '$lib/constants'; | ||||||
|  | 	import Tooltip from '$lib/components/common/Tooltip.svelte'; | ||||||
| 
 | 
 | ||||||
| 	export let modelfiles = []; | 	export let modelfiles = []; | ||||||
| 	export let message; | 	export let message; | ||||||
|  | @ -346,6 +347,7 @@ | ||||||
| 									class=" bg-transparent outline-none w-full resize-none" | 									class=" bg-transparent outline-none w-full resize-none" | ||||||
| 									bind:value={editedContent} | 									bind:value={editedContent} | ||||||
| 									on:input={(e) => { | 									on:input={(e) => { | ||||||
|  | 										e.target.style.height = ''; | ||||||
| 										e.target.style.height = `${e.target.scrollHeight}px`; | 										e.target.style.height = `${e.target.scrollHeight}px`; | ||||||
| 									}} | 									}} | ||||||
| 								/> | 								/> | ||||||
|  | @ -464,6 +466,7 @@ | ||||||
| 											</div> | 											</div> | ||||||
| 										{/if} | 										{/if} | ||||||
| 
 | 
 | ||||||
|  | 										<Tooltip content="Edit" placement="bottom"> | ||||||
| 											<button | 											<button | ||||||
| 												class="{isLastMessage | 												class="{isLastMessage | ||||||
| 													? 'visible' | 													? 'visible' | ||||||
|  | @ -487,7 +490,9 @@ | ||||||
| 													/> | 													/> | ||||||
| 												</svg> | 												</svg> | ||||||
| 											</button> | 											</button> | ||||||
|  | 										</Tooltip> | ||||||
| 
 | 
 | ||||||
|  | 										<Tooltip content="Copy" placement="bottom"> | ||||||
| 											<button | 											<button | ||||||
| 												class="{isLastMessage | 												class="{isLastMessage | ||||||
| 													? 'visible' | 													? 'visible' | ||||||
|  | @ -511,7 +516,9 @@ | ||||||
| 													/> | 													/> | ||||||
| 												</svg> | 												</svg> | ||||||
| 											</button> | 											</button> | ||||||
|  | 										</Tooltip> | ||||||
| 
 | 
 | ||||||
|  | 										<Tooltip content="Good Response" placement="bottom"> | ||||||
| 											<button | 											<button | ||||||
| 												class="{isLastMessage | 												class="{isLastMessage | ||||||
| 													? 'visible' | 													? 'visible' | ||||||
|  | @ -536,6 +543,9 @@ | ||||||
| 													/></svg | 													/></svg | ||||||
| 												> | 												> | ||||||
| 											</button> | 											</button> | ||||||
|  | 										</Tooltip> | ||||||
|  | 
 | ||||||
|  | 										<Tooltip content="Bad Response" placement="bottom"> | ||||||
| 											<button | 											<button | ||||||
| 												class="{isLastMessage | 												class="{isLastMessage | ||||||
| 													? 'visible' | 													? 'visible' | ||||||
|  | @ -560,7 +570,9 @@ | ||||||
| 													/></svg | 													/></svg | ||||||
| 												> | 												> | ||||||
| 											</button> | 											</button> | ||||||
|  | 										</Tooltip> | ||||||
| 
 | 
 | ||||||
|  | 										<Tooltip content="Read Aloud" placement="bottom"> | ||||||
| 											<button | 											<button | ||||||
| 												id="speak-button-{message.id}" | 												id="speak-button-{message.id}" | ||||||
| 												class="{isLastMessage | 												class="{isLastMessage | ||||||
|  | @ -600,7 +612,12 @@ | ||||||
| 															cx="12" | 															cx="12" | ||||||
| 															cy="12" | 															cy="12" | ||||||
| 															r="3" | 															r="3" | ||||||
| 													/><circle class="spinner_S1WN spinner_JApP" cx="20" cy="12" r="3" /></svg | 														/><circle | ||||||
|  | 															class="spinner_S1WN spinner_JApP" | ||||||
|  | 															cx="20" | ||||||
|  | 															cy="12" | ||||||
|  | 															r="3" | ||||||
|  | 														/></svg | ||||||
| 													> | 													> | ||||||
| 												{:else if speaking} | 												{:else if speaking} | ||||||
| 													<svg | 													<svg | ||||||
|  | @ -634,8 +651,10 @@ | ||||||
| 													</svg> | 													</svg> | ||||||
| 												{/if} | 												{/if} | ||||||
| 											</button> | 											</button> | ||||||
|  | 										</Tooltip> | ||||||
| 
 | 
 | ||||||
| 										{#if $config.images} | 										{#if $config.images} | ||||||
|  | 											<Tooltip content="Generate Image" placement="bottom"> | ||||||
| 												<button | 												<button | ||||||
| 													class="{isLastMessage | 													class="{isLastMessage | ||||||
| 														? 'visible' | 														? 'visible' | ||||||
|  | @ -698,9 +717,11 @@ | ||||||
| 														</svg> | 														</svg> | ||||||
| 													{/if} | 													{/if} | ||||||
| 												</button> | 												</button> | ||||||
|  | 											</Tooltip> | ||||||
| 										{/if} | 										{/if} | ||||||
| 
 | 
 | ||||||
| 										{#if message.info} | 										{#if message.info} | ||||||
|  | 											<Tooltip content="Generation Info" placement="bottom"> | ||||||
| 												<button | 												<button | ||||||
| 													class=" {isLastMessage | 													class=" {isLastMessage | ||||||
| 														? 'visible' | 														? 'visible' | ||||||
|  | @ -725,9 +746,11 @@ | ||||||
| 														/> | 														/> | ||||||
| 													</svg> | 													</svg> | ||||||
| 												</button> | 												</button> | ||||||
|  | 											</Tooltip> | ||||||
| 										{/if} | 										{/if} | ||||||
| 
 | 
 | ||||||
| 										{#if isLastMessage} | 										{#if isLastMessage} | ||||||
|  | 											<Tooltip content="Continue Response" placement="bottom"> | ||||||
| 												<button | 												<button | ||||||
| 													type="button" | 													type="button" | ||||||
| 													class="{isLastMessage | 													class="{isLastMessage | ||||||
|  | @ -757,7 +780,9 @@ | ||||||
| 														/> | 														/> | ||||||
| 													</svg> | 													</svg> | ||||||
| 												</button> | 												</button> | ||||||
|  | 											</Tooltip> | ||||||
| 
 | 
 | ||||||
|  | 											<Tooltip content="Regenerate" placement="bottom"> | ||||||
| 												<button | 												<button | ||||||
| 													type="button" | 													type="button" | ||||||
| 													class="{isLastMessage | 													class="{isLastMessage | ||||||
|  | @ -780,6 +805,7 @@ | ||||||
| 														/> | 														/> | ||||||
| 													</svg> | 													</svg> | ||||||
| 												</button> | 												</button> | ||||||
|  | 											</Tooltip> | ||||||
| 										{/if} | 										{/if} | ||||||
| 									</div> | 									</div> | ||||||
| 								{/if} | 								{/if} | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ | ||||||
| 	import Name from './Name.svelte'; | 	import Name from './Name.svelte'; | ||||||
| 	import ProfileImage from './ProfileImage.svelte'; | 	import ProfileImage from './ProfileImage.svelte'; | ||||||
| 	import { modelfiles, settings } from '$lib/stores'; | 	import { modelfiles, settings } from '$lib/stores'; | ||||||
|  | 	import Tooltip from '$lib/components/common/Tooltip.svelte'; | ||||||
| 
 | 
 | ||||||
| 	const i18n = getContext('i18n'); | 	const i18n = getContext('i18n'); | ||||||
| 
 | 
 | ||||||
|  | @ -171,7 +172,8 @@ | ||||||
| 						class=" bg-transparent outline-none w-full resize-none" | 						class=" bg-transparent outline-none w-full resize-none" | ||||||
| 						bind:value={editedContent} | 						bind:value={editedContent} | ||||||
| 						on:input={(e) => { | 						on:input={(e) => { | ||||||
| 							messageEditTextAreaElement.style.height = `${messageEditTextAreaElement.scrollHeight}px`; | 							e.target.style.height = ''; | ||||||
|  | 							e.target.style.height = `${e.target.scrollHeight}px`; | ||||||
| 						}} | 						}} | ||||||
| 					/> | 					/> | ||||||
| 
 | 
 | ||||||
|  | @ -248,6 +250,7 @@ | ||||||
| 							</div> | 							</div> | ||||||
| 						{/if} | 						{/if} | ||||||
| 
 | 
 | ||||||
|  | 						<Tooltip content="Edit" placement="bottom"> | ||||||
| 							<button | 							<button | ||||||
| 								class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button" | 								class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button" | ||||||
| 								on:click={() => { | 								on:click={() => { | ||||||
|  | @ -269,7 +272,9 @@ | ||||||
| 									/> | 									/> | ||||||
| 								</svg> | 								</svg> | ||||||
| 							</button> | 							</button> | ||||||
|  | 						</Tooltip> | ||||||
| 
 | 
 | ||||||
|  | 						<Tooltip content="Copy" placement="bottom"> | ||||||
| 							<button | 							<button | ||||||
| 								class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition" | 								class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition" | ||||||
| 								on:click={() => { | 								on:click={() => { | ||||||
|  | @ -291,8 +296,10 @@ | ||||||
| 									/> | 									/> | ||||||
| 								</svg> | 								</svg> | ||||||
| 							</button> | 							</button> | ||||||
|  | 						</Tooltip> | ||||||
| 
 | 
 | ||||||
| 						{#if !isFirstMessage} | 						{#if !isFirstMessage} | ||||||
|  | 							<Tooltip content="Delete" placement="bottom"> | ||||||
| 								<button | 								<button | ||||||
| 									class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition" | 									class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition" | ||||||
| 									on:click={() => { | 									on:click={() => { | ||||||
|  | @ -314,6 +321,7 @@ | ||||||
| 										/> | 										/> | ||||||
| 									</svg> | 									</svg> | ||||||
| 								</button> | 								</button> | ||||||
|  | 							</Tooltip> | ||||||
| 						{/if} | 						{/if} | ||||||
| 					</div> | 					</div> | ||||||
| 				</div> | 				</div> | ||||||
|  |  | ||||||
|  | @ -4,7 +4,12 @@ | ||||||
| 	const dispatch = createEventDispatcher(); | 	const dispatch = createEventDispatcher(); | ||||||
| 
 | 
 | ||||||
| 	import { getOllamaUrls, getOllamaVersion, updateOllamaUrls } from '$lib/apis/ollama'; | 	import { getOllamaUrls, getOllamaVersion, updateOllamaUrls } from '$lib/apis/ollama'; | ||||||
| 	import { getOpenAIKey, getOpenAIUrl, updateOpenAIKey, updateOpenAIUrl } from '$lib/apis/openai'; | 	import { | ||||||
|  | 		getOpenAIKeys, | ||||||
|  | 		getOpenAIUrls, | ||||||
|  | 		updateOpenAIKeys, | ||||||
|  | 		updateOpenAIUrls | ||||||
|  | 	} from '$lib/apis/openai'; | ||||||
| 	import { toast } from 'svelte-sonner'; | 	import { toast } from 'svelte-sonner'; | ||||||
| 
 | 
 | ||||||
| 	const i18n = getContext('i18n'); | 	const i18n = getContext('i18n'); | ||||||
|  | @ -18,12 +23,14 @@ | ||||||
| 	let OPENAI_API_KEY = ''; | 	let OPENAI_API_KEY = ''; | ||||||
| 	let OPENAI_API_BASE_URL = ''; | 	let OPENAI_API_BASE_URL = ''; | ||||||
| 
 | 
 | ||||||
|  | 	let OPENAI_API_KEYS = ['']; | ||||||
|  | 	let OPENAI_API_BASE_URLS = ['']; | ||||||
|  | 
 | ||||||
| 	let showOpenAI = false; | 	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_URLS = await updateOpenAIUrls(localStorage.token, OPENAI_API_BASE_URLS); | ||||||
| 		OPENAI_API_KEY = await updateOpenAIKey(localStorage.token, OPENAI_API_KEY); | 		OPENAI_API_KEYS = await updateOpenAIKeys(localStorage.token, OPENAI_API_KEYS); | ||||||
| 
 | 
 | ||||||
| 		await models.set(await getModels()); | 		await models.set(await getModels()); | ||||||
| 	}; | 	}; | ||||||
|  | @ -45,8 +52,8 @@ | ||||||
| 	onMount(async () => { | 	onMount(async () => { | ||||||
| 		if ($user.role === 'admin') { | 		if ($user.role === 'admin') { | ||||||
| 			OLLAMA_BASE_URLS = await getOllamaUrls(localStorage.token); | 			OLLAMA_BASE_URLS = await getOllamaUrls(localStorage.token); | ||||||
| 			OPENAI_API_BASE_URL = await getOpenAIUrl(localStorage.token); | 			OPENAI_API_BASE_URLS = await getOpenAIUrls(localStorage.token); | ||||||
| 			OPENAI_API_KEY = await getOpenAIKey(localStorage.token); | 			OPENAI_API_KEYS = await getOpenAIKeys(localStorage.token); | ||||||
| 		} | 		} | ||||||
| 	}); | 	}); | ||||||
| </script> | </script> | ||||||
|  | @ -73,38 +80,75 @@ | ||||||
| 				</div> | 				</div> | ||||||
| 
 | 
 | ||||||
| 				{#if showOpenAI} | 				{#if showOpenAI} | ||||||
| 					<div> | 					<div class="flex flex-col gap-1"> | ||||||
| 						<div class=" mb-2.5 text-sm font-medium">{$i18n.t('API Key')}</div> | 						{#each OPENAI_API_BASE_URLS as url, idx} | ||||||
| 						<div class="flex w-full"> | 							<div class="flex w-full gap-2"> | ||||||
| 								<div class="flex-1"> | 								<div class="flex-1"> | ||||||
| 									<input | 									<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-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" | ||||||
| 									placeholder={$i18n.t('Enter OpenAI API Key')} | 										placeholder={$i18n.t('API Base URL')} | ||||||
| 									bind:value={OPENAI_API_KEY} | 										bind:value={url} | ||||||
| 										autocomplete="off" | 										autocomplete="off" | ||||||
| 									/> | 									/> | ||||||
| 								</div> | 								</div> | ||||||
| 						</div> |  | ||||||
| 					</div> |  | ||||||
| 
 | 
 | ||||||
| 					<div> |  | ||||||
| 						<div class=" mb-2.5 text-sm font-medium">{$i18n.t('API Base URL')}</div> |  | ||||||
| 						<div class="flex w-full"> |  | ||||||
| 								<div class="flex-1"> | 								<div class="flex-1"> | ||||||
| 									<input | 									<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-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" | ||||||
| 									placeholder="Enter OpenAI API Base URL" | 										placeholder={$i18n.t('API Key')} | ||||||
| 									bind:value={OPENAI_API_BASE_URL} | 										bind:value={OPENAI_API_KEYS[idx]} | ||||||
| 										autocomplete="off" | 										autocomplete="off" | ||||||
| 									/> | 									/> | ||||||
| 								</div> | 								</div> | ||||||
| 						</div> | 								<div class="self-center flex items-center"> | ||||||
| 						<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> | 									{#if idx === 0} | ||||||
| 							WebUI will make requests to <span class=" text-gray-200" | 										<button | ||||||
| 								>'{OPENAI_API_BASE_URL}/chat'</span | 											class="px-1" | ||||||
|  | 											on:click={() => { | ||||||
|  | 												OPENAI_API_BASE_URLS = [...OPENAI_API_BASE_URLS, '']; | ||||||
|  | 												OPENAI_API_KEYS = [...OPENAI_API_KEYS, '']; | ||||||
|  | 											}} | ||||||
|  | 											type="button" | ||||||
| 										> | 										> | ||||||
|  | 											<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> | ||||||
|  | 									{:else} | ||||||
|  | 										<button | ||||||
|  | 											class="px-1" | ||||||
|  | 											on:click={() => { | ||||||
|  | 												OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.filter( | ||||||
|  | 													(url, urlIdx) => idx !== urlIdx | ||||||
|  | 												); | ||||||
|  | 												OPENAI_API_KEYS = OPENAI_API_KEYS.filter((key, keyIdx) => idx !== keyIdx); | ||||||
|  | 											}} | ||||||
|  | 											type="button" | ||||||
|  | 										> | ||||||
|  | 											<svg | ||||||
|  | 												xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 												viewBox="0 0 16 16" | ||||||
|  | 												fill="currentColor" | ||||||
|  | 												class="w-4 h-4" | ||||||
|  | 											> | ||||||
|  | 												<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" /> | ||||||
|  | 											</svg> | ||||||
|  | 										</button> | ||||||
|  | 									{/if} | ||||||
| 								</div> | 								</div> | ||||||
| 							</div> | 							</div> | ||||||
|  | 							<div class=" mb-1 text-xs text-gray-400 dark:text-gray-500"> | ||||||
|  | 								WebUI will make requests to <span class=" text-gray-200">'{url}/models'</span> | ||||||
|  | 							</div> | ||||||
|  | 						{/each} | ||||||
|  | 					</div> | ||||||
| 				{/if} | 				{/if} | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
|  |  | ||||||
|  | @ -56,7 +56,7 @@ | ||||||
| 	let modelUploadMode = 'file'; | 	let modelUploadMode = 'file'; | ||||||
| 	let modelInputFile = ''; | 	let modelInputFile = ''; | ||||||
| 	let modelFileUrl = ''; | 	let modelFileUrl = ''; | ||||||
| 	let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSSISTANT:"`; | 	let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`; | ||||||
| 	let modelFileDigest = ''; | 	let modelFileDigest = ''; | ||||||
| 	let uploadProgress = null; | 	let uploadProgress = null; | ||||||
| 
 | 
 | ||||||
|  | @ -517,7 +517,7 @@ | ||||||
| 									{#if !deleteModelTag} | 									{#if !deleteModelTag} | ||||||
| 										<option value="" disabled selected>Select a model</option> | 										<option value="" disabled selected>Select a model</option> | ||||||
| 									{/if} | 									{/if} | ||||||
| 									{#each $models.filter((m) => m.size != null) as model} | 									{#each $models.filter((m) => m.size != null && (selectedOllamaUrlIdx === null ? true : (m?.urls ?? []).includes(selectedOllamaUrlIdx))) as model} | ||||||
| 										<option value={model.name} class="bg-gray-100 dark:bg-gray-700" | 										<option value={model.name} class="bg-gray-100 dark:bg-gray-700" | ||||||
| 											>{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option | 											>{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option | ||||||
| 										> | 										> | ||||||
|  | @ -599,7 +599,7 @@ | ||||||
| 												on:change={() => { | 												on:change={() => { | ||||||
| 													console.log(modelInputFile); | 													console.log(modelInputFile); | ||||||
| 												}} | 												}} | ||||||
| 												accept=".gguf" | 												accept=".gguf,.safetensors" | ||||||
| 												required | 												required | ||||||
| 												hidden | 												hidden | ||||||
| 											/> | 											/> | ||||||
|  |  | ||||||
|  | @ -140,7 +140,9 @@ | ||||||
| 						<button | 						<button | ||||||
| 							class="w-full text-sm font-medium py-3 bg-gray-850 hover:bg-gray-800 text-center rounded-xl" | 							class="w-full text-sm font-medium py-3 bg-gray-850 hover:bg-gray-800 text-center rounded-xl" | ||||||
| 							type="button" | 							type="button" | ||||||
| 							on:click={uploadDocInputElement.click} | 							on:click={() => { | ||||||
|  | 								uploadDocInputElement.click(); | ||||||
|  | 							}} | ||||||
| 						> | 						> | ||||||
| 							{#if inputFiles} | 							{#if inputFiles} | ||||||
| 								{inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected. | 								{inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected. | ||||||
|  |  | ||||||
|  | @ -90,8 +90,3 @@ export const SUPPORTED_FILE_EXTENSIONS = [ | ||||||
| // This feature, akin to $env/static/private, exclusively incorporates environment variables
 | // This feature, akin to $env/static/private, exclusively incorporates environment variables
 | ||||||
| // that are prefixed with config.kit.env.publicPrefix (usually set to PUBLIC_).
 | // that are prefixed with config.kit.env.publicPrefix (usually set to PUBLIC_).
 | ||||||
| // Consequently, these variables can be securely exposed to client-side code.
 | // Consequently, these variables can be securely exposed to client-side code.
 | ||||||
| 
 |  | ||||||
| // Example of the .env configuration:
 |  | ||||||
| // OLLAMA_API_BASE_URL="http://localhost:11434/api"
 |  | ||||||
| // # Public
 |  | ||||||
| // PUBLIC_API_BASE_URL=$OLLAMA_API_BASE_URL
 |  | ||||||
|  |  | ||||||
|  | @ -99,14 +99,11 @@ | ||||||
| 					if (localDBChats.length === 0) { | 					if (localDBChats.length === 0) { | ||||||
| 						await deleteDB('Chats'); | 						await deleteDB('Chats'); | ||||||
| 					} | 					} | ||||||
| 
 |  | ||||||
| 					console.log('localdb', localDBChats); |  | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				console.log(DB); | 				console.log(DB); | ||||||
| 			} catch (error) { | 			} catch (error) { | ||||||
| 				// IndexedDB Not Found | 				// IndexedDB Not Found | ||||||
| 				console.log('IDB Not Found'); |  | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			console.log(); | 			console.log(); | ||||||
|  |  | ||||||
|  | @ -344,7 +344,7 @@ | ||||||
| 						content: $settings.system | 						content: $settings.system | ||||||
| 				  } | 				  } | ||||||
| 				: undefined, | 				: undefined, | ||||||
| 			...messages.filter((message) => !message.deleted) | 			...messages | ||||||
| 		] | 		] | ||||||
| 			.filter((message) => message) | 			.filter((message) => message) | ||||||
| 			.map((message, idx, arr) => ({ | 			.map((message, idx, arr) => ({ | ||||||
|  | @ -558,7 +558,7 @@ | ||||||
| 								content: $settings.system | 								content: $settings.system | ||||||
| 						  } | 						  } | ||||||
| 						: undefined, | 						: undefined, | ||||||
| 					...messages.filter((message) => !message.deleted) | 					...messages | ||||||
| 				] | 				] | ||||||
| 					.filter((message) => message) | 					.filter((message) => message) | ||||||
| 					.map((message, idx, arr) => ({ | 					.map((message, idx, arr) => ({ | ||||||
|  |  | ||||||
|  | @ -354,7 +354,7 @@ | ||||||
| 						content: $settings.system | 						content: $settings.system | ||||||
| 				  } | 				  } | ||||||
| 				: undefined, | 				: undefined, | ||||||
| 			...messages.filter((message) => !message.deleted) | 			...messages | ||||||
| 		] | 		] | ||||||
| 			.filter((message) => message) | 			.filter((message) => message) | ||||||
| 			.map((message, idx, arr) => ({ | 			.map((message, idx, arr) => ({ | ||||||
|  | @ -568,7 +568,7 @@ | ||||||
| 								content: $settings.system | 								content: $settings.system | ||||||
| 						  } | 						  } | ||||||
| 						: undefined, | 						: undefined, | ||||||
| 					...messages.filter((message) => !message.deleted) | 					...messages | ||||||
| 				] | 				] | ||||||
| 					.filter((message) => message) | 					.filter((message) => message) | ||||||
| 					.map((message, idx, arr) => ({ | 					.map((message, idx, arr) => ({ | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue