This commit is contained in:
Timothy Jaeryang Baek
2026-07-23 21:29:33 -04:00
parent 484fb61743
commit 429f2df50c
8 changed files with 146 additions and 118 deletions
+9
View File
@@ -594,6 +594,15 @@ try:
except (ValueError, TypeError): except (ValueError, TypeError):
AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = 10 AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = 10
AIOHTTP_FILE_STREAM_CHUNK_SIZE = os.getenv('AIOHTTP_FILE_STREAM_CHUNK_SIZE', str(1024 * 1024))
try:
AIOHTTP_FILE_STREAM_CHUNK_SIZE = int(AIOHTTP_FILE_STREAM_CHUNK_SIZE)
except Exception:
AIOHTTP_FILE_STREAM_CHUNK_SIZE = 1024 * 1024
if AIOHTTP_FILE_STREAM_CHUNK_SIZE <= 0:
AIOHTTP_FILE_STREAM_CHUNK_SIZE = 1024 * 1024
# SSL verification for tool server connections specifically. # SSL verification for tool server connections specifically.
# Accepts "True", "False", or a path to a CA bundle file. # Accepts "True", "False", or a path to a CA bundle file.
+31 -20
View File
@@ -49,6 +49,7 @@ from open_webui.env import (
AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_SESSION_SSL,
AIOHTTP_CLIENT_TIMEOUT, AIOHTTP_CLIENT_TIMEOUT,
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST, AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST,
AIOHTTP_FILE_STREAM_CHUNK_SIZE,
BYPASS_PYDUB_PREPROCESSING, BYPASS_PYDUB_PREPROCESSING,
DEVICE_TYPE, DEVICE_TYPE,
ENABLE_FORWARD_USER_INFO_HEADERS, ENABLE_FORWARD_USER_INFO_HEADERS,
@@ -678,15 +679,19 @@ async def _transcribe_openai(request, file_path, filename, languages, file_dir,
for key, value in payload.items(): for key, value in payload.items():
form_data.add_field(key, str(value)) form_data.add_field(key, str(value))
with open(file_path, 'rb') as audio_file: async def audio_chunks():
form_data.add_field('file', audio_file, filename=filename) async with aiofiles.open(file_path, 'rb') as audio_file:
while chunk := await audio_file.read(AIOHTTP_FILE_STREAM_CHUNK_SIZE):
yield chunk
r = await session.post( form_data.add_field('file', audio_chunks(), filename=filename)
url=f'{api_base_url}/audio/transcriptions',
headers=headers, r = await session.post(
data=form_data, url=f'{api_base_url}/audio/transcriptions',
ssl=AIOHTTP_CLIENT_SESSION_SSL, headers=headers,
) data=form_data,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
)
if r.status == 200: if r.status == 200:
break break
@@ -823,13 +828,18 @@ async def _transcribe_azure(request, file_path, filename, file_dir, id):
base_url or f'https://{region}.api.cognitive.microsoft.com' base_url or f'https://{region}.api.cognitive.microsoft.com'
) + '/speechtotext/transcriptions:transcribe?api-version=2024-11-15' ) + '/speechtotext/transcriptions:transcribe?api-version=2024-11-15'
form_data = aiohttp.FormData()
form_data.add_field('definition', definition)
form_data.add_field('audio', open(file_path, 'rb'), filename=filename)
r = None r = None
try: try:
session = await get_session() session = await get_session()
form_data = aiohttp.FormData()
form_data.add_field('definition', definition)
async def audio_chunks():
async with aiofiles.open(file_path, 'rb') as audio_file:
while chunk := await audio_file.read(AIOHTTP_FILE_STREAM_CHUNK_SIZE):
yield chunk
form_data.add_field('audio', audio_chunks(), filename=filename)
r = await session.post( r = await session.post(
url=endpoint, url=endpoint,
data=form_data, data=form_data,
@@ -1002,7 +1012,12 @@ async def _transcribe_mistral(request, file_path, filename, metadata, file_dir,
if language: if language:
form_data.add_field('language', language) form_data.add_field('language', language)
form_data.add_field('file', open(file_path, 'rb'), filename=filename, content_type=mime_type) async def audio_chunks():
async with aiofiles.open(file_path, 'rb') as audio_file:
while chunk := await audio_file.read(AIOHTTP_FILE_STREAM_CHUNK_SIZE):
yield chunk
form_data.add_field('file', audio_chunks(), filename=filename, content_type=mime_type)
r = await session.post( r = await session.post(
url=f'{api_base_url}/audio/transcriptions', url=f'{api_base_url}/audio/transcriptions',
@@ -1094,7 +1109,7 @@ async def transcribe(request: Request, file_path: str, metadata: Optional[dict]
for chunk_path in chunk_paths: for chunk_path in chunk_paths:
if chunk_path != file_path and os.path.isfile(chunk_path): if chunk_path != file_path and os.path.isfile(chunk_path):
try: try:
os.remove(chunk_path) await asyncio.to_thread(os.remove, chunk_path)
except Exception: except Exception:
pass pass
@@ -1208,12 +1223,8 @@ async def transcription(
if not os.path.realpath(file_path).startswith(os.path.realpath(file_dir)): if not os.path.realpath(file_path).startswith(os.path.realpath(file_dir)):
raise ValueError('Invalid file path detected') raise ValueError('Invalid file path detected')
def _write_upload(): async with aiofiles.open(file_path, 'wb') as f:
with open(file_path, 'wb') as f: await f.write(contents)
f.write(contents)
# Audio uploads can be large; write to disk off the event loop.
await asyncio.to_thread(_write_upload)
try: try:
metadata = None metadata = None
+5 -4
View File
@@ -13,6 +13,7 @@ from types import SimpleNamespace
from typing import Optional from typing import Optional
from urllib.parse import quote, urlparse from urllib.parse import quote, urlparse
import aiofiles
import aiohttp import aiohttp
from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
@@ -940,10 +941,10 @@ async def image_edits(
if isinstance(file_response, FileResponse): if isinstance(file_response, FileResponse):
file_path = file_response.path file_path = file_response.path
with open(file_path, 'rb') as f: async with aiofiles.open(file_path, 'rb') as f:
file_bytes = f.read() file_bytes = await f.read()
image_data = base64.b64encode(file_bytes).decode('utf-8') image_data = base64.b64encode(file_bytes).decode('utf-8')
mime_type, _ = mimetypes.guess_type(file_path) mime_type, _ = mimetypes.guess_type(file_path)
return f'data:{mime_type};base64,{image_data}' return f'data:{mime_type};base64,{image_data}'
return data return data
+49 -48
View File
@@ -11,6 +11,7 @@ from datetime import datetime
from typing import Optional, Union from typing import Optional, Union
from urllib.parse import urlparse from urllib.parse import urlparse
import aiofiles
import aiohttp import aiohttp
from aiocache import cached from aiocache import cached
from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile
@@ -25,6 +26,7 @@ from open_webui.env import (
AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_SESSION_SSL,
AIOHTTP_CLIENT_TIMEOUT, AIOHTTP_CLIENT_TIMEOUT,
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST, AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST,
AIOHTTP_FILE_STREAM_CHUNK_SIZE,
BYPASS_MODEL_ACCESS_CONTROL, BYPASS_MODEL_ACCESS_CONTROL,
ENABLE_FORWARD_USER_INFO_HEADERS, ENABLE_FORWARD_USER_INFO_HEADERS,
FORWARD_SESSION_INFO_HEADER_CHAT_ID, FORWARD_SESSION_INFO_HEADER_CHAT_ID,
@@ -1529,7 +1531,7 @@ async def download_file_stream(
file_url: str, file_url: str,
file_path: str, file_path: str,
file_name: str, file_name: str,
chunk_size: int = 1024 * 1024, chunk_size: int = AIOHTTP_FILE_STREAM_CHUNK_SIZE,
): ):
"""Stream a model file download from *file_url*, then push the blob to Ollama.""" """Stream a model file download from *file_url*, then push the blob to Ollama."""
current_size = os.path.getsize(file_path) if os.path.exists(file_path) else 0 current_size = os.path.getsize(file_path) if os.path.exists(file_path) else 0
@@ -1544,37 +1546,38 @@ async def download_file_stream(
) as response: ) as response:
total_size = int(response.headers.get('content-length', 0)) + current_size total_size = int(response.headers.get('content-length', 0)) + current_size
with open(file_path, 'ab+') as f: async with aiofiles.open(file_path, 'ab') as f:
async for data in response.content.iter_chunked(chunk_size): async for data in response.content.iter_chunked(chunk_size):
current_size += len(data) current_size += len(data)
f.write(data) await f.write(data)
done = current_size == total_size progress_total = total_size or current_size
progress = round((current_size / total_size) * 100, 2) progress = round((current_size / progress_total) * 100, 2)
yield f'data: {{"progress": {progress}, "completed": {current_size}, "total": {total_size}}}\n\n' yield f'data: {{"progress": {progress}, "completed": {current_size}, "total": {total_size}}}\n\n'
if done: done = True
f.close() hashed = await asyncio.to_thread(calculate_sha256, file_path, chunk_size)
hashed = await asyncio.to_thread(calculate_sha256, file_path, chunk_size)
def _read_blob(): blob_url = f'{ollama_url}/api/blobs/sha256:{hashed}'
with open(file_path, 'rb') as blob_f: blob_size = await asyncio.to_thread(os.path.getsize, file_path)
return blob_f.read()
blob_data = await asyncio.to_thread(_read_blob) async def blob_chunks():
async with aiofiles.open(file_path, 'rb') as blob_file:
while chunk := await blob_file.read(chunk_size):
yield chunk
blob_url = f'{ollama_url}/api/blobs/sha256:{hashed}' async with session.post(
async with session.post( blob_url,
blob_url, data=blob_chunks(),
data=blob_data, headers={'Content-Length': str(blob_size)},
ssl=AIOHTTP_CLIENT_SESSION_SSL, ssl=AIOHTTP_CLIENT_SESSION_SSL,
timeout=aiohttp.ClientTimeout(total=30), timeout=aiohttp.ClientTimeout(total=30),
) as blob_resp: ) as blob_resp:
if blob_resp.ok: if blob_resp.ok:
os.remove(file_path) await asyncio.to_thread(os.remove, file_path)
yield f'data: {json.dumps({"done": done, "blob": f"sha256:{hashed}", "name": file_name})}\n\n' yield f'data: {json.dumps({"done": done, "blob": f"sha256:{hashed}", "name": file_name})}\n\n'
else: else:
raise RuntimeError('Ollama: Could not create blob, Please try again.') raise RuntimeError('Ollama: Could not create blob, Please try again.')
@router.post('/models/download') @router.post('/models/download')
@@ -1621,17 +1624,11 @@ async def upload_model(
os.makedirs(UPLOAD_DIR, exist_ok=True) os.makedirs(UPLOAD_DIR, exist_ok=True)
# Stage 1: persist the uploaded file to disk # Stage 1: persist the uploaded file to disk
chunk_size = 1024 * 1024 * 2 # 2 MiB chunk_size = AIOHTTP_FILE_STREAM_CHUNK_SIZE
def _persist_upload(): async with aiofiles.open(file_path, 'wb') as out_f:
with open(file_path, 'wb') as out_f: while chunk := await file.read(chunk_size):
while True: await out_f.write(chunk)
chunk = file.file.read(chunk_size)
if not chunk:
break
out_f.write(chunk)
await asyncio.to_thread(_persist_upload)
async def file_process_stream(): async def file_process_stream():
nonlocal ollama_url nonlocal ollama_url
@@ -1643,25 +1640,26 @@ async def upload_model(
log.info(f'Model Hash: {file_hash}') log.info(f'Model Hash: {file_hash}')
try: try:
with open(file_path, 'rb') as f: bytes_read = 0
bytes_read = 0 async with aiofiles.open(file_path, 'rb') as f:
while chunk := f.read(chunk_size): while chunk := await f.read(chunk_size):
bytes_read += len(chunk) bytes_read += len(chunk)
progress = round(bytes_read / total_size * 100, 2) progress = round(bytes_read / total_size * 100, 2)
yield f'data: {json.dumps({"progress": progress, "total": total_size, "completed": bytes_read})}\n\n' event = json.dumps({'progress': progress, 'total': total_size, 'completed': bytes_read})
yield f'data: {event}\n\n'
# Stage 3: push blob to Ollama
def _read_blob():
with open(file_path, 'rb') as f:
return f.read()
blob_data = await asyncio.to_thread(_read_blob)
session = await get_session() session = await get_session()
blob_url = f'{ollama_url}/api/blobs/sha256:{file_hash}' blob_url = f'{ollama_url}/api/blobs/sha256:{file_hash}'
async def blob_chunks():
async with aiofiles.open(file_path, 'rb') as blob_file:
while chunk := await blob_file.read(chunk_size):
yield chunk
async with session.post( async with session.post(
blob_url, blob_url,
data=blob_data, data=blob_chunks(),
headers={'Content-Length': str(total_size)},
ssl=AIOHTTP_CLIENT_SESSION_SSL, ssl=AIOHTTP_CLIENT_SESSION_SSL,
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT),
) as resp: ) as resp:
@@ -1669,7 +1667,7 @@ async def upload_model(
raise Exception('Ollama: Could not create blob, Please try again.') raise Exception('Ollama: Could not create blob, Please try again.')
log.info('Uploaded to /api/blobs') log.info('Uploaded to /api/blobs')
os.remove(file_path) await asyncio.to_thread(os.remove, file_path)
# Stage 4: create the model # Stage 4: create the model
model, _ext = os.path.splitext(filename) model, _ext = os.path.splitext(filename)
@@ -1690,7 +1688,10 @@ async def upload_model(
) as create_resp: ) as create_resp:
if create_resp.ok: if create_resp.ok:
log.info('API SUCCESS!') log.info('API SUCCESS!')
yield f'data: {json.dumps({"done": True, "blob": f"sha256:{file_hash}", "name": filename, "model_created": model})}\n\n' event = json.dumps(
{'done': True, 'blob': f'sha256:{file_hash}', 'name': filename, 'model_created': model}
)
yield f'data: {event}\n\n'
else: else:
resp_text = await create_resp.text() resp_text = await create_resp.text()
raise Exception(f'Failed to create model in Ollama. {resp_text}') raise Exception(f'Failed to create model in Ollama. {resp_text}')
+5 -5
View File
@@ -8,6 +8,7 @@ import re
from typing import Optional from typing import Optional
from urllib.parse import quote, urlparse from urllib.parse import quote, urlparse
import aiofiles
import aiohttp import aiohttp
from aiocache import cached from aiocache import cached
from azure.identity import DefaultAzureCredential, get_bearer_token_provider from azure.identity import DefaultAzureCredential, get_bearer_token_provider
@@ -491,13 +492,12 @@ async def speech(request: Request, user=Depends(get_verified_user)):
r.raise_for_status() r.raise_for_status()
# Save the streaming content to a file async with aiofiles.open(file_path, 'wb') as f:
with open(file_path, 'wb') as f:
async for chunk in r.content.iter_chunked(8192): async for chunk in r.content.iter_chunked(8192):
f.write(chunk) await f.write(chunk)
with open(file_body_path, 'w') as f: async with aiofiles.open(file_body_path, 'w') as f:
json.dump(json.loads(body.decode('utf-8')), f) await f.write(json.dumps(json.loads(body.decode('utf-8'))))
# Return the saved file # Return the saved file
return FileResponse(file_path) return FileResponse(file_path)
+27 -25
View File
@@ -1,9 +1,9 @@
import asyncio import asyncio
import logging import logging
import os import os
import shutil
from typing import Optional from typing import Optional
import aiofiles
import aiohttp import aiohttp
from fastapi import ( from fastapi import (
APIRouter, APIRouter,
@@ -18,7 +18,7 @@ from fastapi import (
) )
from open_webui.config import CACHE_DIR from open_webui.config import CACHE_DIR
from open_webui.constants import ERROR_MESSAGES from open_webui.constants import ERROR_MESSAGES
from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_FILE_STREAM_CHUNK_SIZE
from open_webui.events import EVENTS, publish_event from open_webui.events import EVENTS, publish_event
from open_webui.models.config import Config from open_webui.models.config import Config
from open_webui.routers.openai import get_all_models_responses from open_webui.routers.openai import get_all_models_responses
@@ -237,35 +237,37 @@ async def upload_pipeline(
response = None response = None
try: try:
# Save the uploaded file off the event loop (uploads can be large). async with aiofiles.open(file_path, 'wb') as buffer:
def _save_upload(): while chunk := await file.read(AIOHTTP_FILE_STREAM_CHUNK_SIZE):
with open(file_path, 'wb') as buffer: await buffer.write(chunk)
shutil.copyfileobj(file.file, buffer)
await asyncio.to_thread(_save_upload)
url, key = await get_openai_connection(urlIdx) url, key = await get_openai_connection(urlIdx)
headers = {'Authorization': f'Bearer {key}'} headers = {'Authorization': f'Bearer {key}'}
async with aiohttp.ClientSession(trust_env=True) as session: async with aiohttp.ClientSession(trust_env=True) as session:
with open(file_path, 'rb') as f: form_data = aiohttp.FormData()
form_data = aiohttp.FormData()
form_data.add_field(
'file',
f,
filename=filename,
content_type='application/octet-stream',
)
async with session.post( async def pipeline_chunks():
f'{url}/pipelines/upload', async with aiofiles.open(file_path, 'rb') as pipeline_file:
headers=headers, while chunk := await pipeline_file.read(AIOHTTP_FILE_STREAM_CHUNK_SIZE):
data=form_data, yield chunk
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as response: form_data.add_field(
response.raise_for_status() 'file',
data = await response.json() pipeline_chunks(),
filename=filename,
content_type='application/octet-stream',
)
async with session.post(
f'{url}/pipelines/upload',
headers=headers,
data=form_data,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as response:
response.raise_for_status()
data = await response.json()
await publish_event( await publish_event(
request, request,
@@ -297,7 +299,7 @@ async def upload_pipeline(
finally: finally:
# Ensure the file is deleted after the upload is completed or on failure # Ensure the file is deleted after the upload is completed or on failure
if os.path.exists(file_path): if os.path.exists(file_path):
os.remove(file_path) await asyncio.to_thread(os.remove, file_path)
class AddPipelineForm(BaseModel): class AddPipelineForm(BaseModel):
+9 -6
View File
@@ -2910,21 +2910,24 @@ async def reset_upload_dir(request: Request, user=Depends(get_admin_user)) -> bo
folder = f'{UPLOAD_DIR}' folder = f'{UPLOAD_DIR}'
try: try:
# Check if the directory exists # Check if the directory exists
if os.path.exists(folder): if await asyncio.to_thread(os.path.exists, folder):
# Iterate over all the files and directories in the specified directory # Iterate over all the files and directories in the specified directory
for filename in os.listdir(folder): for filename in await asyncio.to_thread(os.listdir, folder):
file_path = os.path.join(folder, filename) file_path = os.path.join(folder, filename)
try: try:
if os.path.isfile(file_path) or os.path.islink(file_path): if await asyncio.to_thread(os.path.isfile, file_path) or await asyncio.to_thread(
os.unlink(file_path) # Remove the file or link os.path.islink, file_path
elif os.path.isdir(file_path): ):
shutil.rmtree(file_path) # Remove the directory await asyncio.to_thread(os.unlink, file_path) # Remove the file or link
elif await asyncio.to_thread(os.path.isdir, file_path):
await asyncio.to_thread(shutil.rmtree, file_path) # Remove the directory
except Exception as e: except Exception as e:
log.exception(f'Failed to delete {file_path}. Reason: {e}') log.exception(f'Failed to delete {file_path}. Reason: {e}')
else: else:
log.warning(f'The directory {folder} does not exist') log.warning(f'The directory {folder} does not exist')
except Exception as e: except Exception as e:
log.exception(f'Failed to process the directory {folder}. Reason: {e}') log.exception(f'Failed to process the directory {folder}. Reason: {e}')
await publish_event( await publish_event(
request, request,
EVENTS.RETRIEVAL_UPLOADS_RESET, EVENTS.RETRIEVAL_UPLOADS_RESET,
+11 -10
View File
@@ -6,6 +6,7 @@ import re
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
import aiofiles
from fastapi import ( from fastapi import (
APIRouter, APIRouter,
Depends, Depends,
@@ -75,7 +76,7 @@ async def get_image_base64_from_url(url: str, user=None) -> Optional[str]:
# file-ID resolver which enforces ownership/access checks. # file-ID resolver which enforces ownership/access checks.
return await get_image_base64_from_file_id(url, user=user) return await get_image_base64_from_file_id(url, user=user)
except Exception as e: except Exception:
return None return None
@@ -200,15 +201,15 @@ async def get_image_base64_from_file_id(id: str, user=None) -> Optional[str]:
# Check if the file already exists in the cache # Check if the file already exists in the cache
if file_path.is_file(): if file_path.is_file():
with open(file_path, 'rb') as image_file: async with aiofiles.open(file_path, 'rb') as image_file:
encoded_string = base64.b64encode(image_file.read()).decode('utf-8') encoded_string = base64.b64encode(await image_file.read()).decode('utf-8')
content_type = mimetypes.guess_type(file_path.name)[0] or (file.meta or {}).get('content_type') content_type = mimetypes.guess_type(file_path.name)[0] or (file.meta or {}).get('content_type')
if not content_type and ENABLE_IMAGE_CONTENT_TYPE_EXTENSION_FALLBACK: if not content_type and ENABLE_IMAGE_CONTENT_TYPE_EXTENSION_FALLBACK:
content_type = _IMAGE_MIME_FALLBACK.get(file_path.suffix.lower()) content_type = _IMAGE_MIME_FALLBACK.get(file_path.suffix.lower())
if not content_type: if not content_type:
return None return None
return f'data:{content_type};base64,{encoded_string}' return f'data:{content_type};base64,{encoded_string}'
else: else:
return None return None
except Exception as e: except Exception:
return None return None