feat: frontend multi models support (#804)

Co-authored-by: StyleZhang <jasonapring2015@outlook.com>
Co-authored-by: Joel <iamjoel007@gmail.com>
This commit is contained in:
takatost
2023-08-12 00:57:13 +08:00
committed by GitHub
parent 5fa2161b05
commit d10ef17f17
259 changed files with 9105 additions and 1392 deletions

View File

@@ -0,0 +1,80 @@
import { ProviderEnum } from '../declarations'
import type { ProviderConfig } from '../declarations'
import { Anthropic, AnthropicText } from '@/app/components/base/icons/src/public/llm'
import { IS_CE_EDITION } from '@/config'
const config: ProviderConfig = {
selector: {
name: {
'en': 'Anthropic',
'zh-Hans': 'Anthropic',
},
icon: <Anthropic className='w-full h-full' />,
},
item: {
key: ProviderEnum.anthropic,
titleIcon: {
'en': <AnthropicText className='h-5' />,
'zh-Hans': <AnthropicText className='h-5' />,
},
subTitleIcon: <Anthropic className='h-6' />,
desc: {
'en': 'Anthropics powerful models, such as Claude 2 and Claude Instant.',
'zh-Hans': 'Anthropic 的强大模型,例如 Claude 2 和 Claude Instant。',
},
bgColor: 'bg-[#F0F0EB]',
},
modal: {
key: ProviderEnum.anthropic,
title: {
'en': 'Anthropic',
'zh-Hans': 'Anthropic',
},
icon: <Anthropic className='h-6' />,
link: {
href: 'https://console.anthropic.com/account/keys',
label: {
'en': 'Get your API key from Anthropic',
'zh-Hans': '从 Anthropic 获取 API Key',
},
},
validateKeys: ['anthropic_api_key'],
fields: [
{
type: 'text',
key: 'anthropic_api_key',
required: true,
label: {
'en': 'API Key',
'zh-Hans': 'API Key',
},
placeholder: {
'en': 'Enter your API key here',
'zh-Hans': '在此输入您的 API Key',
},
},
...(
IS_CE_EDITION
? [{
type: 'text',
key: 'anthropic_api_url',
required: false,
label: {
'en': 'Custom API Domain',
'zh-Hans': '自定义 API 域名',
},
placeholder: {
'en': 'Enter your API domain, eg: https://example.com/xxx(Optional)',
'zh-Hans': '在此输入您的 API 域名https://example.com/xxx选填',
},
help: {
'en': 'Configurable custom Anthropic API server url.',
'zh-Hans': '可配置自定义 Anthropic API 服务器地址。',
},
}]
: []
),
],
},
}
export default config

View File

@@ -0,0 +1,175 @@
import { ProviderEnum } from '../declarations'
import type { ProviderConfig } from '../declarations'
import { AzureOpenaiService, AzureOpenaiServiceText, OpenaiBlue } from '@/app/components/base/icons/src/public/llm'
const config: ProviderConfig = {
selector: {
name: {
'en': 'Azure OpenAI Service',
'zh-Hans': 'Azure OpenAI Service',
},
icon: <OpenaiBlue className='w-full h-full' />,
},
item: {
key: ProviderEnum.azure_openai,
titleIcon: {
'en': <AzureOpenaiServiceText className='h-6' />,
'zh-Hans': <AzureOpenaiServiceText className='h-6' />,
},
},
modal: {
key: ProviderEnum.azure_openai,
title: {
'en': 'Azure OpenAI Service Model',
'zh-Hans': 'Azure OpenAI Service Model',
},
icon: <AzureOpenaiService className='h-6' />,
link: {
href: 'https://azure.microsoft.com/en-us/products/ai-services/openai-service',
label: {
'en': 'Get your API key from Azure',
'zh-Hans': '从 Azure 获取 API Key',
},
},
defaultValue: {
model_type: 'text-generation',
},
validateKeys: [
'model_name',
'model_type',
'openai_api_base',
'openai_api_key',
'base_model_name',
],
fields: [
{
type: 'text',
key: 'model_name',
required: true,
label: {
'en': 'Deployment Name',
'zh-Hans': '部署名称',
},
placeholder: {
'en': 'Enter your Deployment Name here',
'zh-Hans': '在此输入您的部署名称',
},
},
{
type: 'radio',
key: 'model_type',
required: true,
label: {
'en': 'Model Type',
'zh-Hans': '模型类型',
},
options: [
{
key: 'text-generation',
label: {
'en': 'Text Generation',
'zh-Hans': '文本生成',
},
},
{
key: 'embeddings',
label: {
'en': 'Embeddings',
'zh-Hans': 'Embeddings',
},
},
],
},
{
type: 'text',
key: 'openai_api_base',
required: true,
label: {
'en': 'API Endpoint URL',
'zh-Hans': 'API 域名',
},
placeholder: {
'en': 'Enter your API Endpoint, eg: https://example.com/xxx',
'zh-Hans': '在此输入您的 API 域名https://example.com/xxx',
},
},
{
type: 'text',
key: 'openai_api_key',
required: true,
label: {
'en': 'API Key',
'zh-Hans': 'API Key',
},
placeholder: {
'en': 'Enter your API key here',
'zh-Hans': '在此输入您的 API Key',
},
},
{
type: 'select',
key: 'base_model_name',
required: true,
label: {
'en': 'Base Model',
'zh-Hans': '基础模型',
},
options: (v) => {
if (v.model_type === 'text-generation') {
return [
{
key: 'gpt-35-turbo',
label: {
'en': 'gpt-35-turbo',
'zh-Hans': 'gpt-35-turbo',
},
},
{
key: 'gpt-35-turbo-16k',
label: {
'en': 'gpt-35-turbo-16k',
'zh-Hans': 'gpt-35-turbo-16k',
},
},
{
key: 'gpt-4',
label: {
'en': 'gpt-4',
'zh-Hans': 'gpt-4',
},
},
{
key: 'gpt-4-32k',
label: {
'en': 'gpt-4-32k',
'zh-Hans': 'gpt-4-32k',
},
},
{
key: 'text-davinci-003',
label: {
'en': 'text-davinci-003',
'zh-Hans': 'text-davinci-003',
},
},
]
}
if (v.model_type === 'embeddings') {
return [
{
key: 'text-embedding-ada-002',
label: {
'en': 'text-embedding-ada-002',
'zh-Hans': 'text-embedding-ada-002',
},
},
]
}
return []
},
},
],
},
}
export default config

View File

@@ -0,0 +1,69 @@
import { ProviderEnum } from '../declarations'
import type { ProviderConfig } from '../declarations'
import { Chatglm, ChatglmText } from '@/app/components/base/icons/src/public/llm'
const config: ProviderConfig = {
selector: {
name: {
'en': 'ChatGLM',
'zh-Hans': 'ChatGLM',
},
icon: <Chatglm className='w-full h-full' />,
},
item: {
key: ProviderEnum.chatglm,
titleIcon: {
'en': <ChatglmText className='h-6' />,
'zh-Hans': <ChatglmText className='h-6' />,
},
disable: {
tip: {
'en': 'Only supports the ',
'zh-Hans': '仅支持',
},
link: {
href: {
'en': 'https://docs.dify.ai/getting-started/install-self-hosted',
'zh-Hans': 'https://docs.dify.ai/v/zh-hans/getting-started/install-self-hosted',
},
label: {
'en': 'community open-source version',
'zh-Hans': '社区开源版本',
},
},
},
},
modal: {
key: ProviderEnum.chatglm,
title: {
'en': 'ChatGLM',
'zh-Hans': 'ChatGLM',
},
icon: <Chatglm className='h-6' />,
link: {
href: 'https://github.com/THUDM/ChatGLM-6B#api%E9%83%A8%E7%BD%B2',
label: {
'en': 'How to deploy ChatGLM',
'zh-Hans': '如何部署 ChatGLM',
},
},
validateKeys: ['api_base'],
fields: [
{
type: 'text',
key: 'api_base',
required: true,
label: {
'en': 'Custom API Domain',
'zh-Hans': '自定义 API 域名',
},
placeholder: {
'en': 'Enter your API domain, eg: https://example.com/xxx',
'zh-Hans': '在此输入您的 API 域名https://example.com/xxx',
},
},
],
},
}
export default config

View File

@@ -0,0 +1,127 @@
import { ProviderEnum } from '../declarations'
import type { FormValue, ProviderConfig } from '../declarations'
import { Huggingface, HuggingfaceText } from '@/app/components/base/icons/src/public/llm'
const config: ProviderConfig = {
selector: {
name: {
'en': 'Hugging Face',
'zh-Hans': 'Hugging Face',
},
icon: <Huggingface className='w-full h-full' />,
},
item: {
key: ProviderEnum.huggingface_hub,
titleIcon: {
'en': <HuggingfaceText className='h-6' />,
'zh-Hans': <HuggingfaceText className='h-6' />,
},
hit: {
'en': '🐑 Llama 2 Supported',
'zh-Hans': '🐑 Llama 2 已支持',
},
},
modal: {
key: ProviderEnum.huggingface_hub,
title: {
'en': 'Hugging Face Model',
'zh-Hans': 'Hugging Face Model',
},
icon: <Huggingface className='h-6' />,
link: {
href: 'https://huggingface.co/settings/tokens',
label: {
'en': 'Get your API key from Hugging Face Hub',
'zh-Hans': '从 Hugging Face Hub 获取 API Key',
},
},
defaultValue: {
model_type: 'text-generation',
huggingfacehub_api_type: 'hosted_inference_api',
},
validateKeys: (v?: FormValue) => {
if (v?.huggingfacehub_api_type === 'hosted_inference_api') {
return [
'huggingfacehub_api_token',
'model_name',
]
}
if (v?.huggingfacehub_api_type === 'inference_endpoints') {
return [
'huggingfacehub_api_token',
'model_name',
'huggingfacehub_endpoint_url',
]
}
return []
},
fields: [
{
type: 'radio',
key: 'huggingfacehub_api_type',
required: true,
label: {
'en': 'Endpoint Type',
'zh-Hans': '端点类型',
},
options: [
{
key: 'hosted_inference_api',
label: {
'en': 'Hosted Inference API',
'zh-Hans': 'Hosted Inference API',
},
},
{
key: 'inference_endpoints',
label: {
'en': 'Inference Endpoints',
'zh-Hans': 'Inference Endpoints',
},
},
],
},
{
type: 'text',
key: 'huggingfacehub_api_token',
required: true,
label: {
'en': 'API Token',
'zh-Hans': 'API Token',
},
placeholder: {
'en': 'Enter your Hugging Face Hub API Token here',
'zh-Hans': '在此输入您的 Hugging Face Hub API Token',
},
},
{
type: 'text',
key: 'model_name',
required: true,
label: {
'en': 'Model Name',
'zh-Hans': '模型名称',
},
placeholder: {
'en': 'Enter your Model Name here',
'zh-Hans': '在此输入您的模型名称',
},
},
{
hidden: (value?: FormValue) => value?.huggingfacehub_api_type === 'hosted_inference_api',
type: 'text',
key: 'huggingfacehub_endpoint_url',
label: {
'en': 'Endpoint URL',
'zh-Hans': '端点 URL',
},
placeholder: {
'en': 'Enter your Endpoint URL here',
'zh-Hans': '在此输入您的端点 URL',
},
},
],
},
}
export default config

View File

@@ -0,0 +1,23 @@
import openai from './openai'
import anthropic from './anthropic'
import azure_openai from './azure_openai'
import replicate from './replicate'
import huggingface_hub from './huggingface_hub'
import wenxin from './wenxin'
import tongyi from './tongyi'
import spark from './spark'
import minimax from './minimax'
import chatglm from './chatglm'
export default {
openai,
anthropic,
azure_openai,
replicate,
huggingface_hub,
wenxin,
tongyi,
spark,
minimax,
chatglm,
}

View File

@@ -0,0 +1,69 @@
import { ProviderEnum } from '../declarations'
import type { ProviderConfig } from '../declarations'
import { Minimax, MinimaxText } from '@/app/components/base/icons/src/image/llm'
const config: ProviderConfig = {
selector: {
name: {
'en': 'MINIMAX',
'zh-Hans': 'MINIMAX',
},
icon: <Minimax className='w-full h-full' />,
},
item: {
key: ProviderEnum.minimax,
titleIcon: {
'en': <MinimaxText className='w-[84px] h-6' />,
'zh-Hans': <MinimaxText className='w-[84px] h-6' />,
},
},
modal: {
key: ProviderEnum.minimax,
title: {
'en': 'MiniMax',
'zh-Hans': 'MiniMax',
},
icon: <Minimax className='w-6 h-6' />,
link: {
href: 'https://api.minimax.chat/user-center/basic-information/interface-key',
label: {
'en': 'Get your API key from MiniMax',
'zh-Hans': '从 MiniMax 获取 API Key',
},
},
validateKeys: [
'minimax_api_key',
'minimax_group_id',
],
fields: [
{
type: 'text',
key: 'minimax_api_key',
required: true,
label: {
'en': 'API Key',
'zh-Hans': 'API Key',
},
placeholder: {
'en': 'Enter your API key here',
'zh-Hans': '在此输入您的 API Key',
},
},
{
type: 'text',
key: 'minimax_group_id',
required: true,
label: {
'en': 'Group ID',
'zh-Hans': 'Group ID',
},
placeholder: {
'en': 'Enter your Group ID here',
'zh-Hans': '在此输入您的 Group ID',
},
},
],
},
}
export default config

View File

@@ -0,0 +1,93 @@
import { ProviderEnum } from '../declarations'
import type { ProviderConfig } from '../declarations'
import { OpenaiBlack, OpenaiText, OpenaiTransparent } from '@/app/components/base/icons/src/public/llm'
import { IS_CE_EDITION } from '@/config'
const config: ProviderConfig = {
selector: {
name: {
'en': 'OpenAI',
'zh-Hans': 'OpenAI',
},
icon: <OpenaiBlack className='w-full h-full' />,
},
item: {
key: ProviderEnum.openai,
titleIcon: {
'en': <OpenaiText className='h-5' />,
'zh-Hans': <OpenaiText className='h-5' />,
},
subTitleIcon: <OpenaiBlack className='w-6 h-6' />,
desc: {
'en': 'Models provided by OpenAI, such as GPT-3.5-Turbo and GPT-4.',
'zh-Hans': 'OpenAI 提供的模型,例如 GPT-3.5-Turbo 和 GPT-4。',
},
bgColor: 'bg-gray-200',
},
modal: {
key: ProviderEnum.openai,
title: {
'en': 'OpenAI',
'zh-Hans': 'OpenAI',
},
icon: <OpenaiTransparent className='w-6 h-6' />,
link: {
href: 'https://platform.openai.com/account/api-keys',
label: {
'en': 'Get your API key from OpenAI',
'zh-Hans': '从 OpenAI 获取 API Key',
},
},
validateKeys: ['openai_api_key'],
fields: [
{
type: 'text',
key: 'openai_api_key',
required: true,
label: {
'en': 'API Key',
'zh-Hans': 'API Key',
},
placeholder: {
'en': 'Enter your API key here',
'zh-Hans': '在此输入您的 API Key',
},
},
{
type: 'text',
key: 'openai_organization',
required: false,
label: {
'en': 'Organization ID',
'zh-Hans': '组织 ID',
},
placeholder: {
'en': 'Enter your Organization ID(Optional)',
'zh-Hans': '在此输入您的组织 ID选填',
},
},
...(
IS_CE_EDITION
? [{
type: 'text',
key: 'openai_api_base',
required: false,
label: {
'en': 'Custom API Domain',
'zh-Hans': '自定义 API 域名',
},
placeholder: {
'en': 'Enter your API domain, eg: https://example.com/xxx(Optional)',
'zh-Hans': '在此输入您的 API 域名https://example.com/xxx选填',
},
help: {
'en': 'You can configure your server compatible with the OpenAI API specification, or proxy mirror address',
'zh-Hans': '可配置您的兼容 OpenAI API 规范的服务器,或者代理镜像地址',
},
}]
: []
),
],
},
}
export default config

View File

@@ -0,0 +1,115 @@
import { ProviderEnum } from '../declarations'
import type { ProviderConfig } from '../declarations'
import { Replicate, ReplicateText } from '@/app/components/base/icons/src/public/llm'
const config: ProviderConfig = {
selector: {
name: {
'en': 'Replicate',
'zh-Hans': 'Replicate',
},
icon: <Replicate className='w-full h-full' />,
},
item: {
key: ProviderEnum.replicate,
titleIcon: {
'en': <ReplicateText className='h-6' />,
'zh-Hans': <ReplicateText className='h-6' />,
},
hit: {
'en': '🐑 Llama 2 Supported',
'zh-Hans': '🐑 Llama 2 已支持',
},
},
modal: {
key: ProviderEnum.replicate,
title: {
'en': 'Replicate Model',
'zh-Hans': 'Replicate Model',
},
icon: <Replicate className='h-6' />,
link: {
href: 'https://replicate.com/account/api-tokens',
label: {
'en': 'Get your API key from Replicate',
'zh-Hans': '从 Replicate 获取 API Key',
},
},
defaultValue: {
model_type: 'text-generation',
},
validateKeys: [
'model_type',
'replicate_api_token',
'model_name',
'model_version',
],
fields: [
{
type: 'radio',
key: 'model_type',
required: true,
label: {
'en': 'Model Type',
'zh-Hans': '模型类型',
},
options: [
{
key: 'text-generation',
label: {
'en': 'Text Generation',
'zh-Hans': '文本生成',
},
},
{
key: 'embeddings',
label: {
'en': 'Embeddings',
'zh-Hans': 'Embeddings',
},
},
],
},
{
type: 'text',
key: 'replicate_api_token',
required: true,
label: {
'en': 'API Key',
'zh-Hans': 'API Key',
},
placeholder: {
'en': 'Enter your Replicate API key here',
'zh-Hans': '在此输入您的 Replicate API Key',
},
},
{
type: 'text',
key: 'model_name',
required: true,
label: {
'en': 'Model Name',
'zh-Hans': '模型名称',
},
placeholder: {
'en': 'Enter your Model Name here',
'zh-Hans': '在此输入您的模型名称',
},
},
{
type: 'text',
key: 'model_version',
label: {
'en': 'Model Version',
'zh-Hans': '模型版本',
},
placeholder: {
'en': 'Enter your Model Version here',
'zh-Hans': '在此输入您的模型版本',
},
},
],
},
}
export default config

View File

@@ -0,0 +1,83 @@
import { ProviderEnum } from '../declarations'
import type { ProviderConfig } from '../declarations'
import { IflytekSpark, IflytekSparkText, IflytekSparkTextCn } from '@/app/components/base/icons/src/public/llm'
const config: ProviderConfig = {
selector: {
name: {
'en': 'iFLYTEK SPARK',
'zh-Hans': '讯飞星火',
},
icon: <IflytekSpark className='w-full h-full' />,
},
item: {
key: ProviderEnum.spark,
titleIcon: {
'en': <IflytekSparkText className='h-6' />,
'zh-Hans': <IflytekSparkTextCn className='h-6' />,
},
},
modal: {
key: ProviderEnum.spark,
title: {
'en': 'iFLYTEK SPARK',
'zh-Hans': '讯飞星火',
},
icon: <IflytekSpark className='w-6 h-6' />,
link: {
href: 'https://www.xfyun.cn/solutions/xinghuoAPI',
label: {
'en': 'Get your API key from AliCloud',
'zh-Hans': '从阿里云获取 API Key',
},
},
validateKeys: [
'app_id',
'api_key',
'api_secret',
],
fields: [
{
type: 'text',
key: 'app_id',
required: true,
label: {
'en': 'API ID',
'zh-Hans': 'API ID',
},
placeholder: {
'en': 'Enter your API ID here',
'zh-Hans': '在此输入您的 API ID',
},
},
{
type: 'text',
key: 'api_key',
required: true,
label: {
'en': 'API Key',
'zh-Hans': 'API Key',
},
placeholder: {
'en': 'Enter your API key here',
'zh-Hans': '在此输入您的 API Key',
},
},
{
type: 'text',
key: 'api_secret',
required: true,
label: {
'en': 'API Secret',
'zh-Hans': 'API Secret',
},
placeholder: {
'en': 'Enter your API Secret here',
'zh-Hans': '在此输入您的 API Secret',
},
},
],
},
}
export default config

View File

@@ -0,0 +1,53 @@
import { ProviderEnum } from '../declarations'
import type { ProviderConfig } from '../declarations'
import { Tongyi, TongyiText, TongyiTextCn } from '@/app/components/base/icons/src/image/llm'
const config: ProviderConfig = {
selector: {
name: {
'en': 'TONGYI QIANWEN',
'zh-Hans': '通义千问',
},
icon: <Tongyi className='w-full h-full' />,
},
item: {
key: ProviderEnum.tongyi,
titleIcon: {
'en': <TongyiText className='w-[88px] h-6' />,
'zh-Hans': <TongyiTextCn className='w-[100px] h-6' />,
},
},
modal: {
key: ProviderEnum.tongyi,
title: {
'en': 'Tongyi',
'zh-Hans': '通义千问',
},
icon: <Tongyi className='w-6 h-6' />,
link: {
href: 'https://dashscope.console.aliyun.com/api-key_management',
label: {
'en': 'Get your API key from AliCloud',
'zh-Hans': '从阿里云获取 API Key',
},
},
validateKeys: ['dashscope_api_key'],
fields: [
{
type: 'text',
key: 'dashscope_api_key',
required: true,
label: {
'en': 'API Key',
'zh-Hans': 'API Key',
},
placeholder: {
'en': 'Enter your API key here',
'zh-Hans': '在此输入您的 API Key',
},
},
],
},
}
export default config

View File

@@ -0,0 +1,66 @@
import { ProviderEnum } from '../declarations'
import type { ProviderConfig } from '../declarations'
import { Wxyy, WxyyText, WxyyTextCn } from '@/app/components/base/icons/src/image/llm'
const config: ProviderConfig = {
selector: {
name: {
'en': 'WENXIN YIYAN',
'zh-Hans': '文心一言',
},
icon: <Wxyy className='w-full h-full' />,
},
item: {
key: ProviderEnum.wenxin,
titleIcon: {
'en': <WxyyText className='w-[124px] h-6' />,
'zh-Hans': <WxyyTextCn className='w-[100px] h-6' />,
},
},
modal: {
key: ProviderEnum.wenxin,
title: {
'en': 'WENXINYIYAN',
'zh-Hans': '文心一言',
},
icon: <Wxyy className='w-6 h-6' />,
link: {
href: 'https://console.bce.baidu.com/qianfan/ais/console/applicationConsole/application',
label: {
'en': 'Get your API key from Baidu',
'zh-Hans': '从百度获取 API Key',
},
},
validateKeys: ['api_key', 'secret_key'],
fields: [
{
type: 'text',
key: 'api_key',
required: true,
label: {
'en': 'API Key',
'zh-Hans': 'API Key',
},
placeholder: {
'en': 'Enter your API key here',
'zh-Hans': '在此输入您的 API Key',
},
},
{
type: 'text',
key: 'secret_key',
required: true,
label: {
'en': 'Secret Key',
'zh-Hans': 'Secret Key',
},
placeholder: {
'en': 'Enter your Secret key here',
'zh-Hans': '在此输入您的 Secret Key',
},
},
],
},
}
export default config

View File

@@ -0,0 +1,146 @@
import type { ReactElement } from 'react'
export type FormValue = Record<string, string>
export type TypeWithI18N<T = string> = {
'en': T
'zh-Hans': T
}
export type Option = {
key: string
label: TypeWithI18N
}
export type ProviderSelector = {
name: TypeWithI18N
icon: ReactElement
}
export type Field = {
hidden?: (v?: FormValue) => boolean
type: string
key: string
required?: boolean
label: TypeWithI18N
options?: Option[] | ((v: FormValue) => Option[])
placeholder?: TypeWithI18N
help?: TypeWithI18N
}
export enum ProviderEnum {
'openai' = 'openai',
'anthropic' = 'anthropic',
'replicate' = 'replicate',
'azure_openai' = 'azure_openai',
'huggingface_hub' = 'huggingface_hub',
'tongyi' = 'tongyi',
'wenxin' = 'wenxin',
'spark' = 'spark',
'minimax' = 'minimax',
'chatglm' = 'chatglm',
}
export type ProviderConfigItem = {
key: ProviderEnum
titleIcon: TypeWithI18N<ReactElement>
subTitleIcon?: ReactElement
desc?: TypeWithI18N
bgColor?: string
hit?: TypeWithI18N
disable?: {
tip: TypeWithI18N
link: {
href: TypeWithI18N
label: TypeWithI18N
}
}
}
export enum ModelType {
textGeneration = 'text-generation',
embeddings = 'embeddings',
speech2text = 'speech2text',
}
export enum ModelFeature {
agentThought = 'agent_thought',
}
// backend defined model struct: /console/api/workspaces/current/models/model-type/:model_type
export type BackendModel = {
model_name: string
model_type: ModelType
model_provider: {
provider_name: ProviderEnum
provider_type: PreferredProviderTypeEnum
}
features: ModelFeature[]
}
export type ProviderConfigModal = {
key: ProviderEnum
title: TypeWithI18N
icon: ReactElement
defaultValue?: FormValue
validateKeys?: string[] | ((v?: FormValue) => string[])
fields: Field[]
link: {
href: string
label: TypeWithI18N
}
}
export type ProviderConfig = {
selector: ProviderSelector
item: ProviderConfigItem
modal: ProviderConfigModal
}
export enum PreferredProviderTypeEnum {
'system' = 'system',
'custom' = 'custom',
}
export enum ModelFlexibilityEnum {
'fixed' = 'fixed',
'configurable' = 'configurable',
}
export type ProviderCommon = {
provider_name: ProviderEnum
provider_type: PreferredProviderTypeEnum
is_valid: boolean
last_used: number
}
export type ProviderWithQuota = {
quota_type: string
quota_unit: string
quota_limit: number
quota_used: number
} & ProviderCommon
export type ProviderWithConfig = {
config: Record<string, string>
} & ProviderCommon
export type Model = {
model_name: string
model_type: string
config: Record<string, string>
}
export type ProviderWithModels = {
models: Model[]
} & ProviderCommon
export type ProviderInstance = ProviderWithQuota | ProviderWithConfig | ProviderWithModels
export type Provider = {
preferred_provider_type: PreferredProviderTypeEnum
model_flexibility: ModelFlexibilityEnum
providers: ProviderInstance[]
}
export type ProviderMap = {
[k in ProviderEnum]: Provider
}

View File

@@ -0,0 +1,298 @@
import { useState } from 'react'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import type {
BackendModel,
FormValue,
ProviderConfigModal,
ProviderEnum,
} from './declarations'
import ModelSelector from './model-selector'
import ModelCard from './model-card'
import ModelItem from './model-item'
import ModelModal from './model-modal'
import config from './configs'
import { ConfigurableProviders } from './utils'
import { ChevronDownDouble } from '@/app/components/base/icons/src/vender/line/arrows'
// import { HelpCircle } from '@/app/components/base/icons/src/vender/line/general'
import {
changeModelProviderPriority,
deleteModelProvider,
deleteModelProviderModel,
fetchDefaultModal,
fetchModelProviders,
setModelProvider,
updateDefaultModel,
} from '@/service/common'
import { useToastContext } from '@/app/components/base/toast'
import Confirm from '@/app/components/base/confirm/common'
import { ModelType } from '@/app/components/header/account-setting/model-page/declarations'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { useProviderContext } from '@/context/provider-context'
const MODEL_CARD_LIST = [
config.openai,
config.anthropic,
]
const MODEL_LIST = [
config.azure_openai,
config.replicate,
config.huggingface_hub,
config.minimax,
config.spark,
config.tongyi,
config.wenxin,
config.chatglm,
]
const titleClassName = `
flex items-center h-9 text-sm font-medium text-gray-900
`
const tipClassName = `
ml-0.5 w-[14px] h-[14px] text-gray-400
`
type DeleteModel = {
model_name: string
model_type: string
}
const ModelPage = () => {
const { t } = useTranslation()
const { updateModelList } = useProviderContext()
const { data: providers, mutate: mutateProviders } = useSWR('/workspaces/current/model-providers', fetchModelProviders)
const { data: textGenerationDefaultModel, mutate: mutateTextGenerationDefaultModel } = useSWR('/workspaces/current/default-model?model_type=text-generation', fetchDefaultModal)
const { data: embeddingsDefaultModel, mutate: mutateEmbeddingsDefaultModel } = useSWR('/workspaces/current/default-model?model_type=embeddings', fetchDefaultModal)
const { data: speech2textDefaultModel, mutate: mutateSpeech2textDefaultModel } = useSWR('/workspaces/current/default-model?model_type=speech2text', fetchDefaultModal)
const [showMoreModel, setShowMoreModel] = useState(false)
const [showModal, setShowModal] = useState(false)
const { notify } = useToastContext()
const { eventEmitter } = useEventEmitterContextContext()
const [modelModalConfig, setModelModalConfig] = useState<ProviderConfigModal | undefined>(undefined)
const [confirmShow, setConfirmShow] = useState(false)
const [deleteModel, setDeleteModel] = useState<DeleteModel & { providerKey: ProviderEnum }>()
const [modalMode, setModalMode] = useState('add')
const handleOpenModal = (newModelModalConfig: ProviderConfigModal | undefined, editValue?: FormValue) => {
if (newModelModalConfig) {
setShowModal(true)
const defaultValue = editValue ? { ...newModelModalConfig.defaultValue, ...editValue } : newModelModalConfig.defaultValue
setModelModalConfig({
...newModelModalConfig,
defaultValue,
})
if (editValue)
setModalMode('edit')
else
setModalMode('add')
}
}
const handleCancelModal = () => {
setShowModal(false)
}
const handleUpdateProvidersAndModelList = () => {
updateModelList(ModelType.textGeneration)
updateModelList(ModelType.embeddings)
mutateProviders()
}
const handleSave = async (v?: FormValue) => {
if (v && modelModalConfig) {
let body, url
if (ConfigurableProviders.includes(modelModalConfig.key)) {
const { model_name, model_type, ...config } = v
body = {
model_name,
model_type,
config,
}
url = `/workspaces/current/model-providers/${modelModalConfig.key}/models`
}
else {
body = {
config: v,
}
url = `/workspaces/current/model-providers/${modelModalConfig.key}`
}
try {
eventEmitter?.emit('provider-save')
const res = await setModelProvider({ url, body })
if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
handleUpdateProvidersAndModelList()
handleCancelModal()
}
eventEmitter?.emit('')
}
catch (e) {
eventEmitter?.emit('')
}
}
}
const handleConfirm = (deleteModel: DeleteModel, providerKey: ProviderEnum) => {
setDeleteModel({ ...deleteModel, providerKey })
setConfirmShow(true)
}
const handleOperate = async ({ type, value }: Record<string, any>, provierKey: ProviderEnum) => {
if (type === 'delete') {
if (!value) {
const res = await deleteModelProvider({ url: `/workspaces/current/model-providers/${provierKey}` })
if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
handleUpdateProvidersAndModelList()
}
}
else {
handleConfirm(value, provierKey)
}
}
if (type === 'priority') {
const res = await changeModelProviderPriority({
url: `/workspaces/current/model-providers/${provierKey}/preferred-provider-type`,
body: {
preferred_provider_type: value,
},
})
if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
mutateProviders()
}
}
}
const handleDeleteModel = async () => {
const { model_name, model_type, providerKey } = deleteModel || {}
const res = await deleteModelProviderModel({
url: `/workspaces/current/model-providers/${providerKey}/models?model_name=${model_name}&model_type=${model_type}`,
})
if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
setConfirmShow(false)
handleUpdateProvidersAndModelList()
}
}
const mutateDefaultModel = (type: ModelType) => {
if (type === ModelType.textGeneration)
mutateTextGenerationDefaultModel()
if (type === ModelType.embeddings)
mutateEmbeddingsDefaultModel()
if (type === ModelType.speech2text)
mutateSpeech2textDefaultModel()
}
const handleChangeDefaultModel = async (type: ModelType, v: BackendModel) => {
const res = await updateDefaultModel({
url: '/workspaces/current/default-model',
body: {
model_type: type,
provider_name: v.model_provider.provider_name,
model_name: v.model_name,
},
})
if (res.result === 'success') {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
mutateDefaultModel(type)
}
}
return (
<div className='relative pt-1 -mt-2'>
<div className='grid grid-cols-3 gap-4 mb-5'>
<div className='w-full'>
<div className={titleClassName}>
{t('common.modelProvider.systemReasoningModel.key')}
{/* <HelpCircle className={tipClassName} /> */}
</div>
<div>
<ModelSelector
value={textGenerationDefaultModel && { providerName: textGenerationDefaultModel.model_provider.provider_name, modelName: textGenerationDefaultModel.model_name }}
modelType={ModelType.textGeneration}
onChange={v => handleChangeDefaultModel(ModelType.textGeneration, v)}
/>
</div>
</div>
<div className='w-full'>
<div className={titleClassName}>
{t('common.modelProvider.embeddingModel.key')}
{/* <HelpCircle className={tipClassName} /> */}
</div>
<div>
<ModelSelector
value={embeddingsDefaultModel && { providerName: embeddingsDefaultModel.model_provider.provider_name, modelName: embeddingsDefaultModel.model_name }}
modelType={ModelType.embeddings}
onChange={v => handleChangeDefaultModel(ModelType.embeddings, v)}
/>
</div>
</div>
<div className='w-full'>
<div className={titleClassName}>
{t('common.modelProvider.speechToTextModel.key')}
{/* <HelpCircle className={tipClassName} /> */}
</div>
<div>
<ModelSelector
value={speech2textDefaultModel && { providerName: speech2textDefaultModel.model_provider.provider_name, modelName: speech2textDefaultModel.model_name }}
modelType={ModelType.speech2text}
onChange={v => handleChangeDefaultModel(ModelType.speech2text, v)}
/>
</div>
</div>
</div>
<div className='mb-5 h-[0.5px] bg-gray-100' />
<div className='mb-3 text-sm font-medium text-gray-800'>{t('common.modelProvider.models')}</div>
<div className='grid grid-cols-2 gap-4 mb-6'>
{
MODEL_CARD_LIST.map((model, index) => (
<ModelCard
key={index}
modelItem={model.item}
currentProvider={providers?.[model.item.key]}
onOpenModal={editValue => handleOpenModal(model.modal, editValue)}
onOperate={v => handleOperate(v, model.item.key)}
/>
))
}
</div>
{
MODEL_LIST.slice(0, showMoreModel ? MODEL_LIST.length : 3).map((model, index) => (
<ModelItem
key={index}
modelItem={model.item}
currentProvider={providers?.[model.item.key]}
onOpenModal={editValue => handleOpenModal(model.modal, editValue)}
onOperate={v => handleOperate(v, model.item.key)}
onUpdate={mutateProviders}
/>
))
}
{
!showMoreModel && (
<div className='inline-flex items-center px-1 h-[26px] cursor-pointer' onClick={() => setShowMoreModel(true)}>
<ChevronDownDouble className='mr-1 w-3 h-3 text-gray-500' />
<div className='text-xs font-medium text-gray-500'>{t('common.modelProvider.showMoreModelProvider')}</div>
</div>
)
}
<ModelModal
isShow={showModal}
modelModal={modelModalConfig}
onCancel={handleCancelModal}
onSave={handleSave}
mode={modalMode}
/>
<Confirm
isShow={confirmShow}
onCancel={() => setConfirmShow(false)}
title={deleteModel?.model_name || ''}
desc={t('common.modelProvider.item.deleteDesc', { modelName: deleteModel?.model_name }) || ''}
onConfirm={handleDeleteModel}
/>
</div>
)
}
export default ModelPage

