mirror of
https://github.com/langgenius/dify.git
synced 2026-09-09 05:41:00 +08:00
refactor(api): trim workspace features response contract (#41948)
This commit is contained in:
parent
a67f62e9aa
commit
8acdb1538b
@ -41,7 +41,7 @@ register_response_schema_models(
|
||||
@console_ns.route("/features")
|
||||
class FeatureApi(Resource):
|
||||
@console_ns.doc("get_tenant_features")
|
||||
@console_ns.doc(description="Get feature configuration for current tenant")
|
||||
@console_ns.doc(description="Get feature availability and limits for the current workspace")
|
||||
@console_ns.response(
|
||||
200,
|
||||
"Success",
|
||||
@ -50,10 +50,8 @@ class FeatureApi(Resource):
|
||||
@console_account_admission()
|
||||
@cloud_utm_record
|
||||
def get(self, request_context: RequestContext):
|
||||
"""Get feature configuration for current tenant"""
|
||||
payload = application_services().feature_queries.get_features(request_context).model_dump()
|
||||
payload.pop("vector_space", None)
|
||||
return payload
|
||||
"""Get current workspace features."""
|
||||
return dump_response(FeatureModel, application_services().feature_queries.get_features(request_context))
|
||||
|
||||
|
||||
@console_ns.route("/features/vector-space")
|
||||
|
||||
@ -142,10 +142,10 @@ register_response_schema_models(
|
||||
)
|
||||
|
||||
|
||||
def _is_role_enabled(role: TenantAccountRole | str, tenant_id: str) -> bool:
|
||||
def _is_role_enabled(role: TenantAccountRole | str) -> bool:
|
||||
if role != TenantAccountRole.DATASET_OPERATOR:
|
||||
return True
|
||||
return FeatureService.get_features(tenant_id=tenant_id, exclude_vector_space=True).dataset_operator_enabled
|
||||
return dify_config.DATASET_OPERATOR_ENABLED
|
||||
|
||||
|
||||
def _count_new_member_invites(tenant_id: str, emails: list[str]) -> tuple[int, int]:
|
||||
@ -254,7 +254,7 @@ class MemberInviteEmailApi(Resource):
|
||||
inviter = current_user
|
||||
if not inviter.current_tenant:
|
||||
raise ValueError("No current tenant")
|
||||
if not _is_role_enabled(invitee_role, inviter.current_tenant.id):
|
||||
if not _is_role_enabled(invitee_role):
|
||||
raise InvalidMemberRoleError()
|
||||
|
||||
# Check workspace permission for member invitations
|
||||
@ -374,7 +374,7 @@ class MemberUpdateRoleApi(Resource):
|
||||
return {"code": "invalid-role", "message": "Invalid role"}, HTTPStatus.BAD_REQUEST
|
||||
if not current_user.current_tenant:
|
||||
raise ValueError("No current tenant")
|
||||
if not _is_role_enabled(new_role, current_user.current_tenant.id):
|
||||
if not _is_role_enabled(new_role):
|
||||
return {"code": "invalid-role", "message": "Invalid role"}, HTTPStatus.BAD_REQUEST
|
||||
member = db.session.get(Account, str(member_id))
|
||||
if not member:
|
||||
|
||||
@ -6552,7 +6552,9 @@ Check if dataset is in use
|
||||
| 404 | Recommended app not found | |
|
||||
|
||||
### [GET] /features
|
||||
**Get feature configuration for current tenant**
|
||||
**Get current workspace features**
|
||||
|
||||
Get feature availability and limits for the current workspace
|
||||
|
||||
#### Responses
|
||||
|
||||
@ -18453,6 +18455,8 @@ Flask blueprint initialization.
|
||||
|
||||
#### FeatureModel
|
||||
|
||||
Effective feature availability and limits for the current workspace.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| annotation_quota_limit | [LimitationModel](#limitationmodel) | | Yes |
|
||||
@ -18460,20 +18464,15 @@ Flask blueprint initialization.
|
||||
| apps | [LimitationModel](#limitationmodel) | | Yes |
|
||||
| billing | [BillingModel](#billingmodel) | | Yes |
|
||||
| can_replace_logo | boolean | | Yes |
|
||||
| dataset_operator_enabled | boolean | | Yes |
|
||||
| docs_processing | string, <br>**Default:** standard | | Yes |
|
||||
| documents_upload_quota | [LimitationModel](#limitationmodel) | | Yes |
|
||||
| education | [EducationModel](#educationmodel) | | Yes |
|
||||
| enable_skill | boolean, <br>**Default:** true | | Yes |
|
||||
| human_input_email_delivery_enabled | boolean | | Yes |
|
||||
| is_allow_transfer_workspace | boolean, <br>**Default:** true | | Yes |
|
||||
| knowledge_pipeline | [KnowledgePipeline](#knowledgepipeline) | | Yes |
|
||||
| knowledge_rate_limit | integer, <br>**Default:** 10 | | Yes |
|
||||
| members | [LimitationModel](#limitationmodel) | | Yes |
|
||||
| model_load_balancing_enabled | boolean | | Yes |
|
||||
| next_credit_reset_date | integer | | Yes |
|
||||
| trigger_event | [Quota](#quota) | | Yes |
|
||||
| vector_space | [LimitationModel](#limitationmodel) | | Yes |
|
||||
| webapp_copyright_enabled | boolean | | Yes |
|
||||
| workspace_members | [LicenseLimitationModel](#licenselimitationmodel) | | Yes |
|
||||
|
||||
@ -21343,8 +21342,8 @@ Payload for publishing snippet workflow.
|
||||
|
||||
| Name | Type | Description | Required |
|
||||
| ---- | ---- | ----------- | -------- |
|
||||
| limit | integer | | Yes |
|
||||
| reset_date | integer, <br>**Default:** -1 | | Yes |
|
||||
| limit | integer | Quota limit; -1 means unlimited and 0 means no quota | Yes |
|
||||
| reset_date | integer, <br>**Default:** -1 | Next quota reset as a Unix timestamp in seconds; -1 means no reset | Yes |
|
||||
| usage | integer | | Yes |
|
||||
|
||||
#### QuotaConfiguration
|
||||
|
||||
@ -147,15 +147,6 @@ class _VectorSpaceQuota(TypedDict):
|
||||
usage_unknown: NotRequired[bool]
|
||||
|
||||
|
||||
class _KnowledgeRateLimit(TypedDict):
|
||||
# NOTE (hj24):
|
||||
# 1. Return for sandbox users but is null for other plans, it's defined but never used.
|
||||
# 2. Keep it for compatibility for now, can be deprecated in future versions.
|
||||
size: NotRequired[int]
|
||||
# NOTE END
|
||||
limit: int
|
||||
|
||||
|
||||
class _BillingSubscription(TypedDict):
|
||||
plan: str
|
||||
interval: str
|
||||
@ -177,10 +168,8 @@ class BillingInfo(TypedDict):
|
||||
members: _BillingQuota
|
||||
apps: _BillingQuota
|
||||
vector_space: NotRequired[_VectorSpaceQuota]
|
||||
knowledge_rate_limit: _KnowledgeRateLimit
|
||||
documents_upload_quota: _BillingQuota
|
||||
annotation_quota_limit: _BillingQuota
|
||||
docs_processing: str
|
||||
can_replace_logo: bool
|
||||
model_load_balancing_enabled: bool
|
||||
knowledge_pipeline_publish_enabled: bool
|
||||
|
||||
@ -66,8 +66,10 @@ class LicenseLimitationModel(FeatureResponseModel):
|
||||
|
||||
class Quota(FeatureResponseModel):
|
||||
usage: int = 0
|
||||
limit: int = 0
|
||||
reset_date: int = -1
|
||||
limit: int = Field(default=0, description="Quota limit; -1 means unlimited and 0 means no quota")
|
||||
reset_date: int = Field(
|
||||
default=-1, description="Next quota reset as a Unix timestamp in seconds; -1 means no reset"
|
||||
)
|
||||
|
||||
|
||||
class LicenseStatus(StrEnum):
|
||||
@ -142,19 +144,19 @@ class PluginInstallationPermissionModel(FeatureResponseModel):
|
||||
|
||||
|
||||
class FeatureModel(FeatureResponseModel):
|
||||
"""Effective feature availability and limits for the current workspace."""
|
||||
|
||||
billing: BillingModel = BillingModel()
|
||||
education: EducationModel = EducationModel()
|
||||
enable_skill: bool = True
|
||||
members: LimitationModel = LimitationModel(size=0, limit=1)
|
||||
apps: LimitationModel = LimitationModel(size=0, limit=10)
|
||||
vector_space: LimitationModel | None = LimitationModel(size=0, limit=5)
|
||||
knowledge_rate_limit: int = 10
|
||||
# Indexing tasks still consume this internally; Console queries vector usage separately.
|
||||
vector_space: LimitationModel | None = Field(default=LimitationModel(size=0, limit=5), exclude=True)
|
||||
annotation_quota_limit: LimitationModel = LimitationModel(size=0, limit=10)
|
||||
documents_upload_quota: LimitationModel = LimitationModel(size=0, limit=50)
|
||||
docs_processing: str = "standard"
|
||||
can_replace_logo: bool = False
|
||||
model_load_balancing_enabled: bool = False
|
||||
dataset_operator_enabled: bool = False
|
||||
webapp_copyright_enabled: bool = False
|
||||
workspace_members: LicenseLimitationModel = LicenseLimitationModel(enabled=False, size=0, limit=0)
|
||||
is_allow_transfer_workspace: bool = True
|
||||
@ -163,7 +165,8 @@ class FeatureModel(FeatureResponseModel):
|
||||
# Controls whether email delivery is allowed for HumanInput nodes.
|
||||
human_input_email_delivery_enabled: bool = False
|
||||
knowledge_pipeline: KnowledgePipeline = KnowledgePipeline()
|
||||
next_credit_reset_date: int = 0
|
||||
# Workspace and model-provider credit responses own the public reset-date contract.
|
||||
next_credit_reset_date: int = Field(default=0, exclude=True)
|
||||
|
||||
|
||||
class KnowledgeRateLimitModel(FeatureResponseModel):
|
||||
|
||||
@ -112,7 +112,6 @@ class FeatureService:
|
||||
def _fulfill_params_from_env(cls, features: feature_entities.FeatureModel):
|
||||
features.can_replace_logo = dify_config.CAN_REPLACE_LOGO
|
||||
features.model_load_balancing_enabled = dify_config.MODEL_LB_ENABLED
|
||||
features.dataset_operator_enabled = dify_config.DATASET_OPERATOR_ENABLED
|
||||
features.education.enabled = dify_config.EDUCATION_ENABLED
|
||||
features.enable_skill = dify_config.ENABLE_SKILL
|
||||
|
||||
@ -177,22 +176,12 @@ class FeatureService:
|
||||
features.annotation_quota_limit.size = billing_info["annotation_quota_limit"]["size"]
|
||||
features.annotation_quota_limit.limit = billing_info["annotation_quota_limit"]["limit"]
|
||||
|
||||
if "docs_processing" in billing_info:
|
||||
features.docs_processing = billing_info["docs_processing"]
|
||||
|
||||
if "can_replace_logo" in billing_info:
|
||||
features.can_replace_logo = billing_info["can_replace_logo"]
|
||||
|
||||
if "model_load_balancing_enabled" in billing_info:
|
||||
features.model_load_balancing_enabled = billing_info["model_load_balancing_enabled"]
|
||||
|
||||
if "knowledge_rate_limit" in billing_info:
|
||||
# NOTE (hj24):
|
||||
# 1. knowledge_rate_limit size is nullable, currently it's defined but never used, only limit is used.
|
||||
# 2. So be careful if later we decide to use [size], we cannot assume it is always present.
|
||||
features.knowledge_rate_limit = billing_info["knowledge_rate_limit"]["limit"]
|
||||
# NOTE END
|
||||
|
||||
if "knowledge_pipeline_publish_enabled" in billing_info:
|
||||
features.knowledge_pipeline.publish_enabled = billing_info["knowledge_pipeline_publish_enabled"]
|
||||
|
||||
|
||||
@ -36,10 +36,8 @@ class TestFeatureService:
|
||||
"vector_space": {"size": 2, "limit": 10},
|
||||
"documents_upload_quota": {"size": 15, "limit": 100},
|
||||
"annotation_quota_limit": {"size": 8, "limit": 50},
|
||||
"docs_processing": "enhanced",
|
||||
"can_replace_logo": True,
|
||||
"model_load_balancing_enabled": True,
|
||||
"knowledge_rate_limit": {"limit": 100},
|
||||
}
|
||||
|
||||
mock_billing_service.get_knowledge_rate_limit.return_value = {
|
||||
@ -106,7 +104,6 @@ class TestFeatureService:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
|
||||
mock_config.CAN_REPLACE_LOGO = True
|
||||
mock_config.MODEL_LB_ENABLED = True
|
||||
mock_config.DATASET_OPERATOR_ENABLED = True
|
||||
mock_config.EDUCATION_ENABLED = True
|
||||
|
||||
# Act: Execute the method under test
|
||||
@ -142,10 +139,8 @@ class TestFeatureService:
|
||||
assert result.annotation_quota_limit.limit == 50
|
||||
|
||||
# Verify other features
|
||||
assert result.docs_processing == "enhanced"
|
||||
assert result.can_replace_logo is True
|
||||
assert result.model_load_balancing_enabled is True
|
||||
assert result.knowledge_rate_limit == 100
|
||||
|
||||
# Enterprise workspace features are not loaded in Cloud.
|
||||
assert result.workspace_members.enabled is False
|
||||
@ -177,7 +172,6 @@ class TestFeatureService:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
|
||||
mock_config.CAN_REPLACE_LOGO = False
|
||||
mock_config.MODEL_LB_ENABLED = False
|
||||
mock_config.DATASET_OPERATOR_ENABLED = False
|
||||
mock_config.EDUCATION_ENABLED = False
|
||||
|
||||
# Set mock return value inside the patch context
|
||||
@ -188,10 +182,8 @@ class TestFeatureService:
|
||||
"vector_space": {"size": 1, "limit": 2},
|
||||
"documents_upload_quota": {"size": 5, "limit": 20},
|
||||
"annotation_quota_limit": {"size": 2, "limit": 10},
|
||||
"docs_processing": "standard",
|
||||
"can_replace_logo": False,
|
||||
"model_load_balancing_enabled": False,
|
||||
"knowledge_rate_limit": {"limit": 10},
|
||||
}
|
||||
|
||||
# Act: Execute the method under test
|
||||
@ -218,8 +210,6 @@ class TestFeatureService:
|
||||
assert result.is_allow_transfer_workspace is False
|
||||
assert result.can_replace_logo is False
|
||||
assert result.model_load_balancing_enabled is False
|
||||
assert result.docs_processing == "standard"
|
||||
assert result.knowledge_rate_limit == 10
|
||||
|
||||
# Verify mock interactions
|
||||
mock_external_service_dependencies["billing_service"].get_info.assert_called_once_with(tenant_id)
|
||||
@ -495,7 +485,6 @@ class TestFeatureService:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
|
||||
mock_config.CAN_REPLACE_LOGO = True
|
||||
mock_config.MODEL_LB_ENABLED = True
|
||||
mock_config.DATASET_OPERATOR_ENABLED = True
|
||||
mock_config.EDUCATION_ENABLED = True
|
||||
|
||||
tenant_id = self._create_test_tenant_id()
|
||||
@ -510,7 +499,6 @@ class TestFeatureService:
|
||||
# Verify environment-based features
|
||||
assert result.can_replace_logo is True
|
||||
assert result.model_load_balancing_enabled is True
|
||||
assert result.dataset_operator_enabled is True
|
||||
assert result.education.enabled is True
|
||||
|
||||
# Verify default limitations
|
||||
@ -524,8 +512,6 @@ class TestFeatureService:
|
||||
assert result.documents_upload_quota.limit == 50
|
||||
assert result.annotation_quota_limit.size == 0
|
||||
assert result.annotation_quota_limit.limit == 10
|
||||
assert result.knowledge_rate_limit == 10
|
||||
assert result.docs_processing == "standard"
|
||||
|
||||
# Verify no enterprise features
|
||||
assert result.workspace_members.enabled is False
|
||||
@ -580,7 +566,6 @@ class TestFeatureService:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.ENTERPRISE
|
||||
mock_config.CAN_REPLACE_LOGO = False
|
||||
mock_config.MODEL_LB_ENABLED = False
|
||||
mock_config.DATASET_OPERATOR_ENABLED = False
|
||||
mock_config.EDUCATION_ENABLED = False
|
||||
|
||||
tenant_id = self._create_test_tenant_id()
|
||||
@ -603,7 +588,6 @@ class TestFeatureService:
|
||||
# Verify environment-based features
|
||||
assert result.can_replace_logo is False
|
||||
assert result.model_load_balancing_enabled is False
|
||||
assert result.dataset_operator_enabled is False
|
||||
assert result.education.enabled is False
|
||||
|
||||
# Verify default limitations
|
||||
@ -691,7 +675,6 @@ class TestFeatureService:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
|
||||
mock_config.CAN_REPLACE_LOGO = True
|
||||
mock_config.MODEL_LB_ENABLED = False
|
||||
mock_config.DATASET_OPERATOR_ENABLED = True
|
||||
mock_config.EDUCATION_ENABLED = False
|
||||
|
||||
# Act: Execute the method under test
|
||||
@ -704,7 +687,6 @@ class TestFeatureService:
|
||||
# Verify environment-based features
|
||||
assert result.can_replace_logo is True
|
||||
assert result.model_load_balancing_enabled is False
|
||||
assert result.dataset_operator_enabled is True
|
||||
assert result.education.enabled is False
|
||||
|
||||
# Verify default limitations
|
||||
@ -737,7 +719,6 @@ class TestFeatureService:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
|
||||
mock_config.CAN_REPLACE_LOGO = True
|
||||
mock_config.MODEL_LB_ENABLED = False
|
||||
mock_config.DATASET_OPERATOR_ENABLED = True
|
||||
mock_config.EDUCATION_ENABLED = False
|
||||
|
||||
mock_external_service_dependencies["billing_service"].get_info.return_value = {
|
||||
@ -767,8 +748,6 @@ class TestFeatureService:
|
||||
assert result.documents_upload_quota.limit == 50
|
||||
assert result.annotation_quota_limit.size == 0
|
||||
assert result.annotation_quota_limit.limit == 10
|
||||
assert result.knowledge_rate_limit == 10
|
||||
assert result.docs_processing == "standard"
|
||||
|
||||
# Verify paid plan behavior.
|
||||
assert result.webapp_copyright_enabled is True
|
||||
@ -796,7 +775,6 @@ class TestFeatureService:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
|
||||
mock_config.CAN_REPLACE_LOGO = True
|
||||
mock_config.MODEL_LB_ENABLED = False
|
||||
mock_config.DATASET_OPERATOR_ENABLED = True
|
||||
mock_config.EDUCATION_ENABLED = False
|
||||
|
||||
mock_external_service_dependencies["billing_service"].get_info.return_value = {
|
||||
@ -831,8 +809,6 @@ class TestFeatureService:
|
||||
assert result.documents_upload_quota.limit == 50
|
||||
assert result.annotation_quota_limit.size == 0
|
||||
assert result.annotation_quota_limit.limit == 10
|
||||
assert result.knowledge_rate_limit == 10
|
||||
assert result.docs_processing == "standard"
|
||||
|
||||
# Verify mock interactions
|
||||
mock_external_service_dependencies["billing_service"].get_info.assert_called_once_with(tenant_id)
|
||||
@ -912,7 +888,6 @@ class TestFeatureService:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
|
||||
mock_config.CAN_REPLACE_LOGO = True
|
||||
mock_config.MODEL_LB_ENABLED = False
|
||||
mock_config.DATASET_OPERATOR_ENABLED = True
|
||||
mock_config.EDUCATION_ENABLED = False
|
||||
|
||||
mock_external_service_dependencies["billing_service"].get_info.return_value = {
|
||||
@ -947,8 +922,6 @@ class TestFeatureService:
|
||||
assert result.documents_upload_quota.limit == 50
|
||||
assert result.annotation_quota_limit.size == 0
|
||||
assert result.annotation_quota_limit.limit == 10
|
||||
assert result.knowledge_rate_limit == 10
|
||||
assert result.docs_processing == "standard"
|
||||
|
||||
# Verify mock interactions
|
||||
mock_external_service_dependencies["billing_service"].get_info.assert_called_once_with(tenant_id)
|
||||
@ -1231,7 +1204,6 @@ class TestFeatureService:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
|
||||
mock_config.CAN_REPLACE_LOGO = True
|
||||
mock_config.MODEL_LB_ENABLED = False
|
||||
mock_config.DATASET_OPERATOR_ENABLED = True
|
||||
mock_config.EDUCATION_ENABLED = False
|
||||
|
||||
mock_external_service_dependencies["billing_service"].get_info.return_value = {
|
||||
@ -1468,62 +1440,6 @@ class TestFeatureService:
|
||||
# Verify mock interactions
|
||||
mock_external_service_dependencies["enterprise_service"].get_info.assert_called_once()
|
||||
|
||||
def test_get_features_edge_case_docs_processing(
|
||||
self, db_session_with_containers: Session, mock_external_service_dependencies
|
||||
):
|
||||
"""
|
||||
Test feature retrieval with edge case document processing configuration.
|
||||
|
||||
This test verifies:
|
||||
- Proper handling of different document processing modes
|
||||
- Correct integration with billing service
|
||||
- Proper fallback to default values
|
||||
- Return value correctness and structure
|
||||
"""
|
||||
# Arrange: Setup edge case docs processing mock with proper config
|
||||
tenant_id = self._create_test_tenant_id()
|
||||
|
||||
with patch("services.feature_service.dify_config") as mock_config:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
|
||||
mock_config.CAN_REPLACE_LOGO = True
|
||||
mock_config.MODEL_LB_ENABLED = True
|
||||
mock_config.DATASET_OPERATOR_ENABLED = True
|
||||
mock_config.EDUCATION_ENABLED = False
|
||||
|
||||
mock_external_service_dependencies["billing_service"].get_info.return_value = {
|
||||
"subscription": {"plan": CloudPlan.TEAM, "interval": "monthly"},
|
||||
"docs_processing": "advanced",
|
||||
"can_replace_logo": True,
|
||||
"model_load_balancing_enabled": True,
|
||||
}
|
||||
|
||||
# Act: Execute the method under test
|
||||
result = FeatureService.get_features(tenant_id)
|
||||
|
||||
# Assert: Verify the expected outcomes
|
||||
assert result is not None
|
||||
assert isinstance(result, FeatureModel)
|
||||
|
||||
# Verify docs processing configuration
|
||||
assert result.docs_processing == "advanced"
|
||||
assert result.can_replace_logo is True
|
||||
assert result.model_load_balancing_enabled is True
|
||||
|
||||
# Verify paid plan behavior.
|
||||
assert result.webapp_copyright_enabled is True
|
||||
assert result.is_allow_transfer_workspace is True
|
||||
|
||||
# Verify default limitations (no specific billing info)
|
||||
assert result.members.size == 0
|
||||
assert result.members.limit == 1
|
||||
assert result.apps.size == 0
|
||||
assert result.apps.limit == 10
|
||||
assert result.vector_space.size == 0
|
||||
assert result.vector_space.limit == 5
|
||||
|
||||
# Verify mock interactions
|
||||
mock_external_service_dependencies["billing_service"].get_info.assert_called_once_with(tenant_id)
|
||||
|
||||
def test_get_system_features_edge_case_branding(
|
||||
self, db_session_with_containers: Session, mock_external_service_dependencies
|
||||
):
|
||||
@ -1604,13 +1520,11 @@ class TestFeatureService:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
|
||||
mock_config.CAN_REPLACE_LOGO = True
|
||||
mock_config.MODEL_LB_ENABLED = False
|
||||
mock_config.DATASET_OPERATOR_ENABLED = True
|
||||
mock_config.EDUCATION_ENABLED = False
|
||||
|
||||
mock_external_service_dependencies["billing_service"].get_info.return_value = {
|
||||
"subscription": {"plan": CloudPlan.TEAM, "interval": "yearly"},
|
||||
"annotation_quota_limit": {"size": 999, "limit": 1000},
|
||||
"knowledge_rate_limit": {"limit": 500},
|
||||
}
|
||||
|
||||
# Act: Execute the method under test
|
||||
@ -1624,9 +1538,6 @@ class TestFeatureService:
|
||||
assert result.annotation_quota_limit.size == 999
|
||||
assert result.annotation_quota_limit.limit == 1000
|
||||
|
||||
# Verify knowledge rate limit
|
||||
assert result.knowledge_rate_limit == 500
|
||||
|
||||
# Verify paid plan behavior.
|
||||
assert result.webapp_copyright_enabled is True
|
||||
assert result.is_allow_transfer_workspace is True
|
||||
@ -1640,7 +1551,6 @@ class TestFeatureService:
|
||||
assert result.vector_space.limit == 5
|
||||
assert result.documents_upload_quota.size == 0
|
||||
assert result.documents_upload_quota.limit == 50
|
||||
assert result.docs_processing == "standard"
|
||||
|
||||
# Verify mock interactions
|
||||
mock_external_service_dependencies["billing_service"].get_info.assert_called_once_with(tenant_id)
|
||||
@ -1664,7 +1574,6 @@ class TestFeatureService:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
|
||||
mock_config.CAN_REPLACE_LOGO = True
|
||||
mock_config.MODEL_LB_ENABLED = False
|
||||
mock_config.DATASET_OPERATOR_ENABLED = True
|
||||
mock_config.EDUCATION_ENABLED = False
|
||||
|
||||
mock_external_service_dependencies["billing_service"].get_info.return_value = {
|
||||
@ -1673,7 +1582,6 @@ class TestFeatureService:
|
||||
"size": 0, # Edge case: zero current size
|
||||
"limit": 0, # Edge case: zero limit
|
||||
},
|
||||
"knowledge_rate_limit": {"limit": 100},
|
||||
}
|
||||
|
||||
# Act: Execute the method under test
|
||||
@ -1687,9 +1595,6 @@ class TestFeatureService:
|
||||
assert result.documents_upload_quota.size == 0
|
||||
assert result.documents_upload_quota.limit == 0
|
||||
|
||||
# Verify knowledge rate limit
|
||||
assert result.knowledge_rate_limit == 100
|
||||
|
||||
# Verify paid plan behavior.
|
||||
assert result.webapp_copyright_enabled is True
|
||||
assert result.is_allow_transfer_workspace is True
|
||||
@ -1703,7 +1608,6 @@ class TestFeatureService:
|
||||
assert result.vector_space.limit == 5
|
||||
assert result.annotation_quota_limit.size == 0
|
||||
assert result.annotation_quota_limit.limit == 10 # Default value when not provided
|
||||
assert result.docs_processing == "standard"
|
||||
|
||||
# Verify mock interactions
|
||||
mock_external_service_dependencies["billing_service"].get_info.assert_called_once_with(tenant_id)
|
||||
@ -1778,7 +1682,6 @@ class TestFeatureService:
|
||||
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.CLOUD
|
||||
mock_config.CAN_REPLACE_LOGO = True
|
||||
mock_config.MODEL_LB_ENABLED = False
|
||||
mock_config.DATASET_OPERATOR_ENABLED = True
|
||||
mock_config.EDUCATION_ENABLED = False
|
||||
|
||||
mock_external_service_dependencies["billing_service"].get_info.return_value = {
|
||||
@ -1787,7 +1690,6 @@ class TestFeatureService:
|
||||
"interval": "monthly",
|
||||
"education": False, # Education explicitly disabled
|
||||
},
|
||||
"knowledge_rate_limit": {"limit": 100},
|
||||
}
|
||||
|
||||
# Act: Execute the method under test
|
||||
@ -1800,9 +1702,6 @@ class TestFeatureService:
|
||||
# Verify education configuration
|
||||
assert result.education.activated is False
|
||||
|
||||
# Verify knowledge rate limit
|
||||
assert result.knowledge_rate_limit == 100
|
||||
|
||||
# Verify paid plan behavior.
|
||||
assert result.webapp_copyright_enabled is True
|
||||
assert result.is_allow_transfer_workspace is True
|
||||
@ -1818,7 +1717,6 @@ class TestFeatureService:
|
||||
assert result.documents_upload_quota.limit == 50
|
||||
assert result.annotation_quota_limit.size == 0
|
||||
assert result.annotation_quota_limit.limit == 10 # Default value when not provided
|
||||
assert result.docs_processing == "standard"
|
||||
|
||||
# Verify mock interactions
|
||||
mock_external_service_dependencies["billing_service"].get_info.assert_called_once_with(tenant_id)
|
||||
|
||||
@ -39,8 +39,9 @@ class TestFeatureApi:
|
||||
from controllers.console.feature import FeatureApi
|
||||
|
||||
features = FeatureModel(
|
||||
knowledge_rate_limit=42,
|
||||
apps=LimitationModel(size=3, limit=10),
|
||||
vector_space=LimitationModel(size=1, limit=2),
|
||||
next_credit_reset_date=1775001600,
|
||||
)
|
||||
feature_queries = _install_application_services(mocker)
|
||||
get_features = feature_queries.get_features
|
||||
@ -52,9 +53,18 @@ class TestFeatureApi:
|
||||
request_context = _request_context()
|
||||
result = raw_get(api, request_context)
|
||||
|
||||
expected = features.model_dump()
|
||||
expected.pop("vector_space")
|
||||
assert result == expected
|
||||
assert result == features.model_dump(mode="json")
|
||||
assert result["apps"] == {"size": 3, "limit": 10}
|
||||
retired_fields = {
|
||||
"vector_space",
|
||||
"next_credit_reset_date",
|
||||
"docs_processing",
|
||||
"knowledge_rate_limit",
|
||||
"dataset_operator_enabled",
|
||||
}
|
||||
schema = FeatureModel.model_json_schema(mode="serialization")
|
||||
assert retired_fields.isdisjoint(result)
|
||||
assert set(schema["properties"]) == set(schema["required"]) == set(result)
|
||||
get_features.assert_called_once_with(request_context)
|
||||
|
||||
|
||||
|
||||
@ -150,7 +150,9 @@ class TestMemberInviteEmailApi:
|
||||
with patch("controllers.console.workspace.members.redis_client.lock", return_value=nullcontext()):
|
||||
yield
|
||||
|
||||
def test_invite_success(self, app: Flask):
|
||||
@pytest.mark.parametrize("role", ["normal", "dataset_operator"])
|
||||
def test_invite_success(self, app: Flask, config_overrides, role: str):
|
||||
config_overrides(DATASET_OPERATOR_ENABLED=True)
|
||||
api = MemberInviteEmailApi()
|
||||
method = unwrap(api.post)
|
||||
|
||||
@ -162,7 +164,7 @@ class TestMemberInviteEmailApi:
|
||||
|
||||
payload = {
|
||||
"emails": ["A@TEST.com", "a@test.com"],
|
||||
"role": "normal",
|
||||
"role": role,
|
||||
"language": "en-US",
|
||||
}
|
||||
|
||||
@ -182,6 +184,7 @@ class TestMemberInviteEmailApi:
|
||||
mock_count.assert_not_called()
|
||||
mock_invite.assert_called_once()
|
||||
assert mock_invite.call_args.kwargs["email"] == "a@test.com"
|
||||
assert mock_invite.call_args.kwargs["role"] == role
|
||||
|
||||
def test_invite_limit_exceeded(self, app: Flask, config_overrides):
|
||||
config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.ENTERPRISE)
|
||||
@ -263,18 +266,20 @@ class TestMemberInviteEmailApi:
|
||||
assert result["invitation_results"][0]["status"] == "already_member"
|
||||
assert result["invitation_results"][0]["message"] == "Account already in workspace."
|
||||
|
||||
def test_invite_invalid_role(self, app: Flask):
|
||||
@pytest.mark.parametrize("role", ["owner", "dataset_operator"])
|
||||
def test_invite_invalid_role(self, app: Flask, config_overrides, role: str):
|
||||
config_overrides(DATASET_OPERATOR_ENABLED=False)
|
||||
api = MemberInviteEmailApi()
|
||||
method = unwrap(api.post)
|
||||
|
||||
payload = {
|
||||
"emails": ["a@test.com"],
|
||||
"role": "owner",
|
||||
"role": role,
|
||||
}
|
||||
|
||||
with app.test_request_context("/", json=payload):
|
||||
with pytest.raises(InvalidMemberRoleError) as exc_info:
|
||||
method(api, _account())
|
||||
method(api, _account(tenant=_tenant()))
|
||||
|
||||
assert exc_info.value.error_code == "invalid_role"
|
||||
|
||||
@ -540,14 +545,16 @@ class TestCountNewMemberInvites:
|
||||
|
||||
|
||||
class TestMemberUpdateRoleApi:
|
||||
def test_update_invalid_role(self, app: Flask):
|
||||
@pytest.mark.parametrize("role", ["invalid-role", "dataset_operator"])
|
||||
def test_update_invalid_role(self, app: Flask, config_overrides, role: str):
|
||||
config_overrides(DATASET_OPERATOR_ENABLED=False)
|
||||
api = MemberUpdateRoleApi()
|
||||
method = unwrap(api.put)
|
||||
|
||||
payload = {"role": "invalid-role"}
|
||||
payload = {"role": role}
|
||||
|
||||
with app.test_request_context("/", json=payload):
|
||||
result, status = method(api, _account(), "id")
|
||||
result, status = method(api, _account(tenant=_tenant()), "id")
|
||||
|
||||
assert status == 400
|
||||
|
||||
|
||||
@ -434,16 +434,18 @@ class TestBillingServiceSubscriptionInfo:
|
||||
"members": {"size": 1, "limit": 50},
|
||||
"apps": {"size": 1, "limit": 200},
|
||||
"vector_space": {"size": 0.0, "limit": 20480},
|
||||
"knowledge_rate_limit": {"limit": 1000},
|
||||
"documents_upload_quota": {"size": 0, "limit": 1000},
|
||||
"annotation_quota_limit": {"size": 0, "limit": 5000},
|
||||
"docs_processing": "top-priority",
|
||||
"can_replace_logo": True,
|
||||
"model_load_balancing_enabled": True,
|
||||
"knowledge_pipeline_publish_enabled": True,
|
||||
"next_credit_reset_date": 1775952000,
|
||||
}
|
||||
mock_send_request.return_value = expected_response
|
||||
mock_send_request.return_value = {
|
||||
**expected_response,
|
||||
"docs_processing": "top-priority",
|
||||
"knowledge_rate_limit": {"limit": 1000},
|
||||
}
|
||||
|
||||
# Act
|
||||
result = BillingService.get_info(tenant_id)
|
||||
@ -460,10 +462,8 @@ class TestBillingServiceSubscriptionInfo:
|
||||
"subscription": {"plan": "professional", "interval": "month", "education": False},
|
||||
"members": {"size": 1, "limit": 50},
|
||||
"apps": {"size": 1, "limit": 200},
|
||||
"knowledge_rate_limit": {"limit": 1000},
|
||||
"documents_upload_quota": {"size": 0, "limit": 1000},
|
||||
"annotation_quota_limit": {"size": 0, "limit": 5000},
|
||||
"docs_processing": "top-priority",
|
||||
"can_replace_logo": True,
|
||||
"model_load_balancing_enabled": True,
|
||||
"knowledge_pipeline_publish_enabled": True,
|
||||
@ -490,10 +490,8 @@ class TestBillingServiceSubscriptionInfo:
|
||||
"members": {"size": 1, "limit": 50},
|
||||
"apps": {"size": 1, "limit": 200},
|
||||
"vector_space": None,
|
||||
"knowledge_rate_limit": {"limit": 1000},
|
||||
"documents_upload_quota": {"size": 0, "limit": 1000},
|
||||
"annotation_quota_limit": {"size": 0, "limit": 5000},
|
||||
"docs_processing": "top-priority",
|
||||
"can_replace_logo": True,
|
||||
"model_load_balancing_enabled": True,
|
||||
"knowledge_pipeline_publish_enabled": True,
|
||||
@ -545,10 +543,8 @@ class TestBillingServiceSubscriptionInfo:
|
||||
"members": {"size": 1, "limit": 1},
|
||||
"apps": {"size": 1, "limit": 10},
|
||||
"vector_space": {"size": 0.0, "limit": 50, "usage_unknown": True},
|
||||
"knowledge_rate_limit": {"limit": 10},
|
||||
"documents_upload_quota": {"size": 1, "limit": 50},
|
||||
"annotation_quota_limit": {"size": 0, "limit": 10},
|
||||
"docs_processing": "standard",
|
||||
"can_replace_logo": False,
|
||||
"model_load_balancing_enabled": False,
|
||||
"knowledge_pipeline_publish_enabled": False,
|
||||
@ -1750,10 +1746,8 @@ class TestBillingServiceIntegrationScenarios:
|
||||
"members": {"size": 0, "limit": 1},
|
||||
"apps": {"size": 0, "limit": 5},
|
||||
"vector_space": {"size": 0.0, "limit": 50},
|
||||
"knowledge_rate_limit": {"limit": 10},
|
||||
"documents_upload_quota": {"size": 0, "limit": 50},
|
||||
"annotation_quota_limit": {"size": 0, "limit": 10},
|
||||
"docs_processing": "standard",
|
||||
"can_replace_logo": False,
|
||||
"model_load_balancing_enabled": False,
|
||||
"knowledge_pipeline_publish_enabled": False,
|
||||
@ -1825,10 +1819,8 @@ class TestBillingServiceSubscriptionInfoDataType:
|
||||
"members": {"size": 10, "limit": 50},
|
||||
"apps": {"size": 80, "limit": 200},
|
||||
"vector_space": {"size": 5120.75, "limit": 20480},
|
||||
"knowledge_rate_limit": {"limit": 1000},
|
||||
"documents_upload_quota": {"size": 450, "limit": 1000},
|
||||
"annotation_quota_limit": {"size": 1200, "limit": 5000},
|
||||
"docs_processing": "top-priority",
|
||||
"can_replace_logo": True,
|
||||
"model_load_balancing_enabled": True,
|
||||
"knowledge_pipeline_publish_enabled": True,
|
||||
@ -1846,10 +1838,8 @@ class TestBillingServiceSubscriptionInfoDataType:
|
||||
"members": {"size": "10", "limit": "50"},
|
||||
"apps": {"size": "80", "limit": "200"},
|
||||
"vector_space": {"size": 5120.75, "limit": "20480"},
|
||||
"knowledge_rate_limit": {"limit": "1000"},
|
||||
"documents_upload_quota": {"size": "450", "limit": "1000"},
|
||||
"annotation_quota_limit": {"size": "1200", "limit": "5000"},
|
||||
"docs_processing": "top-priority",
|
||||
"can_replace_logo": True,
|
||||
"model_load_balancing_enabled": True,
|
||||
"knowledge_pipeline_publish_enabled": True,
|
||||
@ -1874,15 +1864,12 @@ class TestBillingServiceSubscriptionInfoDataType:
|
||||
if "usage_unknown" in result["vector_space"]:
|
||||
assert isinstance(result["vector_space"]["usage_unknown"], bool)
|
||||
|
||||
assert isinstance(result["knowledge_rate_limit"]["limit"], int)
|
||||
|
||||
assert isinstance(result["documents_upload_quota"]["size"], int)
|
||||
assert isinstance(result["documents_upload_quota"]["limit"], int)
|
||||
|
||||
assert isinstance(result["annotation_quota_limit"]["size"], int)
|
||||
assert isinstance(result["annotation_quota_limit"]["limit"], int)
|
||||
|
||||
assert isinstance(result["docs_processing"], str)
|
||||
assert isinstance(result["can_replace_logo"], bool)
|
||||
assert isinstance(result["model_load_balancing_enabled"], bool)
|
||||
assert isinstance(result["knowledge_pipeline_publish_enabled"], bool)
|
||||
|
||||
@ -15,10 +15,8 @@ def test_get_features_exclude_vector_space_sets_vector_space_to_none(config_over
|
||||
"apps": {"size": 2, "limit": 20},
|
||||
"documents_upload_quota": {"size": 3, "limit": 100},
|
||||
"annotation_quota_limit": {"size": 4, "limit": 50},
|
||||
"docs_processing": "standard",
|
||||
"can_replace_logo": True,
|
||||
"model_load_balancing_enabled": True,
|
||||
"knowledge_rate_limit": {"limit": 100},
|
||||
"knowledge_pipeline_publish_enabled": True,
|
||||
}
|
||||
|
||||
@ -26,7 +24,6 @@ def test_get_features_exclude_vector_space_sets_vector_space_to_none(config_over
|
||||
DEPLOYMENT_EDITION=DeploymentEdition.CLOUD,
|
||||
CAN_REPLACE_LOGO=False,
|
||||
MODEL_LB_ENABLED=False,
|
||||
DATASET_OPERATOR_ENABLED=False,
|
||||
EDUCATION_ENABLED=False,
|
||||
)
|
||||
with (
|
||||
|
||||
@ -25,18 +25,18 @@ export const vectorSpace = {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get feature configuration for current tenant
|
||||
* Get current workspace features
|
||||
*
|
||||
* Get feature configuration for current tenant
|
||||
* Get feature availability and limits for the current workspace
|
||||
*/
|
||||
export const get2 = oc
|
||||
.route({
|
||||
description: 'Get feature configuration for current tenant',
|
||||
description: 'Get feature availability and limits for the current workspace',
|
||||
inputStructure: 'detailed',
|
||||
method: 'GET',
|
||||
operationId: 'getFeatures',
|
||||
path: '/features',
|
||||
summary: 'Get feature configuration for current tenant',
|
||||
summary: 'Get current workspace features',
|
||||
tags: ['console'],
|
||||
})
|
||||
.output(zGetFeaturesResponse)
|
||||
|
||||
@ -10,20 +10,15 @@ export type FeatureModel = {
|
||||
apps: LimitationModel
|
||||
billing: BillingModel
|
||||
can_replace_logo: boolean
|
||||
dataset_operator_enabled: boolean
|
||||
docs_processing: string
|
||||
documents_upload_quota: LimitationModel
|
||||
education: EducationModel
|
||||
enable_skill: boolean
|
||||
human_input_email_delivery_enabled: boolean
|
||||
is_allow_transfer_workspace: boolean
|
||||
knowledge_pipeline: KnowledgePipeline
|
||||
knowledge_rate_limit: number
|
||||
members: LimitationModel
|
||||
model_load_balancing_enabled: boolean
|
||||
next_credit_reset_date: number
|
||||
trigger_event: Quota
|
||||
vector_space: LimitationModel | null
|
||||
webapp_copyright_enabled: boolean
|
||||
workspace_members: LicenseLimitationModel
|
||||
}
|
||||
|
||||
@ -84,6 +84,8 @@ export const zBillingModel = z.object({
|
||||
|
||||
/**
|
||||
* FeatureModel
|
||||
*
|
||||
* Effective feature availability and limits for the current workspace.
|
||||
*/
|
||||
export const zFeatureModel = z.object({
|
||||
annotation_quota_limit: zLimitationModel.default({ limit: 10, size: 0 }),
|
||||
@ -95,24 +97,19 @@ export const zFeatureModel = z.object({
|
||||
apps: zLimitationModel.default({ limit: 10, size: 0 }),
|
||||
billing: zBillingModel.default({ subscription: { interval: '', plan: 'sandbox' } }),
|
||||
can_replace_logo: z.boolean().default(false),
|
||||
dataset_operator_enabled: z.boolean().default(false),
|
||||
docs_processing: z.string().default('standard'),
|
||||
documents_upload_quota: zLimitationModel.default({ limit: 50, size: 0 }),
|
||||
education: zEducationModel.default({ activated: false, enabled: false }),
|
||||
enable_skill: z.boolean().default(true),
|
||||
human_input_email_delivery_enabled: z.boolean().default(false),
|
||||
is_allow_transfer_workspace: z.boolean().default(true),
|
||||
knowledge_pipeline: zKnowledgePipeline.default({ publish_enabled: false }),
|
||||
knowledge_rate_limit: z.int().default(10),
|
||||
members: zLimitationModel.default({ limit: 1, size: 0 }),
|
||||
model_load_balancing_enabled: z.boolean().default(false),
|
||||
next_credit_reset_date: z.int().default(0),
|
||||
trigger_event: zQuota.default({
|
||||
limit: 3000,
|
||||
reset_date: 0,
|
||||
usage: 0,
|
||||
}),
|
||||
vector_space: zLimitationModel.nullable().default({ limit: 5, size: 0 }),
|
||||
webapp_copyright_enabled: z.boolean().default(false),
|
||||
workspace_members: zLicenseLimitationModel.default({
|
||||
enabled: false,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user