diff --git a/docSite/content/zh-cn/docs/development/upgrading/4823.md b/docSite/content/zh-cn/docs/development/upgrading/4823.md index e64839395..0ddcb17ce 100644 --- a/docSite/content/zh-cn/docs/development/upgrading/4823.md +++ b/docSite/content/zh-cn/docs/development/upgrading/4823.md @@ -11,6 +11,7 @@ weight: 802 ## 🚀 新增内容 1. 增加默认“知识库文本理解模型”配置 +2. AI proxy V1版,可替换 OneAPI使用,同时提供完整模型调用日志,便于排查问题。 ## ⚙️ 优化 @@ -18,8 +19,11 @@ weight: 802 2. 集合列表数据统计方式,提高大数据量统计性能。 3. 优化数学公式,转义 Latex 格式成 Markdown 格式。 4. 解析文档图片,图片太大时,自动忽略。 +5. 时间选择器,当天开始时间自动设0,结束设置设 23:59:59,避免 UI 与实际逻辑偏差。 +6. 升级 mongoose 库版本依赖。 ## 🐛 修复 1. 标签过滤时,子文件夹未成功过滤。 -2. 暂时移除 md 阅读优化,避免链接分割错误。 \ No newline at end of file +2. 暂时移除 md 阅读优化,避免链接分割错误。 +3. 离开团队时,未刷新成员列表。 \ No newline at end of file diff --git a/packages/global/common/string/time.ts b/packages/global/common/string/time.ts index 51f6b2370..87b612fd3 100644 --- a/packages/global/common/string/time.ts +++ b/packages/global/common/string/time.ts @@ -7,12 +7,14 @@ import { i18nT } from '../../../web/i18n/utils'; dayjs.extend(utc); dayjs.extend(timezone); -export const formatTime2YMDHMW = (time?: Date) => dayjs(time).format('YYYY-MM-DD HH:mm:ss dddd'); -export const formatTime2YMDHMS = (time?: Date) => +export const formatTime2YMDHMW = (time?: Date | number) => + dayjs(time).format('YYYY-MM-DD HH:mm:ss dddd'); +export const formatTime2YMDHMS = (time?: Date | number) => time ? dayjs(time).format('YYYY-MM-DD HH:mm:ss') : ''; -export const formatTime2YMDHM = (time?: Date) => +export const formatTime2YMDHM = (time?: Date | number) => time ? dayjs(time).format('YYYY-MM-DD HH:mm') : ''; -export const formatTime2YMD = (time?: Date) => (time ? dayjs(time).format('YYYY-MM-DD') : ''); +export const formatTime2YMD = (time?: Date | number) => + time ? dayjs(time).format('YYYY-MM-DD') : ''; export const formatTime2HM = (time: Date = new Date()) => dayjs(time).format('HH:mm'); /** diff --git a/packages/global/common/system/types/index.d.ts b/packages/global/common/system/types/index.d.ts index 892beb480..b7f8334ad 100644 --- a/packages/global/common/system/types/index.d.ts +++ b/packages/global/common/system/types/index.d.ts @@ -54,6 +54,7 @@ export type FastGPTFeConfigsType = { show_promotion?: boolean; show_team_chat?: boolean; show_compliance_copywriting?: boolean; + show_aiproxy?: boolean; concatMd?: string; docUrl?: string; diff --git a/packages/service/core/ai/config.ts b/packages/service/core/ai/config.ts index 3fa62eec9..c2fb5688c 100644 --- a/packages/service/core/ai/config.ts +++ b/packages/service/core/ai/config.ts @@ -11,14 +11,17 @@ import { i18nT } from '../../../web/i18n/utils'; import { OpenaiAccountType } from '@fastgpt/global/support/user/team/type'; import { getLLMModel } from './model'; -export const openaiBaseUrl = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1'; +const aiProxyBaseUrl = process.env.AIPROXY_API_ENDPOINT + ? `${process.env.AIPROXY_API_ENDPOINT}/v1` + : undefined; +const openaiBaseUrl = aiProxyBaseUrl || process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1'; +const openaiBaseKey = process.env.AIPROXY_API_TOKEN || process.env.CHAT_API_KEY || ''; export const getAIApi = (props?: { userKey?: OpenaiAccountType; timeout?: number }) => { const { userKey, timeout } = props || {}; const baseUrl = userKey?.baseUrl || global?.systemEnv?.oneapiUrl || openaiBaseUrl; - const apiKey = userKey?.key || global?.systemEnv?.chatApiKey || process.env.CHAT_API_KEY || ''; - + const apiKey = userKey?.key || global?.systemEnv?.chatApiKey || openaiBaseKey; return new OpenAI({ baseURL: baseUrl, apiKey, @@ -72,6 +75,7 @@ export const createChatCompletion = async ({ userKey, timeout: formatTimeout }); + const response = await ai.chat.completions.create(body, { ...options, ...(modelConstantsData.requestUrl ? { path: modelConstantsData.requestUrl } : {}), diff --git a/packages/service/support/user/team/controller.ts b/packages/service/support/user/team/controller.ts index d48d9bf0b..a3d6fe6d5 100644 --- a/packages/service/support/user/team/controller.ts +++ b/packages/service/support/user/team/controller.ts @@ -15,7 +15,7 @@ import { TeamDefaultPermissionVal } from '@fastgpt/global/support/permission/use import { MongoMemberGroupModel } from '../../permission/memberGroup/memberGroupSchema'; import { mongoSessionRun } from '../../../common/mongo/sessionRun'; import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant'; -import { getAIApi, openaiBaseUrl } from '../../../core/ai/config'; +import { getAIApi } from '../../../core/ai/config'; import { createRootOrg } from '../../permission/org/controllers'; import { refreshSourceAvatar } from '../../../common/file/image/controller'; @@ -152,7 +152,7 @@ export async function updateTeam({ // auth openai key if (openaiAccount?.key) { console.log('auth user openai key', openaiAccount?.key); - const baseUrl = openaiAccount?.baseUrl || openaiBaseUrl; + const baseUrl = openaiAccount?.baseUrl || 'https://api.openai.com/v1'; openaiAccount.baseUrl = baseUrl; const ai = getAIApi({ diff --git a/packages/web/components/common/DateRangePicker/index.tsx b/packages/web/components/common/DateRangePicker/index.tsx index b033cfab8..f62766dde 100644 --- a/packages/web/components/common/DateRangePicker/index.tsx +++ b/packages/web/components/common/DateRangePicker/index.tsx @@ -100,6 +100,13 @@ const DateRangePicker = ({ if (date?.to === undefined) { date.to = date.from; } + + if (date?.from) { + date.from = new Date(date.from.setHours(0, 0, 0, 0)); + } + if (date?.to) { + date.to = new Date(date.to.setHours(23, 59, 59, 999)); + } setRange(date); onChange?.(date); }} diff --git a/packages/web/components/common/Icon/constants.ts b/packages/web/components/common/Icon/constants.ts index b40c72eb1..3d1d713b9 100644 --- a/packages/web/components/common/Icon/constants.ts +++ b/packages/web/components/common/Icon/constants.ts @@ -1,4 +1,5 @@ // @ts-nocheck + export const iconPaths = { book: () => import('./icons/book.svg'), change: () => import('./icons/change.svg'), @@ -32,8 +33,10 @@ export const iconPaths = { 'common/customTitleLight': () => import('./icons/common/customTitleLight.svg'), 'common/data': () => import('./icons/common/data.svg'), 'common/dingtalkFill': () => import('./icons/common/dingtalkFill.svg'), + 'common/disable': () => import('./icons/common/disable.svg'), 'common/downArrowFill': () => import('./icons/common/downArrowFill.svg'), 'common/editor/resizer': () => import('./icons/common/editor/resizer.svg'), + 'common/enable': () => import('./icons/common/enable.svg'), 'common/errorFill': () => import('./icons/common/errorFill.svg'), 'common/file/move': () => import('./icons/common/file/move.svg'), 'common/folderFill': () => import('./icons/common/folderFill.svg'), diff --git a/packages/web/components/common/Icon/icons/common/disable.svg b/packages/web/components/common/Icon/icons/common/disable.svg new file mode 100644 index 000000000..7d226c71c --- /dev/null +++ b/packages/web/components/common/Icon/icons/common/disable.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/common/enable.svg b/packages/web/components/common/Icon/icons/common/enable.svg new file mode 100644 index 000000000..d381622de --- /dev/null +++ b/packages/web/components/common/Icon/icons/common/enable.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/web/components/common/Input/NumberInput/index.tsx b/packages/web/components/common/Input/NumberInput/index.tsx index 3f87817bd..fb817d8e5 100644 --- a/packages/web/components/common/Input/NumberInput/index.tsx +++ b/packages/web/components/common/Input/NumberInput/index.tsx @@ -10,8 +10,9 @@ import React from 'react'; import MyIcon from '../../Icon'; import { UseFormRegister } from 'react-hook-form'; -type Props = Omit & { +type Props = Omit & { onChange?: (e?: number) => any; + onBlur?: (e?: number) => any; placeholder?: string; register?: UseFormRegister; name?: string; @@ -19,11 +20,21 @@ type Props = Omit & { }; const MyNumberInput = (props: Props) => { - const { register, name, onChange, placeholder, bg, ...restProps } = props; + const { register, name, onChange, onBlur, placeholder, bg, ...restProps } = props; return ( { + if (!onBlur) return; + const numE = Number(e.target.value); + if (isNaN(numE)) { + // @ts-ignore + onBlur(''); + } else { + onBlur(numE); + } + }} onChange={(e) => { if (!onChange) return; const numE = Number(e); @@ -38,6 +49,8 @@ const MyNumberInput = (props: Props) => { ({ return ( ({ whiteSpace={'pre-wrap'} fontSize={'sm'} gap={2} + {...menuItemStyles} > {item.icon && } @@ -204,6 +204,7 @@ const MultipleSelect = ({ }} onClick={(e) => { e.stopPropagation(); + e.preventDefault(); onclickItem(item.value); }} /> @@ -230,7 +231,6 @@ const MultipleSelect = ({ overflowY={'auto'} > { e.stopPropagation(); @@ -241,6 +241,7 @@ const MultipleSelect = ({ fontSize={'sm'} gap={2} mb={1} + {...menuItemStyles} > {t('common:common.All')} diff --git a/packages/web/components/common/MySelect/index.tsx b/packages/web/components/common/MySelect/index.tsx index c9eb6230d..d6055b7c1 100644 --- a/packages/web/components/common/MySelect/index.tsx +++ b/packages/web/components/common/MySelect/index.tsx @@ -4,7 +4,8 @@ import React, { useMemo, useEffect, useImperativeHandle, - ForwardedRef + ForwardedRef, + useState } from 'react'; import { Menu, @@ -15,7 +16,8 @@ import { MenuButton, Box, css, - Flex + Flex, + Input } from '@chakra-ui/react'; import type { ButtonProps, MenuItemProps } from '@chakra-ui/react'; import MyIcon from '../Icon'; @@ -33,8 +35,10 @@ import { useScrollPagination } from '../../../hooks/useScrollPagination'; export type SelectProps = ButtonProps & { value?: T; placeholder?: string; + isSearch?: boolean; list: { alias?: string; + icon?: string; label: string | React.ReactNode; description?: string; value: T; @@ -49,6 +53,7 @@ const MySelect = ( { placeholder, value, + isSearch = false, width = '100%', list = [], onchange, @@ -63,6 +68,7 @@ const MySelect = ( const ButtonRef = useRef(null); const MenuListRef = useRef(null); const SelectedItemRef = useRef(null); + const SearchInputRef = useRef(null); const menuItemStyles: MenuItemProps = { borderRadius: 'sm', @@ -79,6 +85,18 @@ const MySelect = ( const { isOpen, onOpen, onClose } = useDisclosure(); const selectItem = useMemo(() => list.find((item) => item.value === value), [list, value]); + const [search, setSearch] = useState(''); + const filterList = useMemo(() => { + if (!isSearch || !search) { + return list; + } + return list.filter((item) => { + const text = `${item.label?.toString()}${item.alias}${item.value}`; + const regx = new RegExp(search, 'gi'); + return regx.test(text); + }); + }, [list, search, isSearch]); + useImperativeHandle(ref, () => ({ focus() { onOpen(); @@ -90,17 +108,19 @@ const MySelect = ( const menu = MenuListRef.current; const selectedItem = SelectedItemRef.current; menu.scrollTop = selectedItem.offsetTop - menu.offsetTop - 100; + + if (isSearch) { + setSearch(''); + } } - }, [isOpen]); + }, [isSearch, isOpen]); const { runAsync: onChange, loading } = useRequest2((val: T) => onchange?.(val)); - const isSelecting = loading || isLoading; - const ListRender = useMemo(() => { return ( <> - {list.map((item, i) => ( + {filterList.map((item, i) => ( ( fontSize={'sm'} display={'block'} > - {item.label} + + {item.icon && } + {item.label} + {item.description && ( {item.description} @@ -135,7 +158,9 @@ const MySelect = ( ))} ); - }, [list, value]); + }, [filterList, value]); + + const isSelecting = loading || isLoading; return ( ( {...props} > - {isSelecting && } - {selectItem?.alias || selectItem?.label || placeholder} + {isSelecting && } + {isSearch && isOpen ? ( + setSearch(e.target.value)} + placeholder={ + selectItem?.alias || + (typeof selectItem?.label === 'string' ? selectItem?.label : placeholder) + } + size={'sm'} + w={'100%'} + color={'myGray.700'} + onBlur={() => { + setTimeout(() => { + SearchInputRef?.current?.focus(); + }, 0); + }} + /> + ) : ( + <> + {selectItem?.icon && } + {selectItem?.alias || selectItem?.label || placeholder} + + )} diff --git a/packages/web/hooks/useScrollPagination.tsx b/packages/web/hooks/useScrollPagination.tsx index a326a20b7..c0ce453c6 100644 --- a/packages/web/hooks/useScrollPagination.tsx +++ b/packages/web/hooks/useScrollPagination.tsx @@ -217,7 +217,7 @@ export function useScrollPagination< const offset = init ? 0 : data.length; setTrue(); - + console.log(offset); try { const res = await api({ offset, diff --git a/packages/web/i18n/en/account_model.json b/packages/web/i18n/en/account_model.json new file mode 100644 index 000000000..6e95eb545 --- /dev/null +++ b/packages/web/i18n/en/account_model.json @@ -0,0 +1,46 @@ +{ + "api_key": "API key", + "azure": "Azure", + "base_url": "Base url", + "channel_name": "Channel", + "channel_priority": "Priority", + "channel_priority_tip": "The higher the priority channel, the easier it is to be requested", + "channel_status": "state", + "channel_status_auto_disabled": "Automatically disable", + "channel_status_disabled": "Disabled", + "channel_status_enabled": "Enable", + "channel_status_unknown": "unknown", + "channel_type": "Manufacturer", + "clear_model": "Clear the model", + "copy_model_id_success": "Copyed model id", + "create_channel": "Added channels", + "default_url": "Default address", + "detail": "Detail", + "duration": "Duration", + "edit": "edit", + "edit_channel": "Channel configuration", + "enable_channel": "Enable", + "forbid_channel": "Disabled", + "key_type": "API key format:", + "log": "Call log", + "log_detail": "Log details", + "log_status": "Status", + "mapping": "Model Mapping", + "mapping_tip": "A valid Json is required. \nThe model can be mapped when sending a request to the actual address. \nFor example:\n{\n \n \"gpt-4o\": \"gpt-4o-test\"\n\n}\n\nWhen FastGPT requests the gpt-4o model, the gpt-4o-test model is sent to the actual address, instead of gpt-4o.", + "model": "Model", + "model_name": "Model name", + "model_test": "Model testing", + "model_tokens": "Input/Output tokens", + "request_at": "Request time", + "request_duration": "Request duration: {{duration}}s", + "running_test": "In testing", + "search_model": "Search for models", + "select_channel": "Select a channel name", + "select_model": "Select a model", + "select_model_placeholder": "Select the model available under this channel", + "select_provider_placeholder": "Search for manufacturers", + "selected_model_empty": "Choose at least one model", + "start_test": "Start testing {{num}} models", + "test_failed": "There are {{num}} models that report errors", + "waiting_test": "Waiting for testing" +} diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index d1b1b39be..a578653b4 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -125,7 +125,6 @@ "common.Copy Successful": "Copied Successfully", "common.Copy_failed": "Copy Failed, Please Copy Manually", "common.Create Failed": "Creation Failed", - "common.Create New": "Create", "common.Create Success": "Created Successfully", "common.Create Time": "Creation Time", "common.Creating": "Creating", diff --git a/packages/web/i18n/zh-CN/account.json b/packages/web/i18n/zh-CN/account.json index 4d1538515..f7ed40859 100644 --- a/packages/web/i18n/zh-CN/account.json +++ b/packages/web/i18n/zh-CN/account.json @@ -3,7 +3,7 @@ "add_default_model": "添加预设模型", "api_key": "API 密钥", "bills_and_invoices": "账单与发票", - "channel": "渠道", + "channel": "模型渠道", "config_model": "模型配置", "confirm_logout": "确认退出登录?", "create_channel": "新增渠道", diff --git a/packages/web/i18n/zh-CN/account_model.json b/packages/web/i18n/zh-CN/account_model.json new file mode 100644 index 000000000..9e8bb55b9 --- /dev/null +++ b/packages/web/i18n/zh-CN/account_model.json @@ -0,0 +1,46 @@ +{ + "api_key": "API 密钥", + "azure": "微软 Azure", + "base_url": "代理地址", + "channel_name": "渠道名", + "channel_priority": "优先级", + "channel_priority_tip": "优先级越高的渠道,越容易被请求到", + "channel_status": "状态", + "channel_status_auto_disabled": "自动禁用", + "channel_status_disabled": "禁用", + "channel_status_enabled": "启用", + "channel_status_unknown": "未知", + "channel_type": "厂商", + "clear_model": "清空模型", + "copy_model_id_success": "已复制模型id", + "create_channel": "新增渠道", + "default_url": "默认地址", + "detail": "详情", + "duration": "耗时", + "edit": "编辑", + "edit_channel": "渠道配置", + "enable_channel": "启用", + "forbid_channel": "禁用", + "key_type": "API key 格式: ", + "log": "调用日志", + "log_detail": "日志详情", + "log_status": "状态", + "mapping": "模型映射", + "mapping_tip": "需填写一个有效 Json。可在向实际地址发送请求时,对模型进行映射。例如:\n{\n \"gpt-4o\": \"gpt-4o-test\"\n}\n当 FastGPT 请求 gpt-4o 模型时,会向实际地址发送 gpt-4o-test 的模型,而不是 gpt-4o。", + "model": "模型", + "model_name": "模型名", + "model_test": "模型测试", + "model_tokens": "输入/输出 Tokens", + "request_at": "请求时间", + "request_duration": "请求时长: {{duration}}s", + "running_test": "测试中", + "search_model": "搜索模型", + "select_channel": "选择渠道名", + "select_model": "选择模型", + "select_model_placeholder": "选择该渠道下可用的模型", + "select_provider_placeholder": "搜索厂商", + "selected_model_empty": "至少选择一个模型", + "start_test": "开始测试{{num}}个模型", + "test_failed": "有{{num}}个模型报错", + "waiting_test": "等待测试" +} diff --git a/packages/web/i18n/zh-CN/common.json b/packages/web/i18n/zh-CN/common.json index 640486d9f..7bf11a18f 100644 --- a/packages/web/i18n/zh-CN/common.json +++ b/packages/web/i18n/zh-CN/common.json @@ -129,7 +129,6 @@ "common.Copy Successful": "复制成功", "common.Copy_failed": "复制失败,请手动复制", "common.Create Failed": "创建异常", - "common.Create New": "新建", "common.Create Success": "创建成功", "common.Create Time": "创建时间", "common.Creating": "创建中", diff --git a/packages/web/i18n/zh-Hant/account.json b/packages/web/i18n/zh-Hant/account.json index 1b0894537..faf7b86f1 100644 --- a/packages/web/i18n/zh-Hant/account.json +++ b/packages/web/i18n/zh-Hant/account.json @@ -3,7 +3,7 @@ "add_default_model": "新增預設模型", "api_key": "API 金鑰", "bills_and_invoices": "帳單與發票", - "channel": "頻道", + "channel": "模型渠道", "config_model": "模型配置", "confirm_logout": "確認登出登入?", "create_channel": "新增頻道", diff --git a/packages/web/i18n/zh-Hant/account_model.json b/packages/web/i18n/zh-Hant/account_model.json new file mode 100644 index 000000000..671c2fae7 --- /dev/null +++ b/packages/web/i18n/zh-Hant/account_model.json @@ -0,0 +1,44 @@ +{ + "api_key": "API 密鑰", + "azure": "Azure", + "base_url": "代理地址", + "channel_name": "渠道名", + "channel_priority": "優先級", + "channel_priority_tip": "優先級越高的渠道,越容易被請求到", + "channel_status": "狀態", + "channel_status_auto_disabled": "自動禁用", + "channel_status_disabled": "禁用", + "channel_status_enabled": "啟用", + "channel_status_unknown": "未知", + "channel_type": "廠商", + "clear_model": "清空模型", + "copy_model_id_success": "已復制模型id", + "create_channel": "新增渠道", + "default_url": "默認地址", + "detail": "詳情", + "edit_channel": "渠道配置", + "enable_channel": "啟用", + "forbid_channel": "禁用", + "key_type": "API key 格式:", + "log": "調用日誌", + "log_detail": "日誌詳情", + "log_status": "狀態", + "mapping": "模型映射", + "mapping_tip": "需填寫一個有效 Json。\n可在向實際地址發送請求時,對模型進行映射。\n例如:\n{\n \n \"gpt-4o\": \"gpt-4o-test\"\n\n}\n\n當 FastGPT 請求 gpt-4o 模型時,會向實際地址發送 gpt-4o-test 的模型,而不是 gpt-4o。", + "model": "模型", + "model_name": "模型名", + "model_test": "模型測試", + "model_tokens": "輸入/輸出 Tokens", + "request_at": "請求時間", + "request_duration": "請求時長: {{duration}}s", + "running_test": "測試中", + "search_model": "搜索模型", + "select_channel": "選擇渠道名", + "select_model": "選擇模型", + "select_model_placeholder": "選擇該渠道下可用的模型", + "select_provider_placeholder": "搜索廠商", + "selected_model_empty": "至少選擇一個模型", + "start_test": "開始測試{{num}}個模型", + "test_failed": "有{{num}}個模型報錯", + "waiting_test": "等待測試" +} diff --git a/packages/web/i18n/zh-Hant/common.json b/packages/web/i18n/zh-Hant/common.json index 1e08fd693..d030c82de 100644 --- a/packages/web/i18n/zh-Hant/common.json +++ b/packages/web/i18n/zh-Hant/common.json @@ -124,7 +124,6 @@ "common.Copy Successful": "複製成功", "common.Copy_failed": "複製失敗,請手動複製", "common.Create Failed": "建立失敗", - "common.Create New": "建立新項目", "common.Create Success": "建立成功", "common.Create Time": "建立時間", "common.Creating": "建立中", diff --git a/packages/web/types/i18next.d.ts b/packages/web/types/i18next.d.ts index 13fa5c442..629568740 100644 --- a/packages/web/types/i18next.d.ts +++ b/packages/web/types/i18next.d.ts @@ -18,6 +18,7 @@ import workflow from '../i18n/zh-CN/workflow.json'; import user from '../i18n/zh-CN/user.json'; import chat from '../i18n/zh-CN/chat.json'; import login from '../i18n/zh-CN/login.json'; +import account_model from '../i18n/zh-CN/account_model.json'; export interface I18nNamespaces { common: typeof common; @@ -39,6 +40,7 @@ export interface I18nNamespaces { account: typeof account; account_team: typeof account_team; account_thirdParty: typeof account_thirdParty; + account_model: typeof account_model; } export type I18nNsType = (keyof I18nNamespaces)[]; @@ -73,7 +75,8 @@ declare module 'i18next' { 'account_promotion', 'account_thirdParty', 'account', - 'account_team' + 'account_team', + 'account_model' ]; resources: I18nNamespaces; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b8c5015ce..e344fb2b8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -206,8 +206,8 @@ importers: specifier: ^1.6.0 version: 1.8.0 mongoose: - specifier: ^7.0.2 - version: 7.8.2 + specifier: ^8.10.1 + version: 8.10.2(socks@2.8.3) multer: specifier: 1.4.5-lts.1 version: 1.4.5-lts.1 @@ -603,7 +603,7 @@ importers: version: 9.0.3 '@shelf/jest-mongodb': specifier: ^4.3.2 - version: 4.3.2(jest-environment-node@29.7.0)(mongodb@6.9.0(socks@2.8.3)) + version: 4.3.2(jest-environment-node@29.7.0)(mongodb@6.13.1(socks@2.8.3)) '@svgr/webpack': specifier: ^6.5.1 version: 6.5.1 @@ -645,7 +645,7 @@ importers: version: 14.2.3(eslint@8.56.0)(typescript@5.5.3) mockingoose: specifier: ^2.16.2 - version: 2.16.2(mongoose@7.8.2) + version: 2.16.2(mongoose@8.10.2(socks@2.8.3)) mongodb-memory-server: specifier: ^10.0.0 version: 10.1.0(socks@2.8.3) @@ -4204,9 +4204,14 @@ packages: resolution: {integrity: sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==} engines: {node: '>=14.20.1'} + bson@6.10.3: + resolution: {integrity: sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==} + engines: {node: '>=16.20.1'} + bson@6.8.0: resolution: {integrity: sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==} engines: {node: '>=16.20.1'} + deprecated: a critical bug affecting only useBigInt64=true deserialization usage is fixed in bson@6.10.3 buffer-alloc-unsafe@1.1.0: resolution: {integrity: sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==} @@ -6507,8 +6512,8 @@ packages: jws@4.0.0: resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} - kareem@2.5.1: - resolution: {integrity: sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==} + kareem@2.6.3: + resolution: {integrity: sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==} engines: {node: '>=12.0.0'} katex@0.16.11: @@ -7159,6 +7164,33 @@ packages: snappy: optional: true + mongodb@6.13.1: + resolution: {integrity: sha512-gdq40tX8StmhP6akMp1pPoEVv+9jTYFSrga/g23JxajPAQhH39ysZrHGzQCSd9PEOnuEQEdjIWqxO7ZSwC0w7Q==} + engines: {node: '>=16.20.1'} + peerDependencies: + '@aws-sdk/credential-providers': ^3.632.0 + '@mongodb-js/zstd': ^1.1.0 || ^2.0.0 + gcp-metadata: ^5.2.0 + kerberos: ^2.0.1 + mongodb-client-encryption: '>=6.0.0 <7' + snappy: ^7.2.2 + socks: ^2.7.1 + peerDependenciesMeta: + '@aws-sdk/credential-providers': + optional: true + '@mongodb-js/zstd': + optional: true + gcp-metadata: + optional: true + kerberos: + optional: true + mongodb-client-encryption: + optional: true + snappy: + optional: true + socks: + optional: true + mongodb@6.9.0: resolution: {integrity: sha512-UMopBVx1LmEUbW/QE0Hw18u583PEDVQmUmVzzBRH0o/xtE9DBRA5ZYLOjpLIa03i8FXjzvQECJcqoMvCXftTUA==} engines: {node: '>=16.20.1'} @@ -7186,9 +7218,9 @@ packages: socks: optional: true - mongoose@7.8.2: - resolution: {integrity: sha512-/KDcZL84gg8hnmOHRRPK49WtxH3Xsph38c7YqvYPdxEB2OsDAXvwAknGxyEC0F2P3RJCqFOp+523iFCa0p3dfw==} - engines: {node: '>=14.20.1'} + mongoose@8.10.2: + resolution: {integrity: sha512-DvqfK1s/JLwP39ogXULC8ygNDdmDber5ZbxZzELYtkzl9VGJ3K5T2MCLdpTs9I9J6DnkDyIHJwt7IOyMxh/Adw==} + engines: {node: '>=16.20.1'} mpath@0.9.0: resolution: {integrity: sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==} @@ -8348,8 +8380,8 @@ packages: resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} engines: {node: '>= 0.4'} - sift@16.0.1: - resolution: {integrity: sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==} + sift@17.1.3: + resolution: {integrity: sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==} siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -12469,11 +12501,11 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} - '@shelf/jest-mongodb@4.3.2(jest-environment-node@29.7.0)(mongodb@6.9.0(socks@2.8.3))': + '@shelf/jest-mongodb@4.3.2(jest-environment-node@29.7.0)(mongodb@6.13.1(socks@2.8.3))': dependencies: debug: 4.3.4 jest-environment-node: 29.7.0 - mongodb: 6.9.0(socks@2.8.3) + mongodb: 6.13.1(socks@2.8.3) mongodb-memory-server: 9.2.0 transitivePeerDependencies: - '@aws-sdk/credential-providers' @@ -13772,6 +13804,8 @@ snapshots: bson@5.5.1: {} + bson@6.10.3: {} + bson@6.8.0: {} buffer-alloc-unsafe@1.1.0: {} @@ -14913,7 +14947,7 @@ snapshots: eslint: 8.56.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint@8.56.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.56.0) eslint-plugin-react: 7.34.4(eslint@8.56.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.56.0) @@ -14937,7 +14971,7 @@ snapshots: enhanced-resolve: 5.17.0 eslint: 8.56.0 eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0) fast-glob: 3.3.2 get-tsconfig: 4.7.5 is-core-module: 2.14.0 @@ -14959,7 +14993,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint@8.56.0))(eslint@8.56.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.5.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -16637,7 +16671,7 @@ snapshots: jwa: 2.0.0 safe-buffer: 5.2.1 - kareem@2.5.1: {} + kareem@2.6.3: {} katex@0.16.11: dependencies: @@ -17564,9 +17598,9 @@ snapshots: dependencies: obliterator: 2.0.4 - mockingoose@2.16.2(mongoose@7.8.2): + mockingoose@2.16.2(mongoose@8.10.2(socks@2.8.3)): dependencies: - mongoose: 7.8.2 + mongoose: 8.10.2(socks@2.8.3) monaco-editor@0.50.0: {} @@ -17660,6 +17694,14 @@ snapshots: optionalDependencies: '@mongodb-js/saslprep': 1.1.9 + mongodb@6.13.1(socks@2.8.3): + dependencies: + '@mongodb-js/saslprep': 1.1.9 + bson: 6.10.3 + mongodb-connection-string-url: 3.0.1 + optionalDependencies: + socks: 2.8.3 + mongodb@6.9.0(socks@2.8.3): dependencies: '@mongodb-js/saslprep': 1.1.9 @@ -17668,21 +17710,23 @@ snapshots: optionalDependencies: socks: 2.8.3 - mongoose@7.8.2: + mongoose@8.10.2(socks@2.8.3): dependencies: - bson: 5.5.1 - kareem: 2.5.1 - mongodb: 5.9.2 + bson: 6.10.3 + kareem: 2.6.3 + mongodb: 6.13.1(socks@2.8.3) mpath: 0.9.0 mquery: 5.0.0 ms: 2.1.3 - sift: 16.0.1 + sift: 17.1.3 transitivePeerDependencies: - '@aws-sdk/credential-providers' - '@mongodb-js/zstd' + - gcp-metadata - kerberos - mongodb-client-encryption - snappy + - socks - supports-color mpath@0.9.0: {} @@ -19015,7 +19059,7 @@ snapshots: get-intrinsic: 1.2.4 object-inspect: 1.13.2 - sift@16.0.1: {} + sift@17.1.3: {} siginfo@2.0.0: {} diff --git a/projects/app/.env.template b/projects/app/.env.template index ff3b1f328..3ecbefa7b 100644 --- a/projects/app/.env.template +++ b/projects/app/.env.template @@ -13,6 +13,10 @@ ROOT_KEY=fdafasd OPENAI_BASE_URL=https://api.openai.com/v1 # OpenAI API Key CHAT_API_KEY=sk-xxxx +# ai proxy api +AIPROXY_API_ENDPOINT=https://xxx.come +AIPROXY_API_TOKEN=xxxxx + # 强制将图片转成 base64 传递给模型 MULTIPLE_DATA_TO_BASE64=true diff --git a/projects/app/src/global/aiproxy/constants.ts b/projects/app/src/global/aiproxy/constants.ts new file mode 100644 index 000000000..5fb0d35c0 --- /dev/null +++ b/projects/app/src/global/aiproxy/constants.ts @@ -0,0 +1,128 @@ +import { ModelProviderIdType } from '@fastgpt/global/core/ai/provider'; +import { ChannelInfoType } from './type'; +import { i18nT } from '@fastgpt/web/i18n/utils'; + +export enum ChannelStatusEnum { + ChannelStatusUnknown = 0, + ChannelStatusEnabled = 1, + ChannelStatusDisabled = 2, + ChannelStatusAutoDisabled = 3 +} +export const ChannelStautsMap = { + [ChannelStatusEnum.ChannelStatusUnknown]: { + label: i18nT('account_model:channel_status_unknown'), + colorSchema: 'gray' + }, + [ChannelStatusEnum.ChannelStatusEnabled]: { + label: i18nT('account_model:channel_status_enabled'), + colorSchema: 'green' + }, + [ChannelStatusEnum.ChannelStatusDisabled]: { + label: i18nT('account_model:channel_status_disabled'), + colorSchema: 'red' + }, + [ChannelStatusEnum.ChannelStatusAutoDisabled]: { + label: i18nT('account_model:channel_status_auto_disabled'), + colorSchema: 'gray' + } +}; + +export const defaultChannel: ChannelInfoType = { + id: 0, + status: ChannelStatusEnum.ChannelStatusEnabled, + type: 1, + created_at: 0, + models: [], + model_mapping: {}, + key: '', + name: '', + base_url: '', + priority: 0 +}; + +export const aiproxyIdMap: Record = { + 1: { + label: 'OpenAI', + provider: 'OpenAI' + }, + 3: { + label: i18nT('account_model:azure'), + provider: 'OpenAI' + }, + 14: { + label: 'Anthropic', + provider: 'Claude' + }, + 12: { + label: 'Google Gemini(OpenAI)', + provider: 'Gemini' + }, + 24: { + label: 'Google Gemini', + provider: 'Gemini' + }, + 28: { + label: 'Mistral AI', + provider: 'MistralAI' + }, + 29: { + label: 'Groq', + provider: 'Groq' + }, + 17: { + label: '阿里云', + provider: 'Qwen' + }, + 40: { + label: '豆包', + provider: 'Doubao' + }, + 36: { + label: 'DeepSeek AI', + provider: 'DeepSeek' + }, + 13: { + label: '百度智能云 V2', + provider: 'Ernie' + }, + 15: { + label: '百度智能云', + provider: 'Ernie' + }, + 16: { + label: '智谱 AI', + provider: 'ChatGLM' + }, + 18: { + label: '讯飞星火', + provider: 'SparkDesk' + }, + 25: { + label: '月之暗面', + provider: 'Moonshot' + }, + 26: { + label: '百川智能', + provider: 'Baichuan' + }, + 27: { + label: 'MiniMax', + provider: 'MiniMax' + }, + 31: { + label: '零一万物', + provider: 'Yi' + }, + 32: { + label: '阶跃星辰', + provider: 'StepFun' + }, + 43: { + label: 'SiliconFlow', + provider: 'Siliconflow' + }, + 30: { + label: 'Ollama', + provider: 'Ollama' + } +}; diff --git a/projects/app/src/global/aiproxy/type.d.ts b/projects/app/src/global/aiproxy/type.d.ts new file mode 100644 index 000000000..8238f2093 --- /dev/null +++ b/projects/app/src/global/aiproxy/type.d.ts @@ -0,0 +1,47 @@ +import { ChannelStatusEnum } from './constants'; + +export type ChannelInfoType = { + model_mapping: Record; + key: string; + name: string; + base_url: string; + models: any[]; + id: number; + status: ChannelStatusEnum; + type: number; + created_at: number; + priority: number; +}; + +// Channel api +export type ChannelListQueryType = { + page: number; + perPage: number; +}; +export type ChannelListResponseType = ChannelInfoType[]; + +export type CreateChannelProps = { + type: number; + model_mapping: Record; + key?: string; + name: string; + base_url: string; + models: string[]; +}; + +// Log +export type ChannelLogListItemType = { + token_name: string; + model: string; + request_id: string; + id: number; + channel: number; + mode: number; + created_at: number; + request_at: number; + code: number; + prompt_tokens: number; + completion_tokens: number; + endpoint: string; + content?: string; +}; diff --git a/projects/app/src/pageComponents/account/model/AddModelBox.tsx b/projects/app/src/pageComponents/account/model/AddModelBox.tsx new file mode 100644 index 000000000..9ae33164f --- /dev/null +++ b/projects/app/src/pageComponents/account/model/AddModelBox.tsx @@ -0,0 +1,722 @@ +import { + Box, + Flex, + HStack, + Table, + TableContainer, + Tbody, + Td, + Th, + Thead, + Tr, + Switch, + ModalBody, + Input, + ModalFooter, + Button, + ButtonProps +} from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import React, { useMemo, useRef, useState } from 'react'; +import { + ModelProviderList, + ModelProviderIdType, + getModelProvider +} from '@fastgpt/global/core/ai/provider'; +import MySelect from '@fastgpt/web/components/common/MySelect'; +import { ModelTypeEnum } from '@fastgpt/global/core/ai/model'; +import Avatar from '@fastgpt/web/components/common/Avatar'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { getSystemModelDefaultConfig, putSystemModel } from '@/web/core/ai/config'; +import { SystemModelItemType } from '@fastgpt/service/core/ai/type'; +import { useForm } from 'react-hook-form'; +import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput'; +import MyTextarea from '@/components/common/Textarea/MyTextarea'; +import JsonEditor from '@fastgpt/web/components/common/Textarea/JsonEditor'; +import MyMenu from '@fastgpt/web/components/common/MyMenu'; +import { useSystemStore } from '@/web/common/system/useSystemStore'; +import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; +import { Prompt_CQJson, Prompt_ExtractJson } from '@fastgpt/global/core/ai/prompt/agent'; +import MyModal from '@fastgpt/web/components/common/MyModal'; + +export const AddModelButton = ({ + onCreate, + ...props +}: { onCreate: (type: ModelTypeEnum) => void } & ButtonProps) => { + const { t } = useTranslation(); + + return ( + {t('account:create_model')}} + menuList={[ + { + children: [ + { + label: t('common:model.type.chat'), + onClick: () => onCreate(ModelTypeEnum.llm) + }, + { + label: t('common:model.type.embedding'), + onClick: () => onCreate(ModelTypeEnum.embedding) + }, + { + label: t('common:model.type.tts'), + onClick: () => onCreate(ModelTypeEnum.tts) + }, + { + label: t('common:model.type.stt'), + onClick: () => onCreate(ModelTypeEnum.stt) + }, + { + label: t('common:model.type.reRank'), + onClick: () => onCreate(ModelTypeEnum.rerank) + } + ] + } + ]} + /> + ); +}; + +const InputStyles = { + maxW: '300px', + bg: 'myGray.50', + w: '100%', + rows: 3 +}; +export const ModelEditModal = ({ + modelData, + onSuccess, + onClose +}: { + modelData: SystemModelItemType; + onSuccess: () => void; + onClose: () => void; +}) => { + const { t } = useTranslation(); + const { feConfigs } = useSystemStore(); + + const { register, getValues, setValue, handleSubmit, watch, reset } = + useForm({ + defaultValues: modelData + }); + + const isCustom = !!modelData.isCustom; + const isLLMModel = modelData?.type === ModelTypeEnum.llm; + const isEmbeddingModel = modelData?.type === ModelTypeEnum.embedding; + const isTTSModel = modelData?.type === ModelTypeEnum.tts; + const isSTTModel = modelData?.type === ModelTypeEnum.stt; + const isRerankModel = modelData?.type === ModelTypeEnum.rerank; + + const provider = watch('provider'); + const providerData = useMemo(() => getModelProvider(provider), [provider]); + + const providerList = useRef<{ label: any; value: ModelProviderIdType }[]>( + ModelProviderList.map((item) => ({ + label: ( + + + {t(item.name as any)} + + ), + value: item.id + })) + ); + + const priceUnit = useMemo(() => { + if (isLLMModel || isEmbeddingModel) return '/ 1k Tokens'; + if (isTTSModel) return `/ 1k ${t('common:unit.character')}`; + if (isSTTModel) return `/ 60 ${t('common:unit.seconds')}`; + return ''; + }, [isLLMModel, isEmbeddingModel, isTTSModel, t, isSTTModel]); + + const { runAsync: updateModel, loading: updatingModel } = useRequest2( + async (data: SystemModelItemType) => { + return putSystemModel({ + model: data.model, + metadata: data + }).then(onSuccess); + }, + { + onSuccess: () => { + onClose(); + }, + successToast: t('common:common.Success') + } + ); + + const [key, setKey] = useState(0); + const { runAsync: loadDefaultConfig, loading: loadingDefaultConfig } = useRequest2( + getSystemModelDefaultConfig, + { + onSuccess(res) { + reset({ + ...getValues(), + ...res + }); + setTimeout(() => { + setKey((prev) => prev + 1); + }, 0); + } + } + ); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + {priceUnit && feConfigs?.isPlus && ( + <> + + + + + {isLLMModel && ( + <> + + + + + + + + + + )} + + )} + {isLLMModel && ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + )} + {isEmbeddingModel && ( + <> + + + + + + + + + + + + + + + + + + )} + {isTTSModel && ( + <> + + + + + + )} + + + + + + + + + +
{t('account:model.param_name')}
+ + {t('account:model.model_id')} + + + + {isCustom ? ( + + ) : ( + modelData?.model + )} +
{t('common:model.provider')} + setValue('provider', value)} + list={providerList.current} + {...InputStyles} + /> +
+ + {t('account:model.alias')} + + + + +
+ + {t('account:model.charsPointsPrice')} + + + + + + + {priceUnit} + + +
+ + {t('account:model.input_price')} + + + + + + + {priceUnit} + + +
+ + {t('account:model.output_price')} + + + + + + + {priceUnit} + + +
{t('common:core.ai.Max context')} + + + +
{t('account:model.max_quote')} + + + +
{t('common:core.chat.response.module maxToken')} + + + +
{t('account:model.max_temperature')} + + + +
+ + {t('account:model.show_top_p')} + + + + + +
+ + {t('account:model.show_stop_sign')} + + + + + +
{t('account:model.response_format')} + { + if (!e) { + setValue('responseFormatList', []); + return; + } + try { + setValue('responseFormatList', JSON.parse(e)); + } catch (error) { + console.error(error); + } + }} + {...InputStyles} + /> +
+ + {t('account:model.normalization')} + + + + + + +
+ + {t('account:model.default_token')} + + + + + + +
{t('common:core.ai.Max context')} + + + +
+ + {t('account:model.defaultConfig')} + + + + + { + if (!e) { + setValue('defaultConfig', {}); + return; + } + try { + setValue('defaultConfig', JSON.parse(e)); + } catch (error) { + console.error(error); + } + }} + {...InputStyles} + /> + +
+ + {t('account:model.voices')} + + + + + { + try { + setValue('voices', JSON.parse(e)); + } catch (error) { + console.error(error); + } + }} + {...InputStyles} + /> + +
+ + {t('account:model.request_url')} + + + + +
+ + {t('account:model.request_auth')} + + + + +
+
+ {isLLMModel && ( + + + + + + + + + + + + + + + + + + + + + + + + + + {feConfigs?.isPlus && ( + + + + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{t('account:model.param_name')}
+ + {t('account:model.tool_choice')} + + + + + + +
+ + {t('account:model.function_call')} + + + + + + +
+ + {t('account:model.vision')} + + + + + + +
+ + {t('account:model.reasoning')} + + + + + + +
+ + {t('account:model.censor')} + + + + + + +
{t('account:model.dataset_process')} + + + +
{t('account:model.used_in_classify')} + + + +
{t('account:model.used_in_extract_fields')} + + + +
{t('account:model.used_in_tool_call')} + + + +
+ + {t('account:model.default_system_chat_prompt')} + + + + +
+ + {t('account:model.custom_cq_prompt')} + + + + +
+ + {t('account:model.custom_extract_prompt')} + + + + +
+ + {t('account:model.default_config')} + + + + { + if (!e) { + setValue('defaultConfig', {}); + return; + } + try { + setValue('defaultConfig', JSON.parse(e)); + } catch (error) { + console.error(error); + } + }} + {...InputStyles} + /> +
+
+ )} +
+
+ + {!modelData.isCustom && ( + + )} + + + +
+ ); +}; + +export default function Dom() { + return <>; +} diff --git a/projects/app/src/pageComponents/account/model/Channel/EditChannelModal.tsx b/projects/app/src/pageComponents/account/model/Channel/EditChannelModal.tsx new file mode 100644 index 000000000..97943fd96 --- /dev/null +++ b/projects/app/src/pageComponents/account/model/Channel/EditChannelModal.tsx @@ -0,0 +1,499 @@ +import { aiproxyIdMap } from '@/global/aiproxy/constants'; +import { ChannelInfoType } from '@/global/aiproxy/type'; +import { + Box, + BoxProps, + Button, + Flex, + Input, + MenuItemProps, + ModalBody, + ModalFooter, + useDisclosure, + Menu, + MenuButton, + MenuList, + MenuItem, + HStack, + useOutsideClick +} from '@chakra-ui/react'; +import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; +import MyModal from '@fastgpt/web/components/common/MyModal'; +import MySelect from '@fastgpt/web/components/common/MySelect'; +import { useTranslation } from 'next-i18next'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { AddModelButton } from '../AddModelBox'; +import dynamic from 'next/dynamic'; +import { SystemModelItemType } from '@fastgpt/service/core/ai/type'; +import { ModelTypeEnum } from '@fastgpt/global/core/ai/model'; +import { useSystemStore } from '@/web/common/system/useSystemStore'; +import { getSystemModelList } from '@/web/core/ai/config'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { getModelProvider } from '@fastgpt/global/core/ai/provider'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import MyAvatar from '@fastgpt/web/components/common/Avatar'; +import MyTag from '@fastgpt/web/components/common/Tag/index'; +import { useCopyData } from '@fastgpt/web/hooks/useCopyData'; +import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; +import JsonEditor from '@fastgpt/web/components/common/Textarea/JsonEditor'; +import { getChannelProviders, postCreateChannel, putChannel } from '@/web/core/ai/channel'; +import CopyBox from '@fastgpt/web/components/common/String/CopyBox'; + +const ModelEditModal = dynamic(() => import('../AddModelBox').then((mod) => mod.ModelEditModal)); + +const LabelStyles: BoxProps = { + fontSize: 'sm', + color: 'myGray.900', + flex: '0 0 70px' +}; +const EditChannelModal = ({ + defaultConfig, + onClose, + onSuccess +}: { + defaultConfig: ChannelInfoType; + onClose: () => void; + onSuccess: () => void; +}) => { + const { t } = useTranslation(); + const { defaultModels } = useSystemStore(); + const isEdit = defaultConfig.id !== 0; + + const { register, handleSubmit, watch, setValue } = useForm({ + defaultValues: defaultConfig + }); + + const providerType = watch('type'); + const { data: providerList = [] } = useRequest2( + () => + getChannelProviders().then((res) => { + return Object.entries(res) + .map(([key, value]) => { + const mapData = aiproxyIdMap[key as any] ?? { + label: value.name, + provider: 'Other' + }; + const provider = getModelProvider(mapData.provider); + return { + order: provider.order, + defaultBaseUrl: value.defaultBaseUrl, + keyHelp: value.keyHelp, + icon: provider.avatar, + label: t(mapData.label as any), + value: Number(key) + }; + }) + .sort((a, b) => a.order - b.order); + }), + { + manual: false + } + ); + const selectedProvider = useMemo(() => { + const res = providerList.find((item) => item.value === providerType); + return res; + }, [providerList, providerType]); + + const [editModelData, setEditModelData] = useState(); + const onCreateModel = (type: ModelTypeEnum) => { + const defaultModel = defaultModels[type]; + + setEditModelData({ + ...defaultModel, + model: '', + name: '', + charsPointsPrice: 0, + inputPrice: undefined, + outputPrice: undefined, + + isCustom: true, + isActive: true, + // @ts-ignore + type + }); + }; + + const models = watch('models'); + const { + data: systemModelList = [], + runAsync: refreshSystemModelList, + loading: loadingModels + } = useRequest2(getSystemModelList, { + manual: false + }); + const modelList = useMemo(() => { + const currentProvider = aiproxyIdMap[providerType]?.provider; + return systemModelList + .map((item) => { + const provider = getModelProvider(item.provider); + + return { + provider: item.provider, + icon: provider.avatar, + label: item.model, + value: item.model + }; + }) + .sort((a, b) => { + // sort by provider, same provider first + if (a.provider === currentProvider && b.provider !== currentProvider) return -1; + if (a.provider !== currentProvider && b.provider === currentProvider) return 1; + return 0; + }); + }, [providerType, systemModelList]); + + const modelMapping = watch('model_mapping'); + + const { runAsync: onSubmit, loading: loadingCreate } = useRequest2( + (data: ChannelInfoType) => { + if (data.models.length === 0) { + return Promise.reject(t('account_model:selected_model_empty')); + } + return isEdit ? putChannel(data) : postCreateChannel(data); + }, + { + onSuccess() { + onSuccess(); + onClose(); + }, + successToast: isEdit ? t('common:common.Update Success') : t('common:common.Create Success'), + manual: true + } + ); + + const isLoading = loadingModels || loadingCreate; + + return ( + <> + + + {/* Chnnel name */} + + + {t('account_model:channel_name')} + + + + {/* Provider */} + + + {t('account_model:channel_type')} + + + { + setValue('type', val); + }} + /> + + + {/* Model */} + + + + {t('account_model:model')}({models.length}) + + + + + + + { + setValue('models', val); + }} + /> + + + {/* Mapping */} + + + {t('account_model:mapping')} + + + + { + if (!val) { + setValue('model_mapping', {}); + } else { + try { + setValue('model_mapping', JSON.parse(val)); + } catch (error) {} + } + }} + /> + + + {/* url and key */} + + + {t('account_model:base_url')} + {selectedProvider && ( + + {'('} + {t('account_model:default_url')}: + + {selectedProvider?.defaultBaseUrl || ''} + + {')'} + + )} + + + + + + {t('account_model:api_key')} + {selectedProvider?.keyHelp && ( + + {'('} + {t('account_model:key_type')} + {selectedProvider.keyHelp} + {')'} + + )} + + + + + + + + + + {!!editModelData && ( + setEditModelData(undefined)} + /> + )} + + ); +}; +export default EditChannelModal; + +type SelectProps = { + list: { + icon?: string; + label: string; + value: string; + }[]; + value: string[]; + onSelect: (val: string[]) => void; +}; +const menuItemStyles: MenuItemProps = { + borderRadius: 'sm', + py: 2, + display: 'flex', + alignItems: 'center', + _hover: { + backgroundColor: 'myGray.100' + }, + _notLast: { + mb: 0.5 + } +}; +const MultipleSelect = ({ value = [], list = [], onSelect }: SelectProps) => { + const ref = useRef(null); + const BoxRef = useRef(null); + + const { t } = useTranslation(); + const { isOpen, onOpen, onClose } = useDisclosure(); + const { copyData } = useCopyData(); + + const onclickItem = useCallback( + (val: string) => { + if (value.includes(val)) { + onSelect(value.filter((i) => i !== val)); + } else { + onSelect([...value, val]); + BoxRef.current?.scrollTo({ + top: BoxRef.current.scrollHeight + }); + } + }, + [value, onSelect] + ); + + const [search, setSearch] = useState(''); + + const filterUnSelected = useMemo(() => { + return list + .filter((item) => !value.includes(item.value)) + .filter((item) => { + if (!search) return true; + const regx = new RegExp(search, 'i'); + return regx.test(item.label); + }); + }, [list, value, search]); + + useOutsideClick({ + ref, + handler: () => { + onClose(); + } + }); + + return ( + + + { + onOpen(); + setSearch(''); + } + })} + > + + + {value.length === 0 ? ( + + {t('account_model:select_model_placeholder')} + + ) : ( + + {value.map((item) => ( + { + e.stopPropagation(); + copyData(item, t('account_model:copy_model_id_success')); + }} + > + {item} + { + e.stopPropagation(); + onclickItem(item); + }} + /> + + ))} + {isOpen && ( + setSearch(e.target.value)} + placeholder={t('account_model:search_model')} + onClick={(e) => { + e.stopPropagation(); + }} + /> + )} + + )} + + + + + + {filterUnSelected.map((item, i) => { + return ( + { + onclickItem(item.value); + }} + whiteSpace={'pre-wrap'} + fontSize={'sm'} + gap={2} + {...menuItemStyles} + > + {item.icon && } + {item.label} + + ); + })} + + + + ); +}; diff --git a/projects/app/src/pageComponents/account/model/Channel/ModelTest.tsx b/projects/app/src/pageComponents/account/model/Channel/ModelTest.tsx new file mode 100644 index 000000000..fc7db5a30 --- /dev/null +++ b/projects/app/src/pageComponents/account/model/Channel/ModelTest.tsx @@ -0,0 +1,196 @@ +import { getSystemModelList, getTestModel } from '@/web/core/ai/config'; +import { + Table, + Thead, + Tbody, + Tr, + Th, + Td, + TableContainer, + Box, + Flex, + Button, + HStack, + ModalBody, + ModalFooter +} from '@chakra-ui/react'; +import { getModelProvider } from '@fastgpt/global/core/ai/provider'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import React, { useRef, useState } from 'react'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import { useTranslation } from 'next-i18next'; +import MyModal from '@fastgpt/web/components/common/MyModal'; +import MyTag from '@fastgpt/web/components/common/Tag/index'; +import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; +import { getErrText } from '@fastgpt/global/common/error/utils'; +import { batchRun } from '@fastgpt/global/common/fn/utils'; +import { useToast } from '@fastgpt/web/hooks/useToast'; + +type ModelTestItem = { + label: React.ReactNode; + model: string; + status: 'waiting' | 'running' | 'success' | 'error'; + message?: string; + duration?: number; +}; + +const ModelTest = ({ models, onClose }: { models: string[]; onClose: () => void }) => { + const { t } = useTranslation(); + const { toast } = useToast(); + const [testModelList, setTestModelList] = useState([]); + + const statusMap = useRef({ + waiting: { + label: t('account_model:waiting_test'), + colorSchema: 'gray' + }, + running: { + label: t('account_model:running_test'), + colorSchema: 'blue' + }, + success: { + label: t('common:common.Success'), + colorSchema: 'green' + }, + error: { + label: t('common:common.failed'), + colorSchema: 'red' + } + }); + const { loading: loadingModels } = useRequest2(getSystemModelList, { + manual: false, + refreshDeps: [models], + onSuccess(res) { + const list = models + .map((model) => { + const modelData = res.find((item) => item.model === model); + if (!modelData) return null; + const provider = getModelProvider(modelData.provider); + + return { + label: ( + + + {t(modelData.name as any)} + + ), + model: modelData.model, + status: 'waiting' + }; + }) + .filter(Boolean) as ModelTestItem[]; + setTestModelList(list); + } + }); + + const { runAsync: onStartTest, loading: isTesting } = useRequest2( + async () => { + { + let errorNum = 0; + const testModel = async (model: string) => { + setTestModelList((prev) => + prev.map((item) => + item.model === model ? { ...item, status: 'running', message: '' } : item + ) + ); + const start = Date.now(); + try { + await getTestModel(model); + const duration = Date.now() - start; + setTestModelList((prev) => + prev.map((item) => + item.model === model + ? { ...item, status: 'success', duration: duration / 1000 } + : item + ) + ); + } catch (error) { + setTestModelList((prev) => + prev.map((item) => + item.model === model + ? { ...item, status: 'error', message: getErrText(error) } + : item + ) + ); + errorNum++; + } + }; + + await batchRun( + testModelList.map((item) => item.model), + testModel, + 5 + ); + + if (errorNum > 0) { + toast({ + status: 'warning', + title: t('account_model:test_failed', { num: errorNum }) + }); + } + } + }, + { + refreshDeps: [testModelList] + } + ); + + console.log(testModelList); + return ( + + + + + + + + + + + + {testModelList.map((item) => { + const data = statusMap.current[item.status]; + return ( + + + + + ); + })} + +
{t('account_model:model')}{t('account_model:channel_status')}
{item.label} + + + {data.label} + + {item.message && } + {item.status === 'success' && item.duration && ( + + {t('account_model:request_duration', { + duration: item.duration.toFixed(2) + })} + + )} + +
+
+
+ + + + +
+ ); +}; + +export default ModelTest; diff --git a/projects/app/src/pageComponents/account/model/Channel/index.tsx b/projects/app/src/pageComponents/account/model/Channel/index.tsx new file mode 100644 index 000000000..0f6ed2142 --- /dev/null +++ b/projects/app/src/pageComponents/account/model/Channel/index.tsx @@ -0,0 +1,230 @@ +import { deleteChannel, getChannelList, putChannel, putChannelStatus } from '@/web/core/ai/channel'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import React, { useState } from 'react'; +import { + Table, + Thead, + Tbody, + Tr, + Th, + Td, + TableContainer, + Box, + Flex, + Button, + HStack +} from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import MyBox from '@fastgpt/web/components/common/MyBox'; +import MyIconButton from '@fastgpt/web/components/common/Icon/button'; +import { useUserStore } from '@/web/support/user/useUserStore'; +import { ChannelInfoType } from '@/global/aiproxy/type'; +import MyTag from '@fastgpt/web/components/common/Tag/index'; +import { + aiproxyIdMap, + ChannelStatusEnum, + ChannelStautsMap, + defaultChannel +} from '@/global/aiproxy/constants'; +import MyMenu from '@fastgpt/web/components/common/MyMenu'; +import dynamic from 'next/dynamic'; +import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; +import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput'; +import { getModelProvider } from '@fastgpt/global/core/ai/provider'; +import MyIcon from '@fastgpt/web/components/common/Icon'; + +const EditChannelModal = dynamic(() => import('./EditChannelModal'), { ssr: false }); +const ModelTest = dynamic(() => import('./ModelTest'), { ssr: false }); + +const ChannelTable = ({ Tab }: { Tab: React.ReactNode }) => { + const { t } = useTranslation(); + const { userInfo } = useUserStore(); + + const isRoot = userInfo?.username === 'root'; + + const { + data: channelList = [], + runAsync: refreshChannelList, + loading: loadingChannelList + } = useRequest2(getChannelList, { + manual: false + }); + + const [editChannel, setEditChannel] = useState(); + + const { runAsync: updateChannel, loading: loadingUpdateChannel } = useRequest2(putChannel, { + manual: true, + onSuccess: () => { + refreshChannelList(); + } + }); + const { runAsync: updateChannelStatus, loading: loadingUpdateChannelStatus } = useRequest2( + putChannelStatus, + { + onSuccess: () => { + refreshChannelList(); + } + } + ); + + const { runAsync: onDeleteChannel, loading: loadingDeleteChannel } = useRequest2(deleteChannel, { + manual: true, + onSuccess: () => { + refreshChannelList(); + } + }); + + const [testModels, setTestModels] = useState(); + + const isLoading = + loadingChannelList || + loadingUpdateChannel || + loadingDeleteChannel || + loadingUpdateChannelStatus; + + return ( + <> + {isRoot && ( + + {Tab} + + + + )} + + + + + + + + + + + + + + + {channelList.map((item) => { + const providerData = aiproxyIdMap[item.type]; + const provider = getModelProvider(providerData?.provider); + + return ( + + + + + + + + + ); + })} + +
ID{t('account_model:channel_name')}{t('account_model:channel_type')}{t('account_model:channel_status')} + {t('account_model:channel_priority')} + +
{item.id}{item.name} + {providerData ? ( + + + {t(providerData?.label as any)} + + ) : ( + 'Invalid provider' + )} + + + {t(ChannelStautsMap[item.status]?.label as any) || + t('account_model:channel_status_unknown')} + + + { + const val = (() => { + if (!e) return 0; + return e; + })(); + updateChannel({ + ...item, + priority: val + }); + }} + /> + + setTestModels(item.models) + }, + ...(item.status === ChannelStatusEnum.ChannelStatusEnabled + ? [ + { + icon: 'common/disable', + label: t('account_model:forbid_channel'), + onClick: () => + updateChannelStatus( + item.id, + ChannelStatusEnum.ChannelStatusDisabled + ) + } + ] + : [ + { + icon: 'common/enable', + label: t('account_model:enable_channel'), + onClick: () => + updateChannelStatus( + item.id, + ChannelStatusEnum.ChannelStatusEnabled + ) + } + ]), + { + icon: 'common/settingLight', + label: t('account_model:edit'), + onClick: () => setEditChannel(item) + }, + { + type: 'danger', + icon: 'delete', + label: t('common:common.Delete'), + onClick: () => onDeleteChannel(item.id) + } + ] + } + ]} + Button={} + /> +
+
+
+ + {!!editChannel && ( + setEditChannel(undefined)} + onSuccess={refreshChannelList} + /> + )} + {!!testModels && setTestModels(undefined)} />} + + ); +}; + +export default ChannelTable; diff --git a/projects/app/src/pageComponents/account/model/Log/index.tsx b/projects/app/src/pageComponents/account/model/Log/index.tsx new file mode 100644 index 000000000..aa8442621 --- /dev/null +++ b/projects/app/src/pageComponents/account/model/Log/index.tsx @@ -0,0 +1,406 @@ +import { getChannelList, getChannelLog, getLogDetail } from '@/web/core/ai/channel'; +import { getSystemModelList } from '@/web/core/ai/config'; +import { useUserStore } from '@/web/support/user/useUserStore'; +import { + Table, + Thead, + Tbody, + Tr, + Th, + Td, + TableContainer, + Box, + Flex, + Button, + HStack, + ModalBody, + Grid, + GridItem, + BoxProps +} from '@chakra-ui/react'; +import { getModelProvider } from '@fastgpt/global/core/ai/provider'; +import DateRangePicker, { DateRangeType } from '@fastgpt/web/components/common/DateRangePicker'; +import MyBox from '@fastgpt/web/components/common/MyBox'; +import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; +import MySelect from '@fastgpt/web/components/common/MySelect'; +import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; +import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; +import { addDays } from 'date-fns'; +import { useTranslation } from 'next-i18next'; +import React, { useCallback, useMemo, useState } from 'react'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import { formatTime2YMDHMS } from '@fastgpt/global/common/string/time'; +import MyModal from '@fastgpt/web/components/common/MyModal'; +import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; + +type LogDetailType = { + id: number; + request_id: string; + channelName: string | number; + model: React.JSX.Element; + duration: number; + request_at: string; + code: number; + prompt_tokens: number; + completion_tokens: number; + endpoint: string; + + content?: string; + request_body?: string; + response_body?: string; +}; +const ChannelLog = ({ Tab }: { Tab: React.ReactNode }) => { + const { t } = useTranslation(); + const { userInfo } = useUserStore(); + + const isRoot = userInfo?.username === 'root'; + const [filterProps, setFilterProps] = useState<{ + channelId?: string; + model?: string; + code_type: 'all' | 'success' | 'error'; + dateRange: DateRangeType; + }>({ + code_type: 'all', + dateRange: { + from: (() => { + const today = addDays(new Date(), -1); + today.setHours(0, 0, 0, 0); + return today; + })(), + to: (() => { + const today = new Date(); + today.setHours(23, 59, 59, 999); + return today; + })() + } + }); + + const { data: channelList = [] } = useRequest2( + async () => { + const res = await getChannelList().then((res) => + res.map((item) => ({ + label: item.name, + value: `${item.id}` + })) + ); + return [ + { + label: t('common:common.All'), + value: '' + }, + ...res + ]; + }, + { + manual: false + } + ); + + const { data: systemModelList = [] } = useRequest2(getSystemModelList, { + manual: false + }); + const modelList = useMemo(() => { + const res = systemModelList + .map((item) => { + const provider = getModelProvider(item.provider); + + return { + order: provider.order, + icon: provider.avatar, + label: item.model, + value: item.model + }; + }) + .sort((a, b) => a.order - b.order); + return [ + { + label: t('common:common.All'), + value: '' + }, + ...res + ]; + }, [systemModelList]); + + const { data, isLoading, ScrollData } = useScrollPagination(getChannelLog, { + pageSize: 20, + refreshDeps: [filterProps], + params: { + channel: filterProps.channelId, + model_name: filterProps.model, + code_type: filterProps.code_type, + start_timestamp: filterProps.dateRange.from?.getTime() || 0, + end_timestamp: filterProps.dateRange.to?.getTime() || 0 + } + }); + + const formatData = useMemo(() => { + return data.map((item) => { + const duration = item.created_at - item.request_at; + const durationSecond = duration / 1000; + + const channelName = channelList.find((channel) => channel.value === `${item.channel}`)?.label; + + const model = systemModelList.find((model) => model.model === item.model); + const provider = getModelProvider(model?.provider); + + return { + id: item.id, + channelName: channelName || item.channel, + model: ( + + + {model?.model} + + ), + duration: durationSecond, + request_at: formatTime2YMDHMS(item.request_at), + code: item.code, + prompt_tokens: item.prompt_tokens, + completion_tokens: item.completion_tokens, + request_id: item.request_id, + endpoint: item.endpoint, + content: item.content + }; + }); + }, [data]); + + const [logDetail, setLogDetail] = useState(); + + return ( + <> + {isRoot && ( + + {Tab} + + + )} + + + {t('common:user.Time')} + + setFilterProps({ ...filterProps, dateRange: e })} + /> + + + + {t('account_model:channel_name')} + + + bg={'myGray.50'} + isSearch + list={channelList} + placeholder={t('account_model:select_channel')} + value={filterProps.channelId} + onchange={(val) => setFilterProps({ ...filterProps, channelId: val })} + /> + + + + {t('account_model:model_name')} + + + bg={'myGray.50'} + isSearch + list={modelList} + placeholder={t('account_model:select_model')} + value={filterProps.model} + onchange={(val) => setFilterProps({ ...filterProps, model: val })} + /> + + + + {t('account_model:log_status')} + + + bg={'myGray.50'} + list={[ + { label: t('common:common.All'), value: 'all' }, + { label: t('common:common.Success'), value: 'success' }, + { label: t('common:common.failed'), value: 'error' } + ]} + value={filterProps.code_type} + onchange={(val) => setFilterProps({ ...filterProps, code_type: val })} + /> + + + + + + + + + + + + + + + + + + + + {formatData.map((item) => ( + + + + + + + + + + ))} + +
{t('account_model:channel_name')}{t('account_model:model')}{t('account_model:model_tokens')}{t('account_model:duration')}{t('account_model:channel_status')}{t('account_model:request_at')}
{item.channelName}{item.model} + {item.prompt_tokens} / {item.completion_tokens} + 10 ? 'red.600' : ''}>{item.duration.toFixed(2)}s + {item.code} + {item.content && } + {item.request_at} + +
+
+
+
+ + {!!logDetail && setLogDetail(undefined)} />} + + ); +}; + +export default ChannelLog; + +const LogDetail = ({ data, onClose }: { data: LogDetailType; onClose: () => void }) => { + const { t } = useTranslation(); + const { data: detailData } = useRequest2( + async () => { + if (data.code === 200) return data; + const res = await getLogDetail(data.id); + return { + ...res, + ...data + }; + }, + { + manual: false + } + ); + + const Title = useCallback(({ children, ...props }: { children: React.ReactNode } & BoxProps) => { + return ( + + {children} + + ); + }, []); + const Container = useCallback( + ({ children, ...props }: { children: React.ReactNode } & BoxProps) => { + return ( + + {children} + + ); + }, + [] + ); + + return ( + + {detailData && ( + + {/* 基本信息表格 */} + + {/* 第一行 */} + + RequestID + {detailData?.request_id} + + + {t('account_model:channel_status')} + + {detailData?.code} + + + + Endpoint + {detailData?.endpoint} + + + {t('account_model:channel_name')} + {detailData?.channelName} + + + {t('account_model:request_at')} + {detailData?.request_at} + + + {t('account_model:duration')} + {detailData?.duration.toFixed(2)}s + + + {t('account_model:model')} + {detailData?.model} + + + {t('account_model:model_tokens')} + + {detailData?.prompt_tokens} / {detailData?.completion_tokens} + + + {detailData?.content && ( + + Content + {detailData?.content} + + )} + {detailData?.request_body && ( + + Request Body + {detailData?.request_body} + + )} + {detailData?.response_body && ( + + Response Body + {detailData?.response_body} + + )} + + + )} + + ); +}; diff --git a/projects/app/src/pageComponents/account/model/ModelConfigTable.tsx b/projects/app/src/pageComponents/account/model/ModelConfigTable.tsx index 30b2e4fc3..c2a59a829 100644 --- a/projects/app/src/pageComponents/account/model/ModelConfigTable.tsx +++ b/projects/app/src/pageComponents/account/model/ModelConfigTable.tsx @@ -33,7 +33,6 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { deleteSystemModel, getModelConfigJson, - getSystemModelDefaultConfig, getSystemModelDetail, getSystemModelList, getTestModel, @@ -44,24 +43,20 @@ import MyBox from '@fastgpt/web/components/common/MyBox'; import { SystemModelItemType } from '@fastgpt/service/core/ai/type'; import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; import MyIconButton from '@fastgpt/web/components/common/Icon/button'; -import { useForm } from 'react-hook-form'; -import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput'; -import MyTextarea from '@/components/common/Textarea/MyTextarea'; import JsonEditor from '@fastgpt/web/components/common/Textarea/JsonEditor'; import { clientInitData } from '@/web/common/system/staticData'; import { useUserStore } from '@/web/support/user/useUserStore'; -import MyMenu from '@fastgpt/web/components/common/MyMenu'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; import { putUpdateWithJson } from '@/web/core/ai/config'; import CopyBox from '@fastgpt/web/components/common/String/CopyBox'; import MyIcon from '@fastgpt/web/components/common/Icon'; import AIModelSelector from '@/components/Select/AIModelSelector'; -import { useRefresh } from '../../../../../../packages/web/hooks/useRefresh'; -import { Prompt_CQJson, Prompt_ExtractJson } from '@fastgpt/global/core/ai/prompt/agent'; import MyDivider from '@fastgpt/web/components/common/MyDivider'; +import { AddModelButton } from './AddModelBox'; const MyModal = dynamic(() => import('@fastgpt/web/components/common/MyModal')); +const ModelEditModal = dynamic(() => import('./AddModelBox').then((mod) => mod.ModelEditModal)); const ModelTable = ({ Tab }: { Tab: React.ReactNode }) => { const { t } = useTranslation(); @@ -271,6 +266,7 @@ const ModelTable = ({ Tab }: { Tab: React.ReactNode }) => { } } ); + const onCreateModel = (type: ModelTypeEnum) => { const defaultModel = defaultModels[type]; @@ -316,37 +312,7 @@ const ModelTable = ({ Tab }: { Tab: React.ReactNode }) => { - {t('account:create_model')}} - menuList={[ - { - children: [ - { - label: t('common:model.type.chat'), - onClick: () => onCreateModel(ModelTypeEnum.llm) - }, - { - label: t('common:model.type.embedding'), - onClick: () => onCreateModel(ModelTypeEnum.embedding) - }, - { - label: t('common:model.type.tts'), - onClick: () => onCreateModel(ModelTypeEnum.tts) - }, - { - label: t('common:model.type.stt'), - onClick: () => onCreateModel(ModelTypeEnum.stt) - }, - { - label: t('common:model.type.reRank'), - onClick: () => onCreateModel(ModelTypeEnum.rerank) - } - ] - } - ]} - /> + )} @@ -512,650 +478,6 @@ const ModelTable = ({ Tab }: { Tab: React.ReactNode }) => { ); }; -const InputStyles = { - maxW: '300px', - bg: 'myGray.50', - w: '100%', - rows: 3 -}; -const ModelEditModal = ({ - modelData, - onSuccess, - onClose -}: { - modelData: SystemModelItemType; - onSuccess: () => void; - onClose: () => void; -}) => { - const { t } = useTranslation(); - const { feConfigs } = useSystemStore(); - - const { register, getValues, setValue, handleSubmit, watch, reset } = - useForm({ - defaultValues: modelData - }); - - const isCustom = !!modelData.isCustom; - const isLLMModel = modelData?.type === ModelTypeEnum.llm; - const isEmbeddingModel = modelData?.type === ModelTypeEnum.embedding; - const isTTSModel = modelData?.type === ModelTypeEnum.tts; - const isSTTModel = modelData?.type === ModelTypeEnum.stt; - const isRerankModel = modelData?.type === ModelTypeEnum.rerank; - - const provider = watch('provider'); - const providerData = useMemo(() => getModelProvider(provider), [provider]); - - const providerList = useRef<{ label: any; value: ModelProviderIdType }[]>( - ModelProviderList.map((item) => ({ - label: ( - - - {t(item.name as any)} - - ), - value: item.id - })) - ); - - const priceUnit = useMemo(() => { - if (isLLMModel || isEmbeddingModel) return '/ 1k Tokens'; - if (isTTSModel) return `/ 1k ${t('common:unit.character')}`; - if (isSTTModel) return `/ 60 ${t('common:unit.seconds')}`; - return ''; - return ''; - }, [isLLMModel, isEmbeddingModel, isTTSModel, t, isSTTModel]); - - const { runAsync: updateModel, loading: updatingModel } = useRequest2( - async (data: SystemModelItemType) => { - return putSystemModel({ - model: data.model, - metadata: data - }).then(onSuccess); - }, - { - onSuccess: () => { - onClose(); - }, - successToast: t('common:common.Success') - } - ); - - const [key, setKey] = useState(0); - const { runAsync: loadDefaultConfig, loading: loadingDefaultConfig } = useRequest2( - getSystemModelDefaultConfig, - { - onSuccess(res) { - reset({ - ...getValues(), - ...res - }); - setTimeout(() => { - setKey((prev) => prev + 1); - }, 0); - } - } - ); - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - {priceUnit && feConfigs?.isPlus && ( - <> - - - - - {isLLMModel && ( - <> - - - - - - - - - - )} - - )} - {isLLMModel && ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )} - {isEmbeddingModel && ( - <> - - - - - - - - - - - - - - - - - - )} - {isTTSModel && ( - <> - - - - - - )} - - - - - - - - - -
{t('account:model.param_name')}
- - {t('account:model.model_id')} - - - - {isCustom ? ( - - ) : ( - modelData?.model - )} -
{t('common:model.provider')} - setValue('provider', value)} - list={providerList.current} - {...InputStyles} - /> -
- - {t('account:model.alias')} - - - - -
- - {t('account:model.charsPointsPrice')} - - - - - - - {priceUnit} - - -
- - {t('account:model.input_price')} - - - - - - - {priceUnit} - - -
- - {t('account:model.output_price')} - - - - - - - {priceUnit} - - -
{t('common:core.ai.Max context')} - - - -
{t('account:model.max_quote')} - - - -
{t('common:core.chat.response.module maxToken')} - - - -
{t('account:model.max_temperature')} - - - -
- - {t('account:model.show_top_p')} - - - - - -
- - {t('account:model.show_stop_sign')} - - - - - -
{t('account:model.response_format')} - { - if (!e) { - setValue('responseFormatList', []); - return; - } - try { - setValue('responseFormatList', JSON.parse(e)); - } catch (error) { - console.error(error); - } - }} - {...InputStyles} - /> -
- - {t('account:model.normalization')} - - - - - - -
- - {t('account:model.default_token')} - - - - - - -
{t('common:core.ai.Max context')} - - - -
- - {t('account:model.defaultConfig')} - - - - - { - if (!e) { - setValue('defaultConfig', {}); - return; - } - try { - setValue('defaultConfig', JSON.parse(e)); - } catch (error) { - console.error(error); - } - }} - {...InputStyles} - /> - -
- - {t('account:model.voices')} - - - - - { - try { - setValue('voices', JSON.parse(e)); - } catch (error) { - console.error(error); - } - }} - {...InputStyles} - /> - -
- - {t('account:model.request_url')} - - - - -
- - {t('account:model.request_auth')} - - - - -
-
- {isLLMModel && ( - - - - - - - - - - - - - - - - - - - - - - - - - - {feConfigs?.isPlus && ( - - - - - )} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{t('account:model.param_name')}
- - {t('account:model.tool_choice')} - - - - - - -
- - {t('account:model.function_call')} - - - - - - -
- - {t('account:model.vision')} - - - - - - -
- - {t('account:model.reasoning')} - - - - - - -
- - {t('account:model.censor')} - - - - - - -
{t('account:model.dataset_process')} - - - -
{t('account:model.used_in_classify')} - - - -
{t('account:model.used_in_extract_fields')} - - - -
{t('account:model.used_in_tool_call')} - - - -
- - {t('account:model.default_system_chat_prompt')} - - - - -
- - {t('account:model.custom_cq_prompt')} - - - - -
- - {t('account:model.custom_extract_prompt')} - - - - -
- - {t('account:model.default_config')} - - - - { - if (!e) { - setValue('defaultConfig', {}); - return; - } - try { - setValue('defaultConfig', JSON.parse(e)); - } catch (error) { - console.error(error); - } - }} - {...InputStyles} - /> -
-
- )} -
-
- - {!modelData.isCustom && ( - - )} - - - -
- ); -}; - const JsonConfigModal = ({ onClose, onSuccess diff --git a/projects/app/src/pages/account/model/index.tsx b/projects/app/src/pages/account/model/index.tsx index 4c796bb00..b833716ad 100644 --- a/projects/app/src/pages/account/model/index.tsx +++ b/projects/app/src/pages/account/model/index.tsx @@ -7,13 +7,17 @@ import { useUserStore } from '@/web/support/user/useUserStore'; import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs'; import { useTranslation } from 'next-i18next'; import dynamic from 'next/dynamic'; +import { useSystemStore } from '@/web/common/system/useSystemStore'; const ModelConfigTable = dynamic(() => import('@/pageComponents/account/model/ModelConfigTable')); +const ChannelTable = dynamic(() => import('@/pageComponents/account/model/Channel')); +const ChannelLog = dynamic(() => import('@/pageComponents/account/model/Log')); -type TabType = 'model' | 'config' | 'channel'; +type TabType = 'model' | 'config' | 'channel' | 'channel_log'; const ModelProvider = () => { const { t } = useTranslation(); + const { feConfigs } = useSystemStore(); const [tab, setTab] = useState('model'); @@ -22,21 +26,29 @@ const ModelProvider = () => { list={[ { label: t('account:active_model'), value: 'model' }, - { label: t('account:config_model'), value: 'config' } - // { label: t('account:channel'), value: 'channel' } + { label: t('account:config_model'), value: 'config' }, + // @ts-ignore + ...(feConfigs?.show_aiproxy + ? [ + { label: t('account:channel'), value: 'channel' }, + { label: t('account_model:log'), value: 'channel_log' } + ] + : []) ]} value={tab} py={1} onChange={setTab} /> ); - }, [t, tab]); + }, [feConfigs.show_aiproxy, t, tab]); return ( {tab === 'model' && } {tab === 'config' && } + {tab === 'channel' && } + {tab === 'channel_log' && } ); @@ -45,7 +57,7 @@ const ModelProvider = () => { export async function getServerSideProps(content: any) { return { props: { - ...(await serviceSideProps(content, ['account'])) + ...(await serviceSideProps(content, ['account', 'account_model'])) } }; } diff --git a/projects/app/src/pages/api/aiproxy/[...path].ts b/projects/app/src/pages/api/aiproxy/[...path].ts new file mode 100644 index 000000000..127044ad2 --- /dev/null +++ b/projects/app/src/pages/api/aiproxy/[...path].ts @@ -0,0 +1,72 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { jsonRes } from '@fastgpt/service/common/response'; +import { request } from 'https'; +import { authSystemAdmin } from '@fastgpt/service/support/permission/user/auth'; + +const baseUrl = process.env.AIPROXY_API_ENDPOINT; +const token = process.env.AIPROXY_API_TOKEN; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + await authSystemAdmin({ req }); + + if (!baseUrl || !token) { + throw new Error('AIPROXY_API_ENDPOINT or AIPROXY_API_TOKEN is not set'); + } + + const { path = [], ...query } = req.query as any; + + if (!path.length) { + throw new Error('url is empty'); + } + + const queryStr = new URLSearchParams(query).toString(); + const requestPath = queryStr + ? `/${path?.join('/')}?${new URLSearchParams(query).toString()}` + : `/${path?.join('/')}`; + + const parsedUrl = new URL(baseUrl); + delete req.headers?.cookie; + delete req.headers?.host; + delete req.headers?.origin; + + const requestResult = request({ + protocol: parsedUrl.protocol, + hostname: parsedUrl.hostname, + port: parsedUrl.port, + path: requestPath, + method: req.method, + headers: { + ...req.headers, + Authorization: `Bearer ${token}` + }, + timeout: 30000 + }); + + req.pipe(requestResult); + + requestResult.on('response', (response) => { + Object.keys(response.headers).forEach((key) => { + // @ts-ignore + res.setHeader(key, response.headers[key]); + }); + response.statusCode && res.writeHead(response.statusCode); + response.pipe(res); + }); + requestResult.on('error', (e) => { + res.send(e); + res.end(); + }); + } catch (error) { + jsonRes(res, { + code: 500, + error + }); + } +} + +export const config = { + api: { + bodyParser: false + } +}; diff --git a/projects/app/src/pages/api/aiproxy/api/createChannel.ts b/projects/app/src/pages/api/aiproxy/api/createChannel.ts new file mode 100644 index 000000000..498454daa --- /dev/null +++ b/projects/app/src/pages/api/aiproxy/api/createChannel.ts @@ -0,0 +1,33 @@ +import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; +import { authSystemAdmin } from '@fastgpt/service/support/permission/user/auth'; +import axios from 'axios'; +import { getErrText } from '@fastgpt/global/common/error/utils'; + +const baseUrl = process.env.AIPROXY_API_ENDPOINT; +const token = process.env.AIPROXY_API_TOKEN; + +async function handler(req: ApiRequestProps, res: ApiResponseType) { + try { + await authSystemAdmin({ req }); + + if (!baseUrl || !token) { + return Promise.reject('AIPROXY_API_ENDPOINT or AIPROXY_API_TOKEN is not set'); + } + + const { data } = await axios.post(`${baseUrl}/api/channel/`, req.body, { + headers: { + Authorization: `Bearer ${token}` + } + }); + + res.json(data); + } catch (error) { + res.json({ + success: false, + message: getErrText(error), + data: error + }); + } +} + +export default handler; diff --git a/projects/app/src/pages/api/core/ai/model/test.ts b/projects/app/src/pages/api/core/ai/model/test.ts index 54ddb9758..292d0e471 100644 --- a/projects/app/src/pages/api/core/ai/model/test.ts +++ b/projects/app/src/pages/api/core/ai/model/test.ts @@ -60,6 +60,7 @@ const testLLMModel = async (model: LLMModelItemType) => { const ai = getAIApi({ timeout: 10000 }); + const requestBody = llmCompletionsBodyFormat( { model: model.model, diff --git a/projects/app/src/pages/app/list/index.tsx b/projects/app/src/pages/app/list/index.tsx index 86d7da1ee..274909c26 100644 --- a/projects/app/src/pages/app/list/index.tsx +++ b/projects/app/src/pages/app/list/index.tsx @@ -218,7 +218,7 @@ const MyApps = () => { size="md" Button={ } menuList={[ diff --git a/projects/app/src/pages/dataset/list/index.tsx b/projects/app/src/pages/dataset/list/index.tsx index f50cd52ba..fe2c2dd5c 100644 --- a/projects/app/src/pages/dataset/list/index.tsx +++ b/projects/app/src/pages/dataset/list/index.tsx @@ -147,7 +147,7 @@ const Dataset = () => { } diff --git a/projects/app/src/service/common/system/index.ts b/projects/app/src/service/common/system/index.ts index 83337c470..cb5c2de89 100644 --- a/projects/app/src/service/common/system/index.ts +++ b/projects/app/src/service/common/system/index.ts @@ -83,7 +83,8 @@ export async function initSystemConfig() { ...fileRes?.feConfigs, ...defaultFeConfigs, ...(dbConfig.feConfigs || {}), - isPlus: !!FastGPTProUrl + isPlus: !!FastGPTProUrl, + show_aiproxy: !!process.env.AIPROXY_API_ENDPOINT }, systemEnv: { ...fileRes.systemEnv, diff --git a/projects/app/src/web/core/ai/channel.ts b/projects/app/src/web/core/ai/channel.ts new file mode 100644 index 000000000..f199d54df --- /dev/null +++ b/projects/app/src/web/core/ai/channel.ts @@ -0,0 +1,183 @@ +import axios, { Method, AxiosResponse } from 'axios'; +import { getWebReqUrl } from '@fastgpt/web/common/system/utils'; +import { + ChannelInfoType, + ChannelListResponseType, + ChannelLogListItemType, + CreateChannelProps +} from '@/global/aiproxy/type'; +import { ChannelStatusEnum } from '@/global/aiproxy/constants'; + +interface ResponseDataType { + success: boolean; + message: string; + data: any; +} + +/** + * 请求成功,检查请求头 + */ +function responseSuccess(response: AxiosResponse) { + return response; +} +/** + * 响应数据检查 + */ +function checkRes(data: ResponseDataType) { + if (data === undefined) { + console.log('error->', data, 'data is empty'); + return Promise.reject('服务器异常'); + } else if (!data.success) { + return Promise.reject(data); + } + return data.data; +} + +/** + * 响应错误 + */ +function responseError(err: any) { + console.log('error->', '请求错误', err); + const data = err?.response?.data || err; + + if (!err) { + return Promise.reject({ message: '未知错误' }); + } + if (typeof err === 'string') { + return Promise.reject({ message: err }); + } + if (typeof data === 'string') { + return Promise.reject(data); + } + + return Promise.reject(data); +} + +/* 创建请求实例 */ +const instance = axios.create({ + timeout: 60000, // 超时时间 + headers: { + 'content-type': 'application/json' + } +}); + +/* 响应拦截 */ +instance.interceptors.response.use(responseSuccess, (err) => Promise.reject(err)); + +function request(url: string, data: any, method: Method): any { + /* 去空 */ + for (const key in data) { + if (data[key] === undefined) { + delete data[key]; + } + } + + return instance + .request({ + baseURL: getWebReqUrl('/api/aiproxy/api'), + url, + method, + data: ['POST', 'PUT'].includes(method) ? data : undefined, + params: !['POST', 'PUT'].includes(method) ? data : undefined + }) + .then((res) => checkRes(res.data)) + .catch((err) => responseError(err)); +} + +/** + * api请求方式 + * @param {String} url + * @param {Any} params + * @param {Object} config + * @returns + */ +export function GET(url: string, params = {}): Promise { + return request(url, params, 'GET'); +} +export function POST(url: string, data = {}): Promise { + return request(url, data, 'POST'); +} +export function PUT(url: string, data = {}): Promise { + return request(url, data, 'PUT'); +} +export function DELETE(url: string, data = {}): Promise { + return request(url, data, 'DELETE'); +} + +// ====== API ====== +export const getChannelList = () => + GET('/channels/all', { + page: 1, + perPage: 10 + }); + +export const getChannelProviders = () => + GET< + Record< + number, + { + defaultBaseUrl: string; + keyHelp: string; + name: string; + } + > + >('/channels/type_metas'); + +export const postCreateChannel = (data: CreateChannelProps) => + POST(`/createChannel`, { + type: data.type, + name: data.name, + base_url: data.base_url, + models: data.models, + model_mapping: data.model_mapping, + key: data.key + }); + +export const putChannelStatus = (id: number, status: ChannelStatusEnum) => + POST(`/channel/${id}/status`, { + status + }); +export const putChannel = (data: ChannelInfoType) => + PUT(`/channel/${data.id}`, { + type: data.type, + name: data.name, + base_url: data.base_url, + models: data.models, + model_mapping: data.model_mapping, + key: data.key, + status: data.status, + priority: data.priority + }); + +export const deleteChannel = (id: number) => DELETE(`/channel/${id}`); + +export const getChannelLog = (params: { + channel?: string; + model_name?: string; + status?: 'all' | 'success' | 'error'; + start_timestamp: number; + end_timestamp: number; + offset: number; + pageSize: number; +}) => + GET<{ + logs: ChannelLogListItemType[]; + total: number; + }>(`/logs/search`, { + ...params, + p: Math.floor(params.offset / params.pageSize) + 1, + per_page: params.pageSize, + offset: undefined, + pageSize: undefined + }).then((res) => { + return { + list: res.logs, + total: res.total + }; + }); + +export const getLogDetail = (id: number) => + GET<{ + request_body: string; + response_body: string; + }>(`/logs/detail/${id}`);