|
@@ -8,6 +8,8 @@ import shutil
|
|
|
import sys
|
|
|
import time
|
|
|
import random
|
|
|
+from typing import AsyncGenerator, Generator, Iterator
|
|
|
+
|
|
|
from contextlib import asynccontextmanager
|
|
|
from urllib.parse import urlencode, parse_qs, urlparse
|
|
|
from pydantic import BaseModel
|
|
@@ -39,7 +41,6 @@ from starlette.responses import Response, StreamingResponse
|
|
|
|
|
|
from open_webui.routers import (
|
|
|
audio,
|
|
|
- chat,
|
|
|
images,
|
|
|
ollama,
|
|
|
openai,
|
|
@@ -90,7 +91,7 @@ from open_webui.routers.webui import (
|
|
|
from open_webui.models.functions import Functions
|
|
|
from open_webui.models.models import Models
|
|
|
from open_webui.models.users import UserModel, Users
|
|
|
-from backend.open_webui.utils.plugin import load_function_module_by_id
|
|
|
+from open_webui.utils.plugin import load_function_module_by_id
|
|
|
|
|
|
|
|
|
from open_webui.constants import TASKS
|
|
@@ -283,8 +284,13 @@ from open_webui.utils.misc import (
|
|
|
add_or_update_system_message,
|
|
|
get_last_user_message,
|
|
|
prepend_to_first_user_message_content,
|
|
|
+ openai_chat_chunk_message_template,
|
|
|
+ openai_chat_completion_message_template,
|
|
|
+)
|
|
|
+from open_webui.utils.payload import (
|
|
|
+ apply_model_params_to_body_openai,
|
|
|
+ apply_model_system_prompt_to_body,
|
|
|
)
|
|
|
-
|
|
|
|
|
|
from open_webui.utils.payload import convert_payload_openai_to_ollama
|
|
|
from open_webui.utils.response import (
|
|
@@ -1441,8 +1447,12 @@ app.add_middleware(
|
|
|
app.mount("/ws", socket_app)
|
|
|
|
|
|
|
|
|
-app.include_router(ollama.router, prefix="/ollama")
|
|
|
-app.include_router(openai.router, prefix="/openai")
|
|
|
+app.include_router(ollama.router, prefix="/ollama", tags=["ollama"])
|
|
|
+app.include_router(openai.router, prefix="/openai", tags=["openai"])
|
|
|
+
|
|
|
+
|
|
|
+app.include_router(pipelines.router, prefix="/pipelines", tags=["pipelines"])
|
|
|
+app.include_router(tasks.router, prefix="/tasks", tags=["tasks"])
|
|
|
|
|
|
|
|
|
app.include_router(images.router, prefix="/api/v1/images", tags=["images"])
|
|
@@ -1473,8 +1483,277 @@ app.include_router(
|
|
|
app.include_router(utils.router, prefix="/api/v1/utils", tags=["utils"])
|
|
|
|
|
|
|
|
|
+##################################
|
|
|
+#
|
|
|
+# Chat Endpoints
|
|
|
+#
|
|
|
+##################################
|
|
|
+
|
|
|
+
|
|
|
+def get_function_module(pipe_id: str):
|
|
|
+ # Check if function is already loaded
|
|
|
+ if pipe_id not in app.state.FUNCTIONS:
|
|
|
+ function_module, _, _ = load_function_module_by_id(pipe_id)
|
|
|
+ app.state.FUNCTIONS[pipe_id] = function_module
|
|
|
+ else:
|
|
|
+ function_module = app.state.FUNCTIONS[pipe_id]
|
|
|
+
|
|
|
+ if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
|
|
|
+ valves = Functions.get_function_valves_by_id(pipe_id)
|
|
|
+ function_module.valves = function_module.Valves(**(valves if valves else {}))
|
|
|
+ return function_module
|
|
|
+
|
|
|
+
|
|
|
+async def get_function_models():
|
|
|
+ pipes = Functions.get_functions_by_type("pipe", active_only=True)
|
|
|
+ pipe_models = []
|
|
|
+
|
|
|
+ for pipe in pipes:
|
|
|
+ function_module = get_function_module(pipe.id)
|
|
|
+
|
|
|
+ # Check if function is a manifold
|
|
|
+ if hasattr(function_module, "pipes"):
|
|
|
+ sub_pipes = []
|
|
|
+
|
|
|
+ # Check if pipes is a function or a list
|
|
|
+
|
|
|
+ try:
|
|
|
+ if callable(function_module.pipes):
|
|
|
+ sub_pipes = function_module.pipes()
|
|
|
+ else:
|
|
|
+ sub_pipes = function_module.pipes
|
|
|
+ except Exception as e:
|
|
|
+ log.exception(e)
|
|
|
+ sub_pipes = []
|
|
|
+
|
|
|
+ log.debug(
|
|
|
+ f"get_function_models: function '{pipe.id}' is a manifold of {sub_pipes}"
|
|
|
+ )
|
|
|
+
|
|
|
+ for p in sub_pipes:
|
|
|
+ sub_pipe_id = f'{pipe.id}.{p["id"]}'
|
|
|
+ sub_pipe_name = p["name"]
|
|
|
+
|
|
|
+ if hasattr(function_module, "name"):
|
|
|
+ sub_pipe_name = f"{function_module.name}{sub_pipe_name}"
|
|
|
+
|
|
|
+ pipe_flag = {"type": pipe.type}
|
|
|
+
|
|
|
+ pipe_models.append(
|
|
|
+ {
|
|
|
+ "id": sub_pipe_id,
|
|
|
+ "name": sub_pipe_name,
|
|
|
+ "object": "model",
|
|
|
+ "created": pipe.created_at,
|
|
|
+ "owned_by": "openai",
|
|
|
+ "pipe": pipe_flag,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ pipe_flag = {"type": "pipe"}
|
|
|
+
|
|
|
+ log.debug(
|
|
|
+ f"get_function_models: function '{pipe.id}' is a single pipe {{ 'id': {pipe.id}, 'name': {pipe.name} }}"
|
|
|
+ )
|
|
|
+
|
|
|
+ pipe_models.append(
|
|
|
+ {
|
|
|
+ "id": pipe.id,
|
|
|
+ "name": pipe.name,
|
|
|
+ "object": "model",
|
|
|
+ "created": pipe.created_at,
|
|
|
+ "owned_by": "openai",
|
|
|
+ "pipe": pipe_flag,
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+ return pipe_models
|
|
|
+
|
|
|
+
|
|
|
+async def generate_function_chat_completion(form_data, user, models: dict = {}):
|
|
|
+ async def execute_pipe(pipe, params):
|
|
|
+ if inspect.iscoroutinefunction(pipe):
|
|
|
+ return await pipe(**params)
|
|
|
+ else:
|
|
|
+ return pipe(**params)
|
|
|
+
|
|
|
+ async def get_message_content(res: str | Generator | AsyncGenerator) -> str:
|
|
|
+ if isinstance(res, str):
|
|
|
+ return res
|
|
|
+ if isinstance(res, Generator):
|
|
|
+ return "".join(map(str, res))
|
|
|
+ if isinstance(res, AsyncGenerator):
|
|
|
+ return "".join([str(stream) async for stream in res])
|
|
|
+
|
|
|
+ def process_line(form_data: dict, line):
|
|
|
+ if isinstance(line, BaseModel):
|
|
|
+ line = line.model_dump_json()
|
|
|
+ line = f"data: {line}"
|
|
|
+ if isinstance(line, dict):
|
|
|
+ line = f"data: {json.dumps(line)}"
|
|
|
+
|
|
|
+ try:
|
|
|
+ line = line.decode("utf-8")
|
|
|
+ except Exception:
|
|
|
+ pass
|
|
|
+
|
|
|
+ if line.startswith("data:"):
|
|
|
+ return f"{line}\n\n"
|
|
|
+ else:
|
|
|
+ line = openai_chat_chunk_message_template(form_data["model"], line)
|
|
|
+ return f"data: {json.dumps(line)}\n\n"
|
|
|
+
|
|
|
+ def get_pipe_id(form_data: dict) -> str:
|
|
|
+ pipe_id = form_data["model"]
|
|
|
+ if "." in pipe_id:
|
|
|
+ pipe_id, _ = pipe_id.split(".", 1)
|
|
|
+ return pipe_id
|
|
|
+
|
|
|
+ def get_function_params(function_module, form_data, user, extra_params=None):
|
|
|
+ if extra_params is None:
|
|
|
+ extra_params = {}
|
|
|
+
|
|
|
+ pipe_id = get_pipe_id(form_data)
|
|
|
+
|
|
|
+ # Get the signature of the function
|
|
|
+ sig = inspect.signature(function_module.pipe)
|
|
|
+ params = {"body": form_data} | {
|
|
|
+ k: v for k, v in extra_params.items() if k in sig.parameters
|
|
|
+ }
|
|
|
+
|
|
|
+ if "__user__" in params and hasattr(function_module, "UserValves"):
|
|
|
+ user_valves = Functions.get_user_valves_by_id_and_user_id(pipe_id, user.id)
|
|
|
+ try:
|
|
|
+ params["__user__"]["valves"] = function_module.UserValves(**user_valves)
|
|
|
+ except Exception as e:
|
|
|
+ log.exception(e)
|
|
|
+ params["__user__"]["valves"] = function_module.UserValves()
|
|
|
+
|
|
|
+ return params
|
|
|
+
|
|
|
+ model_id = form_data.get("model")
|
|
|
+ model_info = Models.get_model_by_id(model_id)
|
|
|
+
|
|
|
+ metadata = form_data.pop("metadata", {})
|
|
|
+
|
|
|
+ files = metadata.get("files", [])
|
|
|
+ tool_ids = metadata.get("tool_ids", [])
|
|
|
+ # Check if tool_ids is None
|
|
|
+ if tool_ids is None:
|
|
|
+ tool_ids = []
|
|
|
+
|
|
|
+ __event_emitter__ = None
|
|
|
+ __event_call__ = None
|
|
|
+ __task__ = None
|
|
|
+ __task_body__ = None
|
|
|
+
|
|
|
+ if metadata:
|
|
|
+ if all(k in metadata for k in ("session_id", "chat_id", "message_id")):
|
|
|
+ __event_emitter__ = get_event_emitter(metadata)
|
|
|
+ __event_call__ = get_event_call(metadata)
|
|
|
+ __task__ = metadata.get("task", None)
|
|
|
+ __task_body__ = metadata.get("task_body", None)
|
|
|
+
|
|
|
+ extra_params = {
|
|
|
+ "__event_emitter__": __event_emitter__,
|
|
|
+ "__event_call__": __event_call__,
|
|
|
+ "__task__": __task__,
|
|
|
+ "__task_body__": __task_body__,
|
|
|
+ "__files__": files,
|
|
|
+ "__user__": {
|
|
|
+ "id": user.id,
|
|
|
+ "email": user.email,
|
|
|
+ "name": user.name,
|
|
|
+ "role": user.role,
|
|
|
+ },
|
|
|
+ "__metadata__": metadata,
|
|
|
+ }
|
|
|
+ extra_params["__tools__"] = get_tools(
|
|
|
+ app,
|
|
|
+ tool_ids,
|
|
|
+ user,
|
|
|
+ {
|
|
|
+ **extra_params,
|
|
|
+ "__model__": models.get(form_data["model"], None),
|
|
|
+ "__messages__": form_data["messages"],
|
|
|
+ "__files__": files,
|
|
|
+ },
|
|
|
+ )
|
|
|
+
|
|
|
+ if model_info:
|
|
|
+ if model_info.base_model_id:
|
|
|
+ form_data["model"] = model_info.base_model_id
|
|
|
+
|
|
|
+ params = model_info.params.model_dump()
|
|
|
+ form_data = apply_model_params_to_body_openai(params, form_data)
|
|
|
+ form_data = apply_model_system_prompt_to_body(params, form_data, user)
|
|
|
+
|
|
|
+ pipe_id = get_pipe_id(form_data)
|
|
|
+ function_module = get_function_module(pipe_id)
|
|
|
+
|
|
|
+ pipe = function_module.pipe
|
|
|
+ params = get_function_params(function_module, form_data, user, extra_params)
|
|
|
+
|
|
|
+ if form_data.get("stream", False):
|
|
|
+
|
|
|
+ async def stream_content():
|
|
|
+ try:
|
|
|
+ res = await execute_pipe(pipe, params)
|
|
|
+
|
|
|
+ # Directly return if the response is a StreamingResponse
|
|
|
+ if isinstance(res, StreamingResponse):
|
|
|
+ async for data in res.body_iterator:
|
|
|
+ yield data
|
|
|
+ return
|
|
|
+ if isinstance(res, dict):
|
|
|
+ yield f"data: {json.dumps(res)}\n\n"
|
|
|
+ return
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ log.error(f"Error: {e}")
|
|
|
+ yield f"data: {json.dumps({'error': {'detail':str(e)}})}\n\n"
|
|
|
+ return
|
|
|
+
|
|
|
+ if isinstance(res, str):
|
|
|
+ message = openai_chat_chunk_message_template(form_data["model"], res)
|
|
|
+ yield f"data: {json.dumps(message)}\n\n"
|
|
|
+
|
|
|
+ if isinstance(res, Iterator):
|
|
|
+ for line in res:
|
|
|
+ yield process_line(form_data, line)
|
|
|
+
|
|
|
+ if isinstance(res, AsyncGenerator):
|
|
|
+ async for line in res:
|
|
|
+ yield process_line(form_data, line)
|
|
|
+
|
|
|
+ if isinstance(res, str) or isinstance(res, Generator):
|
|
|
+ finish_message = openai_chat_chunk_message_template(
|
|
|
+ form_data["model"], ""
|
|
|
+ )
|
|
|
+ finish_message["choices"][0]["finish_reason"] = "stop"
|
|
|
+ yield f"data: {json.dumps(finish_message)}\n\n"
|
|
|
+ yield "data: [DONE]"
|
|
|
+
|
|
|
+ return StreamingResponse(stream_content(), media_type="text/event-stream")
|
|
|
+ else:
|
|
|
+ try:
|
|
|
+ res = await execute_pipe(pipe, params)
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ log.error(f"Error: {e}")
|
|
|
+ return {"error": {"detail": str(e)}}
|
|
|
+
|
|
|
+ if isinstance(res, StreamingResponse) or isinstance(res, dict):
|
|
|
+ return res
|
|
|
+ if isinstance(res, BaseModel):
|
|
|
+ return res.model_dump()
|
|
|
+
|
|
|
+ message = await get_message_content(res)
|
|
|
+ return openai_chat_completion_message_template(form_data["model"], message)
|
|
|
+
|
|
|
+
|
|
|
async def get_all_base_models():
|
|
|
- open_webui_models = []
|
|
|
+ function_models = []
|
|
|
openai_models = []
|
|
|
ollama_models = []
|
|
|
|
|
@@ -1496,9 +1775,44 @@ async def get_all_base_models():
|
|
|
for model in ollama_models["models"]
|
|
|
]
|
|
|
|
|
|
- open_webui_models = await get_open_webui_models()
|
|
|
+ function_models = await get_function_models()
|
|
|
+ models = function_models + openai_models + ollama_models
|
|
|
+
|
|
|
+ # Add arena models
|
|
|
+ if app.state.config.ENABLE_EVALUATION_ARENA_MODELS:
|
|
|
+ arena_models = []
|
|
|
+ if len(app.state.config.EVALUATION_ARENA_MODELS) > 0:
|
|
|
+ arena_models = [
|
|
|
+ {
|
|
|
+ "id": model["id"],
|
|
|
+ "name": model["name"],
|
|
|
+ "info": {
|
|
|
+ "meta": model["meta"],
|
|
|
+ },
|
|
|
+ "object": "model",
|
|
|
+ "created": int(time.time()),
|
|
|
+ "owned_by": "arena",
|
|
|
+ "arena": True,
|
|
|
+ }
|
|
|
+ for model in app.state.config.EVALUATION_ARENA_MODELS
|
|
|
+ ]
|
|
|
+ else:
|
|
|
+ # Add default arena model
|
|
|
+ arena_models = [
|
|
|
+ {
|
|
|
+ "id": DEFAULT_ARENA_MODEL["id"],
|
|
|
+ "name": DEFAULT_ARENA_MODEL["name"],
|
|
|
+ "info": {
|
|
|
+ "meta": DEFAULT_ARENA_MODEL["meta"],
|
|
|
+ },
|
|
|
+ "object": "model",
|
|
|
+ "created": int(time.time()),
|
|
|
+ "owned_by": "arena",
|
|
|
+ "arena": True,
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ models = models + arena_models
|
|
|
|
|
|
- models = open_webui_models + openai_models + ollama_models
|
|
|
return models
|
|
|
|
|
|
|
|
@@ -1628,6 +1942,7 @@ async def get_all_models():
|
|
|
)
|
|
|
log.debug(f"get_all_models() returned {len(models)} models")
|
|
|
|
|
|
+ app.state.MODELS = {model["id"]: model for model in models}
|
|
|
return models
|
|
|
|
|
|
|
|
@@ -1689,16 +2004,8 @@ async def get_base_models(user=Depends(get_admin_user)):
|
|
|
return {"data": models}
|
|
|
|
|
|
|
|
|
-##################################
|
|
|
-#
|
|
|
-# Chat Endpoints
|
|
|
-#
|
|
|
-##################################
|
|
|
-
|
|
|
-
|
|
|
@app.post("/api/chat/completions")
|
|
|
async def generate_chat_completions(
|
|
|
- request: Request,
|
|
|
form_data: dict,
|
|
|
user=Depends(get_verified_user),
|
|
|
bypass_filter: bool = False,
|
|
@@ -1706,7 +2013,7 @@ async def generate_chat_completions(
|
|
|
if BYPASS_MODEL_ACCESS_CONTROL:
|
|
|
bypass_filter = True
|
|
|
|
|
|
- model_list = request.state.models
|
|
|
+ model_list = app.state.MODELS
|
|
|
models = {model["id"]: model for model in model_list}
|
|
|
|
|
|
model_id = form_data["model"]
|
|
@@ -1843,8 +2150,8 @@ async def chat_completed(form_data: dict, user=Depends(get_verified_user)):
|
|
|
try:
|
|
|
urlIdx = filter["urlIdx"]
|
|
|
|
|
|
- url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx]
|
|
|
- key = openai_app.state.config.OPENAI_API_KEYS[urlIdx]
|
|
|
+ url = app.state.config.OPENAI_API_BASE_URLS[urlIdx]
|
|
|
+ key = app.state.config.OPENAI_API_KEYS[urlIdx]
|
|
|
|
|
|
if key != "":
|
|
|
headers = {"Authorization": f"Bearer {key}"}
|