View File

@@ -0,0 +1,116 @@
import { useState } from 'react'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import type { Provider, ProviderWithQuota } from '../declarations'
import Tooltip from '@/app/components/base/tooltip'
import { InfoCircle } from '@/app/components/base/icons/src/vender/line/general'
import { getPayUrl } from '@/service/common'
import Button from '@/app/components/base/button'
type QuotaProps = {
currentProvider: Provider
}
const Quota: FC<QuotaProps> = ({
currentProvider,
}) => {
const { t } = useTranslation()
const [loading, setLoading] = useState(false)
const systemTrial = currentProvider.providers.find(p => p.provider_type === 'system' && (p as ProviderWithQuota)?.quota_type === 'trial') as ProviderWithQuota
const systemPaid = currentProvider.providers.find(p => p.provider_type === 'system' && (p as ProviderWithQuota)?.quota_type === 'paid') as ProviderWithQuota
const QUOTA_UNIT_MAP: Record<string, string> = {
times: t('common.modelProvider.card.callTimes'),
tokens: 'Tokens',
}
const renderStatus = () => {
const totalQuota = (systemPaid?.is_valid ? systemPaid.quota_limit : 0) + systemTrial.quota_limit
const totalUsed = (systemPaid?.is_valid ? systemPaid.quota_used : 0) + systemTrial.quota_used
if (totalQuota === totalUsed) {
return (
<div className='px-1.5 bg-[#FEF3F2] rounded-md text-xs font-semibold text-[#D92D20]'>
{t('common.modelProvider.card.quotaExhausted')}
</div>
)
}
if (systemPaid?.is_valid) {
return (
<div className='px-1.5 bg-[#FFF6ED] rounded-md text-xs font-semibold text-[#EC4A0A]'>
{t('common.modelProvider.card.paid')}
</div>
)
}
return (
<div className='px-1.5 bg-primary-50 rounded-md text-xs font-semibold text-primary-600'>
{t('common.modelProvider.card.onTrial')}
</div>
)
}
const renderQuota = () => {
if (systemPaid?.is_valid)
return systemPaid.quota_limit - systemPaid.quota_used
if (systemTrial.is_valid)
return systemTrial.quota_limit - systemTrial.quota_used
}
const renderUnit = () => {
if (systemPaid?.is_valid)
return QUOTA_UNIT_MAP[systemPaid.quota_unit]
if (systemTrial.is_valid)
return QUOTA_UNIT_MAP[systemTrial.quota_unit]
}
const handleGetPayUrl = async () => {
setLoading(true)
try {
const res = await getPayUrl(`/workspaces/current/model-providers/${systemPaid.provider_name}/checkout-url`)
window.location.href = res.url
}
finally {
setLoading(false)
}
}
return (
<div className='flex justify-between px-4 py-3 border-b-[0.5px] border-b-[rgba(0, 0, 0, 0.5)]'>
<div>
<div className='flex items-center mb-1 h-5'>
<div className='mr-1 text-xs font-medium text-gray-500'>
{t('common.modelProvider.card.quota')}
</div>
{renderStatus()}
</div>
<div className='flex items-center text-gray-700'>
<div className='mr-1 text-sm font-medium'>{renderQuota()}</div>
<div className='mr-1 text-sm'>
{renderUnit()}
</div>
<Tooltip
selector='setting-model-card'
htmlContent={
<div className='w-[261px] text-gray-500'>{t('common.modelProvider.card.tip')}</div>
}
>
<InfoCircle className='w-3 h-3 text-gray-400 hover:text-gray-700' />
</Tooltip>
</div>
</div>
{
systemPaid && (
<Button
type='primary'
className='mt-1.5 !px-3 !h-8 !text-[13px] font-medium !rounded-lg'
onClick={handleGetPayUrl}
disabled={loading}
>
{t('common.modelProvider.card.buyQuota')}
</Button>
)
}
</div>
)
}
export default Quota

View File

@@ -0,0 +1,81 @@
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import type {
FormValue,
Provider,
ProviderConfigItem,
ProviderWithConfig,
} from '../declarations'
import Indicator from '../../../indicator'
import Selector from '../selector'
import Quota from './Quota'
import { IS_CE_EDITION } from '@/config'
import I18n from '@/context/i18n'
import { Plus } from '@/app/components/base/icons/src/vender/line/general'
type ModelCardProps = {
currentProvider?: Provider
modelItem: ProviderConfigItem
onOpenModal: (v?: FormValue) => void
onOperate: (v: Record<string, any>) => void
}
const ModelCard: FC<ModelCardProps> = ({
currentProvider,
modelItem,
onOpenModal,
onOperate,
}) => {
const { locale } = useContext(I18n)
const { t } = useTranslation()
const custom = currentProvider?.providers.find(p => p.provider_type === 'custom') as ProviderWithConfig
return (
<div className='rounded-xl border-[0.5px] border-gray-200 shadow-xs'>
<div className={`flex px-4 pt-4 pb-3 rounded-t-lg ${modelItem.bgColor}`}>
<div className='grow mr-3'>
<div className='mb-1'>
{modelItem.titleIcon[locale]}
</div>
<div className='h-9 text-xs text-black opacity-60'>{modelItem.desc?.[locale]}</div>
</div>
{modelItem.subTitleIcon}
</div>
{
!IS_CE_EDITION && currentProvider && <Quota currentProvider={currentProvider} />
}
{
custom?.is_valid
? (
<div className='flex items-center px-4 h-12'>
<Indicator color='green' className='mr-2' />
<div className='grow text-[13px] font-medium text-gray-700'>API key</div>
<div
className='mr-1 px-2 leading-6 rounded-md text-xs font-medium text-gray-500 hover:bg-gray-50 cursor-pointer'
onClick={() => onOpenModal(custom?.config)}
>
{t('common.operation.edit')}
</div>
<Selector
onOperate={onOperate}
value={currentProvider?.preferred_provider_type}
hiddenOptions={IS_CE_EDITION}
/>
</div>
)
: (
<div
className='inline-flex items-center px-4 h-12 text-gray-500 cursor-pointer hover:text-primary-600'
onClick={() => onOpenModal()}
>
<Plus className='mr-1.5 w-4 h-4'/>
<div className='text-xs font-medium'>{t('common.modelProvider.addApiKey')}</div>
</div>
)
}
</div>
)
}
export default ModelCard

View File

@@ -0,0 +1,69 @@
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import Indicator from '../../../indicator'
import Selector from '../selector'
import type { Model, ProviderEnum } from '../declarations'
import { ProviderEnum as ProviderEnumValue } from '../declarations'
import Button from '@/app/components/base/button'
type CardProps = {
providerType: ProviderEnum
models: any[]
onOpenModal: (v: any) => void
onOperate: (v: Record<string, any>) => void
}
const Card: FC<CardProps> = ({
providerType,
models,
onOpenModal,
onOperate,
}) => {
const { t } = useTranslation()
const renderDesc = (model: Model) => {
if (providerType === ProviderEnumValue.azure_openai)
return model.config.openai_api_base
if (providerType === ProviderEnumValue.replicate)
return `version: ${model.config.model_version}`
if (providerType === ProviderEnumValue.huggingface_hub)
return model.config.huggingfacehub_endpoint_url
}
return (
<div className='px-3 pb-3'>
{
models.map((model: Model) => (
<div key={`${model.model_name}-${model.model_type}`} className='flex mb-1 px-3 py-2 bg-white rounded-lg shadow-xs last:mb-0'>
<div className='grow'>
<div className='flex items-center mb-0.5 h-[18px] text-[13px] font-medium text-gray-700'>
{model.model_name}
<div className='ml-2 px-1.5 rounded-md border border-[rgba(0,0,0,0.08)] text-xs text-gray-600'>{model.model_type}</div>
</div>
<div className='text-xs text-gray-500'>
{renderDesc(model)}
</div>
</div>
<div className='flex items-center'>
<Indicator className='mr-3' />
<Button
className='mr-1 !px-3 !h-7 rounded-md bg-white !text-xs font-medium text-gray-700'
onClick={() => onOpenModal({ model_name: model.model_name, model_type: model.model_type, ...model.config })}
>
{t('common.operation.edit')}
</Button>
<Selector
hiddenOptions
onOperate={v => onOperate({ ...v, value: model })}
className={open => `${open && '!bg-gray-100 shadow-none'} flex justify-center items-center w-7 h-7 bg-white rounded-md border-[0.5px] border-gray-200 shadow-xs cursor-pointer hover:bg-gray-100`}
deleteText={t('common.operation.remove') || ''}
/>
</div>
</div>
))
}
</div>
)
}
export default Card

View File

@@ -0,0 +1,28 @@
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
type QuotaCardProps = {
remainTokens: number
}
const QuotaCard: FC<QuotaCardProps> = ({
remainTokens,
}) => {
const { t } = useTranslation()
return (
<div className='px-3 pb-3'>
<div className='px-3 py-2 bg-white rounded-lg shadow-xs last:mb-0'>
<div className='flex items-center h-[18px] text-xs font-medium text-gray-500'>
{t('common.modelProvider.item.freeQuota')}
</div>
<div className='flex items-center h-5 text-sm font-medium text-gray-700'>
{remainTokens}
<div className='ml-1 font-normal'>Tokens</div>
</div>
</div>
</div>
)
}
export default QuotaCard

View File

@@ -0,0 +1,91 @@
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import type { FormValue, Provider, ProviderConfigItem, ProviderWithConfig, ProviderWithQuota } from '../declarations'
import Indicator from '../../../indicator'
import Selector from '../selector'
import I18n from '@/context/i18n'
import Button from '@/app/components/base/button'
import { IS_CE_EDITION } from '@/config'
type SettingProps = {
currentProvider?: Provider
modelItem: ProviderConfigItem
onOpenModal: (v?: FormValue) => void
onOperate: (v: Record<string, any>) => void
}
const Setting: FC<SettingProps> = ({
currentProvider,
modelItem,
onOpenModal,
onOperate,
}) => {
const { locale } = useContext(I18n)
const { t } = useTranslation()
const configurable = currentProvider?.model_flexibility === 'configurable'
const systemFree = currentProvider?.providers.find(p => p.provider_type === 'system' && (p as ProviderWithQuota).quota_type === 'free') as ProviderWithQuota
const custom = currentProvider?.providers.find(p => p.provider_type === 'custom') as ProviderWithConfig
return (
<div className='flex items-center'>
{
modelItem.disable && !IS_CE_EDITION && (
<div className='flex items-center text-xs text-gray-500'>
{modelItem.disable.tip[locale]}
<a
className={`${locale === 'en' && 'ml-1'} text-primary-600 cursor-pointer`}
href={modelItem.disable.link.href[locale]}
target='_blank'
>
{modelItem.disable.link.label[locale]}
</a>
<div className='mx-2 w-[1px] h-4 bg-black/5' />
</div>
)
}
{
configurable && (
<Button
className={`!px-3 !h-7 rounded-md bg-white !text-xs font-medium text-gray-700 ${!!modelItem.disable && '!text-gray-300'}`}
onClick={() => onOpenModal()}
>
{t('common.operation.add')}
</Button>
)
}
{
!configurable && custom?.config && (
<div className='flex items-center'>
<Indicator className='mr-3' />
<Button
className='mr-1 !px-3 !h-7 rounded-md bg-white !text-xs font-medium text-gray-700'
onClick={() => onOpenModal(custom.config)}
>
{t('common.operation.edit')}
</Button>
<Selector
hiddenOptions={!systemFree?.is_valid || IS_CE_EDITION}
value={currentProvider?.preferred_provider_type}
onOperate={onOperate}
className={open => `${open && '!bg-gray-100 shadow-none'} flex justify-center items-center w-7 h-7 bg-white rounded-md border-[0.5px] border-gray-200 shadow-xs cursor-pointer hover:bg-gray-100`}
/>
</div>
)
}
{
!configurable && !custom?.config && (
<Button
className={`!px-3 !h-7 rounded-md bg-white !text-xs font-medium text-gray-700 ${!!modelItem.disable && !IS_CE_EDITION && '!text-gray-300'}`}
onClick={() => onOpenModal()}
disabled={!!modelItem.disable && !IS_CE_EDITION}
>
{t('common.operation.setup')}
</Button>
)
}
</div>
)
}
export default Setting

