diff --git a/api/controllers/console/workspace/model_providers.py b/api/controllers/console/workspace/model_providers.py index 0c9db660aa..154a90d2dc 100644 --- a/api/controllers/console/workspace/model_providers.py +++ b/api/controllers/console/workspace/model_providers.py @@ -175,6 +175,22 @@ class ModelProviderCredentialSwitchApi(Resource): return {"result": "success"} +class ModelProviderCredentialCancelApi(Resource): + @setup_required + @login_required + @account_initialization_required + def post(self, provider: str): + if not current_user.is_admin_or_owner: + raise Forbidden() + + service = ModelProviderService() + service.cancel_provider_credential( + tenant_id=current_user.current_tenant_id, + provider=provider, + ) + return {"result": "success"} + + class ModelProviderValidateApi(Resource): @setup_required @login_required @@ -289,6 +305,9 @@ api.add_resource(ModelProviderCredentialApi, "/workspaces/current/model-provider api.add_resource( ModelProviderCredentialSwitchApi, "/workspaces/current/model-providers//credentials/switch" ) +api.add_resource( + ModelProviderCredentialCancelApi, "/workspaces/current/model-providers//credentials/cancel" +) api.add_resource(ModelProviderValidateApi, "/workspaces/current/model-providers//credentials/validate") api.add_resource( diff --git a/api/core/entities/provider_configuration.py b/api/core/entities/provider_configuration.py index 5309e4e638..809832cb18 100644 --- a/api/core/entities/provider_configuration.py +++ b/api/core/entities/provider_configuration.py @@ -33,6 +33,7 @@ from core.plugin.entities.plugin import ModelProviderID from extensions.ext_database import db from libs.datetime_utils import naive_utc_now from models.provider import ( + CredentialStatus, LoadBalancingModelConfig, Provider, ProviderCredential, @@ -43,6 +44,7 @@ from models.provider import ( TenantPreferredModelProvider, ) from services.enterprise.plugin_manager_service import PluginCredentialType +from services.entities.model_provider_entities import CustomConfigurationStatus logger = logging.getLogger(__name__) @@ -188,6 +190,18 @@ class ProviderConfiguration(BaseModel): if current_quota_configuration.is_valid else SystemConfigurationStatus.QUOTA_EXCEEDED ) + + def get_custom_configuration_status(self) -> Optional[CustomConfigurationStatus]: + """ + Get custom configuration status. + :return: + """ + if not self.is_custom_configuration_available(): + return CustomConfigurationStatus.NO_CONFIGURE + elif self.custom_configuration.provider.current_credential_status: + return self.custom_configuration.provider.current_credential_status + + return CustomConfigurationStatus.ACTIVE def is_custom_configuration_available(self) -> bool: """ @@ -643,6 +657,7 @@ class ProviderConfiguration(BaseModel): self.switch_preferred_provider_type(provider_type=ProviderType.SYSTEM, session=session) elif provider_record and provider_record.credential_id == credential_id: provider_record.credential_id = None + provider_record.credential_status = CredentialStatus.REMOVED.value provider_record.updated_at = naive_utc_now() provider_model_credentials_cache = ProviderCredentialsCache( @@ -681,6 +696,34 @@ class ProviderConfiguration(BaseModel): try: provider_record.credential_id = credential_record.id + provider_record.credential_status = CredentialStatus.ACTIVE.value + provider_record.updated_at = naive_utc_now() + session.commit() + + provider_model_credentials_cache = ProviderCredentialsCache( + tenant_id=self.tenant_id, + identity_id=provider_record.id, + cache_type=ProviderCredentialsCacheType.PROVIDER, + ) + provider_model_credentials_cache.delete() + self.switch_preferred_provider_type(ProviderType.CUSTOM, session=session) + except Exception: + session.rollback() + raise + + def cancel_provider_credential(self): + """ + Cancel select the active provider credential. + :return: + """ + with Session(db.engine) as session: + provider_record = self._get_provider_record(session) + if not provider_record: + raise ValueError("Provider record not found.") + + try: + provider_record.credential_id = None + provider_record.credential_status = CredentialStatus.CANCEL.value provider_record.updated_at = naive_utc_now() session.commit() diff --git a/api/core/entities/provider_entities.py b/api/core/entities/provider_entities.py index 52acbc1eef..3ba964630f 100644 --- a/api/core/entities/provider_entities.py +++ b/api/core/entities/provider_entities.py @@ -11,6 +11,7 @@ from core.entities.parameter_entities import ( ) from core.model_runtime.entities.model_entities import ModelType from core.tools.entities.common_entities import I18nObject +from models.provider import CredentialStatus class ProviderQuotaType(Enum): @@ -97,6 +98,7 @@ class CustomProviderConfiguration(BaseModel): credentials: dict current_credential_id: Optional[str] = None current_credential_name: Optional[str] = None + current_credential_status: Optional[CredentialStatus] = None available_credentials: list[CredentialConfiguration] = [] diff --git a/api/core/provider_manager.py b/api/core/provider_manager.py index e4e8b09a04..49ea0eb90b 100644 --- a/api/core/provider_manager.py +++ b/api/core/provider_manager.py @@ -711,6 +711,7 @@ class ProviderManager: credentials=provider_credentials, current_credential_name=custom_provider_record.credential_name, current_credential_id=custom_provider_record.credential_id, + current_credential_status=custom_provider_record.credential_status, available_credentials=self.get_provider_available_credentials( tenant_id, custom_provider_record.provider_name ), diff --git a/api/migrations/versions/2025_09_11_1537-cf7c38a32b2d_add_credential_status_for_provider_table.py b/api/migrations/versions/2025_09_11_1537-cf7c38a32b2d_add_credential_status_for_provider_table.py new file mode 100644 index 0000000000..5231028e6f --- /dev/null +++ b/api/migrations/versions/2025_09_11_1537-cf7c38a32b2d_add_credential_status_for_provider_table.py @@ -0,0 +1,33 @@ +"""Add credential status for provider table + +Revision ID: cf7c38a32b2d +Revises: c20211f18133 +Create Date: 2025-09-11 15:37:17.771298 + +""" +from alembic import op +import models as models +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'cf7c38a32b2d' +down_revision = 'c20211f18133' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('providers', schema=None) as batch_op: + batch_op.add_column(sa.Column('credential_status', sa.String(length=20), server_default=sa.text("'active'::character varying"), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('providers', schema=None) as batch_op: + batch_op.drop_column('credential_status') + + # ### end Alembic commands ### diff --git a/api/models/provider.py b/api/models/provider.py index 9a344ea56d..b897a3bd4b 100644 --- a/api/models/provider.py +++ b/api/models/provider.py @@ -1,5 +1,5 @@ from datetime import datetime -from enum import Enum +from enum import Enum, StrEnum from functools import cached_property from typing import Optional @@ -40,6 +40,11 @@ class ProviderQuotaType(Enum): if member.value == value: return member raise ValueError(f"No matching enum found for value '{value}'") + +class CredentialStatus(StrEnum): + ACTIVE = "active" + CANCELED = "canceled" + REMOVED = "removed" class Provider(Base): @@ -65,6 +70,9 @@ class Provider(Base): is_valid: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=text("false")) last_used: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) credential_id: Mapped[Optional[str]] = mapped_column(StringUUID, nullable=True) + credential_status: Mapped[Optional[str]] = mapped_column( + String(20), nullable=True, server_default=text("'active'::character varying") + ) quota_type: Mapped[Optional[str]] = mapped_column( String(40), nullable=True, server_default=text("''::character varying") diff --git a/api/services/entities/model_provider_entities.py b/api/services/entities/model_provider_entities.py index 647052d739..56f07f5bc4 100644 --- a/api/services/entities/model_provider_entities.py +++ b/api/services/entities/model_provider_entities.py @@ -34,6 +34,8 @@ class CustomConfigurationStatus(Enum): ACTIVE = "active" NO_CONFIGURE = "no-configure" + CANCELED = "canceled" + REMOVED = "removed" class CustomConfigurationResponse(BaseModel): diff --git a/api/services/model_provider_service.py b/api/services/model_provider_service.py index 510b1f1fe6..b859db2bb2 100644 --- a/api/services/model_provider_service.py +++ b/api/services/model_provider_service.py @@ -89,9 +89,7 @@ class ModelProviderService: model_credential_schema=provider_configuration.provider.model_credential_schema, preferred_provider_type=provider_configuration.preferred_provider_type, custom_configuration=CustomConfigurationResponse( - status=CustomConfigurationStatus.ACTIVE - if provider_configuration.is_custom_configuration_available() - else CustomConfigurationStatus.NO_CONFIGURE, + status=provider_configuration.get_custom_configuration_status(), current_credential_id=getattr(provider_config, "current_credential_id", None), current_credential_name=getattr(provider_config, "current_credential_name", None), available_credentials=getattr(provider_config, "available_credentials", []), @@ -214,6 +212,16 @@ class ModelProviderService: provider_configuration = self._get_provider_configuration(tenant_id, provider) provider_configuration.switch_active_provider_credential(credential_id=credential_id) + def cancel_provider_credential(self, tenant_id: str, provider: str): + """ + :param tenant_id: workspace id + :param provider: provider name + :param credential_id: credential id + :return: + """ + provider_configuration = self._get_provider_configuration(tenant_id, provider) + provider_configuration.cancel_provider_credential() + def get_model_credential( self, tenant_id: str, provider: str, model_type: str, model: str, credential_id: str | None ) -> Optional[dict]: