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}
/>
+