feat:精简
Some checks failed
Create and publish Docker images with specific build args / build-main-image (linux/amd64, ubuntu-latest) (push) Has been cancelled
Create and publish Docker images with specific build args / build-main-image (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64, ubuntu-latest) (push) Has been cancelled
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Create and publish Docker images with specific build args / build-cuda126-image (linux/amd64, ubuntu-latest) (push) Has been cancelled
Create and publish Docker images with specific build args / build-cuda126-image (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64, ubuntu-latest) (push) Has been cancelled
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Create and publish Docker images with specific build args / build-slim-image (linux/amd64, ubuntu-latest) (push) Has been cancelled
Create and publish Docker images with specific build args / build-slim-image (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Python CI / Format Backend (3.11.x) (push) Has been cancelled
Python CI / Format Backend (3.12.x) (push) Has been cancelled
Frontend Build / Format & Build Frontend (push) Has been cancelled
Frontend Build / Frontend Unit Tests (push) Has been cancelled
Create and publish Docker images with specific build args / merge-main-images (push) Has been cancelled
Create and publish Docker images with specific build args / merge-cuda-images (push) Has been cancelled
Create and publish Docker images with specific build args / merge-cuda126-images (push) Has been cancelled
Create and publish Docker images with specific build args / merge-ollama-images (push) Has been cancelled
Create and publish Docker images with specific build args / merge-slim-images (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
Some checks failed
Create and publish Docker images with specific build args / build-main-image (linux/amd64, ubuntu-latest) (push) Has been cancelled
Create and publish Docker images with specific build args / build-main-image (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64, ubuntu-latest) (push) Has been cancelled
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Create and publish Docker images with specific build args / build-cuda126-image (linux/amd64, ubuntu-latest) (push) Has been cancelled
Create and publish Docker images with specific build args / build-cuda126-image (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64, ubuntu-latest) (push) Has been cancelled
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Create and publish Docker images with specific build args / build-slim-image (linux/amd64, ubuntu-latest) (push) Has been cancelled
Create and publish Docker images with specific build args / build-slim-image (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Python CI / Format Backend (3.11.x) (push) Has been cancelled
Python CI / Format Backend (3.12.x) (push) Has been cancelled
Frontend Build / Format & Build Frontend (push) Has been cancelled
Frontend Build / Frontend Unit Tests (push) Has been cancelled
Create and publish Docker images with specific build args / merge-main-images (push) Has been cancelled
Create and publish Docker images with specific build args / merge-cuda-images (push) Has been cancelled
Create and publish Docker images with specific build args / merge-cuda126-images (push) Has been cancelled
Create and publish Docker images with specific build args / merge-ollama-images (push) Has been cancelled
Create and publish Docker images with specific build args / merge-slim-images (push) Has been cancelled
Close inactive issues / close-issues (push) Has been cancelled
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import json
|
||||
import time
|
||||
import uuid
|
||||
from typing import List, Optional, Tuple
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from decimal import Decimal
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
from sqlalchemy import JSON, BigInteger, Boolean, Column, Integer, Numeric, String, Text
|
||||
|
||||
from open_webui.internal.db import Base, get_db
|
||||
@@ -21,8 +22,19 @@ class SubscriptionPlan(Base):
|
||||
name = Column(String, nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
price = Column(Numeric(precision=10, scale=2), default=0)
|
||||
|
||||
# per-model monthly limits: {"model_id": limit, ...}
|
||||
# -1 = unlimited, 0 = not allowed, positive = monthly limit
|
||||
model_limits = Column(JSON, nullable=True)
|
||||
|
||||
# default limit for models not in model_limits
|
||||
# -1 = unlimited, 0 = not allowed, positive = monthly limit
|
||||
default_model_limit = Column(Integer, default=0)
|
||||
|
||||
# deprecated fields (kept for backward compatibility)
|
||||
monthly_message_limit = Column(Integer, nullable=True)
|
||||
allowed_models = Column(JSON, nullable=True)
|
||||
|
||||
priority = Column(Integer, default=0)
|
||||
is_default = Column(Boolean, default=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
@@ -42,6 +54,11 @@ class UserSubscription(Base):
|
||||
|
||||
current_period_start = Column(BigInteger)
|
||||
current_period_end = Column(BigInteger)
|
||||
|
||||
# per-model usage tracking: {"model_id": count, ...}
|
||||
model_usage = Column(JSON, default={})
|
||||
|
||||
# deprecated field (kept for backward compatibility)
|
||||
messages_used = Column(Integer, default=0)
|
||||
|
||||
created_at = Column(BigInteger)
|
||||
@@ -72,7 +89,7 @@ class RedemptionCode(Base):
|
||||
redemption_type = Column(String, default="duration")
|
||||
plan_id = Column(String, nullable=True)
|
||||
duration_days = Column(Integer, nullable=True)
|
||||
upgrade_expires_at = Column(BigInteger, nullable=True)
|
||||
upgrade_days = Column(Integer, nullable=True)
|
||||
|
||||
user_id = Column(String, index=True, nullable=True)
|
||||
created_at = Column(BigInteger, index=True)
|
||||
@@ -85,6 +102,16 @@ class RedemptionCode(Base):
|
||||
####################
|
||||
|
||||
|
||||
def _parse_json_field(v):
|
||||
"""Parse JSON field from string if needed."""
|
||||
if isinstance(v, str):
|
||||
try:
|
||||
return json.loads(v)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
return v
|
||||
|
||||
|
||||
class SubscriptionPlanModel(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -92,22 +119,43 @@ class SubscriptionPlanModel(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
price: Decimal = Field(default_factory=lambda: Decimal("0"))
|
||||
|
||||
# per-model monthly limits
|
||||
model_limits: Optional[Dict[str, int]] = None
|
||||
default_model_limit: int = 0
|
||||
|
||||
# deprecated fields
|
||||
monthly_message_limit: Optional[int] = None
|
||||
allowed_models: Optional[List[str]] = None
|
||||
|
||||
priority: int = 0
|
||||
is_default: bool = False
|
||||
is_active: bool = True
|
||||
created_at: int = Field(default_factory=lambda: int(time.time()))
|
||||
updated_at: int = Field(default_factory=lambda: int(time.time()))
|
||||
|
||||
@field_validator('model_limits', 'allowed_models', mode='before')
|
||||
@classmethod
|
||||
def parse_json_fields(cls, v):
|
||||
return _parse_json_field(v)
|
||||
|
||||
def get_model_limit(self, model_id: str) -> int:
|
||||
"""
|
||||
Get the usage limit for a specific model.
|
||||
Returns: -1 = unlimited, 0 = not allowed, positive = monthly limit
|
||||
"""
|
||||
if self.model_limits and model_id in self.model_limits:
|
||||
return self.model_limits[model_id]
|
||||
return self.default_model_limit
|
||||
|
||||
|
||||
class SubscriptionPlanForm(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
price: float = 0
|
||||
monthly_message_limit: Optional[int] = None
|
||||
allowed_models: Optional[List[str]] = None
|
||||
model_limits: Optional[Dict[str, int]] = None
|
||||
default_model_limit: int = 0
|
||||
priority: int = 0
|
||||
is_default: bool = False
|
||||
is_active: bool = True
|
||||
@@ -117,8 +165,8 @@ class UpdateSubscriptionPlanForm(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
price: Optional[float] = None
|
||||
monthly_message_limit: Optional[int] = None
|
||||
allowed_models: Optional[List[str]] = None
|
||||
model_limits: Optional[Dict[str, int]] = None
|
||||
default_model_limit: Optional[int] = None
|
||||
priority: Optional[int] = None
|
||||
is_default: Optional[bool] = None
|
||||
is_active: Optional[bool] = None
|
||||
@@ -134,16 +182,48 @@ class UserSubscriptionModel(BaseModel):
|
||||
expires_at: Optional[int] = None
|
||||
current_period_start: int
|
||||
current_period_end: int
|
||||
|
||||
# per-model usage tracking
|
||||
model_usage: Dict[str, int] = Field(default_factory=dict)
|
||||
|
||||
# deprecated field
|
||||
messages_used: int = 0
|
||||
|
||||
created_at: int = Field(default_factory=lambda: int(time.time()))
|
||||
updated_at: int = Field(default_factory=lambda: int(time.time()))
|
||||
|
||||
@field_validator('model_usage', mode='before')
|
||||
@classmethod
|
||||
def parse_model_usage(cls, v):
|
||||
parsed = _parse_json_field(v)
|
||||
return parsed if parsed else {}
|
||||
|
||||
def get_model_usage(self, model_id: str) -> int:
|
||||
"""Get usage count for a specific model."""
|
||||
return self.model_usage.get(model_id, 0)
|
||||
|
||||
|
||||
class ModelUsageInfo(BaseModel):
|
||||
"""Usage info for a single model."""
|
||||
model_id: str
|
||||
limit: int # -1 = unlimited, 0 = not allowed
|
||||
used: int
|
||||
remaining: Optional[int] = None # None if unlimited
|
||||
|
||||
|
||||
class UserSubscriptionWithPlanModel(UserSubscriptionModel):
|
||||
plan: Optional[SubscriptionPlanModel] = None
|
||||
messages_remaining: Optional[int] = None
|
||||
days_remaining: Optional[int] = None
|
||||
|
||||
# per-model usage summary
|
||||
model_usage_info: Optional[List[ModelUsageInfo]] = None
|
||||
|
||||
# deprecated
|
||||
messages_remaining: Optional[int] = None
|
||||
|
||||
# admin view fields
|
||||
username: Optional[str] = None
|
||||
|
||||
|
||||
class SubscriptionUsageLogModel(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -166,7 +246,7 @@ class RedemptionCodeModel(BaseModel):
|
||||
redemption_type: str = "duration"
|
||||
plan_id: Optional[str] = None
|
||||
duration_days: Optional[int] = None
|
||||
upgrade_expires_at: Optional[int] = None
|
||||
upgrade_days: Optional[int] = None
|
||||
user_id: Optional[str] = None
|
||||
created_at: int
|
||||
expired_at: Optional[int] = None
|
||||
@@ -179,7 +259,7 @@ class CreateRedemptionCodeForm(BaseModel):
|
||||
redemption_type: str = "duration"
|
||||
plan_id: str
|
||||
duration_days: Optional[int] = Field(default=None, ge=1)
|
||||
upgrade_expires_at: Optional[int] = Field(default=None, gt=0)
|
||||
upgrade_days: Optional[int] = Field(default=None, ge=1)
|
||||
expired_at: Optional[int] = Field(default=None, gt=0)
|
||||
|
||||
|
||||
@@ -187,7 +267,7 @@ class UpdateRedemptionCodeForm(BaseModel):
|
||||
purpose: Optional[str] = Field(None, min_length=1, max_length=255)
|
||||
plan_id: Optional[str] = None
|
||||
duration_days: Optional[int] = Field(None, ge=1)
|
||||
upgrade_expires_at: Optional[int] = Field(None, gt=0)
|
||||
upgrade_days: Optional[int] = Field(None, ge=1)
|
||||
expired_at: Optional[int] = Field(None, gt=0)
|
||||
|
||||
|
||||
@@ -231,8 +311,8 @@ class SubscriptionPlansTable:
|
||||
name=form_data.name,
|
||||
description=form_data.description,
|
||||
price=Decimal(str(form_data.price)),
|
||||
monthly_message_limit=form_data.monthly_message_limit,
|
||||
allowed_models=form_data.allowed_models,
|
||||
model_limits=form_data.model_limits,
|
||||
default_model_limit=form_data.default_model_limit,
|
||||
priority=form_data.priority,
|
||||
is_default=form_data.is_default,
|
||||
is_active=form_data.is_active,
|
||||
@@ -302,19 +382,28 @@ class UserSubscriptionsTable:
|
||||
plan = SubscriptionPlans.get_plan_by_id(sub.plan_id)
|
||||
now = int(time.time())
|
||||
|
||||
messages_remaining = None
|
||||
if plan and plan.monthly_message_limit:
|
||||
messages_remaining = max(0, plan.monthly_message_limit - sub.messages_used)
|
||||
|
||||
days_remaining = None
|
||||
if sub.expires_at:
|
||||
days_remaining = max(0, (sub.expires_at - now) // 86400)
|
||||
|
||||
# build model usage info
|
||||
model_usage_info = []
|
||||
if plan and plan.model_limits:
|
||||
for model_id, limit in plan.model_limits.items():
|
||||
used = sub.get_model_usage(model_id)
|
||||
remaining = None if limit == -1 else max(0, limit - used)
|
||||
model_usage_info.append(ModelUsageInfo(
|
||||
model_id=model_id,
|
||||
limit=limit,
|
||||
used=used,
|
||||
remaining=remaining,
|
||||
))
|
||||
|
||||
return UserSubscriptionWithPlanModel(
|
||||
**sub.model_dump(),
|
||||
plan=plan,
|
||||
messages_remaining=messages_remaining,
|
||||
days_remaining=days_remaining,
|
||||
model_usage_info=model_usage_info if model_usage_info else None,
|
||||
)
|
||||
|
||||
def get_all_subscriptions(
|
||||
@@ -338,10 +427,6 @@ class UserSubscriptionsTable:
|
||||
plan = SubscriptionPlans.get_plan_by_id(sub_model.plan_id)
|
||||
now = int(time.time())
|
||||
|
||||
messages_remaining = None
|
||||
if plan and plan.monthly_message_limit:
|
||||
messages_remaining = max(0, plan.monthly_message_limit - sub_model.messages_used)
|
||||
|
||||
days_remaining = None
|
||||
if sub_model.expires_at:
|
||||
days_remaining = max(0, (sub_model.expires_at - now) // 86400)
|
||||
@@ -349,7 +434,6 @@ class UserSubscriptionsTable:
|
||||
results.append(UserSubscriptionWithPlanModel(
|
||||
**sub_model.model_dump(),
|
||||
plan=plan,
|
||||
messages_remaining=messages_remaining,
|
||||
days_remaining=days_remaining,
|
||||
))
|
||||
|
||||
@@ -361,11 +445,11 @@ class UserSubscriptionsTable:
|
||||
# create a default free plan if not exists
|
||||
default_plan = SubscriptionPlans.create_plan(SubscriptionPlanForm(
|
||||
id="free",
|
||||
name="Free",
|
||||
description="Default free plan",
|
||||
name="免费版",
|
||||
description="免费套餐,基础模型无限使用",
|
||||
price=0,
|
||||
monthly_message_limit=100,
|
||||
allowed_models=[],
|
||||
model_limits={},
|
||||
default_model_limit=-1, # all models unlimited by default
|
||||
priority=0,
|
||||
is_default=True,
|
||||
is_active=True,
|
||||
@@ -389,6 +473,7 @@ class UserSubscriptionsTable:
|
||||
expires_at=None,
|
||||
current_period_start=now,
|
||||
current_period_end=period_end,
|
||||
model_usage={},
|
||||
messages_used=0,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
@@ -507,6 +592,7 @@ class UserSubscriptionsTable:
|
||||
db.query(UserSubscription).filter(UserSubscription.id == subscription_id).update({
|
||||
"current_period_start": now,
|
||||
"current_period_end": period_end,
|
||||
"model_usage": {},
|
||||
"messages_used": 0,
|
||||
"updated_at": now,
|
||||
})
|
||||
@@ -515,7 +601,31 @@ class UserSubscriptionsTable:
|
||||
sub = db.query(UserSubscription).filter(UserSubscription.id == subscription_id).first()
|
||||
return UserSubscriptionModel.model_validate(sub)
|
||||
|
||||
def increment_model_usage(self, subscription_id: str, model_id: str) -> None:
|
||||
"""Increment usage counter for a specific model."""
|
||||
with get_db() as db:
|
||||
sub = db.query(UserSubscription).filter(UserSubscription.id == subscription_id).first()
|
||||
if not sub:
|
||||
return
|
||||
|
||||
current_usage = sub.model_usage or {}
|
||||
if isinstance(current_usage, str):
|
||||
try:
|
||||
current_usage = json.loads(current_usage)
|
||||
except json.JSONDecodeError:
|
||||
current_usage = {}
|
||||
|
||||
current_usage[model_id] = current_usage.get(model_id, 0) + 1
|
||||
|
||||
db.query(UserSubscription).filter(UserSubscription.id == subscription_id).update({
|
||||
"model_usage": current_usage,
|
||||
"messages_used": UserSubscription.messages_used + 1,
|
||||
"updated_at": int(time.time()),
|
||||
})
|
||||
db.commit()
|
||||
|
||||
def increment_usage(self, subscription_id: str) -> None:
|
||||
"""Deprecated: use increment_model_usage instead."""
|
||||
with get_db() as db:
|
||||
db.query(UserSubscription).filter(UserSubscription.id == subscription_id).update({
|
||||
"messages_used": UserSubscription.messages_used + 1,
|
||||
@@ -659,10 +769,11 @@ class RedemptionCodesTable:
|
||||
duration_days=redemption_code.duration_days,
|
||||
)
|
||||
else:
|
||||
# upgrade type: calculate expires_at from upgrade_days
|
||||
return UserSubscriptions.upgrade_subscription(
|
||||
user_id=user_id,
|
||||
plan_id=redemption_code.plan_id,
|
||||
expires_at=redemption_code.upgrade_expires_at,
|
||||
duration_days=redemption_code.upgrade_days,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user