model auth

This commit is contained in:
zxhlyh 2025-08-13 17:54:39 +08:00
parent e69797d738
commit 61be6f5d2c
14 changed files with 574 additions and 153 deletions

View File

@ -1,34 +1,52 @@
import { useCallback } from 'react' import {
isValidElement,
useCallback,
} from 'react'
import type { ReactNode } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { FormSchema } from '../types' import type { FormSchema } from '../types'
import { useRenderI18nObject } from '@/hooks/use-i18n'
export const useGetValidators = () => { export const useGetValidators = () => {
const { t } = useTranslation() const { t } = useTranslation()
const renderI18nObject = useRenderI18nObject()
const getLabel = useCallback((label: string | Record<string, string> | ReactNode) => {
if (isValidElement(label))
return ''
if (typeof label === 'string')
return label
if (typeof label === 'object' && label !== null)
return renderI18nObject(label as Record<string, string>)
}, [])
const getValidators = useCallback((formSchema: FormSchema) => { const getValidators = useCallback((formSchema: FormSchema) => {
const { const {
name, name,
validators, validators,
required, required,
label,
} = formSchema } = formSchema
let mergedValidators = validators let mergedValidators = validators
const memorizedLabel = getLabel(label)
if (required && !validators) { if (required && !validators) {
mergedValidators = { mergedValidators = {
onMount: ({ value }: any) => { onMount: ({ value }: any) => {
if (!value) if (!value)
return t('common.errorMsg.fieldRequired', { field: name }) return t('common.errorMsg.fieldRequired', { field: memorizedLabel || name })
}, },
onChange: ({ value }: any) => { onChange: ({ value }: any) => {
if (!value) if (!value)
return t('common.errorMsg.fieldRequired', { field: name }) return t('common.errorMsg.fieldRequired', { field: memorizedLabel || name })
}, },
onBlur: ({ value }: any) => { onBlur: ({ value }: any) => {
if (!value) if (!value)
return t('common.errorMsg.fieldRequired', { field: name }) return t('common.errorMsg.fieldRequired', { field: memorizedLabel })
}, },
} }
} }
return mergedValidators return mergedValidators
}, [t]) }, [t, getLabel])
return { return {
getValidators, getValidators,

View File

@ -181,6 +181,11 @@ export type QuotaConfiguration = {
is_valid: boolean is_valid: boolean
} }
export type Credential = {
credential_id: string
credential_name: string
}
export type ModelProvider = { export type ModelProvider = {
provider: string provider: string
label: TypeWithI18N label: TypeWithI18N
@ -207,6 +212,9 @@ export type ModelProvider = {
preferred_provider_type: PreferredProviderTypeEnum preferred_provider_type: PreferredProviderTypeEnum
custom_configuration: { custom_configuration: {
status: CustomConfigurationStatusEnum status: CustomConfigurationStatusEnum
current_credential_id?: string
current_credential_name?: string
available_credentials?: Credential[]
} }
system_configuration: { system_configuration: {
enabled: boolean enabled: boolean

View File

@ -7,6 +7,7 @@ import {
import useSWR, { useSWRConfig } from 'swr' import useSWR, { useSWRConfig } from 'swr'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import type { import type {
Credential,
CustomConfigurationModelFixedFields, CustomConfigurationModelFixedFields,
DefaultModel, DefaultModel,
DefaultModelResponse, DefaultModelResponse,
@ -77,16 +78,17 @@ export const useProviderCredentialsAndLoadBalancing = (
configurationMethod: ConfigurationMethodEnum, configurationMethod: ConfigurationMethodEnum,
configured?: boolean, configured?: boolean,
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
credentialId?: string,
) => { ) => {
const { data: predefinedFormSchemasValue, mutate: mutatePredefined } = useSWR( const { data: predefinedFormSchemasValue, mutate: mutatePredefined } = useSWR(
(configurationMethod === ConfigurationMethodEnum.predefinedModel && configured) (configurationMethod === ConfigurationMethodEnum.predefinedModel && configured && credentialId)
? `/workspaces/current/model-providers/${provider}/credentials` ? `/workspaces/current/model-providers/${provider}/credentials${credentialId ? `?credential_id=${credentialId}` : ''}`
: null, : null,
fetchModelProviderCredentials, fetchModelProviderCredentials,
) )
const { data: customFormSchemasValue, mutate: mutateCustomized } = useSWR( const { data: customFormSchemasValue, mutate: mutateCustomized } = useSWR(
(configurationMethod === ConfigurationMethodEnum.customizableModel && currentCustomConfigurationModelFixedFields) (configurationMethod === ConfigurationMethodEnum.customizableModel && currentCustomConfigurationModelFixedFields && credentialId)
? `/workspaces/current/model-providers/${provider}/models/credentials?model=${currentCustomConfigurationModelFixedFields?.__model_name}&model_type=${currentCustomConfigurationModelFixedFields?.__model_type}` ? `/workspaces/current/model-providers/${provider}/models/credentials?model=${currentCustomConfigurationModelFixedFields?.__model_name}&model_type=${currentCustomConfigurationModelFixedFields?.__model_type}${credentialId ? `&credential_id=${credentialId}` : ''}`
: null, : null,
fetchModelProviderCredentials, fetchModelProviderCredentials,
) )
@ -102,6 +104,7 @@ export const useProviderCredentialsAndLoadBalancing = (
: undefined : undefined
}, [ }, [
configurationMethod, configurationMethod,
credentialId,
currentCustomConfigurationModelFixedFields, currentCustomConfigurationModelFixedFields,
customFormSchemasValue?.credentials, customFormSchemasValue?.credentials,
predefinedFormSchemasValue?.credentials, predefinedFormSchemasValue?.credentials,
@ -313,40 +316,53 @@ export const useMarketplaceAllPlugins = (providers: ModelProvider[], searchText:
} }
} }
export const useModelModalHandler = () => { export const useRefreshModel = () => {
const setShowModelModal = useModalContextSelector(state => state.setShowModelModal) const { eventEmitter } = useEventEmitterContextContext()
const updateModelProviders = useUpdateModelProviders() const updateModelProviders = useUpdateModelProviders()
const updateModelList = useUpdateModelList() const updateModelList = useUpdateModelList()
const { eventEmitter } = useEventEmitterContextContext() const handleRefreshModel = useCallback((provider: ModelProvider, configurationMethod: ConfigurationMethodEnum, CustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => {
updateModelProviders()
provider.supported_model_types.forEach((type) => {
updateModelList(type)
})
if (configurationMethod === ConfigurationMethodEnum.customizableModel
&& provider.custom_configuration.status === CustomConfigurationStatusEnum.active) {
eventEmitter?.emit({
type: UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST,
payload: provider.provider,
} as any)
if (CustomConfigurationModelFixedFields?.__model_type)
updateModelList(CustomConfigurationModelFixedFields.__model_type)
}
}, [eventEmitter, updateModelList, updateModelProviders])
return {
handleRefreshModel,
}
}
export const useModelModalHandler = () => {
const setShowModelModal = useModalContextSelector(state => state.setShowModelModal)
const { handleRefreshModel } = useRefreshModel()
return ( return (
provider: ModelProvider, provider: ModelProvider,
configurationMethod: ConfigurationMethodEnum, configurationMethod: ConfigurationMethodEnum,
CustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields, CustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields,
credential?: Credential,
) => { ) => {
setShowModelModal({ setShowModelModal({
payload: { payload: {
currentProvider: provider, currentProvider: provider,
currentConfigurationMethod: configurationMethod, currentConfigurationMethod: configurationMethod,
currentCustomConfigurationModelFixedFields: CustomConfigurationModelFixedFields, currentCustomConfigurationModelFixedFields: CustomConfigurationModelFixedFields,
credential,
}, },
onSaveCallback: () => { onSaveCallback: () => {
updateModelProviders() handleRefreshModel(provider, configurationMethod, CustomConfigurationModelFixedFields)
provider.supported_model_types.forEach((type) => {
updateModelList(type)
})
if (configurationMethod === ConfigurationMethodEnum.customizableModel
&& provider.custom_configuration.status === CustomConfigurationStatusEnum.active) {
eventEmitter?.emit({
type: UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST,
payload: provider.provider,
} as any)
if (CustomConfigurationModelFixedFields?.__model_type)
updateModelList(CustomConfigurationModelFixedFields.__model_type)
}
}, },
}) })
} }

View File

@ -9,6 +9,7 @@ import SystemModelSelector from './system-model-selector'
import ProviderAddedCard from './provider-added-card' import ProviderAddedCard from './provider-added-card'
import type { import type {
ConfigurationMethodEnum, ConfigurationMethodEnum,
Credential,
CustomConfigurationModelFixedFields, CustomConfigurationModelFixedFields,
ModelProvider, ModelProvider,
} from './declarations' } from './declarations'
@ -126,7 +127,7 @@ const ModelProviderPage = ({ searchText }: Props) => {
<ProviderAddedCard <ProviderAddedCard
key={provider.provider} key={provider.provider}
provider={provider} provider={provider}
onOpenModal={(configurationMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => handleOpenModal(provider, configurationMethod, currentCustomConfigurationModelFixedFields)} onOpenModal={(configurationMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields, credential?: Credential) => handleOpenModal(provider, configurationMethod, currentCustomConfigurationModelFixedFields, credential)}
/> />
))} ))}
</div> </div>
@ -140,7 +141,7 @@ const ModelProviderPage = ({ searchText }: Props) => {
notConfigured notConfigured
key={provider.provider} key={provider.provider}
provider={provider} provider={provider}
onOpenModal={(configurationMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => handleOpenModal(provider, configurationMethod, currentCustomConfigurationModelFixedFields)} onOpenModal={(configurationMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields, credential?: Credential) => handleOpenModal(provider, configurationMethod, currentCustomConfigurationModelFixedFields, credential)}
/> />
))} ))}
</div> </div>

View File

@ -1,83 +0,0 @@
import type { FC } from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiEqualizer2Line,
RiMoreFill,
} from '@remixicon/react'
import type { ModelProvider } from '../declarations'
import {
CustomConfigurationStatusEnum,
} from '../declarations'
import Indicator from '@/app/components/header/indicator'
import Button from '@/app/components/base/button'
import {
AuthCategory,
Authorized,
} from '@/app/components/plugins/plugin-auth'
type AuthPanelProps = {
provider: ModelProvider
onSetup: () => void
}
const AuthPanel: FC<AuthPanelProps> = ({
provider,
onSetup,
}) => {
const authorized = false
const { t } = useTranslation()
const customConfig = provider.custom_configuration
const isCustomConfigured = customConfig.status === CustomConfigurationStatusEnum.active
const renderTrigger = useCallback(() => {
return (
<Button
className='h-6 w-6'
size='small'
>
<RiMoreFill className='h-4 w-4' />
</Button>
)
}, [])
return (
<>
{
provider.provider_credential_schema && (
<div className='relative ml-1 w-[112px] shrink-0 rounded-lg border-[0.5px] border-components-panel-border bg-white/[0.18] p-1'>
<div className='system-xs-medium-uppercase mb-1 flex h-5 items-center justify-between pl-2 pr-[7px] pt-1 text-text-tertiary'>
API-KEY
<Indicator color={isCustomConfigured ? 'green' : 'red'} />
</div>
<div className='flex items-center gap-0.5'>
<Button
className='mr-0.5 grow'
size='small'
onClick={onSetup}
>
<RiEqualizer2Line className='mr-1 h-3.5 w-3.5' />
{
authorized ? t('common.operation.config') : t('common.operation.setup')
}
</Button>
{
authorized && (
<Authorized
pluginPayload={{
category: AuthCategory.model,
provider: provider.provider,
}}
credentials={[]}
renderTrigger={renderTrigger}
/>
)
}
</div>
</div>
)
}
</>
)
}
export default AuthPanel

View File

@ -0,0 +1,203 @@
import {
memo,
useCallback,
useRef,
useState,
} from 'react'
import {
RiEqualizer2Line,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import type {
PortalToFollowElemOptions,
} from '@/app/components/base/portal-to-follow-elem'
import Button from '@/app/components/base/button'
import cn from '@/utils/classnames'
import Confirm from '@/app/components/base/confirm'
import Item from './item'
import { useToastContext } from '@/app/components/base/toast'
import type { Credential } from '../../declarations'
import { useDeleteModelCredential } from '@/service/use-models'
type AuthorizedProps = {
provider: string
credentials: Credential[]
disabled?: boolean
renderTrigger?: (open?: boolean) => React.ReactNode
isOpen?: boolean
onOpenChange?: (open: boolean) => void
offset?: PortalToFollowElemOptions['offset']
placement?: PortalToFollowElemOptions['placement']
triggerPopupSameWidth?: boolean
popupClassName?: string
onItemClick?: (id: string) => void
showItemSelectedIcon?: boolean
selectedCredentialId?: string
onUpdate?: () => void
onSetup: (credential?: Credential) => void
}
const Authorized = ({
provider,
credentials,
disabled,
renderTrigger,
isOpen,
onOpenChange,
offset = 8,
placement = 'bottom-end',
triggerPopupSameWidth = false,
popupClassName,
onItemClick,
showItemSelectedIcon,
selectedCredentialId,
onUpdate,
onSetup,
}: AuthorizedProps) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const [isLocalOpen, setIsLocalOpen] = useState(false)
const mergedIsOpen = isOpen ?? isLocalOpen
const setMergedIsOpen = useCallback((open: boolean) => {
if (onOpenChange)
onOpenChange(open)
setIsLocalOpen(open)
}, [onOpenChange])
const pendingOperationCredentialId = useRef<string | null>(null)
const [deleteCredentialId, setDeleteCredentialId] = useState<string | null>(null)
const openConfirm = useCallback((credentialId?: string) => {
if (credentialId)
pendingOperationCredentialId.current = credentialId
setDeleteCredentialId(pendingOperationCredentialId.current)
}, [])
const closeConfirm = useCallback(() => {
setDeleteCredentialId(null)
pendingOperationCredentialId.current = null
}, [])
const [doingAction, setDoingAction] = useState(false)
const doingActionRef = useRef(doingAction)
const handleSetDoingAction = useCallback((doing: boolean) => {
doingActionRef.current = doing
setDoingAction(doing)
}, [])
const { mutateAsync: deleteModelCredential } = useDeleteModelCredential(provider)
const handleConfirm = useCallback(async () => {
if (doingActionRef.current)
return
if (!pendingOperationCredentialId.current) {
setDeleteCredentialId(null)
return
}
try {
handleSetDoingAction(true)
await deleteModelCredential(pendingOperationCredentialId.current)
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
onUpdate?.()
setDeleteCredentialId(null)
pendingOperationCredentialId.current = null
}
finally {
handleSetDoingAction(false)
}
}, [onUpdate, notify, t, handleSetDoingAction])
const handleEdit = useCallback((credential: Credential) => {
onSetup(credential)
}, [onSetup])
return (
<>
<PortalToFollowElem
open={mergedIsOpen}
onOpenChange={setMergedIsOpen}
placement={placement}
offset={offset}
triggerPopupSameWidth={triggerPopupSameWidth}
>
<PortalToFollowElemTrigger
onClick={() => setMergedIsOpen(!mergedIsOpen)}
asChild
>
{
renderTrigger
? renderTrigger(mergedIsOpen)
: (
<Button
className='grow'
size='small'
>
<RiEqualizer2Line className='mr-1 h-3.5 w-3.5' />
{t('common.operation.config')}
</Button>
)
}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[100]'>
<div className={cn(
'max-h-[360px] w-[360px] overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg',
popupClassName,
)}>
<div className='py-1'>
{
!!credentials.length && (
<div className='p-1'>
<div className={cn(
'system-xs-medium px-3 pb-0.5 pt-1 text-text-tertiary',
showItemSelectedIcon && 'pl-7',
)}>
API Keys
</div>
{
credentials.map(credential => (
<Item
key={credential.credential_id}
credential={credential}
disabled={disabled}
onDelete={openConfirm}
onEdit={handleEdit}
onItemClick={onItemClick}
showSelectedIcon={showItemSelectedIcon}
selectedCredentialId={selectedCredentialId}
/>
))
}
</div>
)
}
</div>
<div className='h-[1px] bg-divider-subtle'></div>
<div className='p-2'>
<Button
onClick={() => onSetup()}
className='w-full'
>
add api key
</Button>
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
{
deleteCredentialId && (
<Confirm
isShow
title={t('datasetDocuments.list.delete.title')}
isDisabled={doingAction}
onCancel={closeConfirm}
onConfirm={handleConfirm}
/>
)
}
</>
)
}
export default memo(Authorized)

View File

@ -0,0 +1,118 @@
import {
memo,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import {
RiCheckLine,
RiDeleteBinLine,
RiEqualizer2Line,
} from '@remixicon/react'
import Indicator from '@/app/components/header/indicator'
import ActionButton from '@/app/components/base/action-button'
import Tooltip from '@/app/components/base/tooltip'
import cn from '@/utils/classnames'
import type { Credential } from '../../declarations'
type ItemProps = {
credential: Credential
disabled?: boolean
onDelete?: (id: string) => void
onEdit?: (credential: Credential) => void
onSetDefault?: (id: string) => void
disableRename?: boolean
disableEdit?: boolean
disableDelete?: boolean
disableSetDefault?: boolean
onItemClick?: (id: string) => void
showSelectedIcon?: boolean
selectedCredentialId?: string
}
const Item = ({
credential,
disabled,
onDelete,
onEdit,
disableRename,
disableEdit,
disableDelete,
disableSetDefault,
onItemClick,
showSelectedIcon,
selectedCredentialId,
}: ItemProps) => {
const { t } = useTranslation()
const showAction = useMemo(() => {
return !(disableRename && disableEdit && disableDelete && disableSetDefault)
}, [disableRename, disableEdit, disableDelete, disableSetDefault])
return (
<div
key={credential.credential_id}
className={cn(
'group flex h-8 items-center rounded-lg p-1 hover:bg-state-base-hover',
)}
onClick={() => onItemClick?.(credential.credential_id)}
>
<div className='flex w-0 grow items-center space-x-1.5'>
{
showSelectedIcon && (
<div className='h-4 w-4'>
{
selectedCredentialId === credential.credential_id && (
<RiCheckLine className='h-4 w-4 text-text-accent' />
)
}
</div>
)
}
<Indicator className='ml-2 mr-1.5 shrink-0' />
<div
className='system-md-regular truncate text-text-secondary'
title={credential.credential_name}
>
{credential.credential_name}
</div>
</div>
{
showAction && (
<div className='ml-2 hidden shrink-0 items-center group-hover:flex'>
{
!disableEdit && (
<Tooltip popupContent={t('common.operation.edit')}>
<ActionButton
disabled={disabled}
onClick={(e) => {
e.stopPropagation()
onEdit?.(credential)
}}
>
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />
</ActionButton>
</Tooltip>
)
}
{
!disableDelete && (
<Tooltip popupContent={t('common.operation.delete')}>
<ActionButton
className='hover:bg-transparent'
disabled={disabled}
onClick={(e) => {
e.stopPropagation()
onDelete?.(credential.credential_id)
}}
>
<RiDeleteBinLine className='h-4 w-4 text-text-tertiary hover:text-text-destructive' />
</ActionButton>
</Tooltip>
)
}
</div>
)
}
</div>
)
}
export default memo(Item)

View File

@ -0,0 +1,56 @@
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import type {
ModelLoadBalancingConfig,
ModelProvider,
} from '../declarations'
import {
genModelNameFormSchema,
genModelTypeFormSchema,
} from '../utils'
import { FormTypeEnum } from '@/app/components/base/form/types'
export const useModelFormSchemas = (
provider: ModelProvider,
providerFormSchemaPredefined: boolean,
draftConfig?: ModelLoadBalancingConfig,
) => {
const { t } = useTranslation()
const {
provider_credential_schema,
supported_model_types,
model_credential_schema,
} = provider
const formSchemas = useMemo(() => {
return providerFormSchemaPredefined
? provider_credential_schema.credential_form_schemas
: [
genModelTypeFormSchema(supported_model_types),
genModelNameFormSchema(model_credential_schema?.model),
...(draftConfig?.enabled ? [] : model_credential_schema.credential_form_schemas),
]
}, [
providerFormSchemaPredefined,
provider_credential_schema?.credential_form_schemas,
supported_model_types,
model_credential_schema?.credential_form_schemas,
model_credential_schema?.model,
draftConfig?.enabled,
])
const formSchemasWithAuthorizationName = useMemo(() => {
return [
{
type: FormTypeEnum.textInput,
variable: '__authorization_name__',
label: t('plugin.auth.authorizationName'),
required: true,
},
...formSchemas,
]
}, [formSchemas, t])
return {
formSchemas: formSchemasWithAuthorizationName,
}
}

View File

@ -45,11 +45,14 @@ import type {
FormRefObject, FormRefObject,
FormSchema, FormSchema,
} from '@/app/components/base/form/types' } from '@/app/components/base/form/types'
import { useModelFormSchemas } from '../model-auth/hooks'
import type { Credential } from '../declarations'
type ModelModalProps = { type ModelModalProps = {
provider: ModelProvider provider: ModelProvider
configurateMethod: ConfigurationMethodEnum configurateMethod: ConfigurationMethodEnum
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
credential?: Credential
onCancel: () => void onCancel: () => void
onSave: () => void onSave: () => void
} }
@ -58,6 +61,7 @@ const ModelModal: FC<ModelModalProps> = ({
provider, provider,
configurateMethod, configurateMethod,
currentCustomConfigurationModelFixedFields, currentCustomConfigurationModelFixedFields,
credential,
onCancel, onCancel,
onSave, onSave,
}) => { }) => {
@ -71,6 +75,7 @@ const ModelModal: FC<ModelModalProps> = ({
configurateMethod, configurateMethod,
providerFormSchemaPredefined && provider.custom_configuration.status === CustomConfigurationStatusEnum.active, providerFormSchemaPredefined && provider.custom_configuration.status === CustomConfigurationStatusEnum.active,
currentCustomConfigurationModelFixedFields, currentCustomConfigurationModelFixedFields,
credential?.credential_id,
) )
const { isCurrentWorkspaceManager } = useAppContext() const { isCurrentWorkspaceManager } = useAppContext()
const isEditMode = !!formSchemasValue && isCurrentWorkspaceManager const isEditMode = !!formSchemasValue && isCurrentWorkspaceManager
@ -95,22 +100,7 @@ const ModelModal: FC<ModelModalProps> = ({
setDraftConfig(originalConfig) setDraftConfig(originalConfig)
}, [draftConfig, originalConfig]) }, [draftConfig, originalConfig])
const formSchemas = useMemo(() => { const { formSchemas } = useModelFormSchemas(provider, providerFormSchemaPredefined, draftConfig)
return providerFormSchemaPredefined
? provider.provider_credential_schema.credential_form_schemas
: [
genModelTypeFormSchema(provider.supported_model_types),
genModelNameFormSchema(provider.model_credential_schema?.model),
...(draftConfig?.enabled ? [] : provider.model_credential_schema.credential_form_schemas),
]
}, [
providerFormSchemaPredefined,
provider.provider_credential_schema?.credential_form_schemas,
provider.supported_model_types,
provider.model_credential_schema?.credential_form_schemas,
provider.model_credential_schema?.model,
draftConfig?.enabled,
])
const formRef = useRef<FormRefObject>(null) const formRef = useRef<FormRefObject>(null)
const extendedSecretFormSchemas = useMemo( const extendedSecretFormSchemas = useMemo(
@ -152,6 +142,7 @@ const ModelModal: FC<ModelModalProps> = ({
}) || { isCheckValidated: false, values: {} } }) || { isCheckValidated: false, values: {} }
if (!isCheckValidated) if (!isCheckValidated)
return return
const res = await saveCredentials( const res = await saveCredentials(
providerFormSchemaPredefined, providerFormSchemaPredefined,
provider.provider, provider.provider,
@ -190,6 +181,7 @@ const ModelModal: FC<ModelModalProps> = ({
providerFormSchemaPredefined, providerFormSchemaPredefined,
provider.provider, provider.provider,
values, values,
credential?.credential_id,
) )
if (res.result === 'success') { if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
@ -227,7 +219,10 @@ const ModelModal: FC<ModelModalProps> = ({
showRadioUI: formSchema.type === FormTypeEnum.radio, showRadioUI: formSchema.type === FormTypeEnum.radio,
} }
}) as FormSchema[]} }) as FormSchema[]}
defaultValues={formSchemasValue} defaultValues={{
...formSchemasValue,
__authorization_name__: credential?.credential_name,
}}
inputClassName='justify-start' inputClassName='justify-start'
ref={formRef} ref={formRef}
/> />

View File

@ -1,7 +1,10 @@
import type { FC } from 'react' import { useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { RiEqualizer2Line } from '@remixicon/react' import { RiEqualizer2Line } from '@remixicon/react'
import type { ModelProvider } from '../declarations' import type {
Credential,
ModelProvider,
} from '../declarations'
import { import {
ConfigurationMethodEnum, ConfigurationMethodEnum,
CustomConfigurationStatusEnum, CustomConfigurationStatusEnum,
@ -19,15 +22,18 @@ import Button from '@/app/components/base/button'
import { changeModelProviderPriority } from '@/service/common' import { changeModelProviderPriority } from '@/service/common'
import { useToastContext } from '@/app/components/base/toast' import { useToastContext } from '@/app/components/base/toast'
import { useEventEmitterContextContext } from '@/context/event-emitter' import { useEventEmitterContextContext } from '@/context/event-emitter'
import Authorized from '../model-auth/authorized'
type CredentialPanelProps = { type CredentialPanelProps = {
provider: ModelProvider provider: ModelProvider
onSetup: () => void onSetup: (credential?: Credential) => void
onUpdate: () => void
} }
const CredentialPanel: FC<CredentialPanelProps> = ({ const CredentialPanel = ({
provider, provider,
onSetup, onSetup,
}) => { onUpdate,
}: CredentialPanelProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { notify } = useToastContext() const { notify } = useToastContext()
const { eventEmitter } = useEventEmitterContextContext() const { eventEmitter } = useEventEmitterContextContext()
@ -38,6 +44,13 @@ const CredentialPanel: FC<CredentialPanelProps> = ({
const priorityUseType = provider.preferred_provider_type const priorityUseType = provider.preferred_provider_type
const isCustomConfigured = customConfig.status === CustomConfigurationStatusEnum.active const isCustomConfigured = customConfig.status === CustomConfigurationStatusEnum.active
const configurateMethods = provider.configurate_methods const configurateMethods = provider.configurate_methods
const {
current_credential_id,
current_credential_name,
available_credentials,
} = provider.custom_configuration
const authorized = current_credential_id && current_credential_name && available_credentials?.every(item => !!item.credential_id)
const authRemoved = !!available_credentials?.length && available_credentials?.every(item => !item.credential_id)
const handleChangePriority = async (key: PreferredProviderTypeEnum) => { const handleChangePriority = async (key: PreferredProviderTypeEnum) => {
const res = await changeModelProviderPriority({ const res = await changeModelProviderPriority({
@ -61,25 +74,58 @@ const CredentialPanel: FC<CredentialPanelProps> = ({
} as any) } as any)
} }
} }
const credentialLabel = useMemo(() => {
if (authorized)
return current_credential_name
if (authRemoved)
return 'Auth removed'
return 'Unauthorized'
}, [authorized, authRemoved, current_credential_name])
return ( return (
<> <>
{ {
provider.provider_credential_schema && ( provider.provider_credential_schema && (
<div className='relative ml-1 w-[112px] shrink-0 rounded-lg border-[0.5px] border-components-panel-border bg-white/[0.18] p-1'> <div className='relative ml-1 w-[112px] shrink-0 rounded-lg border-[0.5px] border-components-panel-border bg-white/[0.18] p-1'>
<div className='system-xs-medium-uppercase mb-1 flex h-5 items-center justify-between pl-2 pr-[7px] pt-1 text-text-tertiary'> <div className='system-xs-medium mb-1 flex h-5 items-center justify-between pl-2 pr-[7px] pt-1 text-text-tertiary'>
API-KEY <div
<Indicator color={isCustomConfigured ? 'green' : 'red'} /> className='grow truncate'
title={credentialLabel}
>
{credentialLabel}
</div>
<Indicator className='shrink-0' color={authorized ? 'green' : 'red'} />
</div> </div>
<div className='flex items-center gap-0.5'> <div className='flex items-center gap-0.5'>
<Button {
className='grow' (!authorized || authRemoved) && (
size='small' <Button
onClick={onSetup} className='grow'
> size='small'
<RiEqualizer2Line className='mr-1 h-3.5 w-3.5' /> onClick={() => onSetup()}
{t('common.operation.setup')} variant={!authorized ? 'secondary-accent' : 'secondary'}
</Button> >
<RiEqualizer2Line className='mr-1 h-3.5 w-3.5' />
{
authRemoved
? t('common.operation.config')
: t('common.operation.setup')
}
</Button>
)
}
{
authorized && (
<Authorized
provider={provider.provider}
onSetup={onSetup}
credentials={available_credentials ?? []}
selectedCredentialId={current_credential_id}
showItemSelectedIcon
onUpdate={onUpdate}
/>
)
}
{ {
systemConfig.enabled && isCustomConfigured && ( systemConfig.enabled && isCustomConfigured && (
<PrioritySelector <PrioritySelector

View File

@ -12,6 +12,7 @@ import type {
ModelProvider, ModelProvider,
} from '../declarations' } from '../declarations'
import { ConfigurationMethodEnum } from '../declarations' import { ConfigurationMethodEnum } from '../declarations'
import type { Credential } from '../declarations'
import { import {
MODEL_PROVIDER_QUOTA_GET_PAID, MODEL_PROVIDER_QUOTA_GET_PAID,
modelTypeFormat, modelTypeFormat,
@ -27,12 +28,13 @@ import { useEventEmitterContextContext } from '@/context/event-emitter'
import { IS_CE_EDITION } from '@/config' import { IS_CE_EDITION } from '@/config'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { useRefreshModel } from '../hooks'
export const UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST = 'UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST' export const UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST = 'UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST'
type ProviderAddedCardProps = { type ProviderAddedCardProps = {
notConfigured?: boolean notConfigured?: boolean
provider: ModelProvider provider: ModelProvider
onOpenModal: (configurationMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => void onOpenModal: (configurationMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields, credential?: Credential) => void
} }
const ProviderAddedCard: FC<ProviderAddedCardProps> = ({ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
notConfigured, notConfigured,
@ -80,6 +82,8 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
getModelList(v.payload) getModelList(v.payload)
}) })
const { handleRefreshModel } = useRefreshModel()
return ( return (
<div <div
className={cn( className={cn(
@ -114,7 +118,8 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
{ {
showCredential && ( showCredential && (
<CredentialPanel <CredentialPanel
onSetup={() => onOpenModal(ConfigurationMethodEnum.predefinedModel)} onSetup={(credential?: Credential) => onOpenModal(ConfigurationMethodEnum.predefinedModel, undefined, credential)}
onUpdate={() => handleRefreshModel(provider, ConfigurationMethodEnum.predefinedModel, undefined)}
provider={provider} provider={provider}
/> />
) )

View File

@ -82,12 +82,14 @@ export const saveCredentials = async (predefined: boolean, provider: string, v:
let body, url let body, url
if (predefined) { if (predefined) {
const { __authorization_name__, ...rest } = v
body = { body = {
config_from: ConfigurationMethodEnum.predefinedModel, config_from: ConfigurationMethodEnum.predefinedModel,
credentials: v, credentials: rest,
load_balancing: loadBalancing, load_balancing: loadBalancing,
name: __authorization_name__,
} }
url = `/workspaces/current/model-providers/${provider}` url = `/workspaces/current/model-providers/${provider}/credentials`
} }
else { else {
const { __model_name, __model_type, ...credentials } = v const { __model_name, __model_type, ...credentials } = v
@ -117,12 +119,17 @@ export const savePredefinedLoadBalancingConfig = async (provider: string, v: For
return setModelProvider({ url, body }) return setModelProvider({ url, body })
} }
export const removeCredentials = async (predefined: boolean, provider: string, v: FormValue) => { export const removeCredentials = async (predefined: boolean, provider: string, v: FormValue, credentialId?: string) => {
let url = '' let url = ''
let body let body
if (predefined) { if (predefined) {
url = `/workspaces/current/model-providers/${provider}` url = `/workspaces/current/model-providers/${provider}/credentials`
if (credentialId) {
body = {
credential_id: credentialId,
}
}
} }
else { else {
if (v) { if (v) {

View File

@ -6,6 +6,7 @@ import { createContext, useContext, useContextSelector } from 'use-context-selec
import { useRouter, useSearchParams } from 'next/navigation' import { useRouter, useSearchParams } from 'next/navigation'
import type { import type {
ConfigurationMethodEnum, ConfigurationMethodEnum,
Credential,
CustomConfigurationModelFixedFields, CustomConfigurationModelFixedFields,
ModelLoadBalancingConfigEntry, ModelLoadBalancingConfigEntry,
ModelProvider, ModelProvider,
@ -79,6 +80,7 @@ export type ModelModalType = {
currentProvider: ModelProvider currentProvider: ModelProvider
currentConfigurationMethod: ConfigurationMethodEnum currentConfigurationMethod: ConfigurationMethodEnum
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
credential?: Credential
} }
export type LoadBalancingEntryModalType = ModelModalType & { export type LoadBalancingEntryModalType = ModelModalType & {
entry?: ModelLoadBalancingConfigEntry entry?: ModelLoadBalancingConfigEntry
@ -337,6 +339,7 @@ export const ModalContextProvider = ({
provider={showModelModal.payload.currentProvider} provider={showModelModal.payload.currentProvider}
configurateMethod={showModelModal.payload.currentConfigurationMethod} configurateMethod={showModelModal.payload.currentConfigurationMethod}
currentCustomConfigurationModelFixedFields={showModelModal.payload.currentCustomConfigurationModelFixedFields} currentCustomConfigurationModelFixedFields={showModelModal.payload.currentCustomConfigurationModelFixedFields}
credential={showModelModal.payload.credential}
onCancel={handleCancelModelModal} onCancel={handleCancelModelModal}
onSave={handleSaveModelModal} onSave={handleSaveModelModal}
/> />

View File

@ -1,8 +1,13 @@
import { get } from './base' import {
del,
get,
post,
} from './base'
import type { import type {
ModelItem, ModelItem,
} from '@/app/components/header/account-setting/model-provider-page/declarations' } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { import {
useMutation,
useQuery, useQuery,
// useQueryClient, // useQueryClient,
} from '@tanstack/react-query' } from '@tanstack/react-query'
@ -15,3 +20,26 @@ export const useModelProviderModelList = (provider: string) => {
queryFn: () => get<{ data: ModelItem[] }>(`/workspaces/current/model-providers/${provider}/models`), queryFn: () => get<{ data: ModelItem[] }>(`/workspaces/current/model-providers/${provider}/models`),
}) })
} }
export const useAddModelCredential = (providerName: string) => {
return useMutation({
mutationFn: (data: any) => post<{ result: string }>(`/workspaces/current/model-providers/${providerName}/credentials`, data),
})
}
export const useGetModelCredential = (providerName: string, credentialId: string) => {
return useQuery({
queryKey: [NAME_SPACE, 'model-credential', providerName, credentialId],
queryFn: () => get<{ data: Credential[] }>(`/workspaces/current/model-providers/${providerName}/credentials?credential_id=${credentialId}`),
})
}
export const useDeleteModelCredential = (providerName: string) => {
return useMutation({
mutationFn: (credentialId: string) => del<{ result: string }>(`/workspaces/current/model-providers/${providerName}/credentials`, {
body: {
credential_id: credentialId,
},
}),
})
}