View File

@@ -0,0 +1,4 @@
.vender {
background: linear-gradient(131deg, #2250F2 0%, #0EBCF3 100%);
background-clip: text;
}

View File

@@ -0,0 +1,71 @@
import type { FC } from 'react'
import { useContext } from 'use-context-selector'
import type {
FormValue,
Provider,
ProviderConfigItem,
ProviderWithModels,
ProviderWithQuota,
} from '../declarations'
import Setting from './Setting'
import Card from './Card'
import QuotaCard from './QuotaCard'
import I18n from '@/context/i18n'
import { IS_CE_EDITION } from '@/config'
type ModelItemProps = {
currentProvider?: Provider
modelItem: ProviderConfigItem
onOpenModal: (v?: FormValue) => void
onOperate: (v: Record<string, any>) => void
onUpdate: () => void
}
const ModelItem: FC<ModelItemProps> = ({
currentProvider,
modelItem,
onOpenModal,
onOperate,
}) => {
const { locale } = useContext(I18n)
const custom = currentProvider?.providers.find(p => p.provider_type === 'custom') as ProviderWithModels
const systemFree = currentProvider?.providers.find(p => p.provider_type === 'system' && (p as ProviderWithQuota).quota_type === 'free') as ProviderWithQuota
return (
<div className='mb-2 bg-gray-50 rounded-xl'>
<div className='flex justify-between items-center px-4 h-14'>
<div className='flex items-center'>
{modelItem.titleIcon[locale]}
{
modelItem.hit && (
<div className='ml-2 text-xs text-gray-500'>{modelItem.hit[locale]}</div>
)
}
</div>
<Setting
currentProvider={currentProvider}
modelItem={modelItem}
onOpenModal={onOpenModal}
onOperate={onOperate}
/>
</div>
{
!!custom?.models?.length && (
<Card
providerType={modelItem.key}
models={custom?.models}
onOpenModal={onOpenModal}
onOperate={onOperate}
/>
)
}
{
systemFree?.is_valid && !IS_CE_EDITION && (
<QuotaCard remainTokens={systemFree.quota_limit - systemFree.quota_used}/>
)
}
</div>
)
}
export default ModelItem

View File

@@ -0,0 +1,177 @@
import { useEffect, useState } from 'react'
import type { Dispatch, FC, SetStateAction } from 'react'
import { useContext } from 'use-context-selector'
import type { Field, FormValue, ProviderConfigModal } from '../declarations'
import { useValidate } from '../../key-validator/hooks'
import { ValidatingTip } from '../../key-validator/ValidateStatus'
import { validateModelProviderFn } from '../utils'
import Input from './Input'
import I18n from '@/context/i18n'
import { SimpleSelect } from '@/app/components/base/select'
type FormProps = {
modelModal?: ProviderConfigModal
initValue?: FormValue
fields: Field[]
onChange: (v: FormValue) => void
onValidatedError: (v: string) => void
mode: string
cleared: boolean
onClearedChange: Dispatch<SetStateAction<boolean>>
onValidating: (validating: boolean) => void
}
const nameClassName = `
py-2 text-sm text-gray-900
`
const Form: FC<FormProps> = ({
modelModal,
initValue = {},
fields,
onChange,
onValidatedError,
mode,
cleared,
onClearedChange,
onValidating,
}) => {
const { locale } = useContext(I18n)
const [value, setValue] = useState(initValue)
const [validate, validating, validatedStatusState] = useValidate(value)
const [changeKey, setChangeKey] = useState('')
useEffect(() => {
onValidatedError(validatedStatusState.message || '')
}, [validatedStatusState, onValidatedError])
useEffect(() => {
onValidating(validating)
}, [validating, onValidating])
const updateValue = (v: FormValue) => {
setValue(v)
onChange(v)
}
const handleMultiFormChange = (v: FormValue, newChangeKey: string) => {
updateValue(v)
setChangeKey(newChangeKey)
const validateKeys = (typeof modelModal?.validateKeys === 'function' ? modelModal?.validateKeys(v) : modelModal?.validateKeys) || []
if (validateKeys.length) {
validate({
before: () => {
for (let i = 0; i < validateKeys.length; i++) {
if (!v[validateKeys[i]])
return false
}
return true
},
run: () => {
return validateModelProviderFn(modelModal!.key, v)
},
})
}
}
const handleClear = (saveValue?: FormValue) => {
const needClearFields = modelModal?.fields.filter(field => field.type !== 'radio')
const newValue: Record<string, string> = {}
needClearFields?.forEach((field) => {
newValue[field.key] = ''
})
updateValue({ ...value, ...newValue, ...saveValue })
onClearedChange(true)
}
const handleFormChange = (k: string, v: string) => {
if (mode === 'edit' && !cleared)
handleClear({ [k]: v })
else
handleMultiFormChange({ ...value, [k]: v }, k)
}
const handleFocus = () => {
if (mode === 'edit' && !cleared)
handleClear()
}
const renderField = (field: Field) => {
const hidden = typeof field.hidden === 'function' ? field.hidden(value) : field.hidden
if (hidden)
return null
if (field.type === 'text') {
return (
<div key={field.key} className='py-3'>
<div className={nameClassName}>{field.label[locale]}</div>
<Input
field={field}
value={value}
onChange={v => handleMultiFormChange(v, field.key)}
onFocus={handleFocus}
validatedStatusState={validatedStatusState}
/>
{validating && changeKey === field.key && <ValidatingTip />}
</div>
)
}
if (field.type === 'radio') {
const options = typeof field.options === 'function' ? field.options(value) : field.options
return (
<div key={field.key} className='py-3'>
<div className={nameClassName}>{field.label[locale]}</div>
<div className='grid grid-cols-2 gap-3'>
{
options?.map(option => (
<div
className={`
flex items-center px-3 h-9 rounded-lg border border-gray-100 bg-gray-25 cursor-pointer
${value?.[field.key] === option.key && 'bg-white border-[1.5px] border-primary-400 shadow-sm'}
`}
onClick={() => handleFormChange(field.key, option.key)}
key={`${field.key}-${option.key}`}
>
<div className={`
flex justify-center items-center mr-2 w-4 h-4 border border-gray-300 rounded-full
${value?.[field.key] === option.key && 'border-[5px] border-primary-600'}
`} />
<div className='text-sm text-gray-900'>{option.label[locale]}</div>
</div>
))
}
</div>
{validating && changeKey === field.key && <ValidatingTip />}
</div>
)
}
if (field.type === 'select') {
const options = typeof field.options === 'function' ? field.options(value) : field.options
return (
<div key={field.key} className='py-3'>
<div className={nameClassName}>{field.label[locale]}</div>
<SimpleSelect
defaultValue={value[field.key]}
items={options!.map(option => ({ value: option.key, name: option.label[locale] }))}
onSelect={item => handleFormChange(field.key, item.value as string)}
/>
{validating && changeKey === field.key && <ValidatingTip />}
</div>
)
}
}
return (
<div>
{
fields.map(field => renderField(field))
}
</div>
)
}
export default Form

View File

@@ -0,0 +1,58 @@
import type { FC } from 'react'
import { useContext } from 'use-context-selector'
import type { Field, FormValue } from '../declarations'
import { ValidatedSuccessIcon } from '../../key-validator/ValidateStatus'
import { ValidatedStatus } from '../../key-validator/declarations'
import type { ValidatedStatusState } from '../../key-validator/declarations'
import I18n from '@/context/i18n'
type InputProps = {
field: Field
value: FormValue
onChange: (v: FormValue) => void
onFocus: () => void
validatedStatusState: ValidatedStatusState
}
const Input: FC<InputProps> = ({
field,
value,
onChange,
onFocus,
validatedStatusState,
}) => {
const { locale } = useContext(I18n)
const showValidatedIcon = validatedStatusState.status === ValidatedStatus.Success && value[field.key]
const getValidatedIcon = () => {
if (showValidatedIcon)
return <div className='absolute top-2.5 right-2.5'><ValidatedSuccessIcon /></div>
}
const handleChange = (v: string) => {
const newFormValue = { ...value, [field.key]: v }
onChange(newFormValue)
}
return (
<div className='relative'>
<input
tabIndex={-1}
className={`
block px-3 w-full h-9 bg-gray-100 text-sm rounded-lg border border-transparent
appearance-none outline-none caret-primary-600
hover:border-[rgba(0,0,0,0.08)] hover:bg-gray-50
focus:bg-white focus:border-gray-300 focus:shadow-xs
placeholder:text-sm placeholder:text-gray-400
${showValidatedIcon && 'pr-[30px]'}
`}
placeholder={field?.placeholder?.[locale] || ''}
onChange={e => handleChange(e.target.value)}
onFocus={onFocus}
value={value[field.key] || ''}
/>
{getValidatedIcon()}
</div>
)
}
export default Input

View File

@@ -0,0 +1,165 @@
import { useCallback, useState } from 'react'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { Portal } from '@headlessui/react'
import type { FormValue, ProviderConfigModal } from '../declarations'
import { ConfigurableProviders } from '../utils'
import Form from './Form'
import I18n from '@/context/i18n'
import Button from '@/app/components/base/button'
import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
import { AlertCircle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import { useEventEmitterContextContext } from '@/context/event-emitter'
type ModelModalProps = {
isShow: boolean
onCancel: () => void
modelModal?: ProviderConfigModal
onSave: (v?: FormValue) => void
mode: string
}
const ModelModal: FC<ModelModalProps> = ({
isShow,
onCancel,
modelModal,
onSave,
mode,
}) => {
const { t } = useTranslation()
const { locale } = useContext(I18n)
const { eventEmitter } = useEventEmitterContextContext()
const [value, setValue] = useState<FormValue | undefined>()
const [loading, setLoading] = useState(false)
const [errorMessage, setErrorMessage] = useState('')
const [cleared, setCleared] = useState(false)
const [prevIsShow, setPrevIsShow] = useState(isShow)
const [validating, setValidating] = useState(false)
if (prevIsShow !== isShow) {
setCleared(false)
setPrevIsShow(isShow)
}
eventEmitter?.useSubscription((v) => {
if (v === 'provider-save')
setLoading(true)
else
setLoading(false)
})
const handleValidatedError = useCallback((newErrorMessage: string) => {
setErrorMessage(newErrorMessage)
}, [])
const handleValidating = useCallback((newValidating: boolean) => {
setValidating(newValidating)
}, [])
const validateRequiredValue = () => {
const validateValue = value || modelModal?.defaultValue
if (modelModal) {
const { fields } = modelModal
const requiredFields = fields.filter(field => !(typeof field.hidden === 'function' ? field.hidden(validateValue) : field.hidden) && field.required)
for (let i = 0; i < requiredFields.length; i++) {
const currentField = requiredFields[i]
if (!validateValue?.[currentField.key]) {
setErrorMessage(t('appDebug.errorMessage.valueOfVarRequired', { key: currentField.label[locale] }) || '')
return false
}
}
return true
}
}
const handleSave = () => {
if (validateRequiredValue())
onSave(value || modelModal?.defaultValue)
}
const renderTitlePrefix = () => {
let prefix
if (mode === 'edit')
prefix = t('common.operation.edit')
else
prefix = ConfigurableProviders.includes(modelModal!.key) ? t('common.operation.create') : t('common.operation.setup')
return `${prefix} ${modelModal?.title[locale]}`
}
if (!isShow)
return null
return (
<Portal>
<div className='fixed inset-0 flex items-center justify-center bg-black/[.25]'>
<div className='w-[640px] max-h-screen bg-white shadow-xl rounded-2xl overflow-y-auto'>
<div className='px-8 pt-8'>
<div className='flex justify-between items-center mb-2'>
<div className='text-xl font-semibold text-gray-900'>{renderTitlePrefix()}</div>
{modelModal?.icon}
</div>
<Form
modelModal={modelModal}
fields={modelModal?.fields || []}
initValue={modelModal?.defaultValue}
onChange={newValue => setValue(newValue)}
onValidatedError={handleValidatedError}
mode={mode}
cleared={cleared}
onClearedChange={setCleared}
onValidating={handleValidating}
/>
<div className='flex justify-between items-center py-6'>
<a
href={modelModal?.link.href}
target='_blank'
className='inline-flex items-center text-xs text-primary-600'
>
{modelModal?.link.label[locale]}
<LinkExternal02 className='ml-1 w-3 h-3' />
</a>
<div>
<Button className='mr-2 !h-9 !text-sm font-medium text-gray-700' onClick={onCancel}>{t('common.operation.cancel')}</Button>
<Button
className='!h-9 !text-sm font-medium'
type='primary'
onClick={handleSave}
disabled={loading || (mode === 'edit' && !cleared) || validating}
>
{t('common.operation.save')}
</Button>
</div>
</div>
</div>
<div className='border-t-[0.5px] border-t-[rgba(0,0,0,0.05)]'>
{
errorMessage
? (
<div className='flex px-[10px] py-3 bg-[#FEF3F2] text-xs text-[#D92D20]'>
<AlertCircle className='mt-[1px] mr-2 w-[14px] h-[14px]' />
{errorMessage}
</div>
)
: (
<div className='flex justify-center items-center py-3 bg-gray-50 text-xs text-gray-500'>
<Lock01 className='mr-1 w-3 h-3 text-gray-500' />
{t('common.modelProvider.encrypted.front')}
<a
className='text-primary-600 mx-1'
target={'_blank'}
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
>
PKCS1_OAEP
</a>
{t('common.modelProvider.encrypted.back')}
</div>
)
}
</div>
</div>
</div>
</Portal>
)
}
export default ModelModal

View File

@@ -0,0 +1,216 @@
import type { FC } from 'react'
import { Fragment, useState } from 'react'
import { Popover, Transition } from '@headlessui/react'
import { useTranslation } from 'react-i18next'
import _ from 'lodash-es'
import cn from 'classnames'
import type { BackendModel, ProviderEnum } from '@/app/components/header/account-setting/model-page/declarations'
import { ModelType } from '@/app/components/header/account-setting/model-page/declarations'
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
import { Check, SearchLg } from '@/app/components/base/icons/src/vender/line/general'
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
import { AlertCircle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
import Tooltip from '@/app/components/base/tooltip'
import ModelIcon from '@/app/components/app/configuration/config-model/model-icon'
import ModelName, { supportI18nModelName } from '@/app/components/app/configuration/config-model/model-name'
import ProviderName from '@/app/components/app/configuration/config-model/provider-name'
import { useProviderContext } from '@/context/provider-context'
type Props = {
value: {
providerName: ProviderEnum
modelName: string
} | undefined
modelType: ModelType
supportAgentThought?: boolean
onChange: (value: BackendModel) => void
popClassName?: string
readonly?: boolean
triggerIconSmall?: boolean
}
const ModelSelector: FC<Props> = ({
value,
modelType,
supportAgentThought,
onChange,
popClassName,
readonly,
triggerIconSmall,
}) => {
const { t } = useTranslation()
const { textGenerationModelList, embeddingsModelList, speech2textModelList, agentThoughtModelList } = useProviderContext()
const [search, setSearch] = useState('')
const modelList = supportAgentThought
? agentThoughtModelList
: ({
[ModelType.textGeneration]: textGenerationModelList,
[ModelType.embeddings]: embeddingsModelList,
[ModelType.speech2text]: speech2textModelList,
})[modelType]
const allModelNames = (() => {
if (!search)
return {}
const res: Record<string, string> = {}
modelList.forEach(({ model_name }) => {
res[model_name] = supportI18nModelName.includes(model_name) ? t(`common.modelName.${model_name}`) : model_name
})
return res
})()
const filteredModelList = search
? modelList.filter(({ model_name }) => {
if (allModelNames[model_name].includes(search))
return true
return false
})
: modelList
const hasRemoved = value && !modelList.find(({ model_name }) => model_name === value.modelName)
const modelOptions: any[] = (() => {
const providers = _.uniq(filteredModelList.map(item => item.model_provider.provider_name))
const res: any[] = []
providers.forEach((providerName) => {
res.push({
type: 'provider',
value: providerName,
})
const models = filteredModelList.filter(m => m.model_provider.provider_name === providerName)
models.forEach(({ model_name }) => {
res.push({
type: 'model',
providerName,
value: model_name,
})
})
})
return res
})()
return (
<div className=''>
<Popover className='relative'>
<Popover.Button className={cn('flex items-center px-2.5 w-full h-9 rounded-lg', readonly ? '!cursor-auto' : 'bg-gray-100', hasRemoved && '!bg-[#FEF3F2]')}>
{
({ open }) => (
<>
{
value
? (
<>
<ModelIcon
className={cn('mr-1.5', !triggerIconSmall && 'w-5 h-5')}
modelId={value.modelName}
providerName={value.providerName}
/>
<div className='mr-1.5 grow text-left text-sm text-gray-900 truncate'><ModelName modelId={value.modelName} /></div>
</>
)
: (
<div className='grow text-left text-sm text-gray-800 opacity-60'>{t('common.modelProvider.selectModel')}</div>
)
}
{
hasRemoved && (
<Tooltip
selector='model-selector-remove-tip'
htmlContent={
<div className='w-[261px] text-gray-500'>{t('common.modelProvider.selector.tip')}</div>
}
>
<AlertCircle className='mr-1 w-4 h-4 text-[#F04438]' />
</Tooltip>
)
}
{!readonly && <ChevronDown className={`w-4 h-4 text-gray-700 ${open ? 'opacity-100' : 'opacity-60'}`} />}
</>
)
}
</Popover.Button>
{!readonly && (
<Transition
as={Fragment}
leave='transition ease-in duration-100'
leaveFrom='opacity-100'
leaveTo='opacity-0'
>
<Popover.Panel className={cn(popClassName, 'absolute top-10 p-1 min-w-[232px] max-w-[260px] max-h-[366px] bg-white border-[0.5px] border-gray-200 rounded-lg shadow-lg overflow-auto z-10')}>
<div className='px-2 pt-2 pb-1'>
<div className='flex items-center px-2 h-8 bg-gray-100 rounded-lg'>
<div className='mr-1.5 p-[1px]'><SearchLg className='w-[14px] h-[14px] text-gray-400' /></div>
<div className='grow px-0.5'>
<input
value={search}
onChange={e => setSearch(e.target.value)}
className={`
block w-full h-8 bg-transparent text-[13px] text-gray-700
outline-none appearance-none border-none
`}
placeholder={t('common.modelProvider.searchModel') || ''}
/>
</div>
{
search && (
<div className='ml-1 p-0.5 cursor-pointer' onClick={() => setSearch('')}>
<XCircle className='w-3 h-3 text-gray-400' />
</div>
)
}
</div>
</div>
{
modelOptions.map((model: any) => {
if (model.type === 'provider') {
return (
<div
className='px-3 pt-2 pb-1 text-xs font-medium text-gray-500'
key={`${model.type}-${model.value}`}
>
<ProviderName provideName={model.value} />
</div>
)
}
if (model.type === 'model') {
return (
<Popover.Button
key={`${model.providerName}-${model.value}`}
className={`
flex items-center px-3 w-full h-8 rounded-lg hover:bg-gray-50
${!readonly ? 'cursor-pointer' : 'cursor-auto'}
${(value?.providerName === model.providerName && value?.modelName === model.value) && 'bg-gray-50'}
`}
onClick={() => {
const selectedModel = modelList.find((item) => {
return item.model_name === model.value && item.model_provider.provider_name === model.providerName
})
onChange(selectedModel as BackendModel)
}}
>
<ModelIcon
className='mr-2 shrink-0'
modelId={model.value}
providerName={model.providerName}
/>
<div className='grow text-left text-sm text-gray-900 truncate'><ModelName modelId={model.value} /></div>
{ (value?.providerName === model.providerName && value?.modelName === model.value) && <Check className='shrink-0 w-4 h-4 text-primary-600' /> }
</Popover.Button>
)
}
return null
})
}
{(search && filteredModelList.length === 0) && (
<div className='px-3 pt-1.5 h-[30px] text-center text-xs text-gray-500'>{t('common.modelProvider.noModelFound', { model: search })}</div>
)}
</Popover.Panel>
</Transition>
)}
</Popover>
</div>
)
}
export default ModelSelector

View File

@@ -0,0 +1,97 @@
import { Fragment } from 'react'
import type { FC } from 'react'
import { Popover, Transition } from '@headlessui/react'
import { useTranslation } from 'react-i18next'
import { Check, DotsHorizontal, Trash03 } from '@/app/components/base/icons/src/vender/line/general'
const itemClassName = `
flex items-center px-3 h-9 text-sm text-gray-700 rounded-lg cursor-pointer
`
type SelectorProps = {
value?: string
onOperate: (v: Record<string, string>) => void
hiddenOptions?: boolean
className?: (v: boolean) => string
deleteText?: string
}
const Selector: FC<SelectorProps> = ({
value,
onOperate,
hiddenOptions,
className,
deleteText,
}) => {
const { t } = useTranslation()
const options = [
{
key: 'custom',
text: 'API',
},
{
key: 'system',
text: t('common.modelProvider.quota'),
},
]
return (
<Popover className='relative'>
<Popover.Button>
{
({ open }) => (
<div className={`
flex justify-center items-center w-6 h-6 rounded-md hover:bg-gray-50 cursor-pointer
${open && 'bg-gray-50'}
${className && className(open)}
`}>
<DotsHorizontal className='w-3 h-3 text-gray-700' />
</div>
)
}
</Popover.Button>
<Transition
as={Fragment}
leave='transition ease-in duration-100'
leaveFrom='opacity-100'
leaveTo='opacity-0'
>
<Popover.Panel className='absolute top-7 right-0 w-[192px] bg-white border-[0.5px] border-gray-200 rounded-lg shadow-lg z-10'>
{
!hiddenOptions && (
<>
<div className='p-1'>
<div className='px-3 pt-2 pb-1 text-sm font-medium text-gray-700'>{t('common.modelProvider.card.priorityUse')}</div>
{
options.map(option => (
<Popover.Button as={Fragment} key={option.key}>
<div
className={`${itemClassName} hover:bg-gray-50`}
onClick={() => onOperate({ type: 'priority', value: option.key })}>
<div className='grow'>{option.text}</div>
{value === option.key && <Check className='w-4 h-4 text-primary-600' />}
</div>
</Popover.Button>
))
}
</div>
<div className='h-[1px] bg-gray-100' />
</>
)
}
<div className='p-1'>
<Popover.Button as={Fragment}>
<div
className={`group ${itemClassName} hover:bg-[#FEF3F2] hover:text-[#D92D20]`}
onClick={() => onOperate({ type: 'delete' })}>
<Trash03 className='mr-2 w-4 h-4 text-gray-500 group-hover:text-[#D92D20]' />
{deleteText || t('common.modelProvider.card.removeKey')}
</div>
</Popover.Button>
</div>
</Popover.Panel>
</Transition>
</Popover>
)
}
export default Selector

View File

@@ -0,0 +1,35 @@
import { ValidatedStatus } from '../key-validator/declarations'
import { ProviderEnum } from './declarations'
import { validateModelProvider } from '@/service/common'
export const ConfigurableProviders = [ProviderEnum.azure_openai, ProviderEnum.replicate, ProviderEnum.huggingface_hub]
export const validateModelProviderFn = async (providerName: ProviderEnum, v: any) => {
let body, url
if (ConfigurableProviders.includes(providerName)) {
const { model_name, model_type, ...config } = v
body = {
model_name,
model_type,
config,
}
url = `/workspaces/current/model-providers/${providerName}/models/validate`
}
else {
body = {
config: v,
}
url = `/workspaces/current/model-providers/${providerName}/validate`
}
try {
const res = await validateModelProvider({ url, body })
if (res.result === 'success')
return Promise.resolve({ status: ValidatedStatus.Success })
else
return Promise.resolve({ status: ValidatedStatus.Error, message: res.error })
}
catch (e: any) {
return Promise.resolve({ status: ValidatedStatus.Error, message: e.message })
}
}