From 72fdf238a8c031b979092a7c46e0a4fbbad404df Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Mon, 27 Jul 2026 09:45:37 +0200 Subject: [PATCH] perf: optional orjson JSON codec behind ENABLE_ORJSON (#27583) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swap the JSON encoder/decoder used across the backend from stdlib json to orjson when ENABLE_ORJSON is set — HTTP request bodies, JSONResponse bodies, upstream provider responses, SSE chunks, and socket.io/Redis payloads. The flag defaults to off, in which case the app uses stdlib json and engineio's codec verbatim, so default behaviour is unchanged. - json_codec exports JSONCodec (stdlib json or the orjson codec) and SOCKETIO_JSON (engineio's codec or the orjson codec); call sites import JSONCodec and stay implementation-agnostic - apply_orjson_http_json() is a no-op when the flag is off, leaving starlette's Request.json / JSONResponse.render untouched - the orjson codec falls back to the stdlib for inputs orjson rejects (non-str dict keys, ints beyond 64 bits, NaN literals) - orjson is imported only when the flag is on - FastAPI(default_response_class=...) is deliberately not used: an explicit default disables the Pydantic direct-to-bytes fast path for response_model routes --- backend/open_webui/env.py | 5 +++ backend/open_webui/main.py | 5 +++ backend/open_webui/routers/ollama.py | 11 ++--- backend/open_webui/routers/openai.py | 23 +++++----- backend/open_webui/socket/main.py | 5 ++- backend/open_webui/socket/utils.py | 20 ++++----- backend/open_webui/utils/json_codec.py | 50 +++++++++++++++++++++ backend/open_webui/utils/json_response.py | 55 +++++++++++++++++++++++ backend/open_webui/utils/middleware.py | 45 ++++++++++--------- backend/open_webui/utils/response.py | 5 ++- backend/requirements-min.txt | 1 + backend/requirements.txt | 1 + pyproject.toml | 1 + 13 files changed, 176 insertions(+), 51 deletions(-) create mode 100644 backend/open_webui/utils/json_codec.py create mode 100644 backend/open_webui/utils/json_response.py diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index bb9c2e92a1..ea19b426ed 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -150,6 +150,11 @@ INSTANCE_ID = os.getenv('INSTANCE_ID', str(uuid4())) ENABLE_DB_MIGRATIONS = os.getenv('ENABLE_DB_MIGRATIONS', 'True').lower() == 'true' +# Swap the JSON encoder/decoder used across the app (HTTP request bodies, JSONResponse +# bodies, upstream provider responses, socket.io payloads) from the stdlib `json` module +# to orjson. Faster, but stricter: see open_webui/utils/json_codec.py for the differences. +ENABLE_ORJSON = os.getenv('ENABLE_ORJSON', 'False').lower() == 'true' + # Function to parse each section def parse_section(section): diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 505c19a8c0..55613ac83d 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -229,6 +229,7 @@ from open_webui.utils.chat_variables import ( normalize_chat_variables, ) from open_webui.utils.embeddings import generate_embeddings +from open_webui.utils.json_response import apply_orjson_http_json from open_webui.utils.logger import start_logger from open_webui.utils.middleware import ( background_tasks_handler, @@ -456,6 +457,10 @@ async def lifespan(app: FastAPI): await publish_event(app, EVENTS.SYSTEM_SHUTDOWN_COMPLETED, source='system') +# Opt-in (ENABLE_ORJSON): orjson for request-body parsing and JSONResponse bodies; +# response_model routes keep FastAPI's Pydantic fast path either way. +apply_orjson_http_json() + app = FastAPI( title='Open WebUI', docs_url='/docs' if ENV == 'dev' else None, diff --git a/backend/open_webui/routers/ollama.py b/backend/open_webui/routers/ollama.py index b02205f709..08bd947d50 100644 --- a/backend/open_webui/routers/ollama.py +++ b/backend/open_webui/routers/ollama.py @@ -42,6 +42,7 @@ from open_webui.utils.access_control import check_model_access from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.headers import get_custom_headers, include_user_info_headers from open_webui.utils.model_ids import strip_provider_model_prefix +from open_webui.utils.json_codec import JSONCodec from open_webui.utils.misc import calculate_sha256 from open_webui.utils.payload import ( apply_model_params_to_body_ollama, @@ -86,7 +87,7 @@ async def send_get_request( ssl=AIOHTTP_CLIENT_SESSION_SSL, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST), ) as r: - return await r.json() + return await r.json(loads=JSONCodec.loads) except Exception as exc: log.error(f'Connection error: {exc}') return None @@ -137,7 +138,7 @@ async def send_request( if not r.ok: try: - res = await r.json() + res = await r.json(loads=JSONCodec.loads) await publish_model_provider_request_failed( request, actor=user, @@ -179,7 +180,7 @@ async def send_request( ) else: try: - return await r.json() + return await r.json(loads=JSONCodec.loads) except Exception: return None @@ -270,12 +271,12 @@ async def verify_connection( ) as r: if r.status != 200: detail = f'HTTP Error: {r.status}' - res = await r.json() + res = await r.json(loads=JSONCodec.loads) if 'error' in res: detail = f'External Error: {res["error"]}' raise Exception(detail) - return await r.json() + return await r.json(loads=JSONCodec.loads) except aiohttp.ClientError as exc: log.exception(f'Client error: {exc}') raise HTTPException(status_code=500, detail=ERROR_MESSAGES.SERVER_CONNECTION_ERROR) diff --git a/backend/open_webui/routers/openai.py b/backend/open_webui/routers/openai.py index a016115f9b..9794f52265 100644 --- a/backend/open_webui/routers/openai.py +++ b/backend/open_webui/routers/openai.py @@ -44,6 +44,7 @@ from open_webui.utils.access_control import check_model_access, has_connection_a from open_webui.utils.anthropic import get_anthropic_models, is_anthropic_url from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.headers import get_custom_headers, include_user_info_headers +from open_webui.utils.json_codec import JSONCodec from open_webui.utils.model_ids import strip_provider_model_prefix from open_webui.utils.misc import ( convert_logit_bias_input_to_json, @@ -112,7 +113,7 @@ async def send_get_request( cookies=cookies, ssl=AIOHTTP_CLIENT_SESSION_SSL, ) as response: - return await response.json() + return await response.json(loads=JSONCodec.loads) except Exception as e: # Handle connection error here log.error(f'Connection error: {e}') @@ -357,7 +358,7 @@ async def count_anthropic_tokens(request: Request, form_data: dict, user: UserMo ) try: - response_data = await response.json() + response_data = await response.json(loads=JSONCodec.loads) except Exception: response_data = await response.text() @@ -509,7 +510,7 @@ async def speech(request: Request, user=Depends(get_verified_user)): detail = None if r is not None: try: - res = await r.json() + res = await r.json(loads=JSONCodec.loads) if 'error' in res: detail = f'External: {res["error"]}' except Exception: @@ -763,14 +764,14 @@ async def get_models(request: Request, url_idx: int | None = None, user=Depends( if r.status != 200: error_detail = f'HTTP Error: {r.status}' try: - res = await r.json() + res = await r.json(loads=JSONCodec.loads) if 'error' in res: error_detail = f'External Error: {res["error"]}' except Exception: pass raise Exception(error_detail) - response_data = await r.json() + response_data = await r.json(loads=JSONCodec.loads) if 'api.openai.com' in url: response_data['data'] = [ @@ -853,7 +854,7 @@ async def verify_connection( ssl=AIOHTTP_CLIENT_SESSION_SSL, ) as r: try: - response_data = await r.json() + response_data = await r.json(loads=JSONCodec.loads) except Exception: response_data = await r.text() @@ -879,7 +880,7 @@ async def verify_connection( ssl=AIOHTTP_CLIENT_SESSION_SSL, ) as r: try: - response_data = await r.json() + response_data = await r.json(loads=JSONCodec.loads) except Exception: response_data = await r.text() @@ -1417,7 +1418,7 @@ async def generate_chat_completion( ) else: try: - response = await r.json() + response = await r.json(loads=JSONCodec.loads) except Exception as e: log.error(e) response = await r.text() @@ -1529,7 +1530,7 @@ async def embeddings(request: Request, form_data: dict, user): ) else: try: - response_data = await r.json() + response_data = await r.json(loads=JSONCodec.loads) except Exception: response_data = await r.text() @@ -1656,7 +1657,7 @@ async def responses( ) else: try: - response_data = await r.json() + response_data = await r.json(loads=JSONCodec.loads) except Exception: response_data = await r.text() @@ -1778,7 +1779,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): ) else: try: - response_data = await r.json() + response_data = await r.json(loads=JSONCodec.loads) except Exception: response_data = await r.text() diff --git a/backend/open_webui/socket/main.py b/backend/open_webui/socket/main.py index 9f0d8002f4..46cc5719bb 100644 --- a/backend/open_webui/socket/main.py +++ b/backend/open_webui/socket/main.py @@ -39,6 +39,7 @@ from open_webui.tasks import create_task, stop_item_tasks from open_webui.utils.access_control import has_permission from open_webui.utils.auth import get_verified_user_by_token from open_webui.utils.chat_id import is_saved_chat_id +from open_webui.utils.json_codec import SOCKETIO_JSON from open_webui.utils.misc import get_output_text from open_webui.utils.redis import ( build_sentinel_url, @@ -70,10 +71,11 @@ if WEBSOCKET_MANAGER == 'redis': if sentinel_hosts else WEBSOCKET_REDIS_URL ) - redis_manager = socketio.AsyncRedisManager(ws_redis_url, redis_options=WEBSOCKET_REDIS_OPTIONS) + redis_manager = socketio.AsyncRedisManager(ws_redis_url, redis_options=WEBSOCKET_REDIS_OPTIONS, json=SOCKETIO_JSON) sio = socketio.AsyncServer( cors_allowed_origins=SOCKETIO_CORS_ORIGINS, async_mode='asgi', + json=SOCKETIO_JSON, transports=(['websocket'] if ENABLE_WEBSOCKET_SUPPORT else ['polling']), allow_upgrades=ENABLE_WEBSOCKET_SUPPORT, always_connect=True, @@ -87,6 +89,7 @@ else: sio = socketio.AsyncServer( cors_allowed_origins=SOCKETIO_CORS_ORIGINS, async_mode='asgi', + json=SOCKETIO_JSON, transports=(['websocket'] if ENABLE_WEBSOCKET_SUPPORT else ['polling']), allow_upgrades=ENABLE_WEBSOCKET_SUPPORT, always_connect=True, diff --git a/backend/open_webui/socket/utils.py b/backend/open_webui/socket/utils.py index 5e72753f42..00f8424aae 100644 --- a/backend/open_webui/socket/utils.py +++ b/backend/open_webui/socket/utils.py @@ -3,11 +3,11 @@ from __future__ import annotations import hashlib -import json import uuid import pycrdt as Y from open_webui.env import REDIS_KEY_PREFIX +from open_webui.utils.json_codec import JSONCodec from open_webui.utils.redis import get_redis_connection YDOC_KEY_PREFIX = f'{REDIS_KEY_PREFIX}:ydoc:documents' @@ -75,14 +75,14 @@ class RedisDict: ) def __setitem__(self, key, value): - serialized_value = json.dumps(value) + serialized_value = JSONCodec.dumps(value) self.redis.hset(self.name, key, serialized_value) def __getitem__(self, key): value = self.redis.hget(self.name, key) if value is None: raise KeyError(key) - return json.loads(value) + return JSONCodec.loads(value) def __delitem__(self, key): result = self.redis.hdel(self.name, key) @@ -99,10 +99,10 @@ class RedisDict: return self.redis.hkeys(self.name) def values(self): - return [json.loads(v) for v in self.redis.hvals(self.name)] + return [JSONCodec.loads(v) for v in self.redis.hvals(self.name)] def items(self): - return [(k, json.loads(v)) for k, v in self.redis.hgetall(self.name).items()] + return [(k, JSONCodec.loads(v)) for k, v in self.redis.hgetall(self.name).items()] def set(self, mapping: dict): if not mapping: @@ -111,7 +111,7 @@ class RedisDict: return # Serialize values once — reused for both the fingerprint and the write. - serialized = {k: json.dumps(v) for k, v in mapping.items()} + serialized = {k: JSONCodec.dumps(v) for k, v in mapping.items()} digest = hashlib.sha256() for key in sorted(serialized): digest.update(key.encode()) @@ -182,7 +182,7 @@ class YdocManager: document_id = document_id.replace(':', '_') if self._redis: redis_key = f'{self._redis_key_prefix}:{document_id}:updates' - await self._redis.rpush(redis_key, json.dumps(list(update))) + await self._redis.rpush(redis_key, JSONCodec.dumps(list(update))) list_len = await self._redis.llen(redis_key) if list_len >= self.COMPACTION_THRESHOLD: await self._compact_updates_redis(document_id) @@ -202,8 +202,8 @@ class YdocManager: mid = len(all_updates) // 2 ydoc = Y.Doc() for raw in all_updates[:mid]: - ydoc.apply_update(bytes(json.loads(raw))) - snapshot = json.dumps(list(ydoc.get_update())) + ydoc.apply_update(bytes(JSONCodec.loads(raw))) + snapshot = JSONCodec.dumps(list(ydoc.get_update())) pipe = self._redis.pipeline() pipe.delete(redis_key) pipe.rpush(redis_key, snapshot, *all_updates[mid:]) @@ -226,7 +226,7 @@ class YdocManager: if self._redis: redis_key = f'{self._redis_key_prefix}:{document_id}:updates' updates = await self._redis.lrange(redis_key, 0, -1) - return [bytes(json.loads(update)) for update in updates] + return [bytes(JSONCodec.loads(update)) for update in updates] else: return self._updates.get(document_id, []) diff --git a/backend/open_webui/utils/json_codec.py b/backend/open_webui/utils/json_codec.py new file mode 100644 index 0000000000..0e5bdbd1b5 --- /dev/null +++ b/backend/open_webui/utils/json_codec.py @@ -0,0 +1,50 @@ +"""The app-wide JSON codec, selected by the ``ENABLE_ORJSON`` env var. + +Every module that would otherwise reach for stdlib ``json`` imports ``JSONCodec`` +from here, so the whole app switches implementation from a single flag. With the +flag off these are stdlib ``json`` and engineio's codec verbatim, so the default +behaviour is exactly what it was before orjson entered the picture. +""" + +from __future__ import annotations + +import json as stdlib_json + +from engineio import json as engineio_json + +from open_webui.env import ENABLE_ORJSON + +if ENABLE_ORJSON: + import orjson + + class ORJSONCodec: + """stdlib-``json``-compatible codec backed by orjson. + + Anything orjson rejects (non-str dict keys, ints beyond 64 bits, ``NaN`` + literals) falls back to engineio's stdlib-based codec, which keeps its + oversized-integer guard for untrusted client payloads. + """ + + JSONDecodeError = engineio_json.JSONDecodeError + + @staticmethod + def dumps(obj, *args, **kwargs): + try: + return orjson.dumps(obj).decode('utf-8') + except (TypeError, ValueError): + return engineio_json.dumps(obj, *args, **kwargs) + + @staticmethod + def loads(s, *args, **kwargs): + try: + return orjson.loads(s) + except (TypeError, ValueError): + return engineio_json.loads(s, *args, **kwargs) + + # Drop-in for stdlib ``json``: ``JSONCodec.dumps`` / ``JSONCodec.loads``. + JSONCodec = ORJSONCodec + # Codec handed to the socket.io/engineio managers, which default to their own. + SOCKETIO_JSON = ORJSONCodec +else: + JSONCodec = stdlib_json + SOCKETIO_JSON = engineio_json diff --git a/backend/open_webui/utils/json_response.py b/backend/open_webui/utils/json_response.py new file mode 100644 index 0000000000..b6b31dd5d3 --- /dev/null +++ b/backend/open_webui/utils/json_response.py @@ -0,0 +1,55 @@ +"""orjson-backed JSON parsing/rendering for starlette/FastAPI requests and responses.""" + +from __future__ import annotations + +import json +from typing import Any + +from starlette.requests import Request +from starlette.responses import JSONResponse + +from open_webui.env import ENABLE_ORJSON + + +def apply_orjson_http_json() -> None: + """Parse request bodies and serialize ``JSONResponse`` with orjson. + + A no-op unless ``ENABLE_ORJSON`` is set, leaving starlette's own + stdlib-``json`` implementations untouched. + + Not ``FastAPI(default_response_class=...)`` on purpose: an explicit + default disables FastAPI's Pydantic direct-to-bytes fast path for + ``response_model`` routes. NaN/Infinity floats serialize as ``null`` + instead of raising. + """ + if not ENABLE_ORJSON: + return + + import orjson + + def render(self, content: Any) -> bytes: + try: + return orjson.dumps(content) + except (TypeError, ValueError): + # Fallback matches starlette's JSONResponse.render exactly. + return json.dumps( + content, + ensure_ascii=False, + allow_nan=False, + indent=None, + separators=(',', ':'), + ).encode('utf-8') + + async def request_json(self) -> Any: + if not hasattr(self, '_json'): + body = await self.body() + try: + self._json = orjson.loads(body) + except (TypeError, ValueError): + # Fallback matches starlette's Request.json exactly, including the + # json.JSONDecodeError that FastAPI turns into a 422. + self._json = json.loads(body) + return self._json + + JSONResponse.render = render + Request.json = request_json diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index f059e480dd..659ec179d4 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -94,6 +94,7 @@ from open_webui.utils.filter import ( process_filter_functions, ) +from open_webui.utils.json_codec import JSONCodec from open_webui.utils.mcp.client import MCPClient from open_webui.utils.memory import add_memory_context, review_memory_after_turn from open_webui.utils.misc import ( @@ -299,7 +300,7 @@ def get_citation_source_from_tool_result( try: try: - tool_result = json.loads(tool_result) + tool_result = JSONCodec.loads(tool_result) except (json.JSONDecodeError, TypeError): pass # keep tool_result as-is (e.g. fetch_url returns plain text) if isinstance(tool_result, dict) and 'error' in tool_result: @@ -1000,7 +1001,7 @@ async def process_tool_result( text = item.get('text', '') if isinstance(text, str): try: - text = json.loads(text) + text = JSONCodec.loads(text) except json.JSONDecodeError: pass tool_response.append(text) @@ -1028,7 +1029,7 @@ async def process_tool_result( text = resource.get('text', '') if isinstance(text, str) and text: try: - text = json.loads(text) + text = JSONCodec.loads(text) except json.JSONDecodeError: pass tool_response.append(text) @@ -1103,7 +1104,7 @@ async def terminal_event_handler( parsed = tool_result if isinstance(parsed, str): try: - parsed = json.loads(parsed) + parsed = JSONCodec.loads(parsed) except (json.JSONDecodeError, TypeError): pass if isinstance(parsed, dict) and parsed.get('exists') is False: @@ -1141,7 +1142,7 @@ async def chat_completion_tools_handler( content = None if hasattr(response, 'body_iterator'): async for chunk in response.body_iterator: - data = json.loads(chunk.decode('utf-8', 'replace')) + data = JSONCodec.loads(chunk.decode('utf-8', 'replace')) content = data['choices'][0]['message']['content'] # Cleanup any remaining background tasks if necessary @@ -1221,7 +1222,7 @@ async def chat_completion_tools_handler( if not content: raise Exception('No JSON object found in the response') - result = json.loads(content) + result = JSONCodec.loads(content) async def tool_call_handler(tool_call): nonlocal skip_files @@ -1390,7 +1391,7 @@ async def chat_web_search_handler(request: Request, form_data: dict, extra_param # user message as the search query. if isinstance(res, JSONResponse): try: - error_body = json.loads(res.body) + error_body = JSONCodec.loads(res.body) detail = error_body.get('detail', 'Query generation failed') except Exception: detail = 'Query generation failed' @@ -1406,7 +1407,7 @@ async def chat_web_search_handler(request: Request, form_data: dict, extra_param raise Exception('No JSON object found in the response') response = response[bracket_start:bracket_end] - queries = json.loads(response) + queries = JSONCodec.loads(response) queries = queries.get('queries', []) except Exception as e: queries = [response] @@ -1735,7 +1736,7 @@ async def chat_image_generation_handler(request: Request, form_data: dict, extra # Handle JSONResponse from error paths if isinstance(res, JSONResponse): try: - error_body = json.loads(res.body) + error_body = JSONCodec.loads(res.body) detail = error_body.get('detail', 'Image prompt generation failed') except Exception: detail = 'Image prompt generation failed' @@ -1751,7 +1752,7 @@ async def chat_image_generation_handler(request: Request, form_data: dict, extra raise Exception('No JSON object found in the response') response = response[bracket_start:bracket_end] - response = json.loads(response) + response = JSONCodec.loads(response) prompt = response.get('prompt', []) except Exception as e: prompt = user_message @@ -1855,7 +1856,7 @@ async def chat_completion_files_handler( raise Exception('No JSON object found in the response') queries_response = queries_response[bracket_start:bracket_end] - queries_response = json.loads(queries_response) + queries_response = JSONCodec.loads(queries_response) except Exception as e: queries_response = {'queries': [queries_response]} @@ -1966,7 +1967,7 @@ def apply_params_to_form_data(form_data, model): if isinstance(value, str): try: # Attempt to parse the string as JSON - custom_params[key] = json.loads(value) + custom_params[key] = JSONCodec.loads(value) except json.JSONDecodeError: # If it fails, keep the original string pass @@ -1988,7 +1989,7 @@ def apply_params_to_form_data(form_data, model): logit_bias = convert_logit_bias_input_to_json(params['logit_bias']) if logit_bias: - form_data['logit_bias'] = json.loads(logit_bias) + form_data['logit_bias'] = JSONCodec.loads(logit_bias) except Exception as e: log.exception(f'Error parsing logit_bias: {e}') @@ -3031,7 +3032,7 @@ def get_response_data(response): if isinstance(response, JSONResponse): if isinstance(response.body, bytes): try: - response_data = json.loads(response.body.decode('utf-8', 'replace')) + response_data = JSONCodec.loads(response.body.decode('utf-8', 'replace')) except json.JSONDecodeError: response_data = {'error': {'detail': 'Invalid JSON response'}} else: @@ -3090,7 +3091,7 @@ def update_assistant_message_from_stream(assistant_message, raw): continue try: - data = json.loads(part) + data = JSONCodec.loads(part) except Exception: continue @@ -3278,7 +3279,7 @@ async def background_tasks_handler(ctx): ] try: - follow_ups = json.loads(follow_ups_string).get('follow_ups', []) + follow_ups = JSONCodec.loads(follow_ups_string).get('follow_ups', []) await event_emitter( { 'type': 'chat:message:follow_ups', @@ -3336,7 +3337,7 @@ async def background_tasks_handler(ctx): title_string = title_string[title_string.find('{') : title_string.rfind('}') + 1] try: - title = json.loads(title_string).get('title', user_message) + title = JSONCodec.loads(title_string).get('title', user_message) except Exception as e: title = '' @@ -3388,7 +3389,7 @@ async def background_tasks_handler(ctx): tags_string = tags_string[tags_string.find('{') : tags_string.rfind('}') + 1] try: - tags = json.loads(tags_string).get('tags', []) + tags = JSONCodec.loads(tags_string).get('tags', []) await Chats.update_chat_tags_by_id(metadata['chat_id'], tags, user) await event_emitter( @@ -4212,7 +4213,7 @@ async def streaming_chat_response_handler(response, ctx): # (without SSE `data:` prefix). Try to normalize these into standard # error events so frontend and DB paths still receive them. try: - raw_obj = json.loads(data) + raw_obj = JSONCodec.loads(data) raw_error = raw_obj.get('error') if isinstance(raw_obj, dict) else None if raw_error: if save_to_chat: @@ -4235,7 +4236,7 @@ async def streaming_chat_response_handler(response, ctx): data = data[5:].strip() try: - data = json.loads(data) + data = JSONCodec.loads(data) if filter_functions: data, _ = await process_filter_functions( @@ -4937,7 +4938,7 @@ async def streaming_chat_response_handler(response, ctx): params = {} if tool_args and tool_args.strip(): try: - params = json.loads(tool_args) + params = JSONCodec.loads(tool_args) except Exception: try: params = ast.literal_eval(tool_args) @@ -5613,7 +5614,7 @@ async def streaming_chat_response_handler(response, ctx): ) if event: - yield wrap_item(json.dumps(event)) + yield wrap_item(JSONCodec.dumps(event)) async for data in original_generator: data, _ = await process_filter_functions( diff --git a/backend/open_webui/utils/response.py b/backend/open_webui/utils/response.py index 463ce499a7..29c021487b 100644 --- a/backend/open_webui/utils/response.py +++ b/backend/open_webui/utils/response.py @@ -2,6 +2,7 @@ import json from numbers import Number from uuid import uuid4 +from open_webui.utils.json_codec import JSONCodec from open_webui.utils.misc import ( openai_chat_chunk_message_template, openai_chat_completion_message_template, @@ -237,7 +238,7 @@ async def convert_streaming_response_ollama_to_openai(ollama_streaming_response) completion_id = f'chatcmpl-{str(uuid4())}' first = True async for data in ollama_streaming_response.body_iterator: - data = json.loads(data) + data = JSONCodec.loads(data) model = data.get('model', 'ollama') message = data.get('message') or {} @@ -268,7 +269,7 @@ async def convert_streaming_response_ollama_to_openai(ollama_streaming_response) if done and has_tool_calls: data['choices'][0]['finish_reason'] = 'tool_calls' - line = f'data: {json.dumps(data)}\n\n' + line = f'data: {JSONCodec.dumps(data)}\n\n' yield line yield 'data: [DONE]\n\n' diff --git a/backend/requirements-min.txt b/backend/requirements-min.txt index a3cb5e2d13..a7a19a2257 100644 --- a/backend/requirements-min.txt +++ b/backend/requirements-min.txt @@ -8,6 +8,7 @@ python-multipart==0.0.32 itsdangerous==2.2.0 python-socketio==5.16.2 +orjson==3.11.9 cryptography bcrypt==5.0.0 argon2-cffi==25.1.0 diff --git a/backend/requirements.txt b/backend/requirements.txt index 84a552d0ae..39f666f6d9 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -5,6 +5,7 @@ python-multipart==0.0.32 itsdangerous==2.2.0 python-socketio==5.16.2 +orjson==3.11.9 cryptography==48.0.0 bcrypt==5.0.0 argon2-cffi==25.1.0 diff --git a/pyproject.toml b/pyproject.toml index e5931d88bd..a5bf684d0a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "itsdangerous==2.2.0", "python-socketio==5.16.2", + "orjson==3.11.9", "cryptography==48.0.0", "bcrypt==5.0.0", "argon2-cffi==25.1.0",