Skip to content

Commit a2fda1d

Browse files
JohnJyongalexcodelf
authored andcommitted
Feat/new saas billing (langgenius#12591)
1 parent 3fa7a11 commit a2fda1d

File tree

9 files changed

+144
-3
lines changed

9 files changed

+144
-3
lines changed

api/controllers/console/datasets/datasets.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@
1010
from controllers.console.apikey import api_key_fields, api_key_list
1111
from controllers.console.app.error import ProviderNotInitializeError
1212
from controllers.console.datasets.error import DatasetInUseError, DatasetNameDuplicateError, IndexingEstimateError
13-
from controllers.console.wraps import account_initialization_required, enterprise_license_required, setup_required
13+
from controllers.console.wraps import (
14+
account_initialization_required,
15+
cloud_edition_billing_rate_limit_check,
16+
enterprise_license_required,
17+
setup_required,
18+
)
1419
from core.errors.error import LLMBadRequestError, ProviderTokenNotInitError
1520
from core.indexing_runner import IndexingRunner
1621
from core.model_runtime.entities.model_entities import ModelType
@@ -93,6 +98,7 @@ def get(self):
9398
@setup_required
9499
@login_required
95100
@account_initialization_required
101+
@cloud_edition_billing_rate_limit_check("knowledge")
96102
def post(self):
97103
parser = reqparse.RequestParser()
98104
parser.add_argument(
@@ -207,6 +213,7 @@ def get(self, dataset_id):
207213
@setup_required
208214
@login_required
209215
@account_initialization_required
216+
@cloud_edition_billing_rate_limit_check("knowledge")
210217
def patch(self, dataset_id):
211218
dataset_id_str = str(dataset_id)
212219
dataset = DatasetService.get_dataset(dataset_id_str)
@@ -310,6 +317,7 @@ def patch(self, dataset_id):
310317
@setup_required
311318
@login_required
312319
@account_initialization_required
320+
@cloud_edition_billing_rate_limit_check("knowledge")
313321
def delete(self, dataset_id):
314322
dataset_id_str = str(dataset_id)
315323

api/controllers/console/datasets/datasets_document.py

+10
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
)
2828
from controllers.console.wraps import (
2929
account_initialization_required,
30+
cloud_edition_billing_rate_limit_check,
3031
cloud_edition_billing_resource_check,
3132
setup_required,
3233
)
@@ -230,6 +231,7 @@ def get(self, dataset_id):
230231
@account_initialization_required
231232
@marshal_with(documents_and_batch_fields)
232233
@cloud_edition_billing_resource_check("vector_space")
234+
@cloud_edition_billing_rate_limit_check("knowledge")
233235
def post(self, dataset_id):
234236
dataset_id = str(dataset_id)
235237

@@ -285,6 +287,7 @@ def post(self, dataset_id):
285287
@setup_required
286288
@login_required
287289
@account_initialization_required
290+
@cloud_edition_billing_rate_limit_check("knowledge")
288291
def delete(self, dataset_id):
289292
dataset_id = str(dataset_id)
290293
dataset = DatasetService.get_dataset(dataset_id)
@@ -308,6 +311,7 @@ class DatasetInitApi(Resource):
308311
@account_initialization_required
309312
@marshal_with(dataset_and_document_fields)
310313
@cloud_edition_billing_resource_check("vector_space")
314+
@cloud_edition_billing_rate_limit_check("knowledge")
311315
def post(self):
312316
# The role of the current user in the ta table must be admin, owner, or editor
313317
if not current_user.is_editor:
@@ -680,6 +684,7 @@ class DocumentProcessingApi(DocumentResource):
680684
@setup_required
681685
@login_required
682686
@account_initialization_required
687+
@cloud_edition_billing_rate_limit_check("knowledge")
683688
def patch(self, dataset_id, document_id, action):
684689
dataset_id = str(dataset_id)
685690
document_id = str(document_id)
@@ -716,6 +721,7 @@ class DocumentDeleteApi(DocumentResource):
716721
@setup_required
717722
@login_required
718723
@account_initialization_required
724+
@cloud_edition_billing_rate_limit_check("knowledge")
719725
def delete(self, dataset_id, document_id):
720726
dataset_id = str(dataset_id)
721727
document_id = str(document_id)
@@ -784,6 +790,7 @@ class DocumentStatusApi(DocumentResource):
784790
@login_required
785791
@account_initialization_required
786792
@cloud_edition_billing_resource_check("vector_space")
793+
@cloud_edition_billing_rate_limit_check("knowledge")
787794
def patch(self, dataset_id, action):
788795
dataset_id = str(dataset_id)
789796
dataset = DatasetService.get_dataset(dataset_id)
@@ -879,6 +886,7 @@ class DocumentPauseApi(DocumentResource):
879886
@setup_required
880887
@login_required
881888
@account_initialization_required
889+
@cloud_edition_billing_rate_limit_check("knowledge")
882890
def patch(self, dataset_id, document_id):
883891
"""pause document."""
884892
dataset_id = str(dataset_id)
@@ -911,6 +919,7 @@ class DocumentRecoverApi(DocumentResource):
911919
@setup_required
912920
@login_required
913921
@account_initialization_required
922+
@cloud_edition_billing_rate_limit_check("knowledge")
914923
def patch(self, dataset_id, document_id):
915924
"""recover document."""
916925
dataset_id = str(dataset_id)
@@ -940,6 +949,7 @@ class DocumentRetryApi(DocumentResource):
940949
@setup_required
941950
@login_required
942951
@account_initialization_required
952+
@cloud_edition_billing_rate_limit_check("knowledge")
943953
def post(self, dataset_id):
944954
"""retry document."""
945955

api/controllers/console/datasets/datasets_segments.py

+11
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from controllers.console.wraps import (
2020
account_initialization_required,
2121
cloud_edition_billing_knowledge_limit_check,
22+
cloud_edition_billing_rate_limit_check,
2223
cloud_edition_billing_resource_check,
2324
setup_required,
2425
)
@@ -106,6 +107,7 @@ def get(self, dataset_id, document_id):
106107
@setup_required
107108
@login_required
108109
@account_initialization_required
110+
@cloud_edition_billing_rate_limit_check("knowledge")
109111
def delete(self, dataset_id, document_id):
110112
# check dataset
111113
dataset_id = str(dataset_id)
@@ -137,6 +139,7 @@ class DatasetDocumentSegmentApi(Resource):
137139
@login_required
138140
@account_initialization_required
139141
@cloud_edition_billing_resource_check("vector_space")
142+
@cloud_edition_billing_rate_limit_check("knowledge")
140143
def patch(self, dataset_id, document_id, action):
141144
dataset_id = str(dataset_id)
142145
dataset = DatasetService.get_dataset(dataset_id)
@@ -192,6 +195,7 @@ class DatasetDocumentSegmentAddApi(Resource):
192195
@account_initialization_required
193196
@cloud_edition_billing_resource_check("vector_space")
194197
@cloud_edition_billing_knowledge_limit_check("add_segment")
198+
@cloud_edition_billing_rate_limit_check("knowledge")
195199
def post(self, dataset_id, document_id):
196200
# check dataset
197201
dataset_id = str(dataset_id)
@@ -242,6 +246,7 @@ class DatasetDocumentSegmentUpdateApi(Resource):
242246
@login_required
243247
@account_initialization_required
244248
@cloud_edition_billing_resource_check("vector_space")
249+
@cloud_edition_billing_rate_limit_check("knowledge")
245250
def patch(self, dataset_id, document_id, segment_id):
246251
# check dataset
247252
dataset_id = str(dataset_id)
@@ -302,6 +307,7 @@ def patch(self, dataset_id, document_id, segment_id):
302307
@setup_required
303308
@login_required
304309
@account_initialization_required
310+
@cloud_edition_billing_rate_limit_check("knowledge")
305311
def delete(self, dataset_id, document_id, segment_id):
306312
# check dataset
307313
dataset_id = str(dataset_id)
@@ -339,6 +345,7 @@ class DatasetDocumentSegmentBatchImportApi(Resource):
339345
@account_initialization_required
340346
@cloud_edition_billing_resource_check("vector_space")
341347
@cloud_edition_billing_knowledge_limit_check("add_segment")
348+
@cloud_edition_billing_rate_limit_check("knowledge")
342349
def post(self, dataset_id, document_id):
343350
# check dataset
344351
dataset_id = str(dataset_id)
@@ -405,6 +412,7 @@ class ChildChunkAddApi(Resource):
405412
@account_initialization_required
406413
@cloud_edition_billing_resource_check("vector_space")
407414
@cloud_edition_billing_knowledge_limit_check("add_segment")
415+
@cloud_edition_billing_rate_limit_check("knowledge")
408416
def post(self, dataset_id, document_id, segment_id):
409417
# check dataset
410418
dataset_id = str(dataset_id)
@@ -503,6 +511,7 @@ def get(self, dataset_id, document_id, segment_id):
503511
@login_required
504512
@account_initialization_required
505513
@cloud_edition_billing_resource_check("vector_space")
514+
@cloud_edition_billing_rate_limit_check("knowledge")
506515
def patch(self, dataset_id, document_id, segment_id):
507516
# check dataset
508517
dataset_id = str(dataset_id)
@@ -546,6 +555,7 @@ class ChildChunkUpdateApi(Resource):
546555
@setup_required
547556
@login_required
548557
@account_initialization_required
558+
@cloud_edition_billing_rate_limit_check("knowledge")
549559
def delete(self, dataset_id, document_id, segment_id, child_chunk_id):
550560
# check dataset
551561
dataset_id = str(dataset_id)
@@ -590,6 +600,7 @@ def delete(self, dataset_id, document_id, segment_id, child_chunk_id):
590600
@login_required
591601
@account_initialization_required
592602
@cloud_edition_billing_resource_check("vector_space")
603+
@cloud_edition_billing_rate_limit_check("knowledge")
593604
def patch(self, dataset_id, document_id, segment_id, child_chunk_id):
594605
# check dataset
595606
dataset_id = str(dataset_id)

api/controllers/console/datasets/hit_testing.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@
22

33
from controllers.console import api
44
from controllers.console.datasets.hit_testing_base import DatasetsHitTestingBase
5-
from controllers.console.wraps import account_initialization_required, setup_required
5+
from controllers.console.wraps import (
6+
account_initialization_required,
7+
cloud_edition_billing_rate_limit_check,
8+
setup_required,
9+
)
610
from libs.login import login_required
711

812

913
class HitTestingApi(Resource, DatasetsHitTestingBase):
1014
@setup_required
1115
@login_required
1216
@account_initialization_required
17+
@cloud_edition_billing_rate_limit_check("knowledge")
1318
def post(self, dataset_id):
1419
dataset_id_str = str(dataset_id)
1520

api/controllers/console/wraps.py

+32-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import json
22
import os
3+
import time
34
from functools import wraps
45

56
from flask import abort, request
67
from flask_login import current_user # type: ignore
78

89
from configs import dify_config
910
from controllers.console.workspace.error import AccountNotInitializedError
11+
from extensions.ext_redis import redis_client
1012
from models.model import DifySetup
1113
from services.feature_service import FeatureService, LicenseStatus
1214
from services.operation_service import OperationService
@@ -66,7 +68,9 @@ def decorated(*args, **kwargs):
6668
elif resource == "apps" and 0 < apps.limit <= apps.size:
6769
abort(403, "The number of apps has reached the limit of your subscription.")
6870
elif resource == "vector_space" and 0 < vector_space.limit <= vector_space.size:
69-
abort(403, "The capacity of the vector space has reached the limit of your subscription.")
71+
abort(
72+
403, "The capacity of the knowledge storage space has reached the limit of your subscription."
73+
)
7074
elif resource == "documents" and 0 < documents_upload_quota.limit <= documents_upload_quota.size:
7175
# The api of file upload is used in the multiple places,
7276
# so we need to check the source of the request from datasets
@@ -111,6 +115,33 @@ def decorated(*args, **kwargs):
111115
return interceptor
112116

113117

118+
def cloud_edition_billing_rate_limit_check(resource: str):
119+
def interceptor(view):
120+
@wraps(view)
121+
def decorated(*args, **kwargs):
122+
if resource == "knowledge":
123+
knowledge_rate_limit = FeatureService.get_knowledge_rate_limit(current_user.current_tenant_id)
124+
if knowledge_rate_limit.enabled:
125+
current_time = int(time.time() * 1000)
126+
key = f"rate_limit_{current_user.current_tenant_id}"
127+
128+
redis_client.zadd(key, {current_time: current_time})
129+
130+
redis_client.zremrangebyscore(key, 0, current_time - 60000)
131+
132+
request_count = redis_client.zcard(key)
133+
134+
if request_count > knowledge_rate_limit.limit:
135+
abort(
136+
403, "Sorry, you have reached the knowledge base request rate limit of your subscription."
137+
)
138+
return view(*args, **kwargs)
139+
140+
return decorated
141+
142+
return interceptor
143+
144+
114145
def cloud_utm_record(view):
115146
@wraps(view)
116147
def decorated(*args, **kwargs):

api/controllers/service_api/wraps.py

+31
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import time
12
from collections.abc import Callable
23
from datetime import UTC, datetime, timedelta
34
from enum import Enum
@@ -13,6 +14,7 @@
1314
from werkzeug.exceptions import Forbidden, Unauthorized
1415

1516
from extensions.ext_database import db
17+
from extensions.ext_redis import redis_client
1618
from libs.login import _get_user
1719
from models.account import Account, Tenant, TenantAccountJoin, TenantStatus
1820
from models.model import ApiToken, App, EndUser
@@ -139,6 +141,35 @@ def decorated(*args, **kwargs):
139141
return interceptor
140142

141143

144+
def cloud_edition_billing_rate_limit_check(resource: str, api_token_type: str):
145+
def interceptor(view):
146+
@wraps(view)
147+
def decorated(*args, **kwargs):
148+
api_token = validate_and_get_api_token(api_token_type)
149+
150+
if resource == "knowledge":
151+
knowledge_rate_limit = FeatureService.get_knowledge_rate_limit(api_token.tenant_id)
152+
if knowledge_rate_limit.enabled:
153+
current_time = int(time.time() * 1000)
154+
key = f"rate_limit_{api_token.tenant_id}"
155+
156+
redis_client.zadd(key, {current_time: current_time})
157+
158+
redis_client.zremrangebyscore(key, 0, current_time - 60000)
159+
160+
request_count = redis_client.zcard(key)
161+
162+
if request_count > knowledge_rate_limit.limit:
163+
raise Forbidden(
164+
"Sorry, you have reached the knowledge base request rate limit of your subscription."
165+
)
166+
return view(*args, **kwargs)
167+
168+
return decorated
169+
170+
return interceptor
171+
172+
142173
def validate_dataset_token(view=None):
143174
def decorator(view):
144175
@wraps(view)

api/core/workflow/nodes/knowledge_retrieval/knowledge_retrieval_node.py

+20
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import time
23
from collections.abc import Mapping, Sequence
34
from typing import Any, cast
45

@@ -19,8 +20,10 @@
1920
from core.workflow.nodes.base import BaseNode
2021
from core.workflow.nodes.enums import NodeType
2122
from extensions.ext_database import db
23+
from extensions.ext_redis import redis_client
2224
from models.dataset import Dataset, Document
2325
from models.workflow import WorkflowNodeExecutionStatus
26+
from services.feature_service import FeatureService
2427

2528
from .entities import KnowledgeRetrievalNodeData
2629
from .exc import (
@@ -61,6 +64,23 @@ def _run(self) -> NodeRunResult:
6164
return NodeRunResult(
6265
status=WorkflowNodeExecutionStatus.FAILED, inputs=variables, error="Query is required."
6366
)
67+
# check rate limit
68+
if self.tenant_id:
69+
knowledge_rate_limit = FeatureService.get_knowledge_rate_limit(self.tenant_id)
70+
if knowledge_rate_limit.enabled:
71+
current_time = int(time.time() * 1000)
72+
key = f"rate_limit_{self.tenant_id}"
73+
redis_client.zadd(key, {current_time: current_time})
74+
redis_client.zremrangebyscore(key, 0, current_time - 60000)
75+
request_count = redis_client.zcard(key)
76+
if request_count > knowledge_rate_limit.limit:
77+
return NodeRunResult(
78+
status=WorkflowNodeExecutionStatus.FAILED,
79+
inputs=variables,
80+
error="Sorry, you have reached the knowledge base request rate limit of your subscription.",
81+
error_type="RateLimitExceeded",
82+
)
83+
6484
# retrieve knowledge
6585
try:
6686
results = self._fetch_dataset_retriever(node_data=self.node_data, query=query)

api/services/billing_service.py

+8
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ def get_info(cls, tenant_id: str):
1919
billing_info = cls._send_request("GET", "/subscription/info", params=params)
2020
return billing_info
2121

22+
@classmethod
23+
def get_knowledge_rate_limit(cls, tenant_id: str):
24+
params = {"tenant_id": tenant_id}
25+
26+
knowledge_rate_limit = cls._send_request("GET", "/subscription/knowledge-rate-limit", params=params)
27+
28+
return knowledge_rate_limit.get("limit", 10)
29+
2230
@classmethod
2331
def get_subscription(cls, plan: str, interval: str, prefilled_email: str = "", tenant_id: str = ""):
2432
params = {"plan": plan, "interval": interval, "prefilled_email": prefilled_email, "tenant_id": tenant_id}

0 commit comments

Comments
 (0)