mirror of
https://github.com/open-webui/open-webui.git
synced 2026-09-06 08:18:53 +00:00
Merge branch 'open-webui:dev' into dev
This commit is contained in:
commit
9dd181b3fa
15 changed files with 399 additions and 457 deletions
|
|
@ -172,8 +172,6 @@ After installation, you can access Open WebUI at [http://localhost:3000](http://
|
|||
|
||||
We offer various installation alternatives, including non-Docker native installation methods, Docker Compose, Kustomize, and Helm. Visit our [Open WebUI Documentation](https://docs.openwebui.com/getting-started/) or join our [Discord community](https://discord.gg/5rJgQTnV4s) for comprehensive guidance.
|
||||
|
||||
Look at the [Local Development Guide](https://docs.openwebui.com/getting-started/development) for instructions on setting up a local development environment.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
Encountering connection issues? Our [Open WebUI Documentation](https://docs.openwebui.com/troubleshooting/) has got you covered. For further assistance and to join our vibrant community, visit the [Open WebUI Discord](https://discord.gg/5rJgQTnV4s).
|
||||
|
|
|
|||
|
|
@ -60,13 +60,14 @@ if USE_CUDA.lower() == 'true':
|
|||
else:
|
||||
DEVICE_TYPE = 'cpu'
|
||||
|
||||
try:
|
||||
import torch
|
||||
if sys.platform == 'darwin':
|
||||
try:
|
||||
import torch
|
||||
|
||||
if torch.backends.mps.is_available() and torch.backends.mps.is_built():
|
||||
DEVICE_TYPE = 'mps'
|
||||
except Exception:
|
||||
pass
|
||||
if torch.backends.mps.is_available() and torch.backends.mps.is_built():
|
||||
DEVICE_TYPE = 'mps'
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
####################################
|
||||
# LOGGING
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ from open_webui.utils.plugin import (
|
|||
load_function_module_by_id,
|
||||
get_function_module_from_cache,
|
||||
)
|
||||
from open_webui.utils.tools import get_tools
|
||||
|
||||
from open_webui.env import GLOBAL_LOG_LEVEL
|
||||
|
||||
|
|
@ -255,17 +254,7 @@ async def generate_function_chat_completion(request, form_data, user, models: di
|
|||
'__oauth_token__': oauth_token,
|
||||
'__request__': request,
|
||||
}
|
||||
extra_params['__tools__'] = await get_tools(
|
||||
request,
|
||||
tool_ids,
|
||||
user,
|
||||
{
|
||||
**extra_params,
|
||||
'__model__': models.get(form_data['model'], None),
|
||||
'__messages__': form_data['messages'],
|
||||
'__files__': files,
|
||||
},
|
||||
)
|
||||
extra_params['__tools__'] = metadata.get('tools', {})
|
||||
|
||||
if model_info:
|
||||
if model_info.base_model_id:
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ from typing import Optional, Union
|
|||
from urllib.parse import urlparse
|
||||
import aiohttp
|
||||
from aiocache import cached
|
||||
import requests
|
||||
|
||||
|
||||
from open_webui.utils.headers import include_user_info_headers
|
||||
from open_webui.models.chats import Chats
|
||||
|
|
@ -107,19 +107,24 @@ async def send_get_request(url, key=None, user: UserModel = None):
|
|||
return None
|
||||
|
||||
|
||||
async def send_post_request(
|
||||
async def send_request(
|
||||
url: str,
|
||||
payload: Union[str, bytes],
|
||||
stream: bool = True,
|
||||
method: str = 'POST',
|
||||
*,
|
||||
payload: Optional[Union[str, bytes]] = None,
|
||||
key: Optional[str] = None,
|
||||
content_type: Optional[str] = None,
|
||||
user: UserModel = None,
|
||||
stream: bool = False,
|
||||
content_type: Optional[str] = None,
|
||||
metadata: Optional[dict] = None,
|
||||
):
|
||||
r = None
|
||||
streaming = False
|
||||
try:
|
||||
session = aiohttp.ClientSession(trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT))
|
||||
session = aiohttp.ClientSession(
|
||||
trust_env=True,
|
||||
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT),
|
||||
)
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
|
|
@ -131,32 +136,29 @@ async def send_post_request(
|
|||
if metadata and metadata.get('chat_id'):
|
||||
headers[FORWARD_SESSION_INFO_HEADER_CHAT_ID] = metadata.get('chat_id')
|
||||
|
||||
r = await session.post(
|
||||
url,
|
||||
data=payload,
|
||||
headers=headers,
|
||||
r = await session.request(
|
||||
method, url, data=payload, headers=headers,
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||
)
|
||||
|
||||
if r.ok is False:
|
||||
if not r.ok:
|
||||
try:
|
||||
res = await r.json()
|
||||
await cleanup_response(r, session)
|
||||
if 'error' in res:
|
||||
raise HTTPException(status_code=r.status, detail=res['error'])
|
||||
except HTTPException as e:
|
||||
raise e # Re-raise HTTPException to be handled by FastAPI
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
log.error(f'Failed to parse error response: {e}')
|
||||
raise HTTPException(
|
||||
status_code=r.status,
|
||||
detail=f'Open WebUI: Server Connection Error',
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=r.status,
|
||||
detail='Open WebUI: Server Connection Error',
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
r.raise_for_status() # Raises an error for bad responses (4xx, 5xx)
|
||||
if stream:
|
||||
response_headers = dict(r.headers)
|
||||
|
||||
if content_type:
|
||||
response_headers['Content-Type'] = content_type
|
||||
|
||||
|
|
@ -167,17 +169,17 @@ async def send_post_request(
|
|||
headers=response_headers,
|
||||
)
|
||||
else:
|
||||
res = await r.json()
|
||||
return res
|
||||
try:
|
||||
return await r.json()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
except HTTPException as e:
|
||||
raise e # Re-raise HTTPException to be handled by FastAPI
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
detail = f'Ollama: {e}'
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status if r else 500,
|
||||
detail=detail if e else 'Open WebUI: Server Connection Error',
|
||||
detail=f'Ollama: {e}' if str(e) else 'Open WebUI: Server Connection Error',
|
||||
)
|
||||
finally:
|
||||
if not streaming:
|
||||
|
|
@ -430,40 +432,7 @@ async def get_ollama_tags(request: Request, url_idx: Optional[int] = None, user=
|
|||
else:
|
||||
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
|
||||
|
||||
r = None
|
||||
try:
|
||||
headers = {
|
||||
**({'Authorization': f'Bearer {key}'} if key else {}),
|
||||
}
|
||||
|
||||
if ENABLE_FORWARD_USER_INFO_HEADERS and user:
|
||||
headers = include_user_info_headers(headers, user)
|
||||
|
||||
r = requests.request(
|
||||
method='GET',
|
||||
url=f'{url}/api/tags',
|
||||
headers=headers,
|
||||
)
|
||||
r.raise_for_status()
|
||||
|
||||
models = r.json()
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
||||
detail = None
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if 'error' in res:
|
||||
detail = f'Ollama: {res["error"]}'
|
||||
except Exception:
|
||||
detail = f'Ollama: {e}'
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=detail if detail else 'Open WebUI: Server Connection Error',
|
||||
)
|
||||
models = await send_request(f'{url}/api/tags', 'GET', key=key, user=user)
|
||||
|
||||
if user.role == 'user' and not BYPASS_MODEL_ACCESS_CONTROL:
|
||||
models['models'] = await get_filtered_models(models, user)
|
||||
|
|
@ -569,29 +538,7 @@ async def get_ollama_versions(request: Request, url_idx: Optional[int] = None):
|
|||
)
|
||||
else:
|
||||
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
|
||||
r = None
|
||||
try:
|
||||
r = requests.request(method='GET', url=f'{url}/api/version')
|
||||
r.raise_for_status()
|
||||
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
||||
detail = None
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if 'error' in res:
|
||||
detail = f'Ollama: {res["error"]}'
|
||||
except Exception:
|
||||
detail = f'Ollama: {e}'
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=detail if detail else 'Open WebUI: Server Connection Error',
|
||||
)
|
||||
return await send_request(f'{url}/api/version', 'GET')
|
||||
else:
|
||||
return {'version': False}
|
||||
|
||||
|
|
@ -640,10 +587,9 @@ async def unload_model(
|
|||
payload = {'model': model_name, 'keep_alive': 0, 'prompt': ''}
|
||||
|
||||
try:
|
||||
res = await send_post_request(
|
||||
url=f'{url}/api/generate',
|
||||
res = await send_request(
|
||||
f'{url}/api/generate',
|
||||
payload=json.dumps(payload),
|
||||
stream=False,
|
||||
key=key,
|
||||
user=user,
|
||||
)
|
||||
|
|
@ -681,11 +627,12 @@ async def pull_model(
|
|||
# Admin should be able to pull models from any source
|
||||
payload = {**form_data, 'insecure': True}
|
||||
|
||||
return await send_post_request(
|
||||
url=f'{url}/api/pull',
|
||||
return await send_request(
|
||||
f'{url}/api/pull',
|
||||
payload=json.dumps(payload),
|
||||
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
||||
user=user,
|
||||
stream=True,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -721,11 +668,12 @@ async def push_model(
|
|||
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
log.debug(f'url: {url}')
|
||||
|
||||
return await send_post_request(
|
||||
url=f'{url}/api/push',
|
||||
return await send_request(
|
||||
f'{url}/api/push',
|
||||
payload=form_data.model_dump_json(exclude_none=True).encode(),
|
||||
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
||||
user=user,
|
||||
stream=True,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -751,11 +699,12 @@ async def create_model(
|
|||
log.debug(f'form_data: {form_data}')
|
||||
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
|
||||
return await send_post_request(
|
||||
url=f'{url}/api/create',
|
||||
return await send_request(
|
||||
f'{url}/api/create',
|
||||
payload=form_data.model_dump_json(exclude_none=True).encode(),
|
||||
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
||||
user=user,
|
||||
stream=True,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -790,41 +739,13 @@ async def copy_model(
|
|||
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
|
||||
|
||||
try:
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
**({'Authorization': f'Bearer {key}'} if key else {}),
|
||||
}
|
||||
|
||||
if ENABLE_FORWARD_USER_INFO_HEADERS and user:
|
||||
headers = include_user_info_headers(headers, user)
|
||||
|
||||
r = requests.request(
|
||||
method='POST',
|
||||
url=f'{url}/api/copy',
|
||||
headers=headers,
|
||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||
)
|
||||
r.raise_for_status()
|
||||
|
||||
log.debug(f'r.text: {r.text}')
|
||||
return True
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
||||
detail = None
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if 'error' in res:
|
||||
detail = f'Ollama: {res["error"]}'
|
||||
except Exception:
|
||||
detail = f'Ollama: {e}'
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=detail if detail else 'Open WebUI: Server Connection Error',
|
||||
)
|
||||
await send_request(
|
||||
f'{url}/api/copy',
|
||||
payload=form_data.model_dump_json(exclude_none=True).encode(),
|
||||
key=key,
|
||||
user=user,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@router.delete('/api/delete')
|
||||
|
|
@ -858,42 +779,13 @@ async def delete_model(
|
|||
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
|
||||
|
||||
r = None
|
||||
try:
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
**({'Authorization': f'Bearer {key}'} if key else {}),
|
||||
}
|
||||
|
||||
if ENABLE_FORWARD_USER_INFO_HEADERS and user:
|
||||
headers = include_user_info_headers(headers, user)
|
||||
|
||||
r = requests.request(
|
||||
method='DELETE',
|
||||
url=f'{url}/api/delete',
|
||||
headers=headers,
|
||||
json=form_data,
|
||||
)
|
||||
r.raise_for_status()
|
||||
|
||||
log.debug(f'r.text: {r.text}')
|
||||
return True
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
||||
detail = None
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if 'error' in res:
|
||||
detail = f'Ollama: {res["error"]}'
|
||||
except Exception:
|
||||
detail = f'Ollama: {e}'
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=detail if detail else 'Open WebUI: Server Connection Error',
|
||||
)
|
||||
await send_request(
|
||||
f'{url}/api/delete', 'DELETE',
|
||||
payload=json.dumps(form_data),
|
||||
key=key,
|
||||
user=user,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
@router.post('/api/show')
|
||||
|
|
@ -920,35 +812,12 @@ async def show_model_info(request: Request, form_data: ModelNameForm, user=Depen
|
|||
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
|
||||
|
||||
try:
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
**({'Authorization': f'Bearer {key}'} if key else {}),
|
||||
}
|
||||
|
||||
if ENABLE_FORWARD_USER_INFO_HEADERS and user:
|
||||
headers = include_user_info_headers(headers, user)
|
||||
|
||||
r = requests.request(method='POST', url=f'{url}/api/show', headers=headers, json=form_data)
|
||||
r.raise_for_status()
|
||||
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
||||
detail = None
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if 'error' in res:
|
||||
detail = f'Ollama: {res["error"]}'
|
||||
except Exception:
|
||||
detail = f'Ollama: {e}'
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=detail if detail else 'Open WebUI: Server Connection Error',
|
||||
)
|
||||
return await send_request(
|
||||
f'{url}/api/show',
|
||||
payload=json.dumps(form_data),
|
||||
key=key,
|
||||
user=user,
|
||||
)
|
||||
|
||||
|
||||
class GenerateEmbedForm(BaseModel):
|
||||
|
|
@ -1004,41 +873,12 @@ async def embed(
|
|||
if prefix_id:
|
||||
form_data.model = form_data.model.replace(f'{prefix_id}.', '')
|
||||
|
||||
try:
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
**({'Authorization': f'Bearer {key}'} if key else {}),
|
||||
}
|
||||
|
||||
if ENABLE_FORWARD_USER_INFO_HEADERS and user:
|
||||
headers = include_user_info_headers(headers, user)
|
||||
|
||||
r = requests.request(
|
||||
method='POST',
|
||||
url=f'{url}/api/embed',
|
||||
headers=headers,
|
||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||
)
|
||||
r.raise_for_status()
|
||||
|
||||
data = r.json()
|
||||
return data
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
||||
detail = None
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if 'error' in res:
|
||||
detail = f'Ollama: {res["error"]}'
|
||||
except Exception:
|
||||
detail = f'Ollama: {e}'
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=detail if detail else 'Open WebUI: Server Connection Error',
|
||||
)
|
||||
return await send_request(
|
||||
f'{url}/api/embed',
|
||||
payload=form_data.model_dump_json(exclude_none=True).encode(),
|
||||
key=key,
|
||||
user=user,
|
||||
)
|
||||
|
||||
|
||||
class GenerateEmbeddingsForm(BaseModel):
|
||||
|
|
@ -1089,41 +929,12 @@ async def embeddings(
|
|||
if prefix_id:
|
||||
form_data.model = form_data.model.replace(f'{prefix_id}.', '')
|
||||
|
||||
try:
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
**({'Authorization': f'Bearer {key}'} if key else {}),
|
||||
}
|
||||
|
||||
if ENABLE_FORWARD_USER_INFO_HEADERS and user:
|
||||
headers = include_user_info_headers(headers, user)
|
||||
|
||||
r = requests.request(
|
||||
method='POST',
|
||||
url=f'{url}/api/embeddings',
|
||||
headers=headers,
|
||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||
)
|
||||
r.raise_for_status()
|
||||
|
||||
data = r.json()
|
||||
return data
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
||||
detail = None
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if 'error' in res:
|
||||
detail = f'Ollama: {res["error"]}'
|
||||
except Exception:
|
||||
detail = f'Ollama: {e}'
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=detail if detail else 'Open WebUI: Server Connection Error',
|
||||
)
|
||||
return await send_request(
|
||||
f'{url}/api/embeddings',
|
||||
payload=form_data.model_dump_json(exclude_none=True).encode(),
|
||||
key=key,
|
||||
user=user,
|
||||
)
|
||||
|
||||
|
||||
class GenerateCompletionForm(BaseModel):
|
||||
|
|
@ -1175,11 +986,12 @@ async def generate_completion(
|
|||
if prefix_id:
|
||||
form_data.model = form_data.model.replace(f'{prefix_id}.', '')
|
||||
|
||||
return await send_post_request(
|
||||
url=f'{url}/api/generate',
|
||||
return await send_request(
|
||||
f'{url}/api/generate',
|
||||
payload=form_data.model_dump_json(exclude_none=True).encode(),
|
||||
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
||||
user=user,
|
||||
stream=True,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1319,13 +1131,13 @@ async def generate_chat_completion(
|
|||
if prefix_id:
|
||||
payload['model'] = payload['model'].replace(f'{prefix_id}.', '')
|
||||
|
||||
return await send_post_request(
|
||||
url=f'{url}/api/chat',
|
||||
return await send_request(
|
||||
f'{url}/api/chat',
|
||||
payload=json.dumps(payload),
|
||||
stream=form_data.stream,
|
||||
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
||||
content_type='application/x-ndjson',
|
||||
user=user,
|
||||
stream=form_data.stream,
|
||||
content_type='application/x-ndjson',
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
|
|
@ -1429,12 +1241,12 @@ async def generate_openai_completion(
|
|||
if prefix_id:
|
||||
payload['model'] = payload['model'].replace(f'{prefix_id}.', '')
|
||||
|
||||
return await send_post_request(
|
||||
url=f'{url}/v1/completions',
|
||||
return await send_request(
|
||||
f'{url}/v1/completions',
|
||||
payload=json.dumps(payload),
|
||||
stream=payload.get('stream', False),
|
||||
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
||||
user=user,
|
||||
stream=payload.get('stream', False),
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
|
|
@ -1514,12 +1326,12 @@ async def generate_openai_chat_completion(
|
|||
if prefix_id:
|
||||
payload['model'] = payload['model'].replace(f'{prefix_id}.', '')
|
||||
|
||||
return await send_post_request(
|
||||
url=f'{url}/v1/chat/completions',
|
||||
return await send_request(
|
||||
f'{url}/v1/chat/completions',
|
||||
payload=json.dumps(payload),
|
||||
stream=payload.get('stream', False),
|
||||
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
||||
user=user,
|
||||
stream=payload.get('stream', False),
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
|
|
@ -1586,13 +1398,91 @@ async def generate_anthropic_messages(
|
|||
if prefix_id:
|
||||
payload['model'] = payload['model'].replace(f'{prefix_id}.', '')
|
||||
|
||||
return await send_post_request(
|
||||
url=f'{url}/v1/messages',
|
||||
return await send_request(
|
||||
f'{url}/v1/messages',
|
||||
payload=json.dumps(payload),
|
||||
stream=payload.get('stream', False),
|
||||
content_type='text/event-stream' if payload.get('stream', False) else None,
|
||||
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
||||
user=user,
|
||||
stream=payload.get('stream', False),
|
||||
content_type='text/event-stream' if payload.get('stream', False) else None,
|
||||
)
|
||||
|
||||
|
||||
class ResponsesForm(BaseModel):
|
||||
model: str
|
||||
|
||||
model_config = ConfigDict(extra='allow')
|
||||
|
||||
|
||||
@router.post('/v1/responses')
|
||||
@router.post('/v1/responses/{url_idx}')
|
||||
async def generate_responses(
|
||||
request: Request,
|
||||
form_data: ResponsesForm,
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
"""
|
||||
Proxy for Ollama's OpenAI-compatible /v1/responses endpoint.
|
||||
|
||||
Forwards the request as-is to the Ollama backend, applying the same
|
||||
model resolution, access control, and prefix_id handling used by
|
||||
the OpenAI-compatible /v1/chat/completions proxy.
|
||||
|
||||
See https://ollama.com/blog/responses-api
|
||||
"""
|
||||
if not request.app.state.config.ENABLE_OLLAMA_API:
|
||||
raise HTTPException(status_code=503, detail='Ollama API is disabled')
|
||||
|
||||
payload = form_data.model_dump()
|
||||
model_id = form_data.model
|
||||
|
||||
model_info = Models.get_model_by_id(model_id)
|
||||
if model_info:
|
||||
if model_info.base_model_id:
|
||||
payload['model'] = model_info.base_model_id
|
||||
|
||||
# Check if user has access to the model
|
||||
if user.role == 'user':
|
||||
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id)}
|
||||
if not (
|
||||
user.id == model_info.user_id
|
||||
or AccessGrants.has_access(
|
||||
user_id=user.id,
|
||||
resource_type='model',
|
||||
resource_id=model_info.id,
|
||||
permission='read',
|
||||
user_group_ids=user_group_ids,
|
||||
)
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail='Model not found',
|
||||
)
|
||||
else:
|
||||
if user.role != 'admin':
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail='Model not found',
|
||||
)
|
||||
|
||||
url, url_idx = await get_ollama_url(request, payload['model'], url_idx)
|
||||
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
|
||||
str(url_idx),
|
||||
request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support
|
||||
)
|
||||
|
||||
prefix_id = api_config.get('prefix_id', None)
|
||||
if prefix_id:
|
||||
payload['model'] = payload['model'].replace(f'{prefix_id}.', '')
|
||||
|
||||
return await send_request(
|
||||
f'{url}/v1/responses',
|
||||
payload=json.dumps(payload),
|
||||
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
||||
user=user,
|
||||
stream=payload.get('stream', False),
|
||||
content_type='text/event-stream' if payload.get('stream', False) else None,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1619,36 +1509,17 @@ async def get_openai_models(
|
|||
|
||||
else:
|
||||
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
try:
|
||||
r = requests.request(method='GET', url=f'{url}/api/tags')
|
||||
r.raise_for_status()
|
||||
model_list = await send_request(f'{url}/api/tags', 'GET')
|
||||
|
||||
model_list = r.json()
|
||||
|
||||
models = [
|
||||
{
|
||||
'id': model['model'],
|
||||
'object': 'model',
|
||||
'created': int(time.time()),
|
||||
'owned_by': 'openai',
|
||||
}
|
||||
for model in models['models']
|
||||
]
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
error_detail = 'Open WebUI: Server Connection Error'
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if 'error' in res:
|
||||
error_detail = f'Ollama: {res["error"]}'
|
||||
except Exception:
|
||||
error_detail = f'Ollama: {e}'
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
models = [
|
||||
{
|
||||
'id': model['model'],
|
||||
'object': 'model',
|
||||
'created': int(time.time()),
|
||||
'owned_by': 'openai',
|
||||
}
|
||||
for model in model_list.get('models', [])
|
||||
]
|
||||
|
||||
if user.role == 'user' and not BYPASS_MODEL_ACCESS_CONTROL:
|
||||
# Filter models based on user access control
|
||||
|
|
@ -1734,13 +1605,16 @@ async def download_file_stream(ollama_url, file_url, file_path, file_name, chunk
|
|||
file.close()
|
||||
hashed = calculate_sha256(file_path, chunk_size)
|
||||
|
||||
with open(file_path, 'rb') as file:
|
||||
chunk_size = 1024 * 1024 * 2
|
||||
url = f'{ollama_url}/api/blobs/sha256:{hashed}'
|
||||
with requests.Session() as session:
|
||||
response = session.post(url, data=file, timeout=30)
|
||||
with open(file_path, 'rb') as f:
|
||||
blob_data = f.read()
|
||||
|
||||
if response.ok:
|
||||
url = f'{ollama_url}/api/blobs/sha256:{hashed}'
|
||||
blob_timeout = aiohttp.ClientTimeout(total=30)
|
||||
async with aiohttp.ClientSession(timeout=blob_timeout, trust_env=True) as blob_session:
|
||||
async with blob_session.post(
|
||||
url, data=blob_data, ssl=AIOHTTP_CLIENT_SESSION_SSL
|
||||
) as blob_response:
|
||||
if blob_response.ok:
|
||||
res = {
|
||||
'done': done,
|
||||
'blob': f'sha256:{hashed}',
|
||||
|
|
@ -1836,47 +1710,53 @@ async def upload_model(
|
|||
|
||||
# --- P3: Upload to ollama /api/blobs ---
|
||||
with open(file_path, 'rb') as f:
|
||||
url = f'{ollama_url}/api/blobs/sha256:{file_hash}'
|
||||
response = requests.post(url, data=f)
|
||||
blob_data = f.read()
|
||||
|
||||
if response.ok:
|
||||
log.info(f'Uploaded to /api/blobs') # DEBUG
|
||||
# Remove local file
|
||||
os.remove(file_path)
|
||||
url = f'{ollama_url}/api/blobs/sha256:{file_hash}'
|
||||
upload_timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
|
||||
async with aiohttp.ClientSession(timeout=upload_timeout, trust_env=True) as upload_session:
|
||||
async with upload_session.post(
|
||||
url, data=blob_data, ssl=AIOHTTP_CLIENT_SESSION_SSL
|
||||
) as response:
|
||||
if not response.ok:
|
||||
raise Exception('Ollama: Could not create blob, Please try again.')
|
||||
|
||||
# Create model in ollama
|
||||
model_name, ext = os.path.splitext(filename)
|
||||
log.info(f'Created Model: {model_name}') # DEBUG
|
||||
log.info(f'Uploaded to /api/blobs') # DEBUG
|
||||
# Remove local file
|
||||
os.remove(file_path)
|
||||
|
||||
create_payload = {
|
||||
'model': model_name,
|
||||
# Reference the file by its original name => the uploaded blob's digest
|
||||
'files': {filename: f'sha256:{file_hash}'},
|
||||
}
|
||||
log.info(f'Model Payload: {create_payload}') # DEBUG
|
||||
# Create model in ollama
|
||||
model_name, ext = os.path.splitext(filename)
|
||||
log.info(f'Created Model: {model_name}') # DEBUG
|
||||
|
||||
# Call ollama /api/create
|
||||
# https://github.com/ollama/ollama/blob/main/docs/api.md#create-a-model
|
||||
create_resp = requests.post(
|
||||
url=f'{ollama_url}/api/create',
|
||||
create_payload = {
|
||||
'model': model_name,
|
||||
# Reference the file by its original name => the uploaded blob's digest
|
||||
'files': {filename: f'sha256:{file_hash}'},
|
||||
}
|
||||
log.info(f'Model Payload: {create_payload}') # DEBUG
|
||||
|
||||
# Call ollama /api/create
|
||||
# https://github.com/ollama/ollama/blob/main/docs/api.md#create-a-model
|
||||
async with aiohttp.ClientSession(timeout=upload_timeout, trust_env=True) as create_session:
|
||||
async with create_session.post(
|
||||
f'{ollama_url}/api/create',
|
||||
headers={'Content-Type': 'application/json'},
|
||||
data=json.dumps(create_payload),
|
||||
)
|
||||
|
||||
if create_resp.ok:
|
||||
log.info(f'API SUCCESS!') # DEBUG
|
||||
done_msg = {
|
||||
'done': True,
|
||||
'blob': f'sha256:{file_hash}',
|
||||
'name': filename,
|
||||
'model_created': model_name,
|
||||
}
|
||||
yield f'data: {json.dumps(done_msg)}\n\n'
|
||||
else:
|
||||
raise Exception(f'Failed to create model in Ollama. {create_resp.text}')
|
||||
|
||||
else:
|
||||
raise Exception('Ollama: Could not create blob, Please try again.')
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||
) as create_resp:
|
||||
if create_resp.ok:
|
||||
log.info(f'API SUCCESS!') # DEBUG
|
||||
done_msg = {
|
||||
'done': True,
|
||||
'blob': f'sha256:{file_hash}',
|
||||
'name': filename,
|
||||
'model_created': model_name,
|
||||
}
|
||||
yield f'data: {json.dumps(done_msg)}\n\n'
|
||||
else:
|
||||
resp_text = await create_resp.text()
|
||||
raise Exception(f'Failed to create model in Ollama. {resp_text}')
|
||||
|
||||
except Exception as e:
|
||||
res = {'error': str(e)}
|
||||
|
|
|
|||
|
|
@ -1131,21 +1131,31 @@ async def generate_chat_completion(
|
|||
is_responses = api_config.get('api_type') == 'responses'
|
||||
|
||||
if api_config.get('azure', False):
|
||||
api_version = api_config.get('api_version', '2023-03-15-preview')
|
||||
request_url, payload = convert_to_azure_payload(url, payload, api_version)
|
||||
|
||||
# Only set api-key header if not using Azure Entra ID authentication
|
||||
auth_type = api_config.get('auth_type', 'bearer')
|
||||
if auth_type not in ('azure_ad', 'microsoft_entra_id'):
|
||||
headers['api-key'] = key
|
||||
|
||||
headers['api-version'] = api_version
|
||||
# Azure v1 format: base URL already ends with /openai/v1,
|
||||
# model stays in the payload, no deployment URL rewriting.
|
||||
is_azure_v1 = bool(re.search(r'/openai/v1(?:/|$)', url))
|
||||
|
||||
if is_responses:
|
||||
payload = convert_to_responses_payload(payload)
|
||||
request_url = f'{request_url}/responses?api-version={api_version}'
|
||||
if is_azure_v1:
|
||||
if is_responses:
|
||||
payload = convert_to_responses_payload(payload)
|
||||
request_url = f'{url.rstrip("/")}/responses'
|
||||
else:
|
||||
request_url = f'{url.rstrip("/")}/chat/completions'
|
||||
else:
|
||||
request_url = f'{request_url}/chat/completions?api-version={api_version}'
|
||||
api_version = api_config.get('api_version', '2023-03-15-preview')
|
||||
request_url, payload = convert_to_azure_payload(url, payload, api_version)
|
||||
headers['api-version'] = api_version
|
||||
|
||||
if is_responses:
|
||||
payload = convert_to_responses_payload(payload)
|
||||
request_url = f'{request_url}/responses?api-version={api_version}'
|
||||
else:
|
||||
request_url = f'{request_url}/chat/completions?api-version={api_version}'
|
||||
else:
|
||||
if is_responses:
|
||||
payload = convert_to_responses_payload(payload)
|
||||
|
|
@ -1357,16 +1367,19 @@ async def responses(
|
|||
headers, cookies = await get_headers_and_cookies(request, url, key, api_config, user=user)
|
||||
|
||||
if api_config.get('azure', False):
|
||||
api_version = api_config.get('api_version', '2023-03-15-preview')
|
||||
|
||||
auth_type = api_config.get('auth_type', 'bearer')
|
||||
if auth_type not in ('azure_ad', 'microsoft_entra_id'):
|
||||
headers['api-key'] = key
|
||||
|
||||
headers['api-version'] = api_version
|
||||
is_azure_v1 = bool(re.search(r'/openai/v1(?:/|$)', url))
|
||||
|
||||
model = payload.get('model', '')
|
||||
request_url = f'{url}/openai/deployments/{model}/responses?api-version={api_version}'
|
||||
if is_azure_v1:
|
||||
request_url = f'{url.rstrip("/")}/responses'
|
||||
else:
|
||||
api_version = api_config.get('api_version', '2023-03-15-preview')
|
||||
headers['api-version'] = api_version
|
||||
model = payload.get('model', '')
|
||||
request_url = f'{url}/openai/deployments/{model}/responses?api-version={api_version}'
|
||||
else:
|
||||
request_url = f'{url}/responses'
|
||||
|
||||
|
|
@ -1459,20 +1472,25 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
|||
headers, cookies = await get_headers_and_cookies(request, url, key, api_config, user=user)
|
||||
|
||||
if api_config.get('azure', False):
|
||||
api_version = api_config.get('api_version', '2023-03-15-preview')
|
||||
|
||||
# Only set api-key header if not using Azure Entra ID authentication
|
||||
auth_type = api_config.get('auth_type', 'bearer')
|
||||
if auth_type not in ('azure_ad', 'microsoft_entra_id'):
|
||||
headers['api-key'] = key
|
||||
|
||||
headers['api-version'] = api_version
|
||||
is_azure_v1 = bool(re.search(r'/openai/v1(?:/|$)', url))
|
||||
|
||||
payload = json.loads(body)
|
||||
url, payload = convert_to_azure_payload(url, payload, api_version)
|
||||
body = json.dumps(payload).encode()
|
||||
if is_azure_v1:
|
||||
qs = request.url.query
|
||||
request_url = f'{url.rstrip("/")}/{path}' + (f'?{qs}' if qs else '')
|
||||
else:
|
||||
api_version = api_config.get('api_version', '2023-03-15-preview')
|
||||
headers['api-version'] = api_version
|
||||
|
||||
request_url = f'{url}/{path}?api-version={api_version}'
|
||||
payload = json.loads(body)
|
||||
url, payload = convert_to_azure_payload(url, payload, api_version)
|
||||
body = json.dumps(payload).encode()
|
||||
|
||||
request_url = f'{url}/{path}?api-version={api_version}'
|
||||
else:
|
||||
request_url = f'{url}/{path}'
|
||||
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ async def calculate_timestamp(
|
|||
|
||||
async def search_web(
|
||||
query: str,
|
||||
count: int = 5,
|
||||
count: Optional[int] = None,
|
||||
__request__: Request = None,
|
||||
__user__: dict = None,
|
||||
) -> str:
|
||||
|
|
@ -158,7 +158,7 @@ async def search_web(
|
|||
or topics not covered in internal documents.
|
||||
|
||||
:param query: The search query to look up
|
||||
:param count: Number of results to return (default: 5)
|
||||
:param count: Number of results to return (default: admin-configured value)
|
||||
:return: JSON with search results containing title, link, and snippet for each result
|
||||
"""
|
||||
if __request__ is None:
|
||||
|
|
@ -168,12 +168,9 @@ async def search_web(
|
|||
engine = __request__.app.state.config.WEB_SEARCH_ENGINE
|
||||
user = UserModel(**__user__) if __user__ else None
|
||||
|
||||
# Enforce maximum result count from config to prevent abuse
|
||||
count = (
|
||||
count
|
||||
if count < __request__.app.state.config.WEB_SEARCH_RESULT_COUNT
|
||||
else __request__.app.state.config.WEB_SEARCH_RESULT_COUNT
|
||||
)
|
||||
configured = __request__.app.state.config.WEB_SEARCH_RESULT_COUNT
|
||||
max_count = 5 if configured is None else configured
|
||||
count = max(1, min(count, max_count)) if count is not None else max_count
|
||||
|
||||
results = await asyncio.to_thread(_search_web, __request__, engine, query, user)
|
||||
|
||||
|
|
|
|||
|
|
@ -2680,9 +2680,12 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
|||
tools_dict[name] = tool_dict
|
||||
|
||||
if tools_dict:
|
||||
# Always store resolved tools in metadata so downstream consumers
|
||||
# (e.g. pipe functions) can access all tools including MCP and builtins.
|
||||
metadata['tools'] = tools_dict
|
||||
|
||||
if metadata.get('params', {}).get('function_calling') == 'native':
|
||||
# If the function calling is native, then call the tools function calling handler
|
||||
metadata['tools'] = tools_dict
|
||||
form_data['tools'] = [
|
||||
{'type': 'function', 'function': tool.get('spec', {})} for tool in tools_dict.values()
|
||||
]
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ from open_webui.utils.misc import parse_duration
|
|||
from open_webui.utils.auth import get_password_hash, create_token
|
||||
from open_webui.utils.webhook import post_webhook
|
||||
from open_webui.utils.groups import apply_default_group_assignment
|
||||
from open_webui.retrieval.web.utils import validate_url
|
||||
|
||||
from mcp.shared.auth import (
|
||||
OAuthClientMetadata as MCPOAuthClientMetadata,
|
||||
|
|
@ -1330,6 +1331,8 @@ class OAuthManager:
|
|||
return '/user.png'
|
||||
|
||||
try:
|
||||
validate_url(picture_url)
|
||||
|
||||
get_kwargs = {}
|
||||
if access_token:
|
||||
get_kwargs['headers'] = {
|
||||
|
|
|
|||
|
|
@ -917,10 +917,8 @@
|
|||
'MCP support is experimental and its specification changes often, which can lead to incompatibilities. OpenAPI specification support is directly maintained by the Open WebUI team, making it the more reliable option for compatibility.'
|
||||
)}
|
||||
|
||||
<a
|
||||
class="font-medium underline"
|
||||
href="https://docs.openwebui.com/features/mcp"
|
||||
target="_blank">{$i18n.t('Read more →')}</a
|
||||
<a class="font-medium underline" href="https://docs.openwebui.com/" target="_blank"
|
||||
>{$i18n.t('Read more →')}</a
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,8 @@
|
|||
selectedTerminalId,
|
||||
showFileNavPath,
|
||||
showFileNavDir,
|
||||
chatRequestQueues
|
||||
chatRequestQueues,
|
||||
desktopEvent
|
||||
} from '$lib/stores';
|
||||
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
|
@ -1225,7 +1226,31 @@
|
|||
showControls.set(true);
|
||||
}
|
||||
|
||||
if ($page.url.searchParams.get('q')) {
|
||||
// Consume one-shot desktop event (e.g. Spotlight query + attachments)
|
||||
if ($desktopEvent) {
|
||||
const { query, files: eventFiles } = $desktopEvent;
|
||||
desktopEvent.set(null);
|
||||
|
||||
// Attach screenshot images from desktop (e.g. Spotlight region capture)
|
||||
if (eventFiles?.length) {
|
||||
for (const ef of eventFiles) {
|
||||
files = [
|
||||
...files,
|
||||
{
|
||||
type: 'image',
|
||||
url: ef.dataUrl,
|
||||
name: ef.name
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (query) {
|
||||
messageInput?.setText(query);
|
||||
await tick();
|
||||
submitHandler(query);
|
||||
}
|
||||
} else if ($page.url.searchParams.get('q')) {
|
||||
const q = $page.url.searchParams.get('q') ?? '';
|
||||
messageInput?.setText(q);
|
||||
|
||||
|
|
|
|||
|
|
@ -103,6 +103,13 @@
|
|||
$: isNotebook = getExt(selectedFile) === 'ipynb';
|
||||
$: isCode = isCodeFile(selectedFile);
|
||||
$: csvDelimiter = getExt(selectedFile) === 'tsv' ? '\t' : ',';
|
||||
|
||||
// For HTML files on system terminals (proxy URL), use path-based serving
|
||||
// so the iframe can resolve relative CSS/JS/image references via cookie auth.
|
||||
$: serveUrl =
|
||||
isHtml && selectedFile && baseUrl && baseUrl.includes('/api/v1/terminals/')
|
||||
? `${baseUrl}/files/serve/${selectedFile.replace(/^\//, '')}`
|
||||
: null;
|
||||
$: renderedHtml =
|
||||
isMarkdown && fileContent
|
||||
? DOMPurify.sanitize(marked.parse(fileContent, { async: false }) as string)
|
||||
|
|
@ -386,7 +393,19 @@
|
|||
{/if}
|
||||
</div>
|
||||
{:else if fileContent !== null}
|
||||
{#if isHtml && !showRaw}
|
||||
{#if isHtml && !showRaw && serveUrl}
|
||||
{#if overlay}
|
||||
<div class="absolute top-0 left-0 right-0 bottom-0 z-10"></div>
|
||||
{/if}
|
||||
<iframe
|
||||
src={serveUrl}
|
||||
sandbox="allow-scripts allow-same-origin allow-downloads{($settings?.iframeSandboxAllowForms ?? false)
|
||||
? ' allow-forms'
|
||||
: ''}"
|
||||
class="w-full h-full border-none bg-white"
|
||||
title="HTML Preview"
|
||||
/>
|
||||
{:else if isHtml && !showRaw}
|
||||
{#if overlay}
|
||||
<div class="absolute top-0 left-0 right-0 bottom-0 z-10"></div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@
|
|||
<Cloud className="size-3.5" strokeWidth="2" />
|
||||
|
||||
{#if $selectedTerminalId && selectedLabel}
|
||||
<span class="truncate text-[13px] max-w-[100px] sm:max-w-[150px] capitalize"
|
||||
<span class="truncate text-[13px] max-w-[100px] sm:max-w-[150px]"
|
||||
>{selectedLabel}</span
|
||||
>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@
|
|||
"Are you sure you want to archive all chats? This action cannot be undone.": "Estàs segur que vols arxivar tots els xats? Aquesta acció no es pot desfer.",
|
||||
"Are you sure you want to clear all memories? This action cannot be undone.": "Estàs segur que vols netejar totes les memòries? Aquesta acció no es pot desfer.",
|
||||
"Are you sure you want to delete \"{{NAME}}\"?": "Estàs segur que vols eliminar \"{{NAME}}\"?",
|
||||
"Are you sure you want to delete **{{modelName}}**?": "",
|
||||
"Are you sure you want to delete **{{modelName}}**?": "Estàs segur que vols suprimir **{{modelName}}**?",
|
||||
"Are you sure you want to delete all chats? This action cannot be undone.": "Estàs segur que vols suprimir tots els xats? Aquesta acció no es pot desfer.",
|
||||
"Are you sure you want to delete this channel?": "Estàs segur que vols eliminar aquest canal?",
|
||||
"Are you sure you want to delete this connection? This action cannot be undone.": "Estàs segur que vols suprimir aquesta connexió? Aquesta acció no es pot desfer.",
|
||||
|
|
@ -200,7 +200,7 @@
|
|||
"Assistant": "Assistent",
|
||||
"Async Embedding Processing": "Procés d'incrustat asíncron",
|
||||
"Attach File From Knowledge": "Adjuntar arxiu del coneixement",
|
||||
"Attach Files": "",
|
||||
"Attach Files": "Adjuntar arxius",
|
||||
"Attach Knowledge": "Adjuntar coneixement",
|
||||
"Attach Notes": "Adjuntar notes",
|
||||
"Attach Webpage": "Adjuntar pàgina web",
|
||||
|
|
@ -223,13 +223,13 @@
|
|||
"AUTOMATIC1111 Base URL": "URL Base d'AUTOMATIC1111",
|
||||
"AUTOMATIC1111 Base URL is required.": "Es requereix la URL Base d'AUTOMATIC1111.",
|
||||
"Automatically inject system tools in native function calling mode (e.g., timestamps, memory, chat history, notes, etc.)": "Injecta automàticament les eines del sistema en el mode de crida de funcions natives (per exemple, marques de temps, memòria, historial de xat, notes, etc.)",
|
||||
"Automation": "",
|
||||
"Automation created": "",
|
||||
"Automation Name": "",
|
||||
"Automation title": "",
|
||||
"Automation triggered": "",
|
||||
"Automation updated": "",
|
||||
"Automations": "",
|
||||
"Automation": "Automatització",
|
||||
"Automation created": "Automatització creada",
|
||||
"Automation Name": "Nom de l'automatització",
|
||||
"Automation title": "Títol de l'automatització",
|
||||
"Automation triggered": "Automatització disparada",
|
||||
"Automation updated": "Automatització actualitzada",
|
||||
"Automations": "Automatització",
|
||||
"Available list": "Llista de disponibles",
|
||||
"Available models": "Models disponibles",
|
||||
"Available Tools": "Eines disponibles",
|
||||
|
|
@ -260,7 +260,7 @@
|
|||
"Boosting or penalizing specific tokens for constrained responses. Bias values will be clamped between -100 and 100 (inclusive). (Default: none)": "Potenciar o penalitzar tokens específics per a respostes limitades. Els valors de biaix es fixaran entre -100 i 100 (inclosos). (Per defecte: cap)",
|
||||
"Brave": "Brave",
|
||||
"Brave Search API Key": "Clau API de Brave Search",
|
||||
"Break down complex requests into trackable steps": "",
|
||||
"Break down complex requests into trackable steps": "Dividir les sol·licituds complexes en passos rastrejables",
|
||||
"Browse and query knowledge bases": "Cerca i fes preguntes a una base de coneixement",
|
||||
"Builtin Tools": "Eines integrades",
|
||||
"Bullet List": "Llista indexada",
|
||||
|
|
@ -394,7 +394,7 @@
|
|||
"Concurrent Requests": "Peticions simultànies",
|
||||
"Config": "Configuració",
|
||||
"Config imported successfully": "Configuració importada correctament",
|
||||
"Configuration": "",
|
||||
"Configuration": "Configuració",
|
||||
"Configure": "Configurar",
|
||||
"Confirm": "Confirmar",
|
||||
"Confirm Password": "Confirmar la contrasenya",
|
||||
|
|
@ -464,7 +464,7 @@
|
|||
"Create new secret key": "Crear una nova clau secreta",
|
||||
"Create note": "Crear una nota",
|
||||
"Create Note": "Crea nota",
|
||||
"Create scheduled prompts that run automatically on a recurring basis.": "",
|
||||
"Create scheduled prompts that run automatically on a recurring basis.": "Crea sol·licituds programades que s'executin automàticament de manera recurrent.",
|
||||
"Create your first note by clicking on the plus button below.": "Crea la teva primera nota prement sobre el botó 'més' inferior",
|
||||
"Created at": "Creat el",
|
||||
"Created At": "Creat el",
|
||||
|
|
@ -486,7 +486,7 @@
|
|||
"Data Controls": "Controls de dades",
|
||||
"Database": "Base de dades",
|
||||
"Datalab Marker API": "API de Datalab Marker",
|
||||
"Day": "",
|
||||
"Day": "Dia",
|
||||
"DD/MM/YYYY": "DD/MM/YYYY",
|
||||
"DDGS Backend": "Backend DDGS",
|
||||
"December": "Desembre",
|
||||
|
|
@ -517,7 +517,7 @@
|
|||
"Delete All": "Eliminar tot",
|
||||
"Delete All Chats": "Eliminar tots els xats",
|
||||
"Delete all contents inside this folder": "Eliminar tot el contingut d'aquesta carpeta",
|
||||
"Delete automation?": "",
|
||||
"Delete automation?": "Eliminar l'automatització",
|
||||
"Delete Chat": "Eliminar xat",
|
||||
"Delete chat?": "Eliminar el xat?",
|
||||
"Delete File": "Eliminar el fitxer",
|
||||
|
|
@ -662,7 +662,7 @@
|
|||
"Embedding Concurrent Requests": "Peticions concurrents d'incrustació",
|
||||
"Embedding Model": "Model d'incrustació",
|
||||
"Embedding Model Engine": "Motor de model d'incrustació",
|
||||
"Emojis": "",
|
||||
"Emojis": "Emojis",
|
||||
"Empty message": "Missatge buit",
|
||||
"Enable All": "Habilitar tot",
|
||||
"Enable API Keys": "Permetre claus API",
|
||||
|
|
@ -754,7 +754,7 @@
|
|||
"Enter Perplexity Search API URL": "Introduïu l'URL de l'API de cerca de Perplexity",
|
||||
"Enter Playwright Timeout": "Introdueix el temps d'espera de Playwright",
|
||||
"Enter Playwright WebSocket URL": "Introdueix la URL de Playwright WebSocket",
|
||||
"Enter prompt here.": "",
|
||||
"Enter prompt here.": "Introdueix el prompt aquí.",
|
||||
"Enter proxy URL (e.g. https://user:password@host:port)": "Entra la URL (p. ex. https://user:password@host:port)",
|
||||
"Enter reasoning effort": "Introdueix l'esforç de raonament",
|
||||
"Enter Score": "Introdueix la puntuació",
|
||||
|
|
@ -779,7 +779,7 @@
|
|||
"Enter system prompt here": "Entra la indicació de sistema aquí",
|
||||
"Enter Tavily API Key": "Introdueix la clau API de Tavily",
|
||||
"Enter Tavily Extract Depth": "Introdueix la profunditat d'extracció de Tavily",
|
||||
"Enter the prompt instructions for this automation...": "",
|
||||
"Enter the prompt instructions for this automation...": "Introdueix les instruccions de la indicació per a aquesta automatització...",
|
||||
"Enter the public URL of your WebUI. This URL will be used to generate links in the notifications.": "Entra la URL pública de WebUI. Aquesta URL s'utilitzarà per generar els enllaços en les notificacions.",
|
||||
"Enter the URL of the function to import": "Introdueix la URL de la funció a importar",
|
||||
"Enter the URL to import": "Introdueix la URL a importar",
|
||||
|
|
@ -819,7 +819,7 @@
|
|||
"Error accessing directory": "Error en accedir al directori",
|
||||
"Error accessing Google Drive: {{error}}": "Error en accedir a Google Drive: {{error}}",
|
||||
"Error accessing media devices.": "Error en accedir als dispositius multimèdia",
|
||||
"Error deleting model: {{error}}": "",
|
||||
"Error deleting model: {{error}}": "Error en eliminar el model: {{error}}",
|
||||
"Error starting recording.": "Error en començar a enregistrar",
|
||||
"Error unloading model: {{error}}": "Error en descarregar el model: {{error}}",
|
||||
"Error uploading file: {{error}}": "Error en pujar l'arxiu: {{error}}",
|
||||
|
|
@ -837,7 +837,7 @@
|
|||
"Execute code": "Executa el codi",
|
||||
"Execute code for analysis": "Executar el codi per analitzar-lo",
|
||||
"Executing **{{NAME}}**...": "Executant **{{NAME}}**...",
|
||||
"Execution Logs": "",
|
||||
"Execution Logs": "Registre de l'execució",
|
||||
"Expand": "Expandir",
|
||||
"Experimental": "Experimental",
|
||||
"Explain": "Explicar",
|
||||
|
|
@ -847,8 +847,8 @@
|
|||
"Export": "Exportar",
|
||||
"Export All Archived Chats": "Exportar tots els xats arxivats",
|
||||
"Export All Chats (All Users)": "Exportar tots els xats (Tots els usuaris)",
|
||||
"Export as CSV": "",
|
||||
"Export as JSON": "",
|
||||
"Export as CSV": "Exportar com a CSV",
|
||||
"Export as JSON": "Exportar com a JSON",
|
||||
"Export chat (.json)": "Exportar el xat (.json)",
|
||||
"Export Chats": "Exportar els xats",
|
||||
"Export Config": "Exportar la configuració",
|
||||
|
|
@ -1103,7 +1103,7 @@
|
|||
"Insert Suggestion Prompt to Input": "Insereix un suggeriment per introduir",
|
||||
"Install from Github URL": "Instal·lar des de la URL de Github",
|
||||
"Instant Auto-Send After Voice Transcription": "Enviament automàtic després de la transcripció de veu",
|
||||
"Instructions": "",
|
||||
"Instructions": "Instruccions",
|
||||
"Integration": "Integració",
|
||||
"Integrations": "Integracions",
|
||||
"Interface": "Interfície",
|
||||
|
|
@ -1163,7 +1163,7 @@
|
|||
"Last 90 days": "Darrers 90 dies",
|
||||
"Last Active": "Activitat recent",
|
||||
"Last Modified": "Modificació",
|
||||
"Last ran": "",
|
||||
"Last ran": "Darrera execució",
|
||||
"Last reply": "Darrera resposta",
|
||||
"LDAP": "LDAP",
|
||||
"LDAP server updated": "Servidor LDAP actualitzat",
|
||||
|
|
@ -1270,7 +1270,7 @@
|
|||
"Model '{{modelName}}' has been successfully downloaded.": "El model '{{modelName}}' s'ha descarregat correctament.",
|
||||
"Model '{{modelTag}}' is already in queue for downloading.": "El model '{{modelTag}}' ja està en cua per ser descarregat.",
|
||||
"Model {{modelId}} not found": "No s'ha trobat el model {{modelId}}",
|
||||
"Model {{modelName}} deleted successfully": "",
|
||||
"Model {{modelName}} deleted successfully": "El model {{modelName}} s'ha eliminat correctament",
|
||||
"Model {{modelName}} is not vision capable": "El model {{modelName}} no és capaç de visió",
|
||||
"Model {{name}} is now {{status}}": "El model {{name}} ara és {{status}}",
|
||||
"Model {{name}} is now hidden": "El model {{name}} està ara amagat",
|
||||
|
|
@ -1320,11 +1320,11 @@
|
|||
"Name": "Nom",
|
||||
"Name and ID are required, please fill them out": "El nom i l'ID són necessaris, emplena'ls, si us plau",
|
||||
"Name your knowledge base": "Anomena la teva base de coneixement",
|
||||
"Name, prompt, and model are required": "",
|
||||
"Name, prompt, and model are required": "Nom, indicació i model són necessaris",
|
||||
"Native": "Natiu",
|
||||
"Never": "",
|
||||
"Never": "Mai",
|
||||
"New": "Nou",
|
||||
"New Automation": "",
|
||||
"New Automation": "Nova automatització",
|
||||
"New Button": "Botó nou",
|
||||
"New Chat": "Nou xat",
|
||||
"New File": "Nou arxiu",
|
||||
|
|
@ -1343,11 +1343,11 @@
|
|||
"New Webhook": "Nou webhook",
|
||||
"new-channel": "nou-canal",
|
||||
"Next message": "Missatge següent",
|
||||
"Next run": "",
|
||||
"Next run": "Següent execució",
|
||||
"No access grants. Private to you.": "Sense permisos d'accés. Privat per a tu.",
|
||||
"No activity data": "No hi ha dades d'activitat",
|
||||
"No authentication": "Sense autenticació",
|
||||
"No automations found": "",
|
||||
"No automations found": "No s'ha trobat cap automatització",
|
||||
"No chats found": "No s'han trobat xats",
|
||||
"No chats found for this user.": "No s'han trobat xats per a aquest usuari.",
|
||||
"No chats found.": "No s'ha trobat xats.",
|
||||
|
|
@ -1358,7 +1358,7 @@
|
|||
"No data": "No hi ha dades",
|
||||
"No data found": "No s'han trobat dades",
|
||||
"No distance available": "No hi ha distància disponible",
|
||||
"No execution logs available yet": "",
|
||||
"No execution logs available yet": "No hi ha registres d'execució encara",
|
||||
"No expiration can pose security risks.": "No posar expiració pot suposar problemes de seguretat.",
|
||||
"No feedback found": "No s'ha trobat cap retorn",
|
||||
"No file selected": "No s'ha escollit cap fitxer",
|
||||
|
|
@ -1405,7 +1405,7 @@
|
|||
"Not factually correct": "No és clarament correcte",
|
||||
"Not helpful": "No ajuda",
|
||||
"Not Registered": "No registrat",
|
||||
"Not scheduled": "",
|
||||
"Not scheduled": "No s'ha programat",
|
||||
"Note": "Nota",
|
||||
"Note deleted successfully": "La nota s'ha eliminat correctament",
|
||||
"Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Nota: Si s'estableix una puntuació mínima, la cerca només retornarà documents amb una puntuació major o igual a la puntuació mínima.",
|
||||
|
|
@ -1479,7 +1479,7 @@
|
|||
"or": "o",
|
||||
"Ordered List": "Llista ordenada",
|
||||
"Other": "Altres",
|
||||
"out of": "",
|
||||
"out of": "de",
|
||||
"Output": "Sortida",
|
||||
"OUTPUT": "SORTIDA",
|
||||
"Output format": "Format de sortida",
|
||||
|
|
@ -1495,7 +1495,7 @@
|
|||
"Password": "Contrasenya",
|
||||
"Passwords do not match.": "Les contrasenyes no coincideixen",
|
||||
"Paste Large Text as File": "Enganxa un text llarg com a fitxer",
|
||||
"Paused": "",
|
||||
"Paused": "Pausat",
|
||||
"PDF document (.pdf)": "Document PDF (.pdf)",
|
||||
"PDF Extract Images (OCR)": "Extreu imatges del PDF (OCR)",
|
||||
"PDF Loader Mode": "Mode de càrrega de PDF",
|
||||
|
|
@ -1600,7 +1600,7 @@
|
|||
"Reason": "Raó",
|
||||
"Reasoning Effort": "Esforç de raonament",
|
||||
"Reasoning Tags": "Etiqueta de raonament",
|
||||
"Recently Used": "",
|
||||
"Recently Used": "Recentment utilitzat",
|
||||
"Record": "Enregistrar",
|
||||
"Record voice": "Enregistrar la veu",
|
||||
"Redirecting you to Open WebUI Community": "Redirigint-te a la comunitat OpenWebUI",
|
||||
|
|
@ -1636,7 +1636,7 @@
|
|||
"Renamed to {{name}}": "S'ha renombrat a {{name}}",
|
||||
"Render Markdown in Previews": "Compila el Markdown a les previsualitzacions",
|
||||
"Reorder Models": "Reordenar els models",
|
||||
"Repeats": "",
|
||||
"Repeats": "Repeticions",
|
||||
"Reply": "Respondre",
|
||||
"Reply in Thread": "Respondre al fil",
|
||||
"Reply to thread...": "Respondra al fil...",
|
||||
|
|
@ -1670,8 +1670,8 @@
|
|||
"RTL": "RTL",
|
||||
"Run": "Executar",
|
||||
"Run All": "Executar tot",
|
||||
"Run now": "",
|
||||
"Run Now": "",
|
||||
"Run now": "Executar ara",
|
||||
"Run Now": "Executar ara",
|
||||
"Running": "S'està executant",
|
||||
"Running...": "S'està executant...",
|
||||
"Runs embedding tasks concurrently to speed up processing. Turn off if rate limits become an issue.": "Executa tasques d'incrustació simultàniament per accelerar el processament. Desactiva-ho si els límits de velocitat es converteixen en un problema.",
|
||||
|
|
@ -1682,15 +1682,15 @@
|
|||
"Save Chat": "Dear el xat",
|
||||
"Saved": "Desat",
|
||||
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Desar els registres de xat directament a l'emmagatzematge del teu navegador ja no està suportat. Si us plau, descarregr i elimina els registres de xat fent clic al botó de sota. No et preocupis, pots tornar a importar fàcilment els teus registres de xat al backend a través de",
|
||||
"Schedule": "",
|
||||
"Scheduled time must be in the future": "",
|
||||
"Schedule": "Programar",
|
||||
"Scheduled time must be in the future": "La data d'execucuío ha de ser en el futur",
|
||||
"Scroll On Branch Change": "Fer scroll en canviar de branca",
|
||||
"Search": "Cercar",
|
||||
"Search a model": "Cercar un model",
|
||||
"Search all emojis": "Cercar tots els emojis",
|
||||
"Search and manage user memories": "Cerca i gestiona les memòries d'usuari",
|
||||
"Search and view user chat history": "Cerca i mostra l'historial de xats",
|
||||
"Search Automations": "",
|
||||
"Search Automations": "Cercar automatitzacions",
|
||||
"Search Base": "Base de cerca",
|
||||
"Search channels and channel messages": "Cerca els canals i els missatges als canals",
|
||||
"Search Chats": "Cercar xats",
|
||||
|
|
@ -1760,7 +1760,7 @@
|
|||
"Select how to split message text for TTS requests": "Seleccionar com separar un missatge per a peticions TTS",
|
||||
"Select Knowledge": "Seleccionar coneixement",
|
||||
"Select Method": "Escollir el mètode",
|
||||
"Select model": "",
|
||||
"Select model": "Escollir el model",
|
||||
"Select only one model to call": "Seleccionar només un model per trucar",
|
||||
"Select view": "Seleccionar una vista",
|
||||
"Selected model: {{modelName}}": "Model seleccionat: {{modelName}}",
|
||||
|
|
@ -1870,7 +1870,7 @@
|
|||
"Start of the channel": "Inici del canal",
|
||||
"Start Tag": "Etiqueta d'inici",
|
||||
"Starting kernel...": "Iniciant el kernel...",
|
||||
"State": "",
|
||||
"State": "Estat",
|
||||
"Status": "Estat",
|
||||
"Status cleared successfully": "S'ha eliminat correctament el teu estat",
|
||||
"Status updated successfully": "S'ha actualitzat correctament el teu estat",
|
||||
|
|
@ -1921,10 +1921,10 @@
|
|||
"Talk to Model": "Parlar amb el model",
|
||||
"Tap to interrupt": "Prem per interrompre",
|
||||
"Task List": "Llista de tasques",
|
||||
"Task Management": "",
|
||||
"Task Management": "Gestió de tasques",
|
||||
"Task Model": "Model de tasques",
|
||||
"Tasks": "Tasques",
|
||||
"tasks completed": "",
|
||||
"tasks completed": "tasques completades",
|
||||
"Tavily API Key": "Clau API de Tavily",
|
||||
"Tavily Extract Depth": "Profunditat d'extracció de Tavily",
|
||||
"Tell us more:": "Dona'ns més informació:",
|
||||
|
|
@ -1991,7 +1991,7 @@
|
|||
"Tika": "Tika",
|
||||
"Tika Server URL required.": "La URL del servidor Tika és obligatòria.",
|
||||
"Tiktoken": "Tiktoken",
|
||||
"Time": "",
|
||||
"Time": "Temps",
|
||||
"Time & Calculation": "Temps i càlculs",
|
||||
"Timeout": "Temps d'espera",
|
||||
"Title": "Títol",
|
||||
|
|
@ -2009,7 +2009,7 @@
|
|||
"To select toolkits here, add them to the \"Tools\" workspace first.": "Per seleccionar kits d'eines aquí, afegeix-los primer a l'espai de treball \"Eines\".",
|
||||
"Toast notifications for new updates": "Notificacions Toast de noves actualitzacions",
|
||||
"Today": "Avui",
|
||||
"Today at": "",
|
||||
"Today at": "Avui a les",
|
||||
"Today at {{LOCALIZED_TIME}}": "Avui a les {{LOCALIZED_TIME}}",
|
||||
"Toggle {{COUNT}} sources": "Activa/Desactiva {{COUNT}} fonts",
|
||||
"Toggle 1 source": "Activa/Desactiva 1 font",
|
||||
|
|
@ -2148,7 +2148,7 @@
|
|||
"Waiting for upload...": "Esperant per pujar...",
|
||||
"Warning": "Avís",
|
||||
"Warning:": "Avís:",
|
||||
"Warning: Enabling this will allow users to run scheduled prompts automatically.": "",
|
||||
"Warning: Enabling this will allow users to run scheduled prompts automatically.": "Avís: Si actives aquesta opció, els usuaris podran executar sol·licituds programades automàticament.",
|
||||
"Warning: Enabling this will allow users to upload arbitrary code on the server.": "Avís: Habilitar això permetrà als usuaris penjar codi arbitrari al servidor.",
|
||||
"Warning: Jupyter execution enables arbitrary code execution, posing severe security risks—proceed with extreme caution.": "Avís: l'execució de Jupyter permet l'execució de codi arbitrari, la qual cosa comporta greus riscos de seguretat; procediu amb extrema precaució.",
|
||||
"Web": "Web",
|
||||
|
|
@ -2183,7 +2183,7 @@
|
|||
"Width": "amplada",
|
||||
"Wikipedia": "Wikipedia",
|
||||
"Won": "Ha guanyat",
|
||||
"Working Directory": "",
|
||||
"Working Directory": "Carpeta de treball",
|
||||
"Works together with top-k. A higher value (e.g., 0.95) will lead to more diverse text, while a lower value (e.g., 0.5) will generate more focused and conservative text.": "Funciona juntament amb top-k. Un valor més alt (p. ex., 0,95) donarà lloc a un text més divers, mentre que un valor més baix (p. ex., 0,5) generarà un text més concentrat i conservador.",
|
||||
"Workspace": "Espai de treball",
|
||||
"Workspace Permissions": "Permisos de l'espai de treball",
|
||||
|
|
|
|||
|
|
@ -111,6 +111,15 @@ export const artifactContents = writable(null);
|
|||
export const embed = writable(null);
|
||||
|
||||
export const temporaryChatEnabled = writable(false);
|
||||
|
||||
// Transient one-shot event from the desktop shell (Spotlight, drag-and-drop, etc.).
|
||||
// Set by +layout.svelte, consumed and cleared by Chat.svelte.
|
||||
export type DesktopEventFile = { name: string; mimeType: string; dataUrl: string };
|
||||
export type DesktopEvent = {
|
||||
query?: string;
|
||||
files?: DesktopEventFile[];
|
||||
};
|
||||
export const desktopEvent: Writable<DesktopEvent | null> = writable(null);
|
||||
export const scrollPaginationEnabled = writable(false);
|
||||
export const currentChatPage = writable(1);
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,8 @@
|
|||
showControls,
|
||||
showFileNavPath,
|
||||
showFileNavDir,
|
||||
pyodideWorker
|
||||
pyodideWorker,
|
||||
desktopEvent
|
||||
} from '$lib/stores';
|
||||
import { getFileContentById } from '$lib/apis/files';
|
||||
import { goto } from '$app/navigation';
|
||||
|
|
@ -710,8 +711,9 @@
|
|||
await goto(event.data.path);
|
||||
return;
|
||||
}
|
||||
if (event.type === 'query' && event.data?.query) {
|
||||
await goto(`/?q=${encodeURIComponent(event.data.query)}`);
|
||||
if (event.type === 'query' && (event.data?.query || event.data?.files?.length)) {
|
||||
desktopEvent.set({ query: event.data.query, files: event.data.files });
|
||||
await goto('/');
|
||||
return;
|
||||
}
|
||||
if (event.type === 'theme:update' && event.data?.theme) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue