From dd7f428989f91232a10ecf17cc3ce90d129077ba Mon Sep 17 00:00:00 2001 From: Gaofeng Date: Tue, 9 Dec 2025 11:26:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=89=A9=E5=B1=95=20model=20=E8=A1=A8?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E4=BB=A5=E6=94=AF=E6=8C=81=E6=96=B0=E7=89=88?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E9=80=89=E6=8B=A9=E5=99=A8=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 provider, icon_url, description, context_length, tags, sort_order 字段, 用于存储分组、图标、简介及排序权重,优化前端展示逻辑。 --- .../85f7b5b5ef68_add_columns_to_model.py | 99 +++++++++++++++++++ backend/open_webui/models/models.py | 81 ++++++++------- 2 files changed, 146 insertions(+), 34 deletions(-) create mode 100644 backend/open_webui/migrations/versions/85f7b5b5ef68_add_columns_to_model.py diff --git a/backend/open_webui/migrations/versions/85f7b5b5ef68_add_columns_to_model.py b/backend/open_webui/migrations/versions/85f7b5b5ef68_add_columns_to_model.py new file mode 100644 index 0000000000..53ba4b35b0 --- /dev/null +++ b/backend/open_webui/migrations/versions/85f7b5b5ef68_add_columns_to_model.py @@ -0,0 +1,99 @@ +"""add_columns_to_model + +Revision ID: 85f7b5b5ef68 +Revises: h1i2j3k4l5m6 +Create Date: 2025-12-09 11:19:15.576715 + +""" + +# 验证示例(手动执行,不会在迁移中运行): +# python -c " +# from open_webui.internal.db import get_db +# from sqlalchemy import inspect +# with get_db() as db: +# insp = inspect(db.bind) +# cols = insp.get_columns('model') +# names = [c['name'] for c in cols] +# print('model columns:', names) +# " + + +from typing import Sequence, Union + +import sqlalchemy as sa + +from alembic import op +from open_webui.internal.db import JSONField + +# revision identifiers, used by Alembic. +revision: str = "85f7b5b5ef68" +down_revision: Union[str, None] = "h1i2j3k4l5m6" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """为 model 表添加 UI/分组/排序相关字段,并为 provider 建索引。""" + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_cols = {col["name"] for col in inspector.get_columns("model")} + existing_indexes = {idx["name"] for idx in inspector.get_indexes("model")} + + if "icon_url" not in existing_cols: + op.add_column("model", sa.Column("icon_url", sa.Text(), nullable=True)) + + if "provider" not in existing_cols: + op.add_column("model", sa.Column("provider", sa.String(length=50), nullable=True)) + if "idx_model_provider" not in existing_indexes: + op.create_index("idx_model_provider", "model", ["provider"]) + + if "description" not in existing_cols: + op.add_column("model", sa.Column("description", sa.Text(), nullable=True)) + + if "context_length" not in existing_cols: + op.add_column( + "model", + sa.Column( + "context_length", + sa.Integer(), + nullable=False, + server_default="4096", + ), + ) + + if "tags" not in existing_cols: + op.add_column("model", sa.Column("tags", JSONField(), nullable=True)) + + if "sort_order" not in existing_cols: + op.add_column( + "model", + sa.Column( + "sort_order", + sa.Integer(), + nullable=False, + server_default="0", + ), + ) + + +def downgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_cols = {col["name"] for col in inspector.get_columns("model")} + existing_indexes = {idx["name"] for idx in inspector.get_indexes("model")} + + if "idx_model_provider" in existing_indexes: + op.drop_index("idx_model_provider", table_name="model") + + if "provider" in existing_cols: + op.drop_column("model", "provider") + if "icon_url" in existing_cols: + op.drop_column("model", "icon_url") + if "description" in existing_cols: + op.drop_column("model", "description") + if "context_length" in existing_cols: + op.drop_column("model", "context_length") + if "tags" in existing_cols: + op.drop_column("model", "tags") + if "sort_order" in existing_cols: + op.drop_column("model", "sort_order") diff --git a/backend/open_webui/models/models.py b/backend/open_webui/models/models.py index 7faa116e8d..510dae7674 100755 --- a/backend/open_webui/models/models.py +++ b/backend/open_webui/models/models.py @@ -11,9 +11,8 @@ from open_webui.models.users import Users, UserResponse from pydantic import BaseModel, ConfigDict -from sqlalchemy import or_, and_, func -from sqlalchemy.dialects import postgresql, sqlite -from sqlalchemy import BigInteger, Column, Text, JSON, Boolean +from sqlalchemy import Index +from sqlalchemy import BigInteger, Column, Text, JSON, Boolean, Integer, String from open_webui.utils.access_control import has_access @@ -54,52 +53,54 @@ class Model(Base): __tablename__ = "model" id = Column(Text, primary_key=True) - """ - The model's id as used in the API. If set to an existing model, it will override the model. - """ + """模型唯一标识符,用于 API 调用""" + user_id = Column(Text) + """模型创建者的用户 ID,用于权限控制""" base_model_id = Column(Text, nullable=True) - """ - An optional pointer to the actual model that should be used when proxying requests. - """ + """指向实际使用的基础模型的 ID,NULL 表示基础模型""" name = Column(Text) - """ - The human-readable display name of the model. - """ + """人类可读的模型显示名称""" + + icon_url = Column(Text, nullable=True) + """模型图标 URL,用于列表展示""" + + provider = Column(String(50), nullable=True) + """供应商标识,如 openai、anthropic、ollama""" + + description = Column(Text, nullable=True) + """模型简介文案,前端展示用""" params = Column(JSONField) - """ - Holds a JSON encoded blob of parameters, see `ModelParams`. - """ + """模型运行参数,存储为 JSON 格式""" meta = Column(JSONField) - """ - Holds a JSON encoded blob of metadata, see `ModelMeta`. - """ + """模型元数据,存储为 JSON 格式""" - access_control = Column(JSON, nullable=True) # Controls data access levels. - # Defines access control rules for this entry. - # - `None`: Public access, available to all users with the "user" role. - # - `{}`: Private access, restricted exclusively to the owner. - # - Custom permissions: Specific access control for reading and writing; - # Can specify group or user-level restrictions: - # { - # "read": { - # "group_ids": ["group_id1", "group_id2"], - # "user_ids": ["user_id1", "user_id2"] - # }, - # "write": { - # "group_ids": ["group_id1", "group_id2"], - # "user_ids": ["user_id1", "user_id2"] - # } - # } + context_length = Column(Integer, nullable=False, default=4096) + """上下文长度限制,默认 4096""" + + tags = Column(JSONField, nullable=True) + """模型标签,用于分组和筛选""" + + sort_order = Column(Integer, nullable=False, default=0) + """排序权重,数值越大越靠前""" + + access_control = Column(JSON, nullable=True) + """访问控制规则,None=公开,{}=私有,JSON=自定义权限""" is_active = Column(Boolean, default=True) + """模型激活状态,False 表示已禁用""" updated_at = Column(BigInteger) + """最后更新时间戳(Unix 时间戳)""" + created_at = Column(BigInteger) + """创建时间戳(Unix 时间戳)""" + + __table_args__ = (Index("idx_model_provider", "provider"),) class ModelModel(BaseModel): @@ -108,8 +109,14 @@ class ModelModel(BaseModel): base_model_id: Optional[str] = None name: str + icon_url: Optional[str] = None + provider: Optional[str] = None + description: Optional[str] = None params: ModelParams meta: ModelMeta + context_length: int = 4096 + tags: Optional[dict] = None + sort_order: int = 0 access_control: Optional[dict] = None @@ -137,8 +144,14 @@ class ModelForm(BaseModel): id: str base_model_id: Optional[str] = None name: str + icon_url: Optional[str] = None + provider: Optional[str] = None + description: Optional[str] = None meta: ModelMeta params: ModelParams + context_length: int = 4096 + tags: Optional[dict] = None + sort_order: int = 0 access_control: Optional[dict] = None is_active: bool = True