Compare commits

...

5 Commits

Author SHA1 Message Date
hjlarry
cb97a93508 fix status bug 2025-09-12 17:36:40 +08:00
zxhlyh
407d66c62d model provider add deselect 2025-09-12 14:24:55 +08:00
hjlarry
9d75d3d04c fix bug 2025-09-12 10:43:15 +08:00
hjlarry
02852ee543 fix bug 2025-09-12 09:33:44 +08:00
hjlarry
32b2d19622 add cancel provider credential 2025-09-11 17:24:19 +08:00
18 changed files with 180 additions and 21 deletions

View File

@@ -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/<path:provider>/credentials/switch"
)
api.add_resource(
ModelProviderCredentialCancelApi, "/workspaces/current/model-providers/<path:provider>/credentials/cancel"
)
api.add_resource(ModelProviderValidateApi, "/workspaces/current/model-providers/<path:provider>/credentials/validate")
api.add_resource(

View File

@@ -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__)
@@ -189,6 +191,22 @@ class ProviderConfiguration(BaseModel):
else SystemConfigurationStatus.QUOTA_EXCEEDED
)
def get_custom_configuration_status(self) -> Optional[CustomConfigurationStatus]:
"""
Get custom configuration status.
:return:
"""
if self.is_custom_configuration_available():
return CustomConfigurationStatus.ACTIVE
provider = self.custom_configuration.provider
if provider and hasattr(provider, "current_credential_status"):
status = provider.current_credential_status
if status:
return status
return CustomConfigurationStatus.NO_CONFIGURE
def is_custom_configuration_available(self) -> bool:
"""
Check custom configuration available.
@@ -643,6 +661,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 +700,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.CANCELED.value
provider_record.updated_at = naive_utc_now()
session.commit()

View File

@@ -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] = []

View File

@@ -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
),

View File

@@ -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 ###

View File

@@ -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
@@ -42,6 +42,12 @@ class ProviderQuotaType(Enum):
raise ValueError(f"No matching enum found for value '{value}'")
class CredentialStatus(StrEnum):
ACTIVE = "active"
CANCELED = "canceled"
REMOVED = "removed"
class Provider(Base):
"""
Provider model representing the API providers and their configurations.
@@ -65,6 +71,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")

View File

@@ -34,6 +34,8 @@ class CustomConfigurationStatus(Enum):
ACTIVE = "active"
NO_CONFIGURE = "no-configure"
CANCELED = "canceled"
REMOVED = "removed"
class CustomConfigurationResponse(BaseModel):

View File

@@ -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]:

View File

