From 27b48b70df2e01af0db4d4668ea3aa29d9f4cd6f Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 9 Oct 2026 17:05:53 +0400 Subject: [PATCH] refac --- backend/open_webui/models/model_history.py | 32 +++++++++++++- backend/open_webui/models/models.py | 4 ++ backend/open_webui/routers/models.py | 13 ++++++ src/lib/apis/models/index.ts | 12 ++++++ .../workspace/Models/ModelHistory.svelte | 42 ++++++++++++++++++- .../workspace/Models/TTSVoiceInput.svelte | 2 +- .../workspace/Models/TerminalSelector.svelte | 2 +- .../Models/VoiceAvatarSettings.svelte | 4 +- 8 files changed, 104 insertions(+), 7 deletions(-) diff --git a/backend/open_webui/models/model_history.py b/backend/open_webui/models/model_history.py index f3c764dec9..6b3ecc1a1a 100644 --- a/backend/open_webui/models/model_history.py +++ b/backend/open_webui/models/model_history.py @@ -4,8 +4,9 @@ import time import uuid from open_webui.internal.db import Base, get_async_db_context +from fastapi import HTTPException from pydantic import BaseModel, ConfigDict -from sqlalchemy import JSON, BigInteger, Column, Text, select +from sqlalchemy import JSON, BigInteger, Column, Text, select, update def model_snapshot(model) -> dict: @@ -51,6 +52,35 @@ class ModelHistoryModel(ModelHistoryResponse): class ModelHistoryTable: + async def delete_history_entry(self, model_id, history_id, db=None): + from open_webui.models.models import Model + + async with get_async_db_context(db) as session: + try: + # Serialize with production switches on both SQLite and PostgreSQL. + await session.execute(update(Model).where(Model.id == model_id).values(version_id=Model.version_id)) + model = await session.get(Model, model_id, populate_existing=True) + if not model: + return False + if model.version_id == history_id: + raise HTTPException(400, 'Cannot delete the current version') + entry = ( + await session.execute(select(ModelHistory).filter_by(id=history_id, model_id=model_id)) + ).scalar_one_or_none() + if not entry: + return False + await session.execute( + update(ModelHistory) + .where(ModelHistory.model_id == model_id, ModelHistory.parent_id == history_id) + .values(parent_id=entry.parent_id) + ) + await session.delete(entry) + await session.commit() + return True + except Exception: + await session.rollback() + raise + def new_entry(self, model_id, snapshot, user_id, parent_id=None, commit_message=None): return ModelHistory( id=str(uuid.uuid4()), diff --git a/backend/open_webui/models/models.py b/backend/open_webui/models/models.py index b8909f908c..c5fc828e8b 100755 --- a/backend/open_webui/models/models.py +++ b/backend/open_webui/models/models.py @@ -281,6 +281,10 @@ class ModelsTable: ) session.add_all([current, entry]) else: + if production_version_id is not None: + # Serialize with history deletion before reading the selected snapshot. + await session.execute(update(Model).where(Model.id == current.id).values(version_id=Model.version_id)) + await session.refresh(current) values = {key: value for key, value in data.items() if key != 'id'} # Omitted operational state must not reset a disabled model. if 'is_active' not in form.model_fields_set: diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py index 94f42eded0..1d916949b7 100644 --- a/backend/open_webui/routers/models.py +++ b/backend/open_webui/routers/models.py @@ -901,6 +901,19 @@ async def get_model_history_entry( return entry +@router.delete('/model/history/{history_id}', response_model=bool) +async def delete_model_history_entry( + id: str, + history_id: str, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await authorized_model_history(id, user, db) + if not await ModelHistories.delete_history_entry(id, history_id, db): + raise HTTPException(404, 'Model version not found') + return True + + async def _verify_version_dependencies(request, form, user, db): from open_webui.models.functions import Functions from open_webui.models.knowledge import Knowledges diff --git a/src/lib/apis/models/index.ts b/src/lib/apis/models/index.ts index ce00ae08c2..440fbf8d7c 100644 --- a/src/lib/apis/models/index.ts +++ b/src/lib/apis/models/index.ts @@ -515,6 +515,18 @@ export const getModelHistoryEntry = async ( return response.json(); }; +export const deleteModelHistoryVersion = async (token: string, id: string, versionId: string) => { + const response = await fetch( + `${WEBUI_API_BASE_URL}/models/model/history/${encodeURIComponent(versionId)}?${new URLSearchParams({ id })}`, + { + method: 'DELETE', + headers: { authorization: `Bearer ${token}` } + } + ); + if (!response.ok) throw await response.json(); + return response.json(); +}; + export const setProductionModelVersion = async (token: string, id: string, versionId: string) => { const response = await fetch( `${WEBUI_API_BASE_URL}/models/model/update/version?${new URLSearchParams({ id })}`, diff --git a/src/lib/components/workspace/Models/ModelHistory.svelte b/src/lib/components/workspace/Models/ModelHistory.svelte index 8630cc8d60..1bb1963498 100644 --- a/src/lib/components/workspace/Models/ModelHistory.svelte +++ b/src/lib/components/workspace/Models/ModelHistory.svelte @@ -9,6 +9,7 @@ import VersionMenuItem from '../common/VersionMenuItem.svelte'; import { getModelHistory, + deleteModelHistoryVersion, getModelHistoryEntry, setProductionModelVersion, type ModelHistoryEntry, @@ -29,6 +30,9 @@ export let promoting = false; let error = ''; let confirmPromotion = false; + let showDeleteVersion = false; + let deleteVersionId = ''; + let deleting = false; let selection = 0; const message = (error: any) => typeof error?.detail === 'string' @@ -71,13 +75,32 @@ } } + async function deleteVersion() { + if (deleting || promoting || !deleteVersionId || deleteVersionId === model.version_id) return; + deleting = true; + try { + await deleteModelHistoryVersion(localStorage.token, model.id, deleteVersionId); + // Invalidate an in-flight preview response for the deleted version. + selection++; + selecting = false; + if (selected?.id === deleteVersionId) selected = null; + page = 1; + await loadHistory(); + toast.success($i18n.t('Version deleted')); + } catch (e) { + toast.error(message(e)); + } finally { + deleting = false; + } + } + export function requestPromotion() { if (dirty) confirmPromotion = true; else promote(); } async function promote() { - if (!selected || promoting) return; + if (!selected || promoting || deleting) return; promoting = true; try { const result = await setProductionModelVersion(localStorage.token, model.id, selected.id); @@ -102,13 +125,23 @@ on:confirm={promote} /> + +