@@ -107,6 +107,8 @@ export const MODEL_STATUS_TEXT: { [k: string]: TypeWithI18N } = {
export enum CustomConfigurationStatusEnum {
active = 'active',
noConfigure = 'no-configure',
canceled = 'canceled',
removed = 'removed',
}
export type FormShowOnObject = {

View File

@@ -46,6 +46,7 @@ const ModelProviderPage = ({ searchText }: Props) => {
providers.forEach((provider) => {
if (
provider.custom_configuration.status === CustomConfigurationStatusEnum.active
|| provider.custom_configuration.status === CustomConfigurationStatusEnum.removed
|| (
provider.system_configuration.enabled === true
&& provider.system_configuration.quota_configurations.find(item => item.quota_type === provider.system_configuration.current_quota_type)

View File

@@ -64,6 +64,7 @@ type AuthorizedProps = {
showModelTitle?: boolean
disableDeleteButShowAction?: boolean
disableDeleteTip?: string
showDeselect?: boolean
}
const Authorized = ({
provider,
@@ -88,6 +89,7 @@ const Authorized = ({
showModelTitle,
disableDeleteButShowAction,
disableDeleteTip,
showDeselect,
}: AuthorizedProps) => {
const { t } = useTranslation()
const [isLocalOpen, setIsLocalOpen] = useState(false)
@@ -112,6 +114,7 @@ const Authorized = ({
handleConfirmDelete,
deleteCredentialId,
handleOpenModal,
handleDeselect,
} = useAuth(
provider,
configurationMethod,
@@ -171,8 +174,15 @@ const Authorized = ({
)}>
{
popupTitle && (
<div className='system-xs-medium px-3 pb-0.5 pt-[10px] text-text-tertiary'>
<div className='system-xs-medium flex items-center justify-between px-3 pb-0.5 pt-[10px] text-text-tertiary'>
{popupTitle}
{
showDeselect && (
<div onClick={() => handleDeselect()} className='cursor-pointer'>
{t('common.modelProvider.auth.deselect')}
</div>
)
}
</div>
)
}

View File

@@ -29,10 +29,11 @@ const ConfigProvider = ({
const { t } = useTranslation()
const {
hasCredential,
authorized,
current_credential_id,
current_credential_name,
available_credentials,
authorized,
unAuthorized,
} = useCredentialStatus(provider)
const notAllowCustomCredential = provider.allow_custom_token === false
@@ -41,11 +42,11 @@ const ConfigProvider = ({
<Button
className='grow'
size='small'
variant={!authorized ? 'secondary-accent' : 'secondary'}
variant={unAuthorized ? 'secondary-accent' : 'secondary'}
>
<RiEqualizer2Line className='mr-1 h-3.5 w-3.5' />
{hasCredential && t('common.operation.config')}
{!hasCredential && t('common.operation.setup')}
{!unAuthorized && t('common.operation.config')}
{unAuthorized && t('common.operation.setup')}
</Button>
)
if (notAllowCustomCredential && !hasCredential) {
@@ -59,7 +60,7 @@ const ConfigProvider = ({
)
}
return Item
}, [authorized, hasCredential, notAllowCustomCredential, t])
}, [hasCredential, notAllowCustomCredential, t])
return (
<Authorized
@@ -68,7 +69,6 @@ const ConfigProvider = ({
currentCustomConfigurationModelFixedFields={currentCustomConfigurationModelFixedFields}
items={[
{
title: t('common.modelProvider.auth.apiKeys'),
credentials: available_credentials ?? [],
selectedCredential: {
credential_id: current_credential_id ?? '',
@@ -77,9 +77,10 @@ const ConfigProvider = ({
},
]}
showItemSelectedIcon
showModelTitle
renderTrigger={renderTrigger}
triggerOnlyOpenModal={!hasCredential && !notAllowCustomCredential}
showDeselect={authorized}
popupTitle={t('common.modelProvider.auth.apiKeys')}
/>
)
}

View File

@@ -18,7 +18,10 @@ import {
useModelModalHandler,
useRefreshModel,
} from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useDeleteModel } from '@/service/use-models'
import {
useDeleteModel,
useDeselectModelCredential,
} from '@/service/use-models'
export const useAuth = (
provider: ModelProvider,
@@ -46,6 +49,7 @@ export const useAuth = (
getAddCredentialService,
} = useAuthService(provider.provider)
const { mutateAsync: deleteModelService } = useDeleteModel(provider.provider)
const { mutateAsync: deselectModelCredentialService } = useDeselectModelCredential(provider.provider)
const handleOpenModelModal = useModelModalHandler()
const { handleRefreshModel } = useRefreshModel()
const pendingOperationCredentialId = useRef<string | null>(null)
@@ -177,6 +181,11 @@ export const useAuth = (
mode,
])
const handleDeselect = useCallback(async () => {
await deselectModelCredentialService()
handleRefreshModel(provider, configurationMethod, undefined)
}, [deselectModelCredentialService, handleRefreshModel, provider, configurationMethod])
return {
pendingOperationCredentialId,
pendingOperationModel,
@@ -189,5 +198,6 @@ export const useAuth = (
deleteModel,
handleSaveCredential,
handleOpenModal,
handleDeselect,
}
}

View File

@@ -2,16 +2,19 @@ import { useMemo } from 'react'
import type {
ModelProvider,
} from '../../declarations'
import { CustomConfigurationStatusEnum } from '../../declarations'
export const useCredentialStatus = (provider: ModelProvider) => {
const {
current_credential_id,
current_credential_name,
available_credentials,
status: customConfigurationStatus,
} = provider.custom_configuration
const hasCredential = !!available_credentials?.length
const authorized = current_credential_id && current_credential_name
const authRemoved = hasCredential && !current_credential_id && !current_credential_name
const authorized = customConfigurationStatus === CustomConfigurationStatusEnum.active
const authRemoved = customConfigurationStatus === CustomConfigurationStatusEnum.removed
const unAuthorized = customConfigurationStatus === CustomConfigurationStatusEnum.noConfigure || customConfigurationStatus === CustomConfigurationStatusEnum.canceled
const currentCredential = available_credentials?.find(credential => credential.credential_id === current_credential_id)
return useMemo(() => ({
@@ -21,6 +24,8 @@ export const useCredentialStatus = (provider: ModelProvider) => {
current_credential_id,
current_credential_name,
available_credentials,
customConfigurationStatus,
notAllowedToUse: currentCredential?.not_allowed_to_use,
unAuthorized,
}), [hasCredential, authorized, authRemoved, current_credential_id, current_credential_name, available_credentials])
}

View File

@@ -45,6 +45,7 @@ const CredentialPanel = ({
authRemoved,
current_credential_name,
notAllowedToUse,
unAuthorized,
} = useCredentialStatus(provider)
const handleChangePriority = async (key: PreferredProviderTypeEnum) => {
@@ -70,23 +71,23 @@ const CredentialPanel = ({
}
}
const credentialLabel = useMemo(() => {
if (!hasCredential)
if (unAuthorized)
return t('common.modelProvider.auth.unAuthorized')
if (authorized)
return current_credential_name
if (authRemoved)
return t('common.modelProvider.auth.authRemoved')
if (authorized)
return current_credential_name
return ''
}, [authorized, authRemoved, current_credential_name, hasCredential])
}, [authorized, authRemoved, current_credential_name, unAuthorized])
const color = useMemo(() => {
if (authRemoved || !hasCredential)
if (authRemoved || !hasCredential || unAuthorized)
return 'red'
if (notAllowedToUse)
return 'gray'
return 'green'
}, [authRemoved, notAllowedToUse, hasCredential])
}, [authRemoved, notAllowedToUse, hasCredential, unAuthorized])
return (
<>

View File

@@ -523,6 +523,7 @@ const translation = {
removeModel: 'Remove Model',
selectModelCredential: 'Select a model credential',
customModelCredentialsDeleteTip: 'Credential is in use and cannot be deleted',
deselect: 'Deselect',
},
},
dataSource: {

View File

@@ -517,6 +517,7 @@ const translation = {
removeModel: '移除模型',
selectModelCredential: '选择模型凭据',
customModelCredentialsDeleteTip: '模型凭据正在使用中,无法删除',
deselect: '取消选择',
},
},
dataSource: {

View File

@@ -153,3 +153,9 @@ export const useUpdateModelLoadBalancingConfig = (provider: string) => {
}),
})
}
export const useDeselectModelCredential = (provider: string) => {
return useMutation({
mutationFn: () => post<{ result: string }>(`/workspaces/current/model-providers/${provider}/credentials/cancel`),
})
}