Feat: tenant llm provider (#14595)

### What problem does this PR solve?

Python implementation of the Go-based model_provider API suite.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

---------

Co-authored-by: bill <yibie_jingnian@163.com>
This commit is contained in:
Lynn
2026-05-29 17:39:41 +08:00
committed by GitHub
parent b79f79d9b9
commit dc4b82523b
148 changed files with 6059 additions and 3075 deletions

View File

@@ -1,13 +1,13 @@
import { LlmModelType } from '@/constants/knowledge';
import { useTranslate } from '@/hooks/common-hooks';
import { useSelectLlmOptionsByModelType } from '@/hooks/use-llm-request';
import { useFetchAllAddedModels } from '@/hooks/use-llm-request';
import { cn } from '@/lib/utils';
import { camelCase } from 'lodash';
import { ReactNode, useMemo } from 'react';
import { useFormContext } from 'react-hook-form';
import { MinerUOptionsFormField } from './mineru-options-form-field';
import { SelectWithSearch } from './originui/select-with-search';
import { buildModelTree } from './model-tree-select';
import { PaddleOCROptionsFormField } from './paddleocr-options-form-field';
import { TreeSelect, TreeSelectNode } from './tree-select';
import {
FormControl,
FormField,
@@ -44,9 +44,9 @@ export function LayoutRecognizeFormField({
const form = useFormContext();
const { t } = useTranslate('knowledgeDetails');
const allOptions = useSelectLlmOptionsByModelType();
const { data: allAddedModels } = useFetchAllAddedModels();
const options = useMemo(() => {
const treeData = useMemo(() => {
const list = optionsWithoutLLM
? optionsWithoutLLM
: [
@@ -60,28 +60,28 @@ export function LayoutRecognizeFormField({
value: x,
}));
const image2TextList = [
...allOptions[LlmModelType.Image2text],
...allOptions[LlmModelType.Ocr],
].map((x) => {
return {
...x,
options: x.options.map((y) => {
return {
...y,
label: (
<div className="flex justify-between items-center gap-2">
{y.label}
<span className="text-red-500 text-sm">Experimental</span>
</div>
),
};
}),
};
});
const prependNodes: TreeSelectNode[] = list.map((x) => ({
id: x.value,
title: x.label,
}));
return [...list, ...image2TextList];
}, [allOptions, optionsWithoutLLM, t]);
const modelTree = buildModelTree(
allAddedModels,
['image2text', 'ocr'],
(node) => (
<div className="flex justify-between items-center gap-2 w-full">
<span className="flex items-center gap-1.5 truncate">
{node.label}
</span>
<span className="text-state-error text-sm flex-shrink-0">
Experimental
</span>
</div>
),
);
return [...prependNodes, ...modelTree];
}, [allAddedModels, optionsWithoutLLM, t]);
return (
<FormField
@@ -107,11 +107,17 @@ export function LayoutRecognizeFormField({
</FormLabel>
<div className={horizontal ? 'w-3/4' : 'w-full'}>
<FormControl>
<SelectWithSearch
<TreeSelect
{...field}
options={options}
data={treeData}
testId={testId}
></SelectWithSearch>
showSearch
defaultExpandAll
renderSelected={(node) => {
if (!node) return null;
return node.label ?? node.title;
}}
/>
</FormControl>
</div>
</div>

View File

@@ -1,21 +1,33 @@
import { getLLMIconName, getLlmNameAndFIdByLlmId } from '@/utils/llm-util';
import { parseModelValue } from '@/utils/llm-util';
import { memo } from 'react';
import { LlmIcon } from '../svg-icon';
interface IProps {
id?: string;
value?: string;
onChange?: (value: string) => void;
disabled?: boolean;
}
const LLMLabel = ({ value }: IProps) => {
const { llmName, fId } = getLlmNameAndFIdByLlmId(value);
export const LLMLabel = ({ value }: IProps) => {
const parsed = value ? parseModelValue(value) : null;
const modelName = parsed?.model_name;
const instanceName = parsed?.model_instance;
const iconName = parsed ? parsed.model_provider : '';
if (!modelName) return null;
return (
<div className="flex items-center gap-1 text-xs text-text-secondary">
<LlmIcon name={getLLMIconName(fId, llmName)} width={20} height={20} />
<span className="flex-1 truncate"> {llmName}</span>
<div className="flex items-center gap-1.5 min-w-0">
<LlmIcon
name={iconName}
width={22}
height={22}
imgClass="size-[22px] flex-shrink-0"
/>
<span className="font-medium truncate">{modelName}</span>
{instanceName && (
<span className="text-slate-400 truncate flex-shrink-0">
{instanceName}
</span>
)}
</div>
);
};

View File

@@ -1,11 +1,11 @@
import { LlmModelType } from '@/constants/knowledge';
import { useComposeLlmOptionsByModelTypes } from '@/hooks/use-llm-request';
import * as SelectPrimitive from '@radix-ui/react-select';
import { forwardRef, memo, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { LlmSettingFieldItems } from '../llm-setting-items/next';
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
import { Select, SelectTrigger, SelectValue } from '../ui/select';
import LLMLabel from './llm-label';
export interface NextInnerLLMSelectProps {
id?: string;
@@ -51,8 +51,6 @@ const NextInnerLLMSelect = forwardRef<
}
}, [filter, ttsModel]);
const modelOptions = useComposeLlmOptionsByModelTypes(modelTypes);
return (
<Select disabled={disabled} value={value}>
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
@@ -66,17 +64,13 @@ const NextInnerLLMSelect = forwardRef<
data-testid={triggerTestId}
>
<SelectValue placeholder={t('common.pleaseSelect')}>
{
modelOptions
.flatMap((x) => x.options)
.find((x) => x.value === value)?.label
}
<LLMLabel value={value} />
</SelectValue>
</SelectTrigger>
</PopoverTrigger>
<PopoverContent side={'left'}>
<LlmSettingFieldItems
options={modelOptions}
modelTypes={modelTypes}
llmOptionTestIdPrefix={optionTestIdPrefix}
></LlmSettingFieldItems>
</PopoverContent>

View File

@@ -1,45 +1,24 @@
import { LlmModelType } from '@/constants/knowledge';
import { useComposeLlmOptionsByModelTypes } from '@/hooks/use-llm-request';
import { ModelTreeSelect } from '@/components/model-tree-select';
import { useTranslation } from 'react-i18next';
import { SelectWithSearch } from '../originui/select-with-search';
import { RAGFlowFormItem } from '../ragflow-form';
export type LLMFormFieldProps = {
options?: any[];
modelTypes?: string[];
name?: string;
testId?: string;
optionTestIdPrefix?: string;
config?: any;
};
export const useModelOptions = () => {
const modelOptions = useComposeLlmOptionsByModelTypes([
LlmModelType.Chat,
LlmModelType.Image2text,
]);
return {
modelOptions,
};
};
export function LLMFormField({
options,
name,
testId,
optionTestIdPrefix,
config,
}: LLMFormFieldProps) {
export function LLMFormField({ name, config, modelTypes }: LLMFormFieldProps) {
const { t } = useTranslation();
const { modelOptions } = useModelOptions();
return (
<RAGFlowFormItem name={name || 'llm_id'} label={t('chat.model')}>
<SelectWithSearch
options={options || modelOptions}
testId={testId}
optionTestIdPrefix={optionTestIdPrefix}
{...config}
></SelectWithSearch>
<ModelTreeSelect
allowClear={config?.allowClear ?? false}
modelTypes={modelTypes}
/>
</RAGFlowFormItem>
);
}

View File

@@ -27,7 +27,7 @@ import { useHandleFreedomChange } from './use-watch-change';
interface LlmSettingFieldItemsProps {
prefix?: string;
options?: any[];
modelTypes?: string[];
llmId?: string;
llmSelectTestId?: string;
llmOptionTestIdPrefix?: string;
@@ -69,7 +69,7 @@ export const LlmSettingSchema = {
export function LlmSettingFieldItems({
prefix,
options,
modelTypes,
llmSelectTestId,
llmOptionTestIdPrefix,
showFields = [
@@ -137,7 +137,7 @@ export function LlmSettingFieldItems({
return (
<div className="space-y-5">
<LLMFormField
options={options}
modelTypes={modelTypes}
name={llmId ?? getFieldWithPrefix('llm_id')}
testId={llmSelectTestId}
optionTestIdPrefix={llmOptionTestIdPrefix}

View File

@@ -0,0 +1,190 @@
import LLMLabel from '@/components/llm-select/llm-label';
import { LlmIcon } from '@/components/svg-icon';
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { useFetchAllAddedModels } from '@/hooks/use-llm-request';
import { IAddedModel } from '@/interfaces/database/llm';
import { getRealModelName } from '@/utils/llm-util';
import { useCallback, useMemo } from 'react';
import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { TreeSelect, TreeSelectNode } from './tree-select';
/** Maps form field names to their supported model types */
export const ModelTypeMap: Record<string, string[]> = {
llm_id: ['chat', 'image2text'],
embd_id: ['embedding'],
img2txt_id: ['image2text'],
asr_id: ['speech2text'],
rerank_id: ['rerank'],
tts_id: ['tts'],
};
export function buildModelTree(
allModels: IAddedModel[],
modelTypes: string[],
renderLeafLabel?: (
node: TreeSelectNode,
model: IAddedModel,
) => React.ReactNode,
): TreeSelectNode[] {
const filtered = allModels.filter((m) =>
m.model_type?.some((t) => modelTypes.includes(t)),
);
const seenLeafIds = new Set<string>();
const providerMap = new Map<string, Map<string, IAddedModel[]>>();
for (const model of filtered) {
let instances = providerMap.get(model.provider_name);
if (!instances) {
instances = new Map();
providerMap.set(model.provider_name, instances);
}
let modelList = instances.get(model.instance_name);
if (!modelList) {
modelList = [];
instances.set(model.instance_name, modelList);
}
modelList.push(model);
}
return Array.from(providerMap.entries()).map(([provider, instances]) => ({
id: provider,
title: provider,
children: Array.from(instances.entries()).map(([instance, models]) => ({
id: `${provider}||${instance}`,
title: instance,
children: models.reduce<TreeSelectNode[]>((acc, m) => {
const modelName = getRealModelName(m.name);
const id = `${modelName}@${m.instance_name}@${m.provider_name}`;
if (seenLeafIds.has(id)) return acc;
seenLeafIds.add(id);
const leafNode: TreeSelectNode = {
id,
title: modelName,
label: (
<span className="flex items-center gap-1.5 truncate">
<LlmIcon
name={m.provider_name}
width={22}
height={22}
imgClass="size-[22px] flex-shrink-0"
/>
<span className="truncate">{modelName}</span>
</span>
),
data: {
provider_name: m.provider_name,
instance_name: m.instance_name,
model_name: modelName,
},
};
if (renderLeafLabel) {
leafNode.label = renderLeafLabel(leafNode, m);
}
acc.push(leafNode);
return acc;
}, []),
})),
}));
}
export interface ModelTreeSelectProps {
modelTypes?: string[];
value?: string;
onChange?: (value: string) => void;
disabled?: boolean;
placeholder?: string;
showSearch?: boolean;
allowClear?: boolean;
className?: string;
renderSelected?: (node: TreeSelectNode | undefined) => React.ReactNode;
testId?: string;
}
export function ModelTreeSelect({
modelTypes = ModelTypeMap.llm_id,
value,
onChange,
disabled,
placeholder,
showSearch = true,
allowClear = false,
className,
renderSelected,
testId,
}: ModelTreeSelectProps) {
const { data: allAddedModels } = useFetchAllAddedModels();
const treeData = useMemo(
() => buildModelTree(allAddedModels, modelTypes),
[allAddedModels, modelTypes],
);
const defaultRenderSelected = useCallback(
(node: TreeSelectNode | undefined) => {
if (!node?.id) return null;
return <LLMLabel value={node.id} />;
},
[],
);
return (
<TreeSelect
data={treeData}
value={value}
onChange={onChange}
placeholder={placeholder}
disabled={disabled}
showSearch={showSearch}
allowClear={allowClear}
defaultExpandAll
className={className}
renderSelected={renderSelected ?? defaultRenderSelected}
testId={testId}
/>
);
}
export interface ModelTreeSelectFormFieldProps extends ModelTreeSelectProps {
name?: string;
label?: string;
tooltip?: string;
}
export function ModelTreeSelectFormField({
name = 'llm_id',
label,
tooltip,
...rest
}: ModelTreeSelectFormFieldProps) {
const form = useFormContext();
const { t } = useTranslation();
return (
<FormField
control={form.control}
name={name}
render={({ field }) => (
<FormItem>
{label && <FormLabel tooltip={tooltip}>{label}</FormLabel>}
<FormControl>
<ModelTreeSelect
{...rest}
value={field.value}
onChange={field.onChange}
placeholder={rest.placeholder ?? t('common.pleaseSelect')}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
}

View File

@@ -1,9 +1,7 @@
import { LlmModelType } from '@/constants/knowledge';
import { ModelTreeSelect } from '@/components/model-tree-select';
import { useTranslate } from '@/hooks/common-hooks';
import { useSelectLlmOptionsByModelType } from '@/hooks/use-llm-request';
import { useFormContext } from 'react-hook-form';
import { z } from 'zod';
import { SelectWithSearch } from './originui/select-with-search';
import { SliderInputFormField } from './slider-input-form-field';
import {
FormControl,
@@ -26,8 +24,6 @@ const RerankId = 'rerank_id';
function RerankFormField() {
const form = useFormContext();
const { t } = useTranslate('knowledgeDetails');
const allOptions = useSelectLlmOptionsByModelType();
const options = allOptions[LlmModelType.Rerank];
return (
<FormField
@@ -37,12 +33,12 @@ function RerankFormField() {
<FormItem>
<FormLabel tooltip={t('rerankTip')}>{t('rerankModel')}</FormLabel>
<FormControl>
<SelectWithSearch
<ModelTreeSelect
modelTypes={['rerank']}
allowClear
placeholder={t('rerankPlaceholder')}
{...field}
options={options}
></SelectWithSearch>
/>
</FormControl>
<FormMessage />
</FormItem>

View File

@@ -0,0 +1,269 @@
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { cn } from '@/lib/utils';
import { ChevronDown, ChevronRight, Search, X } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
export interface TreeSelectNode {
id: string;
title: string;
label?: React.ReactNode;
children?: TreeSelectNode[];
disabled?: boolean;
data?: Record<string, any>;
}
interface TreeSelectProps {
data: TreeSelectNode[];
value?: string;
onChange?: (value: string) => void;
placeholder?: string;
disabled?: boolean;
allowClear?: boolean;
showSearch?: boolean;
className?: string;
defaultExpandAll?: boolean;
renderSelected?: (node: TreeSelectNode | undefined) => React.ReactNode;
testId?: string;
}
export function TreeSelect({
data,
value,
onChange,
placeholder,
disabled,
allowClear,
showSearch,
className,
defaultExpandAll,
renderSelected,
testId,
}: TreeSelectProps) {
const [open, setOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
const { t } = useTranslation();
useEffect(() => {
if (!defaultExpandAll) return;
const ids = new Set<string>();
const walk = (nodes: TreeSelectNode[]) => {
for (const node of nodes) {
if (node.children?.length) {
ids.add(node.id);
walk(node.children);
}
}
};
walk(data);
setExpandedIds(ids);
}, [data, defaultExpandAll]);
const selectedNode = useMemo(() => {
const find = (nodes: TreeSelectNode[]): TreeSelectNode | undefined => {
for (const node of nodes) {
if (node.id === value) return node;
if (node.children) {
const found = find(node.children);
if (found) return found;
}
}
};
return find(data);
}, [data, value]);
const isLeaf = useCallback(
(node: TreeSelectNode) => !node.children?.length,
[],
);
const handleToggle = useCallback((id: string) => {
setExpandedIds((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
}, []);
const handleSelect = useCallback(
(node: TreeSelectNode) => {
if (node.disabled) return;
if (isLeaf(node)) {
onChange?.(node.id);
setOpen(false);
setSearchTerm('');
} else {
handleToggle(node.id);
}
},
[isLeaf, onChange, handleToggle],
);
const handleClear = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation();
onChange?.('');
},
[onChange],
);
const filterTree = useCallback(
(nodes: TreeSelectNode[], term: string): TreeSelectNode[] => {
if (!term) return nodes;
return nodes.reduce<TreeSelectNode[]>((acc, node) => {
const titleMatch = node.title
.toLowerCase()
.includes(term.toLowerCase());
const filteredChildren = node.children
? filterTree(node.children, term)
: undefined;
if (titleMatch || filteredChildren?.length) {
acc.push({ ...node, children: filteredChildren ?? node.children });
}
return acc;
}, []);
},
[],
);
const filteredData = useMemo(
() => filterTree(data, searchTerm),
[data, searchTerm, filterTree],
);
const visibleExpandedIds = useMemo(() => {
if (!searchTerm) return expandedIds;
const ids = new Set<string>();
const walk = (nodes: TreeSelectNode[]) => {
for (const node of nodes) {
if (node.children?.length) {
ids.add(node.id);
walk(node.children);
}
}
};
walk(filteredData);
return ids;
}, [searchTerm, expandedIds, filteredData]);
const renderTree = useCallback(
(nodes: TreeSelectNode[], level = 0): React.ReactNode => {
return nodes.map((node) => {
const leaf = isLeaf(node);
const expanded = visibleExpandedIds.has(node.id);
const selected = value === node.id;
return (
<div key={node.id}>
<div
className={cn(
'flex items-center rounded-sm cursor-pointer text-sm',
'hover:bg-accent transition-colors',
!leaf && 'text-text-primary font-medium',
selected && 'bg-accent text-accent-foreground',
node.disabled && 'opacity-50 pointer-events-none',
)}
style={{
paddingLeft: `${level * 20 + 4}px`,
paddingRight: '8px',
paddingTop: '6px',
paddingBottom: '6px',
}}
onClick={() => handleSelect(node)}
>
<span className="w-4 h-4 mr-0.5 flex-shrink-0 flex items-center justify-center">
{!leaf && (
<>
{expanded ? (
<ChevronDown className="h-3.5 w-3.5 text-text-secondary" />
) : (
<ChevronRight className="h-3.5 w-3.5 text-text-secondary" />
)}
</>
)}
</span>
<span className="truncate">{node.label ?? node.title}</span>
</div>
{!leaf && expanded && node.children && (
<div>{renderTree(node.children, level + 1)}</div>
)}
</div>
);
});
},
[isLeaf, visibleExpandedIds, value, handleSelect],
);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild disabled={disabled}>
<button
type="button"
data-testid={testId}
className={cn(
'flex items-center justify-between border border-border-button rounded-md px-3 py-1.5 w-full',
'bg-bg-input text-sm',
'hover:bg-border-button transition-colors',
disabled && 'opacity-50 cursor-not-allowed',
className,
)}
>
<span className={cn('truncate', !selectedNode && 'text-slate-400')}>
{renderSelected
? renderSelected(selectedNode)
: selectedNode?.title || placeholder || t('common.pleaseSelect')}
</span>
<div className="flex items-center ml-2 flex-shrink-0">
{allowClear && value ? (
<X
className="h-4 w-4 opacity-50 hover:opacity-100"
onClick={handleClear}
/>
) : (
<ChevronDown className="h-4 w-4 opacity-50" />
)}
</div>
</button>
</PopoverTrigger>
<PopoverContent
className="p-0 w-[var(--radix-popover-trigger-width)]"
align="start"
sideOffset={4}
>
{showSearch && (
<div className="flex items-center border-b px-3 py-2">
<Search className="h-4 w-4 text-slate-400 mr-2 flex-shrink-0" />
<input
type="text"
className="flex-1 bg-transparent text-sm outline-none placeholder:text-slate-400"
placeholder={t('common.search')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
)}
<div
className="max-h-60 overflow-auto p-1"
onWheel={(e) => e.stopPropagation()}
>
{filteredData.length > 0 ? (
renderTree(filteredData)
) : (
<div className="py-6 text-center text-sm text-slate-400">
{t('common.noData')}
</div>
)}
</div>
</PopoverContent>
</Popover>
);
}

View File

@@ -1,3 +1,8 @@
export enum ModelStatus {
Active = 'active',
Inactive = 'inactive',
}
export enum LLMFactory {
TongYiQianWen = 'Tongyi-Qianwen',
Moonshot = 'Moonshot',
@@ -140,6 +145,24 @@ export const IconMap = {
[LLMFactory.Perplexity]: 'perplexity',
};
export const ModelTypeToField: Record<string, string> = {
chat: 'llm_id',
embedding: 'embd_id',
image2text: 'img2txt_id',
speech2text: 'asr_id',
rerank: 'rerank_id',
tts: 'tts_id',
};
export const FieldToModelType: Record<string, string> = {
llm_id: 'chat',
embd_id: 'embedding',
img2txt_id: 'vision',
asr_id: 'asr',
rerank_id: 'rerank',
tts_id: 'tts',
};
export const APIMapUrl = {
[LLMFactory.OpenAI]: 'https://platform.openai.com/api-keys',
[LLMFactory.Anthropic]: 'https://console.anthropic.com/settings/keys',

View File

@@ -29,7 +29,7 @@ import {
import { v4 as uuid } from 'uuid';
import { useTranslate } from './common-hooks';
import { useSetPaginationParams } from './route-hook';
import { useFetchTenantInfo, useSaveSetting } from './use-user-setting-request';
import { useSaveSetting } from './use-user-setting-request';
export function usePrevious<T>(value: T) {
const ref = useRef<T>();
@@ -748,12 +748,6 @@ export const useSelectItem = (defaultId?: string) => {
return { selectedId, handleItemClick };
};
export const useFetchModelId = () => {
const { data: tenantInfo } = useFetchTenantInfo(true);
return tenantInfo?.llm_id ?? '';
};
const ChunkTokenNumMap = {
naive: 128,
knowledge_graph: 8192,

View File

@@ -197,6 +197,10 @@ export const useNavigatePage = () => {
[navigate],
);
const navigateToModelSetting = useCallback(() => {
navigate(`${Routes.UserSetting}${Routes.Model}`);
}, [navigate]);
return {
navigateToDatasetList,
navigateToDataset,
@@ -223,5 +227,6 @@ export const useNavigatePage = () => {
navigateToDataSourceDetail,
navigateToMemory,
navigateToMemoryList,
navigateToModelSetting,
};
};

View File

@@ -1,197 +1,68 @@
import { LlmIcon } from '@/components/svg-icon';
import message from '@/components/ui/message';
import { LlmModelType } from '@/constants/knowledge';
import { DefaultOptionType } from '@/interfaces/antd-compat';
import { ResponseGetType } from '@/interfaces/database/base';
import { ModelTypeToField } from '@/constants/llm';
import {
IFactory,
IAddedModel,
IAvailableProvider,
IDefaultModel,
IInstanceModel,
IMyLlmValue,
IThirdOAIModelCollection as IThirdAiModelCollection,
IThirdOAIModel,
IThirdOAIModelCollection,
IProviderInstance,
} from '@/interfaces/database/llm';
import {
IAddLlmRequestBody,
IDeleteLlmRequestBody,
IAddInstanceModelRequestBody,
IAddProviderInstanceRequestBody,
IAddProviderRequestBody,
IDeleteProviderInstanceRequestBody,
IListAllModelsRequestParams,
IListProvidersRequestParams,
ISetDefaultModelRequestBody,
IUpdateModelStatusRequestBody,
} from '@/interfaces/request/llm';
import userService from '@/services/user-service';
import { getLLMIconName, getRealModelName } from '@/utils/llm-util';
import llmService from '@/services/llm-service';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { buildLlmUuid } from '@/utils/llm-util';
import { buildModelValue, parseModelValue } from '@/utils/llm-util';
import { useWarnEmptyModel } from './use-warn-empty-model';
export const enum LLMApiAction {
LlmList = 'llmList',
MyLlmList = 'myLlmList',
MyLlmListDetailed = 'myLlmListDetailed',
FactoryList = 'factoryList',
SaveApiKey = 'saveApiKey',
SaveTenantInfo = 'saveTenantInfo',
AddLlm = 'addLlm',
DeleteLlm = 'deleteLlm',
EnableLlm = 'enableLlm',
DeleteFactory = 'deleteFactory',
AllModels = 'allModels',
AvailableProviders = 'availableProviders',
AddedProviders = 'addedProviders',
AddProvider = 'addProvider',
AddProviderInstance = 'addProviderInstance',
AddInstanceModel = 'addInstanceModel',
DeleteProviderInstance = 'deleteProviderInstance',
ListDefaultModels = 'listDefaultModels',
SetDefaultModel = 'setDefaultModel',
}
export const useFetchLlmList = (modelType?: LlmModelType) => {
const { data } = useQuery<IThirdAiModelCollection>({
queryKey: [LLMApiAction.LlmList],
initialData: {},
queryFn: async () => {
const { data } = await userService.llmList({ model_type: modelType });
return data?.data ?? {};
},
});
return data;
export const LlmKeys = {
availableProviders: () => [LLMApiAction.AvailableProviders] as const,
addedProviders: () => [LLMApiAction.AddedProviders] as const,
allModels: (modelType?: string) =>
[LLMApiAction.AllModels, modelType] as const,
providerInstances: (providerName: string) =>
[LLMApiAction.AddedProviders, providerName, 'instances'] as const,
instanceModels: (providerName: string, instanceName: string) =>
[
LLMApiAction.AddedProviders,
providerName,
instanceName,
'models',
] as const,
defaultModels: () => [LLMApiAction.ListDefaultModels] as const,
};
type IThirdOAIModelWithUuid = IThirdOAIModel & { uuid: string };
export function useSelectFlatLlmList(modelType?: LlmModelType) {
const llmList = useFetchLlmList(modelType);
return Object.values(llmList).reduce<IThirdOAIModelWithUuid[]>((pre, cur) => {
pre.push(...cur.map((x) => ({ ...x, uuid: buildLlmUuid(x) })));
return pre;
}, []);
}
export function useFindLlmByUuid(modelType?: LlmModelType) {
const flatList = useSelectFlatLlmList(modelType);
return (uuid: string) => {
return flatList.find((x) => x.uuid === uuid);
};
}
function buildLlmOptionsWithIcon(x: IThirdOAIModel) {
return {
label: (
<div className="flex items-center justify-center gap-2">
<LlmIcon
name={getLLMIconName(x.fid, x.llm_name)}
width={24}
height={24}
size={'small'}
imgClass="size-6"
/>
<span>{getRealModelName(x.llm_name)}</span>
</div>
),
value: `${x.llm_name}@${x.fid}`,
disabled: !x.available,
is_tools: x.is_tools,
};
}
export const useSelectLlmOptionsByModelType = () => {
const llmInfo: IThirdOAIModelCollection = useFetchLlmList();
const groupImage2TextOptions = useCallback(() => {
const modelType = LlmModelType.Image2text;
const modelTag = modelType.toUpperCase();
return Object.entries(llmInfo)
.map(([key, value]) => {
return {
label: key,
options: value
.filter(
(x) =>
(x.model_type.includes(modelType) ||
(x.tags && x.tags.includes(modelTag))) &&
x.available &&
x.status === '1',
)
.map(buildLlmOptionsWithIcon),
};
})
.filter((x) => x.options.length > 0);
}, [llmInfo]);
const groupOptionsByModelType = useCallback(
(modelType: LlmModelType) => {
return Object.entries(llmInfo)
.filter(([, value]) =>
modelType
? value.some((x) => x.model_type.includes(modelType))
: true,
)
.map(([key, value]) => {
return {
label: key,
options: value
.filter(
(x) =>
(modelType ? x.model_type.includes(modelType) : true) &&
x.available,
)
.map(buildLlmOptionsWithIcon),
};
})
.filter((x) => x.options.length > 0);
},
[llmInfo],
);
return {
[LlmModelType.Chat]: groupOptionsByModelType(LlmModelType.Chat),
[LlmModelType.Embedding]: groupOptionsByModelType(LlmModelType.Embedding),
[LlmModelType.Image2text]: groupImage2TextOptions(),
[LlmModelType.Speech2text]: groupOptionsByModelType(
LlmModelType.Speech2text,
),
[LlmModelType.Rerank]: groupOptionsByModelType(LlmModelType.Rerank),
[LlmModelType.TTS]: groupOptionsByModelType(LlmModelType.TTS),
[LlmModelType.Ocr]: groupOptionsByModelType(LlmModelType.Ocr),
};
};
// Merge different types of models from the same manufacturer under one manufacturer
export const useComposeLlmOptionsByModelTypes = (
modelTypes: LlmModelType[],
) => {
const allOptions = useSelectLlmOptionsByModelType();
return modelTypes.reduce<
(DefaultOptionType & {
options: {
label: JSX.Element;
value: string;
disabled: boolean;
is_tools: boolean;
}[];
})[]
>((pre, cur) => {
const options = allOptions[cur];
options.forEach((x) => {
const item = pre.find((y) => y.label === x.label);
if (item) {
x.options.forEach((y) => {
// A model that is both an image2text and speech2text model
if (!item.options.some((z) => z.value === y.value)) {
item.options.push(y);
}
});
} else {
pre.push(x);
}
});
return pre;
}, []);
};
export const useFetchLlmFactoryList = (): ResponseGetType<IFactory[]> => {
const { data, isFetching: loading } = useQuery({
queryKey: [LLMApiAction.FactoryList],
export const useFetchAvailableProviders = () => {
const { data, isFetching: loading } = useQuery<IAvailableProvider[]>({
queryKey: LlmKeys.availableProviders(),
initialData: [],
gcTime: 0,
queryFn: async () => {
const { data } = await userService.factoriesList();
const params: IListProvidersRequestParams = { available: true };
const { data } = await llmService.listProviders({ params }, true);
return data?.data ?? [];
},
@@ -200,190 +71,198 @@ export const useFetchLlmFactoryList = (): ResponseGetType<IFactory[]> => {
return { data, loading };
};
export const useFetchAddedProviders = () => {
const { data, isFetching: loading } = useQuery<IAvailableProvider[]>({
queryKey: LlmKeys.addedProviders(),
initialData: [],
gcTime: 0,
queryFn: async () => {
const { data } = await llmService.listProviders({ params: {} }, true);
return data?.data ?? [];
},
});
return { data, loading };
};
export const useFetchAllAddedModels = (modelType?: string) => {
const { data, isFetching: loading } = useQuery<IAddedModel[]>({
queryKey: LlmKeys.allModels(modelType),
initialData: [],
gcTime: 0,
queryFn: async () => {
const params: IListAllModelsRequestParams = {};
if (modelType) {
params.type = modelType;
}
const { data } = await llmService.listAllAddedModels({ params }, true);
return data?.data ?? [];
},
});
return { data, loading };
};
export function useFindLlmByUuid() {
const { data: models } = useFetchAllAddedModels();
return (uuid: string) => {
const parsed = parseModelValue(uuid);
if (parsed) {
return models.find(
(m) =>
m.name === parsed.model_name &&
m.instance_name === parsed.model_instance &&
m.provider_name === parsed.model_provider,
);
}
return undefined;
};
}
export const useFetchProviderInstances = (providerName: string) => {
const { data, isFetching: loading } = useQuery<IProviderInstance[]>({
queryKey: LlmKeys.providerInstances(providerName),
initialData: [],
gcTime: 0,
enabled: !!providerName,
queryFn: async () => {
const { data } = await llmService.listProviderInstances(
{ provider_name: providerName },
true,
);
return data?.data ?? [];
},
});
return { data, loading };
};
export const useFetchInstanceModels = (
providerName: string,
instanceName: string,
) => {
const { data, isFetching: loading } = useQuery<IInstanceModel[]>({
queryKey: LlmKeys.instanceModels(providerName, instanceName),
initialData: [],
gcTime: 0,
enabled: !!providerName && !!instanceName,
queryFn: async () => {
const { data } = await llmService.listInstanceModels(
{ provider_name: providerName, instance_name: instanceName },
true,
);
return data?.data ?? [];
},
});
return { data, loading };
};
export type LlmItem = { name: string; logo: string } & IMyLlmValue;
export const useFetchMyLlmList = (): ResponseGetType<
Record<string, IMyLlmValue>
> => {
const { data, isFetching: loading } = useQuery({
queryKey: [LLMApiAction.MyLlmList],
initialData: {},
gcTime: 0,
queryFn: async () => {
const { data } = await userService.myLlm();
return data?.data ?? {};
},
});
return { data, loading };
};
export const useFetchMyLlmListDetailed = (): ResponseGetType<
Record<string, any>
> => {
const { data, isFetching: loading } = useQuery({
queryKey: [LLMApiAction.MyLlmListDetailed],
initialData: {},
gcTime: 0,
queryFn: async () => {
const { data } = await userService.myLlm({ include_details: true });
return data?.data ?? {};
},
});
return { data, loading };
};
export const useSelectLlmList = () => {
const { data: myLlmList, loading: myLlmListLoading } = useFetchMyLlmList();
const { data: factoryList, loading: factoryListLoading } =
useFetchLlmFactoryList();
const nextMyLlmList: Array<LlmItem> = useMemo(() => {
return Object.entries(myLlmList).map(([key, value]) => ({
name: key,
logo: factoryList.find((x) => x.name === key)?.logo ?? '',
...value,
llm: value.llm?.map((x) => ({ ...x, name: x.name })),
}));
}, [myLlmList, factoryList]);
const nextFactoryList = useMemo(() => {
const currentList = factoryList.filter((x) =>
Object.keys(myLlmList).every((y) => y !== x.name),
);
return currentList;
// return sortLLmFactoryListBySpecifiedOrder(currentList);
}, [factoryList, myLlmList]);
return {
myLlmList: nextMyLlmList,
factoryList: nextFactoryList,
loading: myLlmListLoading || factoryListLoading,
};
};
export interface IApiKeySavingParams {
llm_factory: string;
api_key: string;
llm_name?: string;
model_type?: string;
base_url?: string;
source_fid?: string;
verify?: boolean;
}
export const useSaveApiKey = () => {
const queryClient = useQueryClient();
// const { t } = useTranslation();
export const useAddProvider = () => {
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [LLMApiAction.SaveApiKey],
mutationFn: async (params: IApiKeySavingParams) => {
const { data } = await userService.setApiKey(params);
if (data.code === 0) {
// message.success(t('message.modified'));
queryClient.invalidateQueries({ queryKey: [LLMApiAction.MyLlmList] });
queryClient.invalidateQueries({
queryKey: [LLMApiAction.MyLlmListDetailed],
});
queryClient.invalidateQueries({ queryKey: [LLMApiAction.FactoryList] });
mutationKey: [LLMApiAction.AddProvider],
mutationFn: async (params: IAddProviderRequestBody) => {
try {
const { data: listRes } = await llmService.listProviders(
{ params: {} },
true,
);
const isProviderAdded = listRes?.data?.some(
(p: IAvailableProvider) => p.name === params.provider_name,
);
if (isProviderAdded) {
return { code: 0, data: null };
}
} catch {
// ignore list failure and proceed to add
}
const { data } = await llmService.addProvider(params);
return data;
},
});
return { data, loading, saveApiKey: mutateAsync };
return { data, loading, addProvider: mutateAsync };
};
export interface ISystemModelSettingSavingParams {
tenant_id: string;
name?: string;
asr_id: string;
embd_id: string;
img2txt_id: string;
llm_id: string;
}
export const useSaveTenantInfo = () => {
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [LLMApiAction.SaveTenantInfo],
mutationFn: async (params: ISystemModelSettingSavingParams) => {
const { data } = await userService.setTenantInfo(params);
if (data.code === 0) {
message.success(t('message.modified'));
}
return data.code;
},
});
return { data, loading, saveTenantInfo: mutateAsync };
};
export const useAddLlm = () => {
export const useAddProviderInstance = () => {
const { addProvider } = useAddProvider();
const queryClient = useQueryClient();
// const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [LLMApiAction.AddLlm],
mutationFn: async (params: IAddLlmRequestBody & { verify?: boolean }) => {
const { data } = await userService.addLlm(params);
mutationKey: [LLMApiAction.AddProviderInstance],
mutationFn: async (
params: IAddProviderInstanceRequestBody & { verify?: boolean },
) => {
try {
await addProvider({ provider_name: params.llm_factory });
const { data: instancesRes } = await llmService.listProviderInstances(
{ provider_name: params.llm_factory },
true,
);
const instanceExists = instancesRes?.data?.some(
(i: IProviderInstance) => i.instance_name === params.instance_name,
);
if (instanceExists && !params.verify) {
return { code: 0, data: null };
}
} catch {
// ignore list failure and proceed to add
}
const { data } = await llmService.addProviderInstance(params);
if (data.code === 0 && !params.verify) {
queryClient.invalidateQueries({ queryKey: [LLMApiAction.MyLlmList] });
queryClient.invalidateQueries({
queryKey: [LLMApiAction.MyLlmListDetailed],
queryKey: LlmKeys.addedProviders(),
});
queryClient.invalidateQueries({ queryKey: [LLMApiAction.FactoryList] });
queryClient.invalidateQueries({ queryKey: [LLMApiAction.LlmList] });
// message.success(t('message.modified'));
}
return data;
},
});
return { data, loading, addLlm: mutateAsync };
return { data, loading, addProviderInstance: mutateAsync };
};
export const useDeleteLlm = () => {
export const useAddInstanceModel = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [LLMApiAction.DeleteLlm],
mutationFn: async (params: IDeleteLlmRequestBody) => {
const { data } = await userService.deleteLlm(params);
mutationKey: [LLMApiAction.AddInstanceModel],
mutationFn: async (
params: {
provider_name: string;
instance_name: string;
} & IAddInstanceModelRequestBody,
) => {
const { data } = await llmService.addInstanceModel(params);
if (data.code === 0) {
queryClient.invalidateQueries({ queryKey: [LLMApiAction.MyLlmList] });
queryClient.invalidateQueries({
queryKey: [LLMApiAction.MyLlmListDetailed],
queryKey: LlmKeys.addedProviders(),
});
queryClient.invalidateQueries({ queryKey: [LLMApiAction.FactoryList] });
message.success(t('message.deleted'));
}
return data.code;
return data;
},
});
return { data, loading, deleteLlm: mutateAsync };
return { data, loading, addInstanceModel: mutateAsync };
};
export const useEnableLlm = () => {
export const useDeleteProviderInstance = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const {
@@ -391,52 +270,104 @@ export const useEnableLlm = () => {
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [LLMApiAction.EnableLlm],
mutationFn: async (params: IDeleteLlmRequestBody & { enable: boolean }) => {
const reqParam: IDeleteLlmRequestBody & {
enable?: boolean;
status?: 1 | 0;
} = { ...params, status: params.enable ? 1 : 0 };
delete reqParam.enable;
const { data } = await userService.enableLlm(reqParam);
mutationKey: [LLMApiAction.DeleteProviderInstance],
mutationFn: async (params: IDeleteProviderInstanceRequestBody) => {
const { data } = await llmService.deleteProviderInstance(params);
if (data.code === 0) {
queryClient.invalidateQueries({ queryKey: [LLMApiAction.MyLlmList] });
queryClient.invalidateQueries({
queryKey: [LLMApiAction.MyLlmListDetailed],
queryKey: LlmKeys.addedProviders(),
exact: true,
});
queryClient.invalidateQueries({ queryKey: [LLMApiAction.FactoryList] });
queryClient.invalidateQueries({
queryKey: LlmKeys.providerInstances(params.provider_name),
});
queryClient.invalidateQueries({
queryKey: LlmKeys.defaultModels(),
});
message.success(t('message.deleted'));
}
return data;
},
});
return { data, loading, deleteProviderInstance: mutateAsync };
};
export const useUpdateModelStatus = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const { isPending: loading, mutateAsync } = useMutation({
mutationKey: [LLMApiAction.AddedProviders, 'updateModelStatus'],
mutationFn: async (params: IUpdateModelStatusRequestBody) => {
const { data } = await llmService.updateModelStatus(params);
if (data.code === 0) {
message.success(t('message.modified'));
queryClient.invalidateQueries({
queryKey: LlmKeys.defaultModels(),
});
queryClient.invalidateQueries({
queryKey: LlmKeys.instanceModels(
params.provider_name,
params.instance_name,
),
});
}
return data.code;
return data;
},
});
return { data, loading, enableLlm: mutateAsync };
return { loading, updateModelStatus: mutateAsync };
};
export const useDeleteFactory = () => {
export const useFetchDefaultModels = () => {
const { data, isFetching: loading } = useQuery<IDefaultModel[]>({
queryKey: LlmKeys.defaultModels(),
initialData: [],
gcTime: 0,
queryFn: async () => {
const { data } = await llmService.listDefaultModels({}, true);
return data?.data?.models ?? [];
},
});
return { data, loading };
};
export const useFetchDefaultModelDictionary = (showEmptyModelWarn = false) => {
const { data: defaultModels } = useFetchDefaultModels();
const result = useMemo(() => {
const dict: Record<string, string> = {};
Object.entries(ModelTypeToField).forEach(([key, field]) => {
const model = defaultModels.find((m) => m.model_type === key);
dict[field] = model && model.enable ? buildModelValue(model) : '';
});
return dict;
}, [defaultModels]);
useWarnEmptyModel(showEmptyModelWarn, result.embd_id, result.llm_id);
return result;
};
export const useSetDefaultModel = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [LLMApiAction.DeleteFactory],
mutationFn: async (params: IDeleteLlmRequestBody) => {
const { data } = await userService.deleteFactory(params);
const { isPending: loading, mutateAsync } = useMutation({
mutationKey: [LLMApiAction.SetDefaultModel],
mutationFn: async (params: ISetDefaultModelRequestBody) => {
const { data } = await llmService.setDefaultModel(params);
if (data.code === 0) {
queryClient.invalidateQueries({ queryKey: [LLMApiAction.MyLlmList] });
message.success(t('message.modified'));
queryClient.invalidateQueries({
queryKey: [LLMApiAction.MyLlmListDetailed],
queryKey: LlmKeys.defaultModels(),
});
queryClient.invalidateQueries({ queryKey: [LLMApiAction.FactoryList] });
queryClient.invalidateQueries({ queryKey: [LLMApiAction.LlmList] });
message.success(t('message.deleted'));
}
return data.code;
return data;
},
});
return { data, loading, deleteFactory: mutateAsync };
return { loading, setDefaultModel: mutateAsync };
};

View File

@@ -1,5 +1,4 @@
import message from '@/components/ui/message';
import { Modal } from '@/components/ui/modal/modal';
import { ResponseGetType } from '@/interfaces/database/base';
import { IToken } from '@/interfaces/database/chat';
import { ITenantInfo } from '@/interfaces/database/dataset';
@@ -11,7 +10,6 @@ import {
} from '@/interfaces/database/user-setting';
import { ISetLangfuseConfigRequestBody } from '@/interfaces/request/system';
import { DEFAULT_LANGUAGE_CODE, supportedLanguages } from '@/locales/config';
import { Routes } from '@/routes';
import userService, {
addTenantUser,
agreeTenant,
@@ -20,11 +18,10 @@ import userService, {
listTenantUser,
} from '@/services/user-service';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import DOMPurify from 'dompurify';
import { isEmpty } from 'lodash';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router';
import { useWarnEmptyModel } from './use-warn-empty-model';
export const enum UserSettingApiAction {
UserInfo = 'userInfo',
@@ -69,11 +66,10 @@ export const useFetchUserInfo = (): ResponseGetType<IUserInfo> => {
return { data, loading };
};
// Stop using this interface to retrieve the default model; instead, directly call `useFetchDefaultModelDictionary`.
export const useFetchTenantInfo = (
showEmptyModelWarn = false,
): ResponseGetType<ITenantInfo> => {
const { t } = useTranslation();
const navigate = useNavigate();
const { data, isFetching: loading } = useQuery({
queryKey: [UserSettingApiAction.TenantInfo, showEmptyModelWarn],
initialData: {},
@@ -84,27 +80,6 @@ export const useFetchTenantInfo = (
// llm_id is chat_id
// asr_id is speech2txt
const { data } = res;
if (
showEmptyModelWarn &&
(isEmpty(data.embd_id) || isEmpty(data.llm_id))
) {
Modal.warning({
title: t('common.warn'),
content: (
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(t('setting.modelProvidersWarn')),
}}
></div>
),
closable: false,
showCancel: false,
onOk() {
// window.open('/user-setting/model', '_self');
navigate(`${Routes.UserSetting}${Routes.Model}`);
},
});
}
data.chat_id = data.llm_id;
data.speech2text_id = data.asr_id;
@@ -115,6 +90,8 @@ export const useFetchTenantInfo = (
},
});
useWarnEmptyModel(showEmptyModelWarn, data?.embd_id, data?.llm_id);
return { data, loading };
};

View File

@@ -0,0 +1,43 @@
import { Modal } from '@/components/ui/modal/modal';
import DOMPurify from 'dompurify';
import { isEmpty } from 'lodash';
import { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigatePage } from './logic-hooks/navigate-hooks';
export const useWarnEmptyModel = (
showEmptyModelWarn: boolean,
embdId?: string,
llmId?: string,
) => {
const { t } = useTranslation();
const warnedRef = useRef(false);
const { navigateToModelSetting } = useNavigatePage();
useEffect(() => {
if (
showEmptyModelWarn &&
!warnedRef.current &&
(isEmpty(embdId) || isEmpty(llmId)) &&
typeof embdId === 'string' &&
typeof llmId === 'string'
) {
warnedRef.current = true;
Modal.warning({
title: t('common.warn'),
content: (
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(t('setting.modelProvidersWarn')),
}}
></div>
),
closable: false,
showCancel: false,
onOk() {
navigateToModelSetting();
},
});
}
}, [showEmptyModelWarn, embdId, llmId, navigateToModelSetting, t]);
};

View File

@@ -40,3 +40,41 @@ export interface Llm {
status: '0' | '1';
used_token: number;
}
export interface IAvailableProvider {
name: string;
model_types: string[];
url: { default?: string; [key: string]: string | undefined };
}
export interface IProviderInstance {
api_key: string;
id: string;
instance_name: string;
provider_id: string;
region: string;
status: string;
}
export interface IAddedModel {
model_type: string[];
name: string;
provider_id: string;
provider_name: string;
instance_id: string;
instance_name: string;
}
export interface IInstanceModel {
max_tokens: number;
model_type: string[];
name: string;
status: string;
}
export interface IDefaultModel {
enable: boolean;
model_instance: string;
model_name: string;
model_provider: string;
model_type: string;
}

View File

@@ -1,7 +1,7 @@
export interface IAddLlmRequestBody {
llm_factory: string; // Ollama
llm_name: string;
model_type: string;
model_type: string | string[];
api_base?: string; // chat|embedding|speech2text|image2text
api_key?: string | Record<string, any>;
max_tokens: number;
@@ -12,3 +12,50 @@ export interface IDeleteLlmRequestBody {
llm_factory: string; // Ollama
llm_name?: string;
}
export interface IListProvidersRequestParams {
available?: boolean;
}
export interface IAddProviderRequestBody {
provider_name: string;
}
export type IAddProviderInstanceRequestBody = IAddLlmRequestBody & {
instance_name: string;
};
export interface IDeleteProviderInstanceRequestBody {
provider_name: string;
instances: string[];
}
export interface IShowProviderInstanceRequestParams {
provider_name: string;
instance_name: string;
}
export interface IAddInstanceModelRequestBody {
model_name: string;
model_type: string[];
max_tokens: number;
extra?: Record<string, any>;
}
export interface IListAllModelsRequestParams {
type?: string;
}
export interface IUpdateModelStatusRequestBody {
provider_name: string;
instance_name: string;
model_name: string;
status: 'active' | 'inactive';
}
export interface ISetDefaultModelRequestBody {
model_provider: string;
model_instance: string;
model_type: string;
model_name: string;
}

View File

@@ -1514,6 +1514,10 @@ Example: Virtual Hosted Style`,
addLlmTitle: 'Add LLM',
editLlmTitle: 'Edit {{name}} model',
editModel: 'Edit model',
instanceName: 'Instance name',
instanceNameMessage: 'Please input the instance name!',
instanceNameTip:
'A unique name to identify this provider instance under the same factory.',
modelName: 'Model name',
modelID: 'Model ID',
modelUid: 'Model UID',

View File

@@ -1099,7 +1099,8 @@ NER使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系
gmailTokenTip:
'请上传由 Google Console 生成的 OAuth JSON。如果仅包含 client credentials请通过浏览器授权一次以获取长期有效的刷新 Token。',
dropboxDescription: '连接 Dropbox同步指定账号下的文件与文件夹。',
teamsDescription: '通过 Microsoft Graph 连接 Microsoft Teams同步频道帖子与回复。',
teamsDescription:
'通过 Microsoft Graph 连接 Microsoft Teams同步频道帖子与回复。',
teamsTenantIdTip:
'Azure AD 租户 ID。需要具备 Team.ReadBasic.All 与 ChannelMessage.Read.All 应用权限(管理员同意)的应用。',
slackDescription: '连接你的 Slack 工作区,同步频道消息与讨论串。',
@@ -1107,7 +1108,8 @@ NER使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系
'Slack 机器人用户 OAuth Token以 xoxb- 开头)。应用需具备 channels:read、channels:history 和 users:read 权限。',
slackChannelsTip:
'可选:需要同步的频道名称(例如 general。留空则同步所有可访问的频道。',
sharepointDescription: '通过 Microsoft Graph 连接 SharePoint 站点,同步其文档库。',
sharepointDescription:
'通过 Microsoft Graph 连接 SharePoint 站点,同步其文档库。',
sharepointSiteUrlTip:
'要索引的 SharePoint 站点完整 URL例如 https://contoso.sharepoint.com/sites/MySite。需要具备 Sites.Read.All 与 Files.Read.All 应用权限(管理员同意)的 Azure AD 应用。',
boxDescription: '连接你的 Box 云盘以同步文件和文件夹。',
@@ -1238,6 +1240,9 @@ NER使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系
addLlmTitle: '添加 LLM',
editLlmTitle: '编辑 {{name}} 模型',
editModel: '编辑模型',
instanceName: '实例名称',
instanceNameMessage: '请输入实例名称!',
instanceNameTip: '用于在同一厂商下唯一标识该实例的名称。',
modelName: '模型名称',
modelID: '模型ID',
modelUid: '模型UID',

View File

@@ -17,8 +17,9 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { useSelectFlatLlmList } from '@/hooks/use-llm-request';
import { useFetchAllAddedModels } from '@/hooks/use-llm-request';
import { cn } from '@/lib/utils';
import { parseModelValue } from '@/utils/llm-util';
import { PropsWithChildren, useMemo } from 'react';
export function CardWithForm() {
@@ -80,12 +81,23 @@ export function LabelCard({ children, className, ...props }: LabelCardProps) {
}
export function LLMLabelCard({ llmId }: { llmId?: string }) {
const flatLlmList = useSelectFlatLlmList();
const { data: allAddedModels } = useFetchAllAddedModels();
const isValidLlm = useMemo(() => {
if (!llmId) return false;
return flatLlmList.some((llm) => llm.uuid === llmId);
}, [flatLlmList, llmId]);
const parsed = parseModelValue(llmId);
if (parsed) {
return allAddedModels.some(
(m) =>
m.name === parsed.model_name &&
m.instance_name === parsed.model_instance &&
m.provider_name === parsed.model_provider,
);
}
return false;
}, [allAddedModels, llmId]);
return (
<LabelCard

View File

@@ -19,6 +19,7 @@ import { Input, NumberInput } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Separator } from '@/components/ui/separator';
import { Switch } from '@/components/ui/switch';
import { LlmModelType } from '@/constants/knowledge';
import { useFindLlmByUuid } from '@/hooks/use-llm-request';
import { zodResolver } from '@hookform/resolvers/zod';
import { get } from 'lodash';
@@ -158,7 +159,9 @@ function AgentForm({ node }: INextOperatorForm) {
<FormWrapper>
{isSubAgent && <DescriptionField></DescriptionField>}
<LargeModelFormField showSpeech2TextModel></LargeModelFormField>
{findLlmByUuid(llmId)?.tags?.includes('IMAGE2TEXT') && (
{findLlmByUuid(llmId)?.model_type?.includes(
LlmModelType.Image2text,
) && (
<QueryVariable
name="visual_files_var"
label="Visual Input File"

View File

@@ -1,4 +1,4 @@
import { useFetchModelId } from '@/hooks/logic-hooks';
import { useFetchDefaultModelDictionary } from '@/hooks/use-llm-request';
import { RAGFlowNodeType } from '@/interfaces/database/agent';
import { get, isEmpty, omit } from 'lodash';
import { useMemo } from 'react';
@@ -13,15 +13,15 @@ function omitToolsAndMcp(values: Record<string, any>) {
}
export function useValues(node?: RAGFlowNodeType) {
const llmId = useFetchModelId();
const defaultModelDictionary = useFetchDefaultModelDictionary();
const defaultValues = useMemo(
() => ({
...omitToolsAndMcp(initialAgentValues),
llm_id: llmId,
llm_id: defaultModelDictionary.llm_id,
prompts: '',
}),
[llmId],
[defaultModelDictionary],
);
const values = useMemo(() => {

View File

@@ -1,9 +1,5 @@
import { crossLanguageOptions } from '@/components/cross-language-form-field';
import { LayoutRecognizeFormField } from '@/components/layout-recognize-form-field';
import {
LLMFormField,
LLMFormFieldProps,
} from '@/components/llm-setting-items/llm-form-field';
import {
SelectWithSearch,
SelectWithSearchFlagOptionType,
@@ -75,19 +71,6 @@ export function ParserMethodFormField({
);
}
export function LargeModelFormField({
prefix,
options,
}: CommonProps & Pick<LLMFormFieldProps, 'options'>) {
return (
<LLMFormField
name={buildFieldNameWithPrefix('vlm.llm_id', prefix)}
options={options}
config={{ allowClear: true }}
></LLMFormField>
);
}
export function FlattenMediaToTextFormField({ prefix }: CommonProps) {
const { t } = useTranslation();
return (

View File

@@ -1,11 +1,13 @@
import { ParseDocumentType } from '@/components/layout-recognize-form-field';
import {
ModelTreeSelectFormField,
ModelTypeMap,
} from '@/components/model-tree-select';
import {
SelectWithSearch,
SelectWithSearchFlagOptionType,
} from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { LlmModelType } from '@/constants/knowledge';
import { useComposeLlmOptionsByModelTypes } from '@/hooks/use-llm-request';
import { isEmpty } from 'lodash';
import { useEffect, useMemo } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
@@ -13,7 +15,6 @@ import { useTranslation } from 'react-i18next';
import {
FlattenMediaToTextFormField,
LanguageFormField,
LargeModelFormField,
ParserMethodFormField,
RemoveHeaderFooterFormField,
RmdirFormField,
@@ -38,9 +39,6 @@ export function PdfFormFields({ prefix }: CommonProps) {
const form = useFormContext();
const parseMethodName = buildFieldNameWithPrefix('parse_method', prefix);
const modelOptions = useComposeLlmOptionsByModelTypes([
LlmModelType.Image2text,
]);
const parseMethod = useWatch({
name: parseMethodName,
});
@@ -109,10 +107,12 @@ export function PdfFormFields({ prefix }: CommonProps) {
<ParserMethodFormField prefix={prefix}></ParserMethodFormField>
<FlattenMediaToTextFormField prefix={prefix} />
{!flattenMediaToText && (
<LargeModelFormField
prefix={prefix}
options={modelOptions}
></LargeModelFormField>
<ModelTreeSelectFormField
name={buildFieldNameWithPrefix('vlm.llm_id', prefix)}
label={t('chat.model')}
modelTypes={ModelTypeMap.img2txt_id}
allowClear
/>
)}
{languageShown && <LanguageFormField prefix={prefix}></LanguageFormField>}
{tcadpOptionsShown && (

View File

@@ -1,18 +1,19 @@
import { ParseDocumentType } from '@/components/layout-recognize-form-field';
import {
ModelTreeSelectFormField,
ModelTypeMap,
} from '@/components/model-tree-select';
import {
SelectWithSearch,
SelectWithSearchFlagOptionType,
} from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { LlmModelType } from '@/constants/knowledge';
import { useComposeLlmOptionsByModelTypes } from '@/hooks/use-llm-request';
import { isEmpty } from 'lodash';
import { useEffect, useMemo } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import {
FlattenMediaToTextFormField,
LargeModelFormField,
ParserMethodFormField,
} from './common-form-fields';
import { CommonProps } from './interface';
@@ -31,9 +32,6 @@ const markdownImageResponseTypeOptions: SelectWithSearchFlagOptionType[] = [
export function SpreadsheetFormFields({ prefix }: CommonProps) {
const { t } = useTranslation();
const form = useFormContext();
const modelOptions = useComposeLlmOptionsByModelTypes([
LlmModelType.Image2text,
]);
const parseMethodName = buildFieldNameWithPrefix('parse_method', prefix);
@@ -103,10 +101,12 @@ export function SpreadsheetFormFields({ prefix }: CommonProps) {
></ParserMethodFormField>
<FlattenMediaToTextFormField prefix={prefix} />
{!flattenMediaToText && (
<LargeModelFormField
prefix={prefix}
options={modelOptions}
></LargeModelFormField>
<ModelTreeSelectFormField
name={buildFieldNameWithPrefix('vlm.llm_id', prefix)}
label={t('chat.model')}
modelTypes={ModelTypeMap.img2txt_id}
allowClear
/>
)}
{tcadpOptionsShown && (
<>

View File

@@ -1,9 +1,11 @@
import { LlmModelType } from '@/constants/knowledge';
import { useComposeLlmOptionsByModelTypes } from '@/hooks/use-llm-request';
import {
ModelTreeSelectFormField,
ModelTypeMap,
} from '@/components/model-tree-select';
import { useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import {
FlattenMediaToTextFormField,
LargeModelFormField,
RemoveHeaderFooterFormField,
RmdirFormField,
} from './common-form-fields';
@@ -11,9 +13,7 @@ import { CommonProps } from './interface';
import { buildFieldNameWithPrefix } from './utils';
export function TextMarkdownFormFields({ prefix }: CommonProps) {
const modelOptions = useComposeLlmOptionsByModelTypes([
LlmModelType.Image2text,
]);
const { t } = useTranslation();
const flattenMediaToText = useWatch({
name: buildFieldNameWithPrefix('flatten_media_to_text', prefix),
});
@@ -23,10 +23,12 @@ export function TextMarkdownFormFields({ prefix }: CommonProps) {
<RmdirFormField prefix={prefix} />
<FlattenMediaToTextFormField prefix={prefix} />
{!flattenMediaToText && (
<LargeModelFormField
prefix={prefix}
options={modelOptions}
></LargeModelFormField>
<ModelTreeSelectFormField
name={buildFieldNameWithPrefix('vlm.llm_id', prefix)}
label={t('chat.model')}
modelTypes={ModelTypeMap.img2txt_id}
allowClear
/>
)}
</>
);

View File

@@ -1,38 +1,39 @@
import { LlmModelType } from '@/constants/knowledge';
import { useComposeLlmOptionsByModelTypes } from '@/hooks/use-llm-request';
import {
LargeModelFormField,
OutputFormatFormFieldProps,
} from './common-form-fields';
ModelTreeSelectFormField,
ModelTypeMap,
} from '@/components/model-tree-select';
import { useTranslation } from 'react-i18next';
import { OutputFormatFormFieldProps } from './common-form-fields';
import { buildFieldNameWithPrefix } from './utils';
export function AudioFormFields({ prefix }: OutputFormatFormFieldProps) {
const modelOptions = useComposeLlmOptionsByModelTypes([
LlmModelType.Speech2text,
]);
const { t } = useTranslation();
return (
<>
{/* Multimodal Model */}
<LargeModelFormField
prefix={prefix}
options={modelOptions}
></LargeModelFormField>
<ModelTreeSelectFormField
name={buildFieldNameWithPrefix('vlm.llm_id', prefix)}
label={t('chat.model')}
modelTypes={ModelTypeMap.asr_id}
allowClear
/>
</>
);
}
export function VideoFormFields({ prefix }: OutputFormatFormFieldProps) {
const modelOptions = useComposeLlmOptionsByModelTypes([
LlmModelType.Image2text,
]);
const { t } = useTranslation();
return (
<>
{/* Multimodal Model */}
<LargeModelFormField
prefix={prefix}
options={modelOptions}
></LargeModelFormField>
<ModelTreeSelectFormField
name={buildFieldNameWithPrefix('vlm.llm_id', prefix)}
label={t('chat.model')}
modelTypes={ModelTypeMap.img2txt_id}
allowClear
/>
</>
);
}

View File

@@ -1,9 +1,11 @@
import { LlmModelType } from '@/constants/knowledge';
import { useComposeLlmOptionsByModelTypes } from '@/hooks/use-llm-request';
import {
ModelTreeSelectFormField,
ModelTypeMap,
} from '@/components/model-tree-select';
import { useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import {
FlattenMediaToTextFormField,
LargeModelFormField,
OutputFormatFormFieldProps,
RemoveHeaderFooterFormField,
RmdirFormField,
@@ -11,9 +13,7 @@ import {
import { buildFieldNameWithPrefix } from './utils';
export function WordFormFields({ prefix }: OutputFormatFormFieldProps) {
const modelOptions = useComposeLlmOptionsByModelTypes([
LlmModelType.Image2text,
]);
const { t } = useTranslation();
const flattenMediaToText = useWatch({
name: buildFieldNameWithPrefix('flatten_media_to_text', prefix),
});
@@ -24,10 +24,12 @@ export function WordFormFields({ prefix }: OutputFormatFormFieldProps) {
<RemoveHeaderFooterFormField prefix={prefix} />
<FlattenMediaToTextFormField prefix={prefix} />
{!flattenMediaToText && (
<LargeModelFormField
prefix={prefix}
options={modelOptions}
></LargeModelFormField>
<ModelTreeSelectFormField
name={buildFieldNameWithPrefix('vlm.llm_id', prefix)}
label={t('chat.model')}
modelTypes={ModelTypeMap.img2txt_id}
allowClear
/>
)}
</>
);

View File

@@ -1,5 +1,5 @@
import { NextLLMSelect } from '@/components/llm-select/next';
import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item';
import { ModelTreeSelectFormField } from '@/components/model-tree-select';
import {
Form,
FormControl,
@@ -24,20 +24,10 @@ const RewriteQuestionForm = ({ form }: INextOperatorForm) => {
e.preventDefault();
}}
>
<FormField
control={form.control}
<ModelTreeSelectFormField
name="llm_id"
render={({ field }) => (
<FormItem>
<FormLabel tooltip={t('chat.modelTip')}>
{t('chat.model')}
</FormLabel>
<FormControl>
<NextLLMSelect {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
label={t('chat.model')}
tooltip={t('chat.modelTip')}
/>
<FormField
control={form.control}

View File

@@ -1,4 +1,4 @@
import { useFetchModelId } from '@/hooks/logic-hooks';
import { useFetchDefaultModelDictionary } from '@/hooks/use-llm-request';
import { Connection, Node, Position, ReactFlowInstance } from '@xyflow/react';
import humanId from 'human-id';
import { t } from 'i18next';
@@ -123,13 +123,17 @@ function useAddGroupNode() {
return { addGroupNode };
}
export const useInitializeOperatorParams = () => {
const llmId = useFetchModelId();
const defaultModelDictionary = useFetchDefaultModelDictionary();
const llmId = defaultModelDictionary.llm_id;
const initialFormValuesMap = useMemo(() => {
return {
[Operator.Begin]: initialBeginValues,
[Operator.Retrieval]: initialRetrievalValues,
[Operator.Categorize]: { ...initialCategorizeValues, llm_id: llmId },
[Operator.Categorize]: {
...initialCategorizeValues,
llm_id: llmId,
},
[Operator.RewriteQuestion]: {
...initialRewriteQuestionValues,
llm_id: llmId,

View File

@@ -3,10 +3,8 @@ import {
FormFieldType,
RenderField,
} from '@/components/dynamic-form';
import {
SelectWithSearch,
SelectWithSearchFlagOptionType,
} from '@/components/originui/select-with-search';
import { ModelTreeSelect, ModelTypeMap } from '@/components/model-tree-select';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { SliderInputFormField } from '@/components/slider-input-form-field';
import { Button } from '@/components/ui/button';
import {
@@ -19,9 +17,8 @@ import {
import { Radio } from '@/components/ui/radio';
import { Spin } from '@/components/ui/spin';
import { Switch } from '@/components/ui/switch';
import { LlmModelType, ParseType } from '@/constants/knowledge';
import { ParseType } from '@/constants/knowledge';
import { useTranslate } from '@/hooks/common-hooks';
import { useComposeLlmOptionsByModelTypes } from '@/hooks/use-llm-request';
import { cn } from '@/lib/utils';
import { history } from '@/utils/simple-history-util';
import { t } from 'i18next';
@@ -51,7 +48,6 @@ import {
useHandleKbEmbedding,
useHasParsedDocument,
useSelectChunkMethodList,
useSelectEmbeddingModelOptions,
} from '../hooks';
interface IProps {
line?: 1 | 2;
@@ -117,13 +113,12 @@ export const EmbeddingSelect = ({
}) => {
const { t } = useTranslate('knowledgeConfiguration');
const form = useFormContext();
const embeddingModelOptions = useSelectEmbeddingModelOptions();
const { handleChange } = useHandleKbEmbedding();
const oldValue = useMemo(() => {
const embdStr = form.getValues(name || 'embedding_model');
return embdStr || '';
}, [form]);
}, [form, name]);
const [loading, setLoading] = useState(false);
return (
<Spin
@@ -132,14 +127,14 @@ export const EmbeddingSelect = ({
'opacity-20': loading,
})}
>
<SelectWithSearch
<ModelTreeSelect
modelTypes={ModelTypeMap.embd_id}
onChange={async (value) => {
field.onChange(value);
if (isEdit && disabled) {
setLoading(true);
const res = await handleChange({
embed_id: value,
// callback: field.onChange,
});
if (res.code !== 0) {
field.onChange(oldValue);
@@ -149,7 +144,6 @@ export const EmbeddingSelect = ({
}}
disabled={disabled && !isEdit}
value={field.value}
options={embeddingModelOptions}
placeholder={t('embeddingModelPlaceholder')}
testId={testId}
/>
@@ -544,18 +538,14 @@ export const LLMSelect = ({
disabled?: boolean;
}) => {
const { t } = useTranslate('knowledgeConfiguration');
const modelOptions = useComposeLlmOptionsByModelTypes([
LlmModelType.Chat,
LlmModelType.Image2text,
]);
return (
<SelectWithSearch
onChange={async (value) => {
<ModelTreeSelect
modelTypes={ModelTypeMap.llm_id}
onChange={(value) => {
field.onChange(value);
}}
disabled={disabled && !isEdit}
value={field.value}
options={modelOptions as SelectWithSearchFlagOptionType[]}
placeholder={t('embeddingModelPlaceholder')}
/>
);

View File

@@ -1,8 +1,6 @@
import { LlmModelType } from '@/constants/knowledge';
import { useSetModalState } from '@/hooks/common-hooks';
import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request';
import { useSelectLlmOptionsByModelType } from '@/hooks/use-llm-request';
import { useSelectParserList } from '@/hooks/use-user-setting-request';
import { checkEmbedding } from '@/services/knowledge-service';
import { useIsFetching } from '@tanstack/react-query';
@@ -22,11 +20,6 @@ export function useSelectChunkMethodList() {
return parserList.filter((x) => !HiddenFields.some((y) => y === x.value));
}
export function useSelectEmbeddingModelOptions() {
const allOptions = useSelectLlmOptionsByModelType();
return allOptions[LlmModelType.Embedding];
}
export function useHasParsedDocument(isEdit?: boolean) {
const { data: knowledgeDetails } = useFetchKnowledgeBaseConfiguration({
isEdit,

View File

@@ -19,7 +19,7 @@ import {
import { Input } from '@/components/ui/input';
import { FormLayout } from '@/constants/form';
import { ParseType } from '@/constants/knowledge';
import { useFetchTenantInfo } from '@/hooks/use-user-setting-request';
import { useFetchDefaultModelDictionary } from '@/hooks/use-llm-request';
import { IModalProps } from '@/interfaces/common';
import { zodResolver } from '@hookform/resolvers/zod';
import { omit } from 'lodash';
@@ -39,7 +39,7 @@ const ChunkMethodName = 'chunk_method';
export function InputForm({ onOk }: IModalProps<any>) {
const { t } = useTranslation();
const { data: tenantInfo } = useFetchTenantInfo();
const defaultModelDictionary = useFetchDefaultModelDictionary();
const FormSchema = z
.object({
@@ -87,7 +87,7 @@ export function InputForm({ onOk }: IModalProps<any>) {
name: '',
parseType: ParseType.BuiltIn,
[ChunkMethodName]: '',
embedding_model: tenantInfo?.embd_id,
embedding_model: defaultModelDictionary?.embd_id,
},
});

View File

@@ -5,7 +5,7 @@ import { useHandleFilterSubmit } from '@/components/list-filter-bar/use-handle-f
import message from '@/components/ui/message';
import { useSetModalState } from '@/hooks/common-hooks';
import { useHandleSearchChange } from '@/hooks/logic-hooks';
import { useFetchTenantInfo } from '@/hooks/use-user-setting-request';
import { useFetchDefaultModelDictionary } from '@/hooks/use-llm-request';
import memoryService, { updateMemoryById } from '@/services/memory-service';
import {
buildOwnersFilter,
@@ -243,13 +243,13 @@ export const useRenameMemory = () => {
const { updateMemory } = useUpdateMemory();
const { createMemory } = useCreateMemory();
const [loading, setLoading] = useState(false);
const { data: tenantInfo } = useFetchTenantInfo();
const defaultModelDictionary = useFetchDefaultModelDictionary();
const handleShowChatRenameModal = useCallback(
(record?: IMemory) => {
if (record) {
const embd_id = record.embd_id || tenantInfo?.embd_id;
const llm_id = record.llm_id || tenantInfo?.llm_id;
const embd_id = record.embd_id || defaultModelDictionary?.embd_id;
const llm_id = record.llm_id || defaultModelDictionary?.llm_id;
setMemory({
...record,
embd_id,
@@ -258,7 +258,7 @@ export const useRenameMemory = () => {
}
showChatRenameModal();
},
[showChatRenameModal, tenantInfo],
[showChatRenameModal, defaultModelDictionary],
);
const handleHideModal = useCallback(() => {

View File

@@ -1,5 +1,5 @@
import { FormFieldType, RenderField } from '@/components/dynamic-form';
import { useModelOptions } from '@/components/llm-setting-items/llm-form-field';
import { ModelTreeSelect } from '@/components/model-tree-select';
import { EmbeddingSelect } from '@/pages/dataset/dataset-setting/configuration/common-item';
import { MemoryOptions, MemoryType } from '@/pages/memories/constants';
import { TFunction } from 'i18next';
@@ -28,7 +28,6 @@ export const defaultMemoryModelForm = {
memory_size: 0,
};
export const MemoryModelForm = () => {
const { modelOptions } = useModelOptions();
const { t } = useTranslation();
const { data } = useFetchMemoryMessageList();
return (
@@ -40,7 +39,6 @@ export const MemoryModelForm = () => {
placeholder: t('memories.selectModel'),
required: true,
horizontal: true,
// hideLabel: true,
type: FormFieldType.Custom,
disabled: true,
render: (field) => (
@@ -58,12 +56,17 @@ export const MemoryModelForm = () => {
field={{
name: 'llm_id',
label: t('memories.llm'),
placeholder: t('memories.selectModel'),
required: true,
horizontal: true,
type: FormFieldType.Select,
type: FormFieldType.Custom,
disabled: data?.messages?.total_count > 0,
options: modelOptions as { value: string; label: string }[],
render: (field) => (
<ModelTreeSelect
value={field.value}
onChange={field.onChange}
placeholder={t('memories.selectModel')}
/>
),
tooltip: t('memories.llmTooltip'),
}}
/>
@@ -93,7 +96,6 @@ export const MemoryModelForm = () => {
type: FormFieldType.Number,
horizontal: true,
tooltip: t('memory.config.memorySizeTooltip'),
// placeholder: t('memory.config.memorySizePlaceholder'),
required: false,
}}
/>

View File

@@ -157,7 +157,7 @@ const ChatCard = forwardRef(function ChatCard(
},
},
});
}, [currentDialog, dialogId, form, patchChat]);
}, [currentDialog, dialogId, form, patchChat, findLlmByUuid]);
useImperativeHandle(
ref,

View File

@@ -1,18 +1,26 @@
import { LlmModelType } from '@/constants/knowledge';
import { useComposeLlmOptionsByModelTypes } from '@/hooks/use-llm-request';
import { useMount } from 'ahooks';
import { ModelTypeMap } from '@/components/model-tree-select';
import { useFetchAllAddedModels } from '@/hooks/use-llm-request';
import { getRealModelName } from '@/utils/llm-util';
import { useEffect, useRef } from 'react';
import { UseFormReturn } from 'react-hook-form';
export function useSetDefaultModel(form: UseFormReturn<any>) {
const modelOptions = useComposeLlmOptionsByModelTypes([
LlmModelType.Chat,
LlmModelType.Image2text,
]);
const { data: allAddedModels } = useFetchAllAddedModels();
const hasSet = useRef(false);
useMount(() => {
const firstModel = modelOptions.at(0)?.options.at(0)?.value;
if (firstModel) {
form.setValue('llm_id', firstModel);
useEffect(() => {
if (hasSet.current || !allAddedModels.length) return;
const chatModels = allAddedModels.filter((m) =>
m.model_type?.some((t) => ModelTypeMap.llm_id.includes(t)),
);
const first = chatModels[0];
if (first) {
const modelName = getRealModelName(first.name);
form.setValue(
'llm_id',
`${modelName}@${first.instance_name}@${first.provider_name}`,
);
hasSet.current = true;
}
});
}, [allAddedModels, form]);
}

View File

@@ -0,0 +1,65 @@
import { useSetModalState } from '@/hooks/common-hooks';
import { useCreateChat } from '@/hooks/use-chat-request';
import { useFetchDefaultModelDictionary } from '@/hooks/use-llm-request';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
export const useCreateChatDialog = () => {
const {
visible: createChatVisible,
hideModal: hideCreateChatModal,
showModal: showCreateChatModal,
} = useSetModalState();
const { createChat, loading: createLoading } = useCreateChat();
const { t } = useTranslation();
const defaultModelDictionary =
useFetchDefaultModelDictionary(createChatVisible);
const InitialData = useMemo(
() => ({
name: '',
icon: '',
language: 'English',
description: '',
dataset_ids: [],
prompt_config: {
empty_response: '',
prologue: t('chat.setAnOpenerInitial'),
quote: true,
keyword: false,
tts: false,
system: t('chat.systemInitialValue'),
refine_multiturn: false,
use_kg: false,
reasoning: false,
parameters: [{ key: 'knowledge', optional: false }],
toc_enhance: false,
},
llm_id: defaultModelDictionary?.llm_id,
llm_setting: {},
similarity_threshold: 0.2,
vector_similarity_weight: 0.3,
top_n: 8,
top_k: 1024,
}),
[t, defaultModelDictionary?.llm_id],
);
const onCreateChatOk = useCallback(
async (name: string) => {
const ret = await createChat({ ...InitialData, name });
if (ret === 0) {
hideCreateChatModal();
}
},
[InitialData, createChat, hideCreateChatModal],
);
return {
createChatLoading: createLoading,
onCreateChatOk,
createChatVisible,
hideCreateChatModal,
showCreateChatModal,
};
};

View File

@@ -1,11 +1,7 @@
import { useSetModalState } from '@/hooks/common-hooks';
import { useCreateChat, usePatchChat } from '@/hooks/use-chat-request';
import { useFindLlmByUuid } from '@/hooks/use-llm-request';
import { useFetchTenantInfo } from '@/hooks/use-user-setting-request';
import { usePatchChat } from '@/hooks/use-chat-request';
import { IDialog } from '@/interfaces/database/chat';
import { isEmpty } from 'lodash';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useCallback, useState } from 'react';
export const useRenameChat = () => {
const [chat, setChat] = useState<IDialog>({} as IDialog);
@@ -14,70 +10,25 @@ export const useRenameChat = () => {
hideModal: hideChatRenameModal,
showModal: showChatRenameModal,
} = useSetModalState();
const { createChat, loading: createLoading } = useCreateChat();
const { patchChat, loading: patchLoading } = usePatchChat();
const { t } = useTranslation();
const tenantInfo = useFetchTenantInfo();
const findLlmByUuid = useFindLlmByUuid();
const InitialData = useMemo(
() => ({
name: '',
icon: '',
language: 'English',
description: '',
dataset_ids: [],
prompt_config: {
empty_response: '',
prologue: t('chat.setAnOpenerInitial'),
quote: true,
keyword: false,
tts: false,
system: t('chat.systemInitialValue'),
refine_multiturn: false,
use_kg: false,
reasoning: false,
parameters: [{ key: 'knowledge', optional: false }],
toc_enhance: false,
},
llm_id: tenantInfo.data.llm_id,
llm_setting: {
model_type: findLlmByUuid(tenantInfo.data.llm_id)?.model_type || 'chat',
},
similarity_threshold: 0.2,
vector_similarity_weight: 0.3,
top_n: 8,
top_k: 1024,
}),
[t, tenantInfo.data.llm_id, findLlmByUuid],
);
const onChatRenameOk = useCallback(
async (name: string) => {
let ret: number | undefined;
if (isEmpty(chat)) {
ret = await createChat({ ...InitialData, name });
} else {
ret = await patchChat({
chatId: chat.id,
params: { name },
});
}
const ret = await patchChat({
chatId: chat.id,
params: { name },
});
if (ret === 0) {
hideChatRenameModal();
}
},
[chat, InitialData, createChat, patchChat, hideChatRenameModal],
[chat.id, patchChat, hideChatRenameModal],
);
const handleShowChatRenameModal = useCallback(
(record?: IDialog) => {
if (record) {
setChat(record);
} else {
setChat({} as IDialog);
}
(record: IDialog) => {
setChat(record);
showChatRenameModal();
},
[showChatRenameModal],
@@ -89,7 +40,7 @@ export const useRenameChat = () => {
}, [hideChatRenameModal]);
return {
chatRenameLoading: createLoading || patchLoading,
chatRenameLoading: patchLoading,
initialChatName: chat?.name,
onChatRenameOk,
chatRenameVisible,

View File

@@ -8,10 +8,11 @@ import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import { useFetchChatList } from '@/hooks/use-chat-request';
import { pick } from 'lodash';
import { Plus } from 'lucide-react';
import { useCallback, useEffect } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useSearchParams } from 'react-router';
import { ChatCard } from './chat-card';
import { useCreateChatDialog } from './hooks/use-create-chat';
import { useRenameChat } from './hooks/use-rename-chat';
export default function ChatList() {
@@ -26,6 +27,13 @@ export default function ChatList() {
onChatRenameOk,
chatRenameLoading,
} = useRenameChat();
const {
createChatVisible,
showCreateChatModal,
hideCreateChatModal,
onCreateChatOk,
createChatLoading,
} = useCreateChatDialog();
const handlePageChange = useCallback(
(page: number, pageSize?: number) => {
@@ -35,8 +43,8 @@ export default function ChatList() {
);
const handleShowCreateModal = useCallback(() => {
showChatRenameModal();
}, [showChatRenameModal]);
showCreateChatModal();
}, [showCreateChatModal]);
const [searchParams, setSearchParams] = useSearchParams();
const isCreate = searchParams.get('isCreate') === 'true';
@@ -48,6 +56,39 @@ export default function ChatList() {
}
}, [isCreate, handleShowCreateModal, searchParams, setSearchParams]);
const renameDialogProps = useMemo(() => {
if (chatRenameVisible) {
return {
hideModal: hideChatRenameModal,
onOk: onChatRenameOk,
initialName: initialChatName,
loading: chatRenameLoading,
title: initialChatName,
};
}
if (createChatVisible) {
return {
hideModal: hideCreateChatModal,
onOk: onCreateChatOk,
initialName: '',
loading: createChatLoading,
title: t('chat.createChat'),
};
}
return null;
}, [
chatRenameVisible,
createChatVisible,
hideChatRenameModal,
onChatRenameOk,
initialChatName,
chatRenameLoading,
hideCreateChatModal,
onCreateChatOk,
createChatLoading,
t,
]);
return (
<>
{data.chats?.length || searchString ? (
@@ -115,14 +156,8 @@ export default function ChatList() {
</article>
)}
{chatRenameVisible && (
<RenameDialog
hideModal={hideChatRenameModal}
onOk={onChatRenameOk}
initialName={initialChatName}
loading={chatRenameLoading}
title={initialChatName || t('chat.createChat')}
></RenameDialog>
{renameDialogProps && (
<RenameDialog {...renameDialogProps}></RenameDialog>
)}
</>
);

View File

@@ -10,6 +10,7 @@ import {
MetadataFilter,
MetadataFilterSchema,
} from '@/components/metadata-filter';
import { ModelTreeSelect } from '@/components/model-tree-select';
import { SimilaritySliderFormField } from '@/components/similarity-slider';
import { Button } from '@/components/ui/button';
import { SingleFormSlider } from '@/components/ui/dual-range-slider';
@@ -23,14 +24,9 @@ import {
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { MultiSelect } from '@/components/ui/multi-select';
import { RAGFlowSelect } from '@/components/ui/select';
import { Spin } from '@/components/ui/spin';
import { Switch } from '@/components/ui/switch';
import { useFetchKnowledgeMetadataKeys } from '@/hooks/use-knowledge-request';
import {
useComposeLlmOptionsByModelTypes,
useSelectLlmOptionsByModelType,
} from '@/hooks/use-llm-request';
import { useFetchTenantInfo } from '@/hooks/use-user-setting-request';
import { cn } from '@/lib/utils';
import { zodResolver } from '@hookform/resolvers/zod';
@@ -39,7 +35,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { LlmModelType } from '../dataset/dataset/constant';
import {
ISearchAppDetailProps,
IUpdateSearchProps,
@@ -188,16 +183,6 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
}
}, [open]);
const allOptions = useSelectLlmOptionsByModelType();
const rerankModelOptions = useMemo(() => {
return allOptions[LlmModelType.Rerank];
}, [allOptions]);
const aiSummeryModelOptions = useComposeLlmOptionsByModelTypes([
LlmModelType.Chat,
LlmModelType.Image2text,
]);
const rerankModelDisabled = useWatch({
control: formMethods.control,
name: 'search_config.use_rerank',
@@ -344,15 +329,9 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
>
<Form {...formMethods}>
<form
onSubmit={formMethods.handleSubmit(
(data) => {
console.log('Form submitted with data:', data);
onSubmit(data as unknown as IUpdateSearchProps);
},
(errors) => {
console.log('Validation errors:', errors);
},
)}
onSubmit={formMethods.handleSubmit((data) => {
onSubmit(data as unknown as IUpdateSearchProps);
})}
className="space-y-6"
>
<AvatarNameDescription avatarField="avatar" />
@@ -452,11 +431,9 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
{t('chat.model')}
</FormLabel>
<FormControl>
<RAGFlowSelect
<ModelTreeSelect
modelTypes={['rerank']}
{...field}
options={rerankModelOptions}
triggerClassName={'bg-bg-input'}
// disabled={disabled}
placeholder={t('chat.model')}
/>
</FormControl>
@@ -524,7 +501,6 @@ const SearchSetting: React.FC<SearchSettingProps> = ({
// ></LlmSettingFieldItems>
<LlmSettingFieldItems
prefix="search_config.llm_setting"
options={aiSummeryModelOptions}
showFields={[
'temperature',
'top_p',

View File

@@ -1,7 +1,4 @@
import {
SelectWithSearch,
SelectWithSearchFlagOptionType,
} from '@/components/originui/select-with-search';
import { ModelTreeSelect } from '@/components/model-tree-select';
import { Button } from '@/components/ui/button';
import {
Dialog,
@@ -17,10 +14,8 @@ import { Label } from '@/components/ui/label';
import message from '@/components/ui/message';
import { Slider } from '@/components/ui/slider';
import { Switch } from '@/components/ui/switch';
import { LlmModelType } from '@/constants/knowledge';
import { useSelectLlmOptionsByModelType } from '@/hooks/use-llm-request';
import { SkillSearchConfig } from '@/services/skill-space-service';
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import type {
@@ -73,14 +68,6 @@ export const SearchConfigModal: React.FC<SearchConfigModalProps> = ({
const [saving, setSaving] = useState(false);
const [reindexing, setReindexing] = useState(false);
// Get embedding model options from user's configured LLMs
const llmOptions = useSelectLlmOptionsByModelType();
const embeddingModelOptions = useMemo(() => {
return llmOptions[
LlmModelType.Embedding
] as SelectWithSearchFlagOptionType[];
}, [llmOptions]);
useEffect(() => {
if (open) {
if (config) {
@@ -166,12 +153,12 @@ export const SearchConfigModal: React.FC<SearchConfigModalProps> = ({
{/* Embedding Model */}
<div className="space-y-2">
<Label htmlFor="embd_id">{t('skillSearch.embeddingModel')}</Label>
<SelectWithSearch
<ModelTreeSelect
modelTypes={['embedding']}
value={formData.embd_id}
onChange={(value) =>
setValue('embd_id', value, { shouldDirty: true })
}
options={embeddingModelOptions}
placeholder={t('skillSearch.embeddingModelPlaceholder')}
/>
</div>

View File

@@ -1605,8 +1605,8 @@ export const DataSourceFormDefaultValues = {
tenant_id: '',
client_id: '',
client_secret: '',
}
}
},
},
},
[DataSourceKey.SLACK]: {
name: '',

View File

@@ -1,4 +1,5 @@
export interface ApiKeyPostBody {
instance_name: string;
api_key: string;
base_url: string;
group_id?: string;

View File

@@ -1,212 +0,0 @@
// src/components/ModelProviderCard.tsx
import {
ConfirmDeleteDialog,
ConfirmDeleteDialogNode,
} from '@/components/confirm-delete-dialog';
import { LlmIcon } from '@/components/svg-icon';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { useSetModalState, useTranslate } from '@/hooks/common-hooks';
import { LlmItem } from '@/hooks/use-llm-request';
import { getRealModelName } from '@/utils/llm-util';
import { EditOutlined, SettingOutlined } from '@ant-design/icons';
import { ChevronsDown, ChevronsUp, Trash2 } from 'lucide-react';
import { FC } from 'react';
import { isLocalLlmFactory } from '../../utils';
import {
useHandleDeleteFactory,
useHandleDeleteLlm,
useHandleEnableLlm,
} from '../hooks';
import { mapModelKey } from './un-add-model';
interface IModelCardProps {
item: LlmItem;
clickApiKey: (llmFactory: string) => void;
handleEditModel: (model: any, factory: LlmItem) => void;
}
type TagType =
| 'LLM'
| 'TEXT EMBEDDING'
| 'TEXT RE-RANK'
| 'TTS'
| 'SPEECH2TEXT'
| 'IMAGE2TEXT'
| 'MODERATION';
const sortTags = (tags: string) => {
const orderMap: Record<TagType, number> = {
LLM: 1,
'TEXT EMBEDDING': 2,
'TEXT RE-RANK': 3,
TTS: 4,
SPEECH2TEXT: 5,
IMAGE2TEXT: 6,
MODERATION: 7,
};
return tags
.split(',')
.map((tag) => tag.trim())
.sort(
(a, b) =>
(orderMap[a as TagType] || 999) - (orderMap[b as TagType] || 999),
);
};
export const ModelProviderCard: FC<IModelCardProps> = ({
item,
clickApiKey,
handleEditModel,
}) => {
const { visible, switchVisible } = useSetModalState();
const { t } = useTranslate('setting');
const { handleEnableLlm } = useHandleEnableLlm(item.name);
const { deleteFactory } = useHandleDeleteFactory(item.name);
const { handleDeleteLlm } = useHandleDeleteLlm(item.name);
const handleApiKeyClick = () => {
clickApiKey(item.name);
};
const handleShowMoreClick = () => {
switchVisible();
};
return (
<div
className={`w-full rounded-lg border border-border-button`}
data-testid="added-model-card"
data-provider={item.name}
>
{/* Header */}
<div className="flex h-16 items-center justify-between p-4 cursor-pointer transition-colors text-text-secondary">
<div className="flex items-center space-x-3">
<LlmIcon name={item.name} width={32} />
<div>
<div className="font-medium text-xl text-text-primary">
{item.name}
</div>
</div>
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
onClick={(e) => {
e.stopPropagation();
handleApiKeyClick();
}}
>
<SettingOutlined />
{isLocalLlmFactory(item.name) ? t('addTheModel') : 'API-Key'}
</Button>
<Button
variant="outline"
onClick={(e) => {
e.stopPropagation();
handleShowMoreClick();
}}
>
<span>{visible ? t('hideModels') : t('showMoreModels')}</span>
{!visible ? <ChevronsDown /> : <ChevronsUp />}
</Button>
<ConfirmDeleteDialog
onOk={() => deleteFactory({ llm_factory: item.name })}
title={t('deleteModel')}
content={{
node: (
<ConfirmDeleteDialogNode>
<div className="flex items-center gap-2 border-0.5 text-text-secondary border-border-button rounded-lg px-3 py-4">
<LlmIcon name={item.name} />
{item.name}
</div>
</ConfirmDeleteDialogNode>
),
}}
>
<Button
size="icon"
variant="danger-hover"
// onClick={(e) => {
// e.stopPropagation();
// handleDeleteFactory(item);
// }}
>
<Trash2 />
</Button>
</ConfirmDeleteDialog>
</div>
</div>
{/* Content */}
{visible && (
<div className="">
<div className="px-4 flex flex-wrap gap-1 mt-1">
{sortTags(item.tags).map((tag, index) => (
<span
key={index}
className="px-2 py-1 text-xs bg-bg-card text-text-secondary rounded-md"
>
{mapModelKey[tag.trim() as keyof typeof mapModelKey] ||
tag.trim()}
</span>
))}
</div>
<div className="m-4 bg-bg-card rounded-lg max-h-96 overflow-auto scrollbar-auto">
<ul>
{item.llm.map((model) => (
<li
key={model.name}
className="flex items-center border-b-[0.5px] border-border-button justify-between p-3 hover:bg-bg-card transition-colors"
>
<div className="flex items-center space-x-3">
<span className="font-medium">
{getRealModelName(model.name)}
</span>
<span className="px-2 py-1 text-xs bg-bg-card text-text-secondary rounded-md">
{model.type}
</span>
</div>
<div className="flex items-center gap-3">
{isLocalLlmFactory(item.name) && (
<Button
variant="secondary"
size="icon-sm"
onClick={() => handleEditModel(model, item)}
>
<EditOutlined />
</Button>
)}
<Switch
checked={model.status === '1'}
onCheckedChange={(value) => {
handleEnableLlm(model.name, value);
}}
/>
<Button
size="icon-sm"
variant="danger-hover"
onClick={(e) => {
e.stopPropagation();
handleDeleteLlm(model.name);
}}
>
<Trash2 size={16} />
</Button>
</div>
</li>
))}
</ul>
</div>
</div>
)}
</div>
);
};

View File

@@ -1,176 +1,136 @@
import {
SelectWithSearch,
SelectWithSearchFlagOptionType,
} from '@/components/originui/select-with-search';
import { ModelTreeSelect, ModelTypeMap } from '@/components/model-tree-select';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { LlmModelType } from '@/constants/knowledge';
import { FieldToModelType } from '@/constants/llm';
import { useTranslate } from '@/hooks/common-hooks';
import {
ISystemModelSettingSavingParams,
useComposeLlmOptionsByModelTypes,
useFetchDefaultModelDictionary,
useSetDefaultModel,
} from '@/hooks/use-llm-request';
import { parseModelValue } from '@/utils/llm-util';
import { CircleQuestionMark } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useFetchSystemModelSettingOnMount } from '../hooks';
import { useCallback, useMemo } from 'react';
interface IProps {
loading: boolean;
onOk: (
payload: Omit<ISystemModelSettingSavingParams, 'tenant_id' | 'name'>,
) => void;
interface ModelFieldItemProps {
id: string;
label: string;
value: string;
tooltip?: string;
isRequired?: boolean;
onChange: (id: string, value: string) => void;
}
const SystemSetting = ({ onOk, loading }: IProps) => {
const { systemSetting: initialValues, allOptions } =
useFetchSystemModelSettingOnMount();
function ModelFieldItem({
label,
value,
tooltip,
id,
isRequired,
onChange,
}: ModelFieldItemProps) {
const { t } = useTranslate('setting');
const [formData, setFormData] = useState({
llm_id: '',
embd_id: '',
img2txt_id: '',
asr_id: '',
rerank_id: '',
tts_id: '',
});
return (
<div className="flex gap-3">
<label className="block text-sm font-normal text-text-secondary mb-1 w-1/4">
{isRequired && <span className="text-state-error">*</span>}
{label}
{tooltip && (
<Tooltip>
<TooltipContent>{tooltip}</TooltipContent>
<TooltipTrigger>
<CircleQuestionMark
size={12}
className="ml-1 text-text-secondary text-xs"
/>
</TooltipTrigger>
</Tooltip>
)}
</label>
<div className="w-3/4">
<ModelTreeSelect
modelTypes={ModelTypeMap[id as keyof typeof ModelTypeMap] ?? ['chat']}
value={value}
onChange={(val) => onChange(id, val)}
placeholder={t('selectModelPlaceholder')}
showSearch
allowClear={id !== 'llm_id'}
/>
</div>
</div>
);
}
function SystemSetting() {
const { t } = useTranslate('setting');
const defaultModelDictionary = useFetchDefaultModelDictionary();
const { setDefaultModel } = useSetDefaultModel();
const handleFieldChange = useCallback(
(field: string, value: string) => {
const updatedData = { ...formData, [field]: value || '' };
setFormData(updatedData);
console.log('updatedData', updatedData);
onOk(updatedData);
async (field: string, value: string) => {
const modelType = FieldToModelType[field];
if (!modelType) return;
if (!value) {
await setDefaultModel({
model_provider: '',
model_instance: '',
model_name: '',
model_type: modelType,
});
} else {
const parsed = parseModelValue(value);
if (!parsed) return;
await setDefaultModel({ ...parsed, model_type: modelType });
}
},
[formData, onOk],
[setDefaultModel],
);
useEffect(() => {
setFormData({
llm_id: initialValues.llm_id ?? '',
embd_id: initialValues.embd_id ?? '',
img2txt_id: initialValues.img2txt_id ?? '',
asr_id: initialValues.asr_id ?? '',
rerank_id: initialValues.rerank_id ?? '',
tts_id: initialValues.tts_id ?? '',
});
}, [initialValues]);
const modelOptions = useComposeLlmOptionsByModelTypes([
LlmModelType.Chat,
LlmModelType.Image2text,
]);
const llmList = useMemo(() => {
return [
{
id: 'llm_id',
label: t('chatModel'),
isRequired: true,
value: formData.llm_id,
options: modelOptions as SelectWithSearchFlagOptionType[],
value: defaultModelDictionary.llm_id,
tooltip: t('chatModelTip'),
testId: 'default-llm-combobox',
},
{
id: 'embd_id',
label: t('embeddingModel'),
value: formData.embd_id,
options: allOptions[
LlmModelType.Embedding
] as SelectWithSearchFlagOptionType[],
value: defaultModelDictionary.embd_id,
tooltip: t('embeddingModelTip'),
testId: 'default-embedding-combobox',
},
{
id: 'img2txt_id',
label: t('img2txtModel'),
value: formData.img2txt_id,
options: allOptions[
LlmModelType.Image2text
] as SelectWithSearchFlagOptionType[],
value: defaultModelDictionary.img2txt_id,
tooltip: t('img2txtModelTip'),
},
{
id: 'asr_id',
label: t('sequence2txtModel'),
value: formData.asr_id,
options: allOptions[
LlmModelType.Speech2text
] as SelectWithSearchFlagOptionType[],
value: defaultModelDictionary.asr_id,
tooltip: t('sequence2txtModelTip'),
},
{
id: 'rerank_id',
label: t('rerankModel'),
value: formData.rerank_id,
options: allOptions[
LlmModelType.Rerank
] as SelectWithSearchFlagOptionType[],
value: defaultModelDictionary.rerank_id,
tooltip: t('rerankModelTip'),
},
{
id: 'tts_id',
label: t('ttsModel'),
value: formData.tts_id,
options: allOptions[
LlmModelType.TTS
] as SelectWithSearchFlagOptionType[],
value: defaultModelDictionary.tts_id,
tooltip: t('ttsModelTip'),
},
];
}, [formData, modelOptions, t, allOptions]);
const Items = ({
label,
value,
options,
tooltip,
id,
isRequired,
testId,
}: {
id: string;
label: string;
value: string;
options: SelectWithSearchFlagOptionType[];
tooltip?: string;
isRequired?: boolean;
testId?: string;
}) => {
return (
<div className="flex gap-3">
<label className="block text-sm font-normal text-text-secondary mb-1 w-1/4">
{isRequired && <span className="text-red-500">*</span>}
{label}
{tooltip && (
<Tooltip>
<TooltipContent>{tooltip}</TooltipContent>
<TooltipTrigger>
<CircleQuestionMark
size={12}
className="ml-1 text-text-secondary text-xs"
/>
</TooltipTrigger>
</Tooltip>
)}
</label>
<SelectWithSearch
triggerClassName="w-3/4 flex items-center"
allowClear={id !== 'llm_id'}
value={value}
options={options}
onChange={(value) => handleFieldChange(id, value)}
placeholder={t('selectModelPlaceholder')}
emptyData={t('modelEmptyTip')}
testId={testId}
/>
</div>
);
};
}, [defaultModelDictionary, t]);
return (
<article className="rounded-lg w-full">
@@ -185,20 +145,15 @@ const SystemSetting = ({ onOk, loading }: IProps) => {
<div className="px-7 py-6 space-y-6 max-h-[70vh] overflow-y-auto border border-border-button rounded-lg">
{llmList.map((item) => (
<Items key={item.id} {...item} />
<ModelFieldItem
key={item.id}
{...item}
onChange={handleFieldChange}
/>
))}
</div>
{/* <div className="border-t px-6 py-4 flex justify-end">
<Button
onClick={hideModal}
disabled={loading}
className="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
>
{t('common:cancel')}
</Button>
</div> */}
</article>
);
};
}
export default SystemSetting;

View File

@@ -3,49 +3,56 @@ import { LlmIcon } from '@/components/svg-icon';
import { Button } from '@/components/ui/button';
import { SearchInput } from '@/components/ui/input';
import { APIMapUrl } from '@/constants/llm';
import { useTranslate } from '@/hooks/common-hooks';
import { useSelectLlmList } from '@/hooks/use-llm-request';
import { useFetchAvailableProviders } from '@/hooks/use-llm-request';
import { ArrowUpRight, Plus } from 'lucide-react';
import { FC, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
export const mapModelKey = {
IMAGE2TEXT: 'VLM',
'TEXT EMBEDDING': 'Embedding',
SPEECH2TEXT: 'ASR',
'TEXT RE-RANK': 'Rerank',
chat: 'LLM',
vision: 'VLM',
embedding: 'Embedding',
asr: 'ASR',
rerank: 'Rerank',
tts: 'TTS',
ocr: 'OCR',
};
const orderMap: Record<TagType, number> = {
LLM: 1,
'TEXT EMBEDDING': 2,
'TEXT RE-RANK': 3,
TTS: 4,
SPEECH2TEXT: 5,
IMAGE2TEXT: 6,
MODERATION: 7,
};
type TagType =
| 'LLM'
| 'TEXT EMBEDDING'
| 'TEXT RE-RANK'
| 'TTS'
| 'SPEECH2TEXT'
| 'IMAGE2TEXT'
| 'MODERATION';
const sortTags = (tags: string) => {
return tags
.split(',')
.map((tag) => tag.trim())
.sort(
(a, b) =>
(orderMap[a as TagType] || 999) - (orderMap[b as TagType] || 999),
);
const orderMap: Record<ModelType, number> = {
chat: 1,
embedding: 2,
rerank: 3,
tts: 4,
asr: 5,
vision: 6,
ocr: 7,
};
type ModelType =
| 'chat'
| 'embedding'
| 'rerank'
| 'tts'
| 'asr'
| 'vision'
| 'ocr';
const sortModelTypes = (modelTypes: string[]) => {
return [...modelTypes].sort(
(a, b) =>
(orderMap[a as ModelType] || 999) - (orderMap[b as ModelType] || 999),
);
};
export const AvailableModels: FC<{
handleAddModel: (factory: string) => void;
}> = ({ handleAddModel }) => {
const { t } = useTranslate('setting');
const { factoryList } = useSelectLlmList();
const { t } = useTranslation();
const { data: factoryList } = useFetchAvailableProviders();
const [searchTerm, setSearchTerm] = useState('');
const [selectedTag, setSelectedTag] = useState<string | null>(null);
@@ -57,7 +64,7 @@ export const AvailableModels: FC<{
.includes(searchTerm.toLowerCase());
const matchesTag =
selectedTag === null ||
model.tags.split(',').some((tag) => tag.trim() === selectedTag);
model.model_types.some((type) => type === selectedTag);
return matchesSearch && matchesTag;
});
return models;
@@ -66,11 +73,11 @@ export const AvailableModels: FC<{
const allTags = useMemo(() => {
const tagsSet = new Set<string>();
factoryList.forEach((model) => {
model.tags.split(',').forEach((tag) => tagsSet.add(tag.trim()));
model.model_types.forEach((type) => tagsSet.add(type));
});
return Array.from(tagsSet).sort(
(a, b) =>
(orderMap[a as TagType] || 999) - (orderMap[b as TagType] || 999),
(orderMap[a as ModelType] || 999) - (orderMap[b as ModelType] || 999),
);
}, [factoryList]);
@@ -84,14 +91,16 @@ export const AvailableModels: FC<{
data-testid="available-models-section"
>
<header className="p-4 space-y-3">
<h3 className="text-text-primary text-base">{t('availableModels')}</h3>
<h3 className="text-text-primary text-base">
{t('setting.availableModels')}
</h3>
{/* Search Bar */}
<div>
{/* <div className="relative"> */}
<SearchInput
data-testid="model-providers-search"
type="text"
placeholder={t('search')}
placeholder={t('setting.search')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full px-4 py-2 pl-10 bg-bg-input border border-border-default rounded-lg focus:outline-none focus:ring-1 focus:ring-border-button transition-colors"
@@ -165,19 +174,17 @@ export const AvailableModels: FC<{
className="px-2 opacity-0 transition-all group-hover:opacity-100 group-focus-within:opacity-100"
>
<Plus size={12} />
{t('addTheModel')}
{t('setting.addTheModel')}
</Button>
</div>
<div className="flex flex-wrap gap-1">
{sortTags(model.tags).map((tag, index) => (
{sortModelTypes(model.model_types).map((type, index) => (
<span
key={index}
className="px-1 flex items-center h-5 text-xs bg-bg-card text-text-secondary rounded-md"
>
{/* {tag} */}
{mapModelKey[tag.trim() as keyof typeof mapModelKey] ||
tag.trim()}
{mapModelKey[type as keyof typeof mapModelKey] || type}
</span>
))}
</div>

View File

@@ -1,15 +1,38 @@
import { LlmItem, useSelectLlmList } from '@/hooks/use-llm-request';
import { t } from 'i18next';
import { ModelProviderCard } from './modal-card';
import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
import { LlmIcon } from '@/components/svg-icon';
import { Button } from '@/components/ui/button';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible';
import { Switch } from '@/components/ui/switch';
import { ModelStatus } from '@/constants/llm';
import {
useDeleteProviderInstance,
useFetchAddedProviders,
useFetchInstanceModels,
useFetchProviderInstances,
useUpdateModelStatus,
} from '@/hooks/use-llm-request';
import {
IAvailableProvider,
IInstanceModel,
IProviderInstance,
} from '@/interfaces/database/llm';
import { ChevronsDown, ChevronsUp, Trash2 } from 'lucide-react';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { mapModelKey } from './un-add-model';
export const UsedModel = ({
export function UsedModel({
handleAddModel,
handleEditModel,
}: {
handleAddModel: (factory: string) => void;
handleEditModel: (model: any, factory: LlmItem) => void;
}) => {
const { myLlmList: llmList } = useSelectLlmList();
}) {
const { t } = useTranslation();
const { data: providerList } = useFetchAddedProviders();
return (
<div
className="flex flex-col w-full gap-5 mb-4"
@@ -18,16 +41,218 @@ export const UsedModel = ({
<div className="text-text-primary text-2xl font-medium mb-2 mt-4">
{t('setting.addedModels')}
</div>
{llmList.map((llm) => {
return (
<ModelProviderCard
key={llm.name}
item={llm}
clickApiKey={handleAddModel}
handleEditModel={handleEditModel}
/>
);
})}
{providerList.map((provider) => (
<ProviderCard
key={provider.name}
provider={provider}
handleAddModel={handleAddModel}
/>
))}
</div>
);
};
}
function ProviderCard({
provider,
handleAddModel,
}: {
provider: IAvailableProvider;
handleAddModel: (factory: string) => void;
}) {
const { data: instances } = useFetchProviderInstances(provider.name);
return (
<div
className="w-full rounded-lg border border-border-button"
data-testid="added-model-card"
data-provider={provider.name}
>
{/* Provider header */}
<div className="flex h-16 items-center p-4 text-text-secondary">
<div className="flex items-center space-x-3">
<LlmIcon name={provider.name} width={32} />
<div className="font-medium text-xl text-text-primary">
{provider.name}
</div>
</div>
</div>
{/* Instances */}
{instances.length > 0 && (
<div className="border-t border-border-button">
{instances.map((instance) => (
<InstanceRow
key={instance.id}
instance={instance}
providerName={provider.name}
handleAddModel={handleAddModel}
/>
))}
</div>
)}
</div>
);
}
function InstanceRow({
instance,
providerName,
// handleAddModel,
}: {
instance: IProviderInstance;
providerName: string;
handleAddModel: (factory: string) => void;
}) {
const { t } = useTranslation();
const [visible, setVisible] = useState(false);
const { deleteProviderInstance } = useDeleteProviderInstance();
const handleDelete = async () => {
await deleteProviderInstance({
provider_name: providerName,
instances: [instance.instance_name],
});
};
return (
<Collapsible
open={visible}
onOpenChange={setVisible}
className="border-b border-border-button last:border-b-0"
>
<div>
{/* Instance header */}
<div className="flex items-center justify-between p-4">
<span className="font-medium text-text-primary">
{instance.instance_name}
</span>
<div className="flex items-center space-x-2">
{/* <Button variant="outline" size="sm" onClick={handleApiKeyClick}>
{t('setting.apiKey')}
</Button> */}
<CollapsibleTrigger asChild>
<Button variant="outline" size="sm">
<span>
{visible
? t('setting.hideModels')
: t('setting.showMoreModels')}
</span>
{visible ? (
<ChevronsUp size={16} />
) : (
<ChevronsDown size={16} />
)}
</Button>
</CollapsibleTrigger>
<ConfirmDeleteDialog onOk={handleDelete}>
<Button size="icon" variant="danger-hover">
<Trash2 size={16} />
</Button>
</ConfirmDeleteDialog>
</div>
</div>
{/* Models */}
<CollapsibleContent>
<InstanceModelList
providerName={providerName}
instanceName={instance.instance_name}
/>
</CollapsibleContent>
</div>
</Collapsible>
);
}
function InstanceModelList({
providerName,
instanceName,
}: {
providerName: string;
instanceName: string;
}) {
const { data: models } = useFetchInstanceModels(providerName, instanceName);
const modelTypes = useMemo(() => {
const types = new Set<string>();
models.forEach((m) => {
if (m.model_type) {
m.model_type.forEach((type) => types.add(type));
}
});
return Array.from(types);
}, [models]);
return (
<div className="px-4 pb-4">
{/* Model type tags */}
{modelTypes.length > 0 && (
<div className="flex flex-wrap gap-2 mb-3">
{modelTypes.map((type) => (
<span
key={type}
className="px-2 py-1 text-xs bg-bg-card text-text-secondary rounded-md"
>
{mapModelKey[type.trim() as keyof typeof mapModelKey] || type}
</span>
))}
</div>
)}
{/* Model list */}
<div className="bg-bg-card rounded-lg max-h-96 overflow-auto scrollbar-auto">
<ul>
{models.map((model) => (
<ModelListItem
key={model.name}
model={model}
providerName={providerName}
instanceName={instanceName}
/>
))}
</ul>
</div>
</div>
);
}
function ModelListItem({
model,
providerName,
instanceName,
}: {
model: IInstanceModel;
providerName: string;
instanceName: string;
}) {
const { updateModelStatus } = useUpdateModelStatus();
const handleStatusChange = (checked: boolean) => {
updateModelStatus({
provider_name: providerName,
instance_name: instanceName,
model_name: model.name,
status: checked ? ModelStatus.Active : ModelStatus.Inactive,
});
};
return (
<li className="flex items-center border-b-[0.5px] border-border-button justify-between p-3 hover:bg-bg-card transition-colors last:border-b-0">
<div className="flex items-center space-x-3">
<span className="font-medium text-text-primary">{model.name}</span>
{model.model_type.map((modelType) => (
<span
className="px-2 py-1 text-xs bg-bg-card text-text-secondary rounded-md"
key={modelType}
>
{modelType}
</span>
))}
</div>
<Switch
checked={model.status === ModelStatus.Active}
onCheckedChange={handleStatusChange}
/>
</li>
);
}

View File

@@ -1,55 +1,114 @@
import { LLMFactory } from '@/constants/llm';
import { useSetModalState, useShowDeleteConfirm } from '@/hooks/common-hooks';
import { useSetModalState } from '@/hooks/common-hooks';
import {
IApiKeySavingParams,
ISystemModelSettingSavingParams,
useAddLlm,
useDeleteFactory,
useDeleteLlm,
useEnableLlm,
useSaveApiKey,
useSaveTenantInfo,
useSelectLlmOptionsByModelType,
useAddInstanceModel,
useAddProviderInstance,
useFetchAddedProviders,
useFetchProviderInstances,
} from '@/hooks/use-llm-request';
import { useFetchTenantInfo } from '@/hooks/use-user-setting-request';
import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { IAddProviderInstanceRequestBody } from '@/interfaces/request/llm';
import { getRealModelName } from '@/utils/llm-util';
import { useQueryClient } from '@tanstack/react-query';
import { useCallback, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { ApiKeyPostBody } from '../interface';
import { MinerUFormValues } from './modal/mineru-modal';
import { splitProviderPayload } from './payload-utils';
type SavingParamsState = Omit<IApiKeySavingParams, 'api_key'>;
type SavingParamsState = {
llm_factory: string;
llm_name?: string;
model_type?: string;
instance_name?: string;
base_url?: string;
};
export type VerifyResult = {
isValid: boolean | null;
logs: string;
};
const useSubmitProviderInstance = () => {
const { addProviderInstance } = useAddProviderInstance();
const { addInstanceModel } = useAddInstanceModel();
return useCallback(
async (payload: IAddProviderInstanceRequestBody, isVerify = false) => {
if (isVerify) {
return addProviderInstance({ ...payload, verify: true });
}
const { instancePayload, modelPayload } = splitProviderPayload(payload);
const hasModelPayload =
!!modelPayload.model_name && !!modelPayload.model_type;
const instanceRet = await addProviderInstance({
...instancePayload,
llm_factory: payload.llm_factory,
instance_name: payload.instance_name,
} as IAddProviderInstanceRequestBody);
if (instanceRet.code !== 0 || !hasModelPayload) {
return instanceRet;
}
if (!hasModelPayload) {
return { code: 0, data: null } as any;
}
return addInstanceModel({
provider_name: payload.llm_factory,
instance_name: payload.instance_name,
...modelPayload,
});
},
[addProviderInstance, addInstanceModel],
);
};
export const useFetchInstanceNameSet = (providerName: string) => {
const { data: addedProviders } = useFetchAddedProviders();
const providerExists = useMemo(
() => addedProviders.some((p) => p.name === providerName),
[addedProviders, providerName],
);
const { data: instances } = useFetchProviderInstances(
providerExists ? providerName : '',
);
const instanceNameSet = useMemo(
() => new Set(instances.map((i) => i.instance_name)),
[instances],
);
return { instanceNameSet, providerExists };
};
export const useHideWhenInstanceExists = (instanceNameSet: Set<string>) => {
return useCallback(
(formValues: any) => {
const name = ((formValues?.instance_name as string) || '').trim();
return !(name && instanceNameSet.has(name));
},
[instanceNameSet],
);
};
export const useSubmitApiKey = () => {
const [savingParams, setSavingParams] = useState<SavingParamsState>(
{} as SavingParamsState,
);
const [editMode, setEditMode] = useState(false);
const { saveApiKey } = useSaveApiKey();
const submitProviderInstance = useSubmitProviderInstance();
const [saveLoading, setSaveLoading] = useState(false);
const {
visible: apiKeyVisible,
hideModal: hideApiKeyModal,
showModal: showApiKeyModal,
} = useSetModalState();
const queryClient = useQueryClient();
const onApiKeySavingOk = useCallback(
async (postBody: ApiKeyPostBody, isVerify = false) => {
if (!isVerify) {
setSaveLoading(true);
}
const payload: IApiKeySavingParams = {
...savingParams,
...postBody,
verify: isVerify,
};
let apiKey: string | Record<string, any> = postBody.api_key || '';
if (savingParams.llm_factory === LLMFactory.SILICONFLOW) {
let sourceFid = LLMFactory.SILICONFLOW;
let sourceFid: string = LLMFactory.SILICONFLOW;
const baseUrl = postBody.base_url;
if (baseUrl) {
try {
@@ -65,14 +124,24 @@ export const useSubmitApiKey = () => {
// ignore invalid URL and keep default sourceFid
}
}
payload.source_fid = sourceFid;
apiKey = { api_key: postBody.api_key, source_fid: sourceFid };
}
const ret = await saveApiKey(payload);
const req: IAddProviderInstanceRequestBody = {
instance_name:
postBody.instance_name || savingParams.instance_name || '',
llm_factory: savingParams.llm_factory,
llm_name: savingParams.llm_name || '',
model_type: savingParams.model_type || '',
api_key: apiKey,
api_base: postBody.base_url || '',
max_tokens: 0,
};
const ret = await submitProviderInstance(req, isVerify);
if (!isVerify) {
setSaveLoading(false);
if (ret.code === 0) {
queryClient.invalidateQueries({ queryKey: ['llmList'] });
hideApiKeyModal();
setEditMode(false);
}
@@ -93,7 +162,7 @@ export const useSubmitApiKey = () => {
return res;
}
},
[hideApiKeyModal, saveApiKey, savingParams, queryClient],
[hideApiKeyModal, submitProviderInstance, savingParams],
);
const onShowApiKeyModal = useCallback(
@@ -117,57 +186,14 @@ export const useSubmitApiKey = () => {
};
};
export const useSubmitSystemModelSetting = () => {
const { data: systemSetting } = useFetchTenantInfo();
const { saveTenantInfo: saveSystemModelSetting, loading } =
useSaveTenantInfo();
const {
visible: systemSettingVisible,
hideModal: hideSystemSettingModal,
showModal: showSystemSettingModal,
} = useSetModalState();
const onSystemSettingSavingOk = useCallback(
async (
payload: Omit<ISystemModelSettingSavingParams, 'tenant_id' | 'name'>,
) => {
const ret = await saveSystemModelSetting({
tenant_id: systemSetting.tenant_id,
name: systemSetting.name,
...payload,
});
if (ret === 0) {
hideSystemSettingModal();
}
},
[hideSystemSettingModal, saveSystemModelSetting, systemSetting],
);
return {
saveSystemModelSettingLoading: loading,
onSystemSettingSavingOk,
systemSettingVisible,
hideSystemSettingModal,
showSystemSettingModal,
};
};
export const useFetchSystemModelSettingOnMount = () => {
const { data: systemSetting } = useFetchTenantInfo();
const allOptions = useSelectLlmOptionsByModelType();
return { systemSetting, allOptions };
};
export const useSubmitOllama = () => {
const [selectedLlmFactory, setSelectedLlmFactory] = useState<string>('');
const [editMode, setEditMode] = useState(false);
const [initialValues, setInitialValues] = useState<
Partial<IAddLlmRequestBody> & { provider_order?: string }
Partial<IAddProviderInstanceRequestBody> & { provider_order?: string }
>();
const [saveLoading, setSaveLoading] = useState(false);
const { addLlm } = useAddLlm();
const submitProviderInstance = useSubmitProviderInstance();
const {
visible: llmAddingVisible,
hideModal: hideLlmAddingModal,
@@ -175,16 +201,20 @@ export const useSubmitOllama = () => {
} = useSetModalState();
const onLlmAddingOk = useCallback(
async (payload: IAddLlmRequestBody, isVerify = false) => {
async (payload: IAddProviderInstanceRequestBody, isVerify = false) => {
if (!isVerify) {
setSaveLoading(true);
}
const cleanedPayload = { ...payload };
if (!cleanedPayload.api_key || cleanedPayload.api_key.trim() === '') {
delete cleanedPayload.api_key;
}
// if (
// !cleanedPayload.api_key ||
// (typeof cleanedPayload.api_key === 'string' &&
// cleanedPayload.api_key.trim() === '')
// ) {
// delete cleanedPayload.api_key;
// }
const ret = await addLlm({ ...cleanedPayload, verify: isVerify });
const ret = await submitProviderInstance(cleanedPayload, isVerify);
if (!isVerify) {
setSaveLoading(false);
if (ret.code === 0) {
@@ -209,7 +239,7 @@ export const useSubmitOllama = () => {
return res;
}
},
[hideLlmAddingModal, addLlm, setSaveLoading],
[hideLlmAddingModal, submitProviderInstance, setSaveLoading],
);
const handleShowLlmAddingModal = (
@@ -223,6 +253,8 @@ export const useSubmitOllama = () => {
if (isEdit && detailedData) {
const initialVals = {
instance_name:
detailedData.instance_name || getRealModelName(detailedData.name),
llm_name: getRealModelName(detailedData.name),
model_type: detailedData.type,
api_base: detailedData.api_base || '',
@@ -251,7 +283,7 @@ export const useSubmitOllama = () => {
export const useSubmitVolcEngine = () => {
const [saveLoading, setSaveLoading] = useState(false);
const { addLlm } = useAddLlm();
const submitProviderInstance = useSubmitProviderInstance();
const {
visible: volcAddingVisible,
hideModal: hideVolcAddingModal,
@@ -259,11 +291,11 @@ export const useSubmitVolcEngine = () => {
} = useSetModalState();
const onVolcAddingOk = useCallback(
async (payload: IAddLlmRequestBody, isVerify = false) => {
async (payload: IAddProviderInstanceRequestBody, isVerify = false) => {
if (!isVerify) {
setSaveLoading(true);
}
const ret = await addLlm({ ...payload, verify: isVerify });
const ret = await submitProviderInstance(payload, isVerify);
if (!isVerify) {
setSaveLoading(false);
if (ret.code === 0) {
@@ -286,7 +318,7 @@ export const useSubmitVolcEngine = () => {
return res;
}
},
[hideVolcAddingModal, addLlm, setSaveLoading],
[hideVolcAddingModal, submitProviderInstance, setSaveLoading],
);
return {
@@ -300,7 +332,7 @@ export const useSubmitVolcEngine = () => {
export const useSubmitTencentCloud = () => {
const [saveLoading, setSaveLoading] = useState(false);
const { addLlm } = useAddLlm();
const submitProviderInstance = useSubmitProviderInstance();
const {
visible: TencentCloudAddingVisible,
hideModal: hideTencentCloudAddingModal,
@@ -308,11 +340,11 @@ export const useSubmitTencentCloud = () => {
} = useSetModalState();
const onTencentCloudAddingOk = useCallback(
async (payload: IAddLlmRequestBody, isVerify = false) => {
async (payload: IAddProviderInstanceRequestBody, isVerify = false) => {
if (!isVerify) {
setSaveLoading(true);
}
const ret = await addLlm({ ...payload, verify: isVerify });
const ret = await submitProviderInstance(payload, isVerify);
if (!isVerify) {
setSaveLoading(false);
if (ret.code === 0) {
@@ -335,7 +367,7 @@ export const useSubmitTencentCloud = () => {
return res;
}
},
[hideTencentCloudAddingModal, addLlm, setSaveLoading],
[hideTencentCloudAddingModal, submitProviderInstance, setSaveLoading],
);
return {
@@ -349,7 +381,7 @@ export const useSubmitTencentCloud = () => {
export const useSubmitSpark = () => {
const [saveLoading, setSaveLoading] = useState(false);
const { addLlm } = useAddLlm();
const submitProviderInstance = useSubmitProviderInstance();
const {
visible: SparkAddingVisible,
hideModal: hideSparkAddingModal,
@@ -357,11 +389,11 @@ export const useSubmitSpark = () => {
} = useSetModalState();
const onSparkAddingOk = useCallback(
async (payload: IAddLlmRequestBody, isVerify = false) => {
async (payload: IAddProviderInstanceRequestBody, isVerify = false) => {
if (!isVerify) {
setSaveLoading(true);
}
const ret = await addLlm({ ...payload, verify: isVerify });
const ret = await submitProviderInstance(payload, isVerify);
if (!isVerify) {
setSaveLoading(false);
if (ret.code === 0) {
@@ -384,7 +416,7 @@ export const useSubmitSpark = () => {
return res;
}
},
[hideSparkAddingModal, addLlm, setSaveLoading],
[hideSparkAddingModal, submitProviderInstance, setSaveLoading],
);
return {
@@ -398,7 +430,7 @@ export const useSubmitSpark = () => {
export const useSubmityiyan = () => {
const [saveLoading, setSaveLoading] = useState(false);
const { addLlm } = useAddLlm();
const submitProviderInstance = useSubmitProviderInstance();
const {
visible: yiyanAddingVisible,
hideModal: hideyiyanAddingModal,
@@ -406,11 +438,11 @@ export const useSubmityiyan = () => {
} = useSetModalState();
const onyiyanAddingOk = useCallback(
async (payload: IAddLlmRequestBody, isVerify = false) => {
async (payload: IAddProviderInstanceRequestBody, isVerify = false) => {
if (!isVerify) {
setSaveLoading(true);
}
const ret = await addLlm({ ...payload, verify: isVerify });
const ret = await submitProviderInstance(payload, isVerify);
if (!isVerify) {
setSaveLoading(false);
if (ret.code === 0) {
@@ -433,7 +465,7 @@ export const useSubmityiyan = () => {
return res;
}
},
[hideyiyanAddingModal, addLlm, setSaveLoading],
[hideyiyanAddingModal, submitProviderInstance, setSaveLoading],
);
return {
@@ -447,7 +479,7 @@ export const useSubmityiyan = () => {
export const useSubmitFishAudio = () => {
const [saveLoading, setSaveLoading] = useState(false);
const { addLlm } = useAddLlm();
const submitProviderInstance = useSubmitProviderInstance();
const {
visible: FishAudioAddingVisible,
hideModal: hideFishAudioAddingModal,
@@ -455,11 +487,11 @@ export const useSubmitFishAudio = () => {
} = useSetModalState();
const onFishAudioAddingOk = useCallback(
async (payload: IAddLlmRequestBody, isVerify = false) => {
async (payload: IAddProviderInstanceRequestBody, isVerify = false) => {
if (!isVerify) {
setSaveLoading(true);
}
const ret = await addLlm({ ...payload, verify: isVerify });
const ret = await submitProviderInstance(payload, isVerify);
if (!isVerify) {
setSaveLoading(false);
if (ret.code === 0) {
@@ -482,7 +514,7 @@ export const useSubmitFishAudio = () => {
return res;
}
},
[hideFishAudioAddingModal, addLlm, setSaveLoading],
[hideFishAudioAddingModal, submitProviderInstance, setSaveLoading],
);
return {
@@ -496,7 +528,7 @@ export const useSubmitFishAudio = () => {
export const useSubmitGoogle = () => {
const [saveLoading, setSaveLoading] = useState(false);
const { addLlm } = useAddLlm();
const submitProviderInstance = useSubmitProviderInstance();
const {
visible: GoogleAddingVisible,
hideModal: hideGoogleAddingModal,
@@ -504,11 +536,11 @@ export const useSubmitGoogle = () => {
} = useSetModalState();
const onGoogleAddingOk = useCallback(
async (payload: IAddLlmRequestBody, isVerify = false) => {
async (payload: IAddProviderInstanceRequestBody, isVerify = false) => {
if (!isVerify) {
setSaveLoading(true);
}
const ret = await addLlm({ ...payload, verify: isVerify });
const ret = await submitProviderInstance(payload, isVerify);
if (!isVerify) {
setSaveLoading(false);
if (ret.code === 0) {
@@ -531,7 +563,7 @@ export const useSubmitGoogle = () => {
return res;
}
},
[hideGoogleAddingModal, addLlm, setSaveLoading],
[hideGoogleAddingModal, submitProviderInstance, setSaveLoading],
);
return {
@@ -545,7 +577,7 @@ export const useSubmitGoogle = () => {
export const useSubmitBedrock = () => {
const [saveLoading, setSaveLoading] = useState(false);
const { addLlm } = useAddLlm();
const submitProviderInstance = useSubmitProviderInstance();
const {
visible: bedrockAddingVisible,
hideModal: hideBedrockAddingModal,
@@ -553,11 +585,11 @@ export const useSubmitBedrock = () => {
} = useSetModalState();
const onBedrockAddingOk = useCallback(
async (payload: IAddLlmRequestBody, isVerify = false) => {
async (payload: IAddProviderInstanceRequestBody, isVerify = false) => {
if (!isVerify) {
setSaveLoading(true);
}
const ret = await addLlm({ ...payload, verify: isVerify });
const ret = await submitProviderInstance(payload, isVerify);
if (!isVerify) {
setSaveLoading(false);
if (ret.code === 0) {
@@ -580,7 +612,7 @@ export const useSubmitBedrock = () => {
return res;
}
},
[hideBedrockAddingModal, addLlm, setSaveLoading],
[hideBedrockAddingModal, submitProviderInstance, setSaveLoading],
);
return {
@@ -594,7 +626,7 @@ export const useSubmitBedrock = () => {
export const useSubmitAzure = () => {
const [saveLoading, setSaveLoading] = useState(false);
const { addLlm } = useAddLlm();
const submitProviderInstance = useSubmitProviderInstance();
const {
visible: AzureAddingVisible,
hideModal: hideAzureAddingModal,
@@ -602,11 +634,11 @@ export const useSubmitAzure = () => {
} = useSetModalState();
const onAzureAddingOk = useCallback(
async (payload: IAddLlmRequestBody, isVerify = false) => {
async (payload: IAddProviderInstanceRequestBody, isVerify = false) => {
if (!isVerify) {
setSaveLoading(true);
}
const ret = await addLlm({ ...payload, verify: isVerify });
const ret = await submitProviderInstance(payload, isVerify);
if (!isVerify) {
setSaveLoading(false);
if (ret.code === 0) {
@@ -629,7 +661,7 @@ export const useSubmitAzure = () => {
return res;
}
},
[hideAzureAddingModal, addLlm, setSaveLoading],
[hideAzureAddingModal, submitProviderInstance, setSaveLoading],
);
return {
@@ -641,49 +673,9 @@ export const useSubmitAzure = () => {
};
};
export const useHandleDeleteLlm = (llmFactory: string) => {
const { deleteLlm } = useDeleteLlm();
const showDeleteConfirm = useShowDeleteConfirm();
const handleDeleteLlm = (name: string) => {
showDeleteConfirm({
onOk: async () => {
deleteLlm({ llm_factory: llmFactory, llm_name: name });
},
});
};
return { handleDeleteLlm };
};
export const useHandleEnableLlm = (llmFactory: string) => {
const { enableLlm } = useEnableLlm();
const handleEnableLlm = (name: string, enable: boolean) => {
enableLlm({ llm_factory: llmFactory, llm_name: name, enable });
};
return { handleEnableLlm };
};
export const useHandleDeleteFactory = (llmFactory: string) => {
const { deleteFactory } = useDeleteFactory();
const showDeleteConfirm = useShowDeleteConfirm();
const handleDeleteFactory = () => {
showDeleteConfirm({
onOk: async () => {
deleteFactory({ llm_factory: llmFactory });
},
});
};
return { handleDeleteFactory, deleteFactory };
};
export const useSubmitMinerU = () => {
const [saveLoading, setSaveLoading] = useState(false);
const { addLlm } = useAddLlm();
const submitProviderInstance = useSubmitProviderInstance();
const {
visible: mineruVisible,
hideModal: hideMineruModal,
@@ -691,7 +683,10 @@ export const useSubmitMinerU = () => {
} = useSetModalState();
const onMineruOk = useCallback(
async (payload: MinerUFormValues, isVerify = false) => {
async (
payload: MinerUFormValues & { instance_name: string },
isVerify = false,
) => {
if (!isVerify) {
setSaveLoading(true);
}
@@ -700,10 +695,12 @@ export const useSubmitMinerU = () => {
mineru_delete_output:
(payload.mineru_delete_output ?? true) ? '1' : '0',
};
delete cfg.instance_name;
if (payload.mineru_backend !== 'vlm-http-client') {
delete cfg.mineru_server_url;
}
const req: IAddLlmRequestBody = {
const req: IAddProviderInstanceRequestBody = {
instance_name: payload.instance_name,
llm_factory: LLMFactory.MinerU,
llm_name: payload.llm_name,
model_type: 'ocr',
@@ -711,7 +708,7 @@ export const useSubmitMinerU = () => {
api_base: '',
max_tokens: 0,
};
const ret = await addLlm({ ...req, verify: isVerify });
const ret = await submitProviderInstance(req, isVerify);
if (!isVerify) {
setSaveLoading(false);
if (ret.code === 0) {
@@ -734,7 +731,7 @@ export const useSubmitMinerU = () => {
return res;
}
},
[addLlm, hideMineruModal, setSaveLoading],
[submitProviderInstance, hideMineruModal, setSaveLoading],
);
return {
@@ -748,7 +745,7 @@ export const useSubmitMinerU = () => {
export const useSubmitPaddleOCR = () => {
const [saveLoading, setSaveLoading] = useState(false);
const { addLlm } = useAddLlm();
const submitProviderInstance = useSubmitProviderInstance();
const {
visible: paddleocrVisible,
hideModal: hidePaddleOCRModal,
@@ -763,7 +760,9 @@ export const useSubmitPaddleOCR = () => {
const cfg: any = {
...payload,
};
const req: IAddLlmRequestBody = {
delete cfg.instance_name;
const req: IAddProviderInstanceRequestBody = {
instance_name: payload.instance_name,
llm_factory: LLMFactory.PaddleOCR,
llm_name: payload.llm_name,
model_type: 'ocr',
@@ -771,7 +770,7 @@ export const useSubmitPaddleOCR = () => {
api_base: '',
max_tokens: 0,
};
const ret = await addLlm({ ...req, verify: isVerify });
const ret = await submitProviderInstance(req, isVerify);
if (!isVerify) {
setSaveLoading(false);
if (ret.code === 0) {
@@ -796,7 +795,7 @@ export const useSubmitPaddleOCR = () => {
}
return false;
},
[addLlm, hidePaddleOCRModal, setSaveLoading],
[submitProviderInstance, hidePaddleOCRModal, setSaveLoading],
);
return {
@@ -810,7 +809,7 @@ export const useSubmitPaddleOCR = () => {
export const useSubmitOpenDataLoader = () => {
const [saveLoading, setSaveLoading] = useState(false);
const { addLlm } = useAddLlm();
const submitProviderInstance = useSubmitProviderInstance();
const {
visible: opendataloaderVisible,
hideModal: hideOpenDataLoaderModal,
@@ -822,15 +821,18 @@ export const useSubmitOpenDataLoader = () => {
if (!isVerify) {
setSaveLoading(true);
}
const req: IAddLlmRequestBody = {
const cfg: any = { ...payload };
delete cfg.instance_name;
const req: IAddProviderInstanceRequestBody = {
instance_name: payload.instance_name,
llm_factory: LLMFactory.OpenDataLoader,
llm_name: payload.llm_name,
model_type: 'ocr',
api_key: { ...payload },
api_key: cfg,
api_base: '',
max_tokens: 0,
};
const ret = await addLlm({ ...req, verify: isVerify });
const ret = await submitProviderInstance(req, isVerify);
if (!isVerify) {
setSaveLoading(false);
if (ret.code === 0) {
@@ -846,7 +848,7 @@ export const useSubmitOpenDataLoader = () => {
}
return false;
},
[addLlm, hideOpenDataLoaderModal, setSaveLoading],
[submitProviderInstance, hideOpenDataLoaderModal, setSaveLoading],
);
return {
@@ -867,7 +869,7 @@ export const useVerifySettings = ({
isVerify?: boolean,
) => Promise<VerifyResult | undefined>)
| ((
payload: IAddLlmRequestBody,
payload: IAddProviderInstanceRequestBody,
isVerify?: boolean,
) => Promise<VerifyResult | undefined>)
| ((

View File

@@ -1,6 +1,6 @@
import Spotlight from '@/components/spotlight';
import { LLMFactory } from '@/constants/llm';
import { LlmItem, useFetchMyLlmListDetailed } from '@/hooks/use-llm-request';
// import { LlmItem, useFetchMyLlmListDetailed } from '@/hooks/use-llm-request';
import { useCallback, useMemo } from 'react';
import { isLocalLlmFactory } from '../utils';
import SystemSetting from './components/system-setting';
@@ -17,7 +17,6 @@ import {
useSubmitOpenDataLoader,
useSubmitPaddleOCR,
useSubmitSpark,
useSubmitSystemModelSetting,
useSubmitTencentCloud,
useSubmitVolcEngine,
useSubmityiyan,
@@ -37,9 +36,7 @@ import SparkModal from './modal/spark-modal';
import VolcEngineModal from './modal/volcengine-modal';
import YiyanModal from './modal/yiyan-modal';
const ModelProviders = () => {
const { saveSystemModelSettingLoading, onSystemSettingSavingOk } =
useSubmitSystemModelSetting();
const { data: detailedLlmList } = useFetchMyLlmListDetailed();
// const { data: detailedLlmList } = useFetchMyLlmListDetailed();
const {
saveApiKeyLoading,
initialApiKey,
@@ -192,31 +189,31 @@ const ModelProviders = () => {
[showApiKeyModal, showLlmAddingModal, ModalMap],
);
const handleEditModel = useCallback(
(model: any, factory: LlmItem) => {
if (factory) {
const detailedFactory = detailedLlmList[factory.name];
const detailedModel = detailedFactory?.llm?.find(
(m: any) => m.name === model.name,
);
// const handleEditModel = useCallback(
// (model: any, factory: LlmItem) => {
// if (factory) {
// const detailedFactory = detailedLlmList[factory.name];
// const detailedModel = detailedFactory?.llm?.find(
// (m: any) => m.name === model.name,
// );
const editData = {
llm_factory: factory.name,
llm_name: model.name,
model_type: model.type,
};
// const editData = {
// llm_factory: factory.name,
// llm_name: model.name,
// model_type: model.type,
// };
if (isLocalLlmFactory(factory.name)) {
showLlmAddingModal(factory.name, true, editData, detailedModel);
} else if (factory.name in ModalMap) {
ModalMap[factory.name as keyof typeof ModalMap]();
} else {
showApiKeyModal(editData, true);
}
}
},
[showApiKeyModal, showLlmAddingModal, ModalMap, detailedLlmList],
);
// if (isLocalLlmFactory(factory.name)) {
// showLlmAddingModal(factory.name, true, editData, detailedModel);
// } else if (factory.name in ModalMap) {
// ModalMap[factory.name as keyof typeof ModalMap]();
// } else {
// showApiKeyModal(editData, true);
// }
// }
// },
// [showApiKeyModal, showLlmAddingModal, ModalMap, detailedLlmList],
// );
const handleOk = useMemo(() => {
if (apiKeyVisible) {
@@ -296,14 +293,8 @@ const ModelProviders = () => {
<div className="flex w-full border-[0.5px] border-border-button rounded-lg relative ">
<Spotlight />
<section className="flex flex-col gap-4 w-3/5 px-5 border-r-[0.5px] border-border-button overflow-auto scrollbar-auto">
<SystemSetting
onOk={onSystemSettingSavingOk}
loading={saveSystemModelSettingLoading}
/>
<UsedModel
handleAddModel={handleAddModel}
handleEditModel={handleEditModel}
/>
<SystemSetting />
<UsedModel handleAddModel={handleAddModel} />
</section>
<section className="flex flex-col w-2/5 overflow-auto scrollbar-auto">
<AvailableModels handleAddModel={handleAddModel} />

View File

@@ -1,12 +1,6 @@
import { IModalManagerChildrenProps } from '@/components/modal-manager';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Form } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Modal } from '@/components/ui/modal/modal';
import { LLMFactory } from '@/constants/llm';
@@ -31,6 +25,7 @@ interface IProps extends Omit<IModalManagerChildrenProps, 'showModal'> {
}
type FieldType = {
instance_name?: string;
api_key?: string;
base_url?: string;
group_id?: string;
@@ -50,7 +45,7 @@ const ApiKeyModal = ({
llmFactory,
loading,
initialValue,
editMode = false,
// editMode = false,
onOk,
onVerify,
}: IProps) => {
@@ -92,108 +87,100 @@ const ApiKeyModal = ({
>
<Form {...form}>
<div className="space-y-4 py-4">
<FormField
name="api_key"
rules={{ required: t('apiKeyMessage') }}
render={({ field }) => (
<FormItem>
<FormLabel
className="text-sm font-medium text-text-secondary"
required
>
{t('apiKey')}
</FormLabel>
<FormControl>
<Input
{...field}
data-testid="apikey-input"
onKeyDown={handleKeyDown}
className="w-full"
/>
</FormControl>
<FormMessage />
</FormItem>
<RAGFlowFormItem
name="instance_name"
label={t('instanceName')}
tooltip={t('instanceNameTip')}
rules={{ required: t('instanceNameMessage') }}
required
labelClassName="text-sm font-medium text-text-secondary"
>
{(field) => (
<Input
{...field}
placeholder={t('instanceNameMessage')}
onKeyDown={handleKeyDown}
className="w-full"
/>
)}
/>
</RAGFlowFormItem>
<RAGFlowFormItem
name="api_key"
label={t('apiKey')}
rules={{ required: t('apiKeyMessage') }}
required
labelClassName="text-sm font-medium text-text-secondary"
>
{(field) => (
<Input
{...field}
data-testid="apikey-input"
onKeyDown={handleKeyDown}
className="w-full"
/>
)}
</RAGFlowFormItem>
{modelsWithBaseUrl.some((x) => x === llmFactory) && (
<FormField
<RAGFlowFormItem
name="base_url"
render={({ field }) => (
<FormItem>
<FormLabel
className="text-sm font-medium text-text-primary"
tooltip={
llmFactory === LLMFactory.MiniMax
? t('minimaxBaseUrlTip')
: llmFactory === LLMFactory.TongYiQianWen
? t('tongyiBaseUrlTip')
: llmFactory === LLMFactory.SILICONFLOW
? t('siliconBaseUrlTip')
: t('baseUrlTip')
}
>
{t('baseUrl')}
</FormLabel>
<FormControl>
<Input
{...field}
placeholder={
llmFactory === LLMFactory.TongYiQianWen
? t('tongyiBaseUrlPlaceholder')
: llmFactory === LLMFactory.MiniMax
? t('minimaxBaseUrlPlaceholder')
: llmFactory === LLMFactory.SILICONFLOW
? 'https://api.siliconflow.cn/v1'
: 'https://api.openai.com/v1'
}
onKeyDown={handleKeyDown}
className="w-full"
/>
</FormControl>
<FormMessage />
</FormItem>
label={t('baseUrl')}
tooltip={
llmFactory === LLMFactory.MiniMax
? t('minimaxBaseUrlTip')
: llmFactory === LLMFactory.TongYiQianWen
? t('tongyiBaseUrlTip')
: llmFactory === LLMFactory.SILICONFLOW
? t('siliconBaseUrlTip')
: t('baseUrlTip')
}
labelClassName="text-sm font-medium text-text-primary"
>
{(field) => (
<Input
{...field}
placeholder={
llmFactory === LLMFactory.TongYiQianWen
? t('tongyiBaseUrlPlaceholder')
: llmFactory === LLMFactory.MiniMax
? t('minimaxBaseUrlPlaceholder')
: llmFactory === LLMFactory.SILICONFLOW
? 'https://api.siliconflow.cn/v1'
: 'https://api.openai.com/v1'
}
onKeyDown={handleKeyDown}
className="w-full"
/>
)}
/>
</RAGFlowFormItem>
)}
{llmFactory?.toLowerCase() === 'Anthropic'.toLowerCase() && (
<FormField
<RAGFlowFormItem
name="base_url"
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium text-text-primary">
{t('baseUrl')}
</FormLabel>
<FormControl>
<Input
{...field}
placeholder="https://api.anthropic.com/v1"
onKeyDown={handleKeyDown}
className="w-full"
/>
</FormControl>
<FormMessage />
</FormItem>
label={t('baseUrl')}
labelClassName="text-sm font-medium text-text-primary"
>
{(field) => (
<Input
{...field}
placeholder="https://api.anthropic.com/v1"
onKeyDown={handleKeyDown}
className="w-full"
/>
)}
/>
</RAGFlowFormItem>
)}
{llmFactory?.toLowerCase() === 'Minimax'.toLowerCase() && (
<FormField
<RAGFlowFormItem
name="group_id"
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium text-text-primary">
Group ID
</FormLabel>
<FormControl>
<Input {...field} className="w-full" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
label="Group ID"
labelClassName="text-sm font-medium text-text-primary"
>
{(field) => <Input {...field} className="w-full" />}
</RAGFlowFormItem>
)}
<VerifyButton onVerify={onVerify} />

View File

@@ -8,9 +8,13 @@ import { Modal } from '@/components/ui/modal/modal';
import { useCommonTranslation, useTranslate } from '@/hooks/common-hooks';
import { useBuildModelTypeOptions } from '@/hooks/logic-hooks/use-build-options';
import { IModalProps } from '@/interfaces/common';
import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { VerifyResult } from '@/pages/user-setting/setting-model/hooks';
import { memo, useCallback, useRef } from 'react';
import { IAddProviderInstanceRequestBody } from '@/interfaces/request/llm';
import {
useFetchInstanceNameSet,
useHideWhenInstanceExists,
VerifyResult,
} from '@/pages/user-setting/setting-model/hooks';
import { memo, useCallback, useMemo, useRef } from 'react';
import { FieldValues } from 'react-hook-form';
import { LLMHeader } from '../../components/llm-header';
import VerifyButton from '../../modal/verify-button';
@@ -22,7 +26,7 @@ const AzureOpenAIModal = ({
onVerify,
loading,
llmFactory,
}: IModalProps<IAddLlmRequestBody> & {
}: IModalProps<IAddProviderInstanceRequestBody> & {
llmFactory: string;
onVerify?: (
postBody: any,
@@ -32,87 +36,107 @@ const AzureOpenAIModal = ({
const { t: tg } = useCommonTranslation();
const { buildModelTypeOptions } = useBuildModelTypeOptions();
const formRef = useRef<DynamicFormRef>(null);
const { instanceNameSet } = useFetchInstanceNameSet(llmFactory);
const fields: FormFieldConfig[] = [
{
name: 'model_type',
label: t('modelType'),
type: FormFieldType.Select,
required: true,
options: buildModelTypeOptions(['chat', 'embedding', 'image2text']),
defaultValue: 'embedding',
validation: {
message: t('modelTypeMessage'),
const hideWhenInstanceExists = useHideWhenInstanceExists(instanceNameSet);
const fields: FormFieldConfig[] = useMemo(
() => [
{
name: 'instance_name',
label: t('instanceName'),
type: FormFieldType.Text,
required: true,
placeholder: t('instanceNameMessage'),
tooltip: t('instanceNameTip'),
validation: {
message: t('instanceNameMessage'),
},
},
},
{
name: 'api_base',
label: t('addLlmBaseUrl'),
type: FormFieldType.Text,
required: true,
placeholder: t('baseUrlNameMessage'),
validation: {
message: t('baseUrlNameMessage'),
{
name: 'model_type',
label: t('modelType'),
type: FormFieldType.MultiSelect,
required: true,
options: buildModelTypeOptions(['chat', 'embedding', 'image2text']),
defaultValue: ['embedding'],
},
},
{
name: 'api_key',
label: t('apiKey'),
type: FormFieldType.Text,
required: false,
placeholder: t('apiKeyMessage'),
},
{
name: 'llm_name',
label: t('modelName'),
type: FormFieldType.Text,
required: true,
placeholder: t('modelNameMessage'),
defaultValue: 'gpt-3.5-turbo',
validation: {
message: t('modelNameMessage'),
{
name: 'api_base',
label: t('addLlmBaseUrl'),
type: FormFieldType.Text,
required: true,
placeholder: t('baseUrlNameMessage'),
validation: {
message: t('baseUrlNameMessage'),
},
shouldRender: hideWhenInstanceExists,
},
},
{
name: 'api_version',
label: t('apiVersion'),
type: FormFieldType.Text,
required: false,
placeholder: t('apiVersionMessage'),
defaultValue: '2024-02-01',
},
{
name: 'max_tokens',
label: t('maxTokens'),
type: FormFieldType.Number,
required: true,
placeholder: t('maxTokensTip'),
validation: {
min: 0,
message: t('maxTokensMessage'),
{
name: 'api_key',
label: t('apiKey'),
type: FormFieldType.Text,
required: false,
placeholder: t('apiKeyMessage'),
shouldRender: hideWhenInstanceExists,
},
},
{
name: 'vision',
label: t('vision'),
type: FormFieldType.Switch,
defaultValue: false,
dependencies: ['model_type'],
shouldRender: (formValues: any) => {
return formValues?.model_type === 'chat';
{
name: 'llm_name',
label: t('modelName'),
type: FormFieldType.Text,
required: true,
placeholder: t('modelNameMessage'),
defaultValue: 'gpt-3.5-turbo',
validation: {
message: t('modelNameMessage'),
},
},
},
];
{
name: 'api_version',
label: t('apiVersion'),
type: FormFieldType.Text,
required: false,
placeholder: t('apiVersionMessage'),
defaultValue: '2024-02-01',
},
{
name: 'max_tokens',
label: t('maxTokens'),
type: FormFieldType.Number,
required: true,
placeholder: t('maxTokensTip'),
validation: {
min: 0,
message: t('maxTokensMessage'),
},
},
{
name: 'vision',
label: t('vision'),
type: FormFieldType.Switch,
defaultValue: false,
dependencies: ['model_type'],
shouldRender: (formValues: any) => {
const modelType = formValues?.model_type;
if (Array.isArray(modelType)) {
return modelType.includes('chat');
}
return modelType === 'chat';
},
},
],
[t, buildModelTypeOptions, hideWhenInstanceExists],
);
const handleOk = async (values?: FieldValues) => {
if (!values) return;
const modelType =
values.model_type === 'chat' && values.vision
? 'image2text'
: values.model_type;
const modelType = values.model_type.map((t: string) =>
t === 'chat' && values.vision ? 'image2text' : t,
);
const data: IAddLlmRequestBody & { api_version?: string } = {
const data: IAddProviderInstanceRequestBody & { api_version?: string } = {
instance_name: values.instance_name as string,
llm_factory: llmFactory,
llm_name: values.llm_name as string,
model_type: modelType,
@@ -127,13 +151,11 @@ const AzureOpenAIModal = ({
const verifyParamsFunc = useCallback(() => {
const values = formRef.current?.getValues();
const modelType =
values.model_type === 'chat' && values.vision
? 'image2text'
: values.model_type;
return {
llm_factory: llmFactory,
model_type: modelType,
model_type: values.model_type.map((t: string) =>
t === 'chat' && values.vision ? 'image2text' : t,
),
};
}, [llmFactory]);
@@ -162,10 +184,12 @@ const AzureOpenAIModal = ({
ref={formRef}
defaultValues={
{
model_type: 'embedding',
instance_name: '',
model_type: ['embedding'],
llm_name: 'gpt-3.5-turbo',
api_version: '2024-02-01',
vision: false,
max_tokens: 8192,
} as FieldValues
}
labelClassName="font-normal"

View File

@@ -4,26 +4,31 @@ import { ButtonLoading } from '@/components/ui/button';
import { Form } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Modal } from '@/components/ui/modal/modal';
import { MultiSelect } from '@/components/ui/multi-select';
import { Segmented } from '@/components/ui/segmented';
import { useCommonTranslation, useTranslate } from '@/hooks/common-hooks';
import { useBuildModelTypeOptions } from '@/hooks/logic-hooks/use-build-options';
import { IModalProps } from '@/interfaces/common';
import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { VerifyResult } from '@/pages/user-setting/setting-model/hooks';
import { IAddProviderInstanceRequestBody } from '@/interfaces/request/llm';
import {
useFetchInstanceNameSet,
VerifyResult,
} from '@/pages/user-setting/setting-model/hooks';
import { zodResolver } from '@hookform/resolvers/zod';
import { memo, useCallback, useMemo } from 'react';
import { memo, useCallback, useMemo, useRef } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { z } from 'zod';
import { LLMHeader } from '../../components/llm-header';
import { BedrockRegionList } from '../../constant';
import VerifyButton from '../../modal/verify-button';
type FieldType = IAddLlmRequestBody & {
type FieldType = Omit<IAddProviderInstanceRequestBody, 'model_type'> & {
auth_mode?: 'access_key_secret' | 'iam_role' | 'assume_role';
bedrock_ak: string;
bedrock_sk: string;
bedrock_region: string;
aws_role_arn?: string;
model_type: string[];
};
const BedrockModal = ({
@@ -33,7 +38,7 @@ const BedrockModal = ({
onVerify,
loading,
llmFactory,
}: IModalProps<IAddLlmRequestBody> & {
}: IModalProps<IAddProviderInstanceRequestBody> & {
llmFactory: string;
onVerify?: (
postBody: any,
@@ -42,61 +47,84 @@ const BedrockModal = ({
const { t } = useTranslate('setting');
const { t: ct } = useCommonTranslation();
const { buildModelTypeOptions } = useBuildModelTypeOptions();
const { instanceNameSet } = useFetchInstanceNameSet(llmFactory);
const instanceExistsRef = useRef(false);
const FormSchema = z
.object({
model_type: z.enum(['chat', 'embedding'], {
required_error: t('modelTypeMessage'),
}),
llm_name: z.string().min(1, { message: t('bedrockModelNameMessage') }),
bedrock_region: z.string().min(1, { message: t('bedrockRegionMessage') }),
max_tokens: z
.number({
required_error: t('maxTokensMessage'),
invalid_type_error: t('maxTokensInvalidMessage'),
const FormSchema = useMemo(
() =>
z
.object({
instance_name: z
.string()
.min(1, { message: t('instanceNameMessage') }),
model_type: z
.array(z.enum(['chat', 'embedding']))
.min(1, { message: t('modelTypeMessage') }),
llm_name: z
.string()
.min(1, { message: t('bedrockModelNameMessage') }),
bedrock_region: z.string().optional(),
max_tokens: z
.number({
required_error: t('maxTokensMessage'),
invalid_type_error: t('maxTokensInvalidMessage'),
})
.nonnegative({ message: t('maxTokensMinMessage') }),
auth_mode: z
.enum(['access_key_secret', 'iam_role', 'assume_role'])
.default('access_key_secret'),
bedrock_ak: z.string().optional(),
bedrock_sk: z.string().optional(),
aws_role_arn: z.string().optional(),
})
.nonnegative({ message: t('maxTokensMinMessage') }),
auth_mode: z
.enum(['access_key_secret', 'iam_role', 'assume_role'])
.default('access_key_secret'),
bedrock_ak: z.string().optional(),
bedrock_sk: z.string().optional(),
aws_role_arn: z.string().optional(),
})
.superRefine((data, ctx) => {
if (data.auth_mode === 'access_key_secret') {
if (!data.bedrock_ak || data.bedrock_ak.trim() === '') {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t('bedrockAKMessage'),
path: ['bedrock_ak'],
});
}
if (!data.bedrock_sk || data.bedrock_sk.trim() === '') {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t('bedrockSKMessage'),
path: ['bedrock_sk'],
});
}
}
.superRefine((data, ctx) => {
if (instanceExistsRef.current) return;
if (data.auth_mode === 'iam_role') {
if (!data.aws_role_arn || data.aws_role_arn.trim() === '') {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t('awsRoleArnMessage'),
path: ['aws_role_arn'],
});
}
}
});
if (!data.bedrock_region || data.bedrock_region.trim() === '') {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t('bedrockRegionMessage'),
path: ['bedrock_region'],
});
}
if (data.auth_mode === 'access_key_secret') {
if (!data.bedrock_ak || data.bedrock_ak.trim() === '') {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t('bedrockAKMessage'),
path: ['bedrock_ak'],
});
}
if (!data.bedrock_sk || data.bedrock_sk.trim() === '') {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t('bedrockSKMessage'),
path: ['bedrock_sk'],
});
}
}
if (data.auth_mode === 'iam_role') {
if (!data.aws_role_arn || data.aws_role_arn.trim() === '') {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: t('awsRoleArnMessage'),
path: ['aws_role_arn'],
});
}
}
}),
[t],
);
const form = useForm<FieldType>({
resolver: zodResolver(FormSchema),
defaultValues: {
model_type: 'chat',
instance_name: '',
model_type: ['chat'],
auth_mode: 'access_key_secret',
max_tokens: 8192,
},
});
@@ -105,6 +133,17 @@ const BedrockModal = ({
name: 'auth_mode',
});
const instanceName = useWatch({
control: form.control,
name: 'instance_name',
});
const instanceExists = useMemo(() => {
const trimmed = (instanceName || '').trim();
return !!trimmed && instanceNameSet.has(trimmed);
}, [instanceName, instanceNameSet]);
instanceExistsRef.current = instanceExists;
const options = useMemo(
() => BedrockRegionList.map((x) => ({ value: x, label: t(x) })),
[t],
@@ -133,9 +172,10 @@ const BedrockModal = ({
...cleanedValues,
llm_factory: llmFactory,
max_tokens: values.max_tokens,
model_type: values.model_type,
};
onOk?.(data as unknown as IAddLlmRequestBody);
onOk?.(data as unknown as IAddProviderInstanceRequestBody);
};
const verifyParamsFunc = useCallback(() => {
@@ -160,6 +200,7 @@ const BedrockModal = ({
...cleanedValues,
llm_factory: llmFactory,
max_tokens: values.max_tokens,
model_type: values.model_type,
};
}, [llmFactory, authMode, form]);
@@ -199,13 +240,24 @@ const BedrockModal = ({
className="space-y-6"
id="bedrock-form"
>
<RAGFlowFormItem
name="instance_name"
label={t('instanceName')}
tooltip={t('instanceNameTip')}
required
>
<Input placeholder={t('instanceNameMessage')} />
</RAGFlowFormItem>
<RAGFlowFormItem name="model_type" label={t('modelType')} required>
{(field) => (
<SelectWithSearch
value={field.value}
onChange={field.onChange}
<MultiSelect
options={buildModelTypeOptions(['chat', 'embedding'])}
placeholder={t('modelTypeMessage')}
onValueChange={field.onChange}
defaultValue={field.value}
variant="inverted"
maxCount={100}
/>
)}
</RAGFlowFormItem>
@@ -214,86 +266,93 @@ const BedrockModal = ({
<Input placeholder={t('bedrockModelNameMessage')} />
</RAGFlowFormItem>
<div className="mb-4">
<RAGFlowFormItem name="auth_mode">
{(field) => (
<Segmented
value={field.value}
onChange={(value) => {
// Clear non-active fields so they won't be validated/submitted by accident.
if (value !== 'access_key_secret') {
form.setValue('bedrock_ak', '');
form.setValue('bedrock_sk', '');
}
if (value !== 'iam_role') {
form.setValue('aws_role_arn', '');
}
field.onChange(value);
}}
options={[
{
label: t('awsAuthModeAccessKeySecret'),
value: 'access_key_secret',
},
{ label: t('awsAuthModeIamRole'), value: 'iam_role' },
{ label: t('awsAuthModeAssumeRole'), value: 'assume_role' },
]}
/>
)}
</RAGFlowFormItem>
</div>
{authMode === 'access_key_secret' && (
{!instanceExists && (
<>
<div className="mb-4">
<RAGFlowFormItem name="auth_mode">
{(field) => (
<Segmented
value={field.value}
onChange={(value) => {
// Clear non-active fields so they won't be validated/submitted by accident.
if (value !== 'access_key_secret') {
form.setValue('bedrock_ak', '');
form.setValue('bedrock_sk', '');
}
if (value !== 'iam_role') {
form.setValue('aws_role_arn', '');
}
field.onChange(value);
}}
options={[
{
label: t('awsAuthModeAccessKeySecret'),
value: 'access_key_secret',
},
{ label: t('awsAuthModeIamRole'), value: 'iam_role' },
{
label: t('awsAuthModeAssumeRole'),
value: 'assume_role',
},
]}
/>
)}
</RAGFlowFormItem>
</div>
{authMode === 'access_key_secret' && (
<>
<RAGFlowFormItem
name="bedrock_ak"
label={t('awsAccessKeyId')}
required
>
<Input placeholder={t('bedrockAKMessage')} />
</RAGFlowFormItem>
<RAGFlowFormItem
name="bedrock_sk"
label={t('awsSecretAccessKey')}
required
>
<Input placeholder={t('bedrockSKMessage')} />
</RAGFlowFormItem>
</>
)}
{authMode === 'iam_role' && (
<RAGFlowFormItem
name="aws_role_arn"
label={t('awsRoleArn')}
required
>
<Input placeholder={t('awsRoleArnMessage')} />
</RAGFlowFormItem>
)}
{authMode === 'assume_role' && (
<div className="text-sm text-text-secondary mt-2 mb-4">
{t('awsAssumeRoleTip')}
</div>
)}
<RAGFlowFormItem
name="bedrock_ak"
label={t('awsAccessKeyId')}
name="bedrock_region"
label={t('bedrockRegion')}
required
>
<Input placeholder={t('bedrockAKMessage')} />
</RAGFlowFormItem>
<RAGFlowFormItem
name="bedrock_sk"
label={t('awsSecretAccessKey')}
required
>
<Input placeholder={t('bedrockSKMessage')} />
{(field) => (
<SelectWithSearch
value={field.value}
onChange={field.onChange}
options={options}
placeholder={t('bedrockRegionMessage')}
allowClear
/>
)}
</RAGFlowFormItem>
</>
)}
{authMode === 'iam_role' && (
<RAGFlowFormItem
name="aws_role_arn"
label={t('awsRoleArn')}
required
>
<Input placeholder={t('awsRoleArnMessage')} />
</RAGFlowFormItem>
)}
{authMode === 'assume_role' && (
<div className="text-sm text-text-secondary mt-2 mb-4">
{t('awsAssumeRoleTip')}
</div>
)}
<RAGFlowFormItem
name="bedrock_region"
label={t('bedrockRegion')}
required
>
{(field) => (
<SelectWithSearch
value={field.value}
onChange={field.onChange}
options={options}
placeholder={t('bedrockRegionMessage')}
allowClear
/>
)}
</RAGFlowFormItem>
<RAGFlowFormItem name="max_tokens" label={t('maxTokens')} required>
{(field) => (
<Input

View File

@@ -7,9 +7,13 @@ import { Modal } from '@/components/ui/modal/modal';
import { useCommonTranslation, useTranslate } from '@/hooks/common-hooks';
import { useBuildModelTypeOptions } from '@/hooks/logic-hooks/use-build-options';
import { IModalProps } from '@/interfaces/common';
import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { VerifyResult } from '@/pages/user-setting/setting-model/hooks';
import { memo, useCallback } from 'react';
import { IAddProviderInstanceRequestBody } from '@/interfaces/request/llm';
import {
useFetchInstanceNameSet,
useHideWhenInstanceExists,
VerifyResult,
} from '@/pages/user-setting/setting-model/hooks';
import { memo, useCallback, useMemo } from 'react';
import { FieldValues } from 'react-hook-form';
import { LLMHeader } from '../../components/llm-header';
import VerifyButton from '../../modal/verify-button';
@@ -21,7 +25,7 @@ const FishAudioModal = ({
onVerify,
loading,
llmFactory,
}: IModalProps<IAddLlmRequestBody> & {
}: IModalProps<IAddProviderInstanceRequestBody> & {
llmFactory: string;
onVerify?: (
postBody: any,
@@ -30,58 +34,78 @@ const FishAudioModal = ({
const { t } = useTranslate('setting');
const { t: tc } = useCommonTranslation();
const { buildModelTypeOptions } = useBuildModelTypeOptions();
const { instanceNameSet } = useFetchInstanceNameSet(llmFactory);
const fields: FormFieldConfig[] = [
{
name: 'model_type',
label: t('modelType'),
type: FormFieldType.Select,
required: true,
options: buildModelTypeOptions(['tts']),
defaultValue: 'tts',
validation: { message: t('modelTypeMessage') },
},
{
name: 'llm_name',
label: t('modelName'),
type: FormFieldType.Text,
required: true,
placeholder: t('FishAudioModelNameMessage'),
validation: { message: t('FishAudioModelNameMessage') },
},
{
name: 'fish_audio_ak',
label: t('addFishAudioAK'),
type: FormFieldType.Text,
required: true,
placeholder: t('FishAudioAKMessage'),
validation: { message: t('FishAudioAKMessage') },
},
{
name: 'fish_audio_refid',
label: t('addFishAudioRefID'),
type: FormFieldType.Text,
required: true,
placeholder: t('FishAudioRefIDMessage'),
validation: { message: t('FishAudioRefIDMessage') },
},
{
name: 'max_tokens',
label: t('maxTokens'),
type: FormFieldType.Number,
required: true,
placeholder: t('maxTokensTip'),
validation: {
min: 0,
message: t('maxTokensInvalidMessage'),
const hideWhenInstanceExists = useHideWhenInstanceExists(instanceNameSet);
const fields: FormFieldConfig[] = useMemo(
() => [
{
name: 'instance_name',
label: t('instanceName'),
type: FormFieldType.Text,
required: true,
placeholder: t('instanceNameMessage'),
tooltip: t('instanceNameTip'),
validation: { message: t('instanceNameMessage') },
},
},
];
{
name: 'model_type',
label: t('modelType'),
type: FormFieldType.MultiSelect,
required: true,
options: buildModelTypeOptions(['tts']),
defaultValue: ['tts'],
},
{
name: 'llm_name',
label: t('modelName'),
type: FormFieldType.Text,
required: true,
placeholder: t('FishAudioModelNameMessage'),
validation: { message: t('FishAudioModelNameMessage') },
},
{
name: 'fish_audio_ak',
label: t('addFishAudioAK'),
type: FormFieldType.Text,
required: true,
placeholder: t('FishAudioAKMessage'),
validation: { message: t('FishAudioAKMessage') },
shouldRender: hideWhenInstanceExists,
},
{
name: 'fish_audio_refid',
label: t('addFishAudioRefID'),
type: FormFieldType.Text,
required: true,
placeholder: t('FishAudioRefIDMessage'),
validation: { message: t('FishAudioRefIDMessage') },
shouldRender: hideWhenInstanceExists,
},
{
name: 'max_tokens',
label: t('maxTokens'),
type: FormFieldType.Number,
required: true,
placeholder: t('maxTokensTip'),
validation: {
min: 0,
message: t('maxTokensInvalidMessage'),
},
},
],
[t, buildModelTypeOptions, hideWhenInstanceExists],
);
const handleOk = async (values?: FieldValues) => {
if (!values) return;
const data: Record<string, any> = {
const data: IAddProviderInstanceRequestBody & {
fish_audio_ak: string;
fish_audio_refid: string;
} = {
instance_name: values.instance_name as string,
llm_factory: llmFactory,
llm_name: values.llm_name as string,
model_type: values.model_type,
@@ -90,8 +114,7 @@ const FishAudioModal = ({
max_tokens: values.max_tokens as number,
};
console.info(data);
await onOk?.(data as IAddLlmRequestBody);
await onOk?.(data);
};
const handleVerify = useCallback(
@@ -114,7 +137,11 @@ const FishAudioModal = ({
<DynamicForm.Root
fields={fields}
onSubmit={(data) => console.log(data)}
defaultValues={{ model_type: 'tts' }}
defaultValues={{
instance_name: '',
model_type: ['tts'],
max_tokens: 8192,
}}
labelClassName="font-normal"
>
{onVerify && (

View File

@@ -7,9 +7,13 @@ import { Modal } from '@/components/ui/modal/modal';
import { useCommonTranslation, useTranslate } from '@/hooks/common-hooks';
import { useBuildModelTypeOptions } from '@/hooks/logic-hooks/use-build-options';
import { IModalProps } from '@/interfaces/common';
import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { VerifyResult } from '@/pages/user-setting/setting-model/hooks';
import { memo, useCallback } from 'react';
import { IAddProviderInstanceRequestBody } from '@/interfaces/request/llm';
import {
useFetchInstanceNameSet,
useHideWhenInstanceExists,
VerifyResult,
} from '@/pages/user-setting/setting-model/hooks';
import { memo, useCallback, useMemo } from 'react';
import { FieldValues } from 'react-hook-form';
import { LLMHeader } from '../../components/llm-header';
import VerifyButton from '../../modal/verify-button';
@@ -21,7 +25,7 @@ const GoogleModal = ({
onVerify,
loading,
llmFactory,
}: IModalProps<IAddLlmRequestBody> & {
}: IModalProps<IAddProviderInstanceRequestBody> & {
llmFactory: string;
onVerify?: (
postBody: any,
@@ -30,85 +34,101 @@ const GoogleModal = ({
const { t } = useTranslate('setting');
const { t: tc } = useCommonTranslation();
const { buildModelTypeOptions } = useBuildModelTypeOptions();
const { instanceNameSet } = useFetchInstanceNameSet(llmFactory);
const fields: FormFieldConfig[] = [
{
name: 'model_type',
label: t('modelType'),
type: FormFieldType.Select,
required: true,
options: buildModelTypeOptions(['chat', 'image2text']),
defaultValue: 'chat',
validation: {
message: t('modelTypeMessage'),
const hideWhenInstanceExists = useHideWhenInstanceExists(instanceNameSet);
const fields: FormFieldConfig[] = useMemo(
() => [
{
name: 'instance_name',
label: t('instanceName'),
type: FormFieldType.Text,
required: true,
placeholder: t('instanceNameMessage'),
tooltip: t('instanceNameTip'),
validation: { message: t('instanceNameMessage') },
},
},
{
name: 'llm_name',
label: t('modelID'),
type: FormFieldType.Text,
required: true,
placeholder: t('GoogleModelIDMessage'),
validation: {
message: t('GoogleModelIDMessage'),
{
name: 'model_type',
label: t('modelType'),
type: FormFieldType.MultiSelect,
required: true,
options: buildModelTypeOptions(['chat', 'image2text']),
defaultValue: ['chat'],
},
},
{
name: 'google_project_id',
label: t('addGoogleProjectID'),
type: FormFieldType.Text,
required: true,
placeholder: t('GoogleProjectIDMessage'),
validation: {
message: t('GoogleProjectIDMessage'),
{
name: 'llm_name',
label: t('modelID'),
type: FormFieldType.Text,
required: true,
placeholder: t('GoogleModelIDMessage'),
validation: {
message: t('GoogleModelIDMessage'),
},
},
},
{
name: 'google_region',
label: t('addGoogleRegion'),
type: FormFieldType.Text,
required: true,
placeholder: t('GoogleRegionMessage'),
validation: {
message: t('GoogleRegionMessage'),
{
name: 'google_project_id',
label: t('addGoogleProjectID'),
type: FormFieldType.Text,
required: true,
placeholder: t('GoogleProjectIDMessage'),
validation: {
message: t('GoogleProjectIDMessage'),
},
shouldRender: hideWhenInstanceExists,
},
},
{
name: 'google_service_account_key',
label: t('addGoogleServiceAccountKey'),
type: FormFieldType.Text,
required: true,
placeholder: t('GoogleServiceAccountKeyMessage'),
validation: {
message: t('GoogleServiceAccountKeyMessage'),
{
name: 'google_region',
label: t('addGoogleRegion'),
type: FormFieldType.Text,
required: true,
placeholder: t('GoogleRegionMessage'),
validation: {
message: t('GoogleRegionMessage'),
},
shouldRender: hideWhenInstanceExists,
},
},
{
name: 'max_tokens',
label: t('maxTokens'),
type: FormFieldType.Number,
required: true,
placeholder: t('maxTokensTip'),
validation: {
min: 0,
message: t('maxTokensMinMessage'),
{
name: 'google_service_account_key',
label: t('addGoogleServiceAccountKey'),
type: FormFieldType.Text,
required: true,
placeholder: t('GoogleServiceAccountKeyMessage'),
validation: {
message: t('GoogleServiceAccountKeyMessage'),
},
shouldRender: hideWhenInstanceExists,
},
customValidate: (value: any) => {
if (value === undefined || value === null || value === '') {
return t('maxTokensMessage');
}
if (value < 0) {
return t('maxTokensMinMessage');
}
return true;
{
name: 'max_tokens',
label: t('maxTokens'),
type: FormFieldType.Number,
required: true,
placeholder: t('maxTokensTip'),
validation: {
min: 0,
message: t('maxTokensMinMessage'),
},
customValidate: (value: any) => {
if (value === undefined || value === null || value === '') {
return t('maxTokensMessage');
}
if (value < 0) {
return t('maxTokensMinMessage');
}
return true;
},
},
},
];
],
[t, buildModelTypeOptions, hideWhenInstanceExists],
);
const handleOk = async (values?: FieldValues) => {
if (!values) return;
const data = {
instance_name: values.instance_name as string,
llm_factory: llmFactory,
model_type: values.model_type,
llm_name: values.llm_name,
@@ -116,7 +136,7 @@ const GoogleModal = ({
google_region: values.google_region,
google_service_account_key: values.google_service_account_key,
max_tokens: values.max_tokens,
} as IAddLlmRequestBody;
} as IAddProviderInstanceRequestBody;
await onOk?.(data);
};
@@ -150,7 +170,9 @@ const GoogleModal = ({
}}
defaultValues={
{
model_type: 'chat',
instance_name: '',
model_type: ['chat'],
max_tokens: 8192,
} as FieldValues
}
labelClassName="font-normal"

View File

@@ -25,6 +25,9 @@ import { LLMHeader } from '../../components/llm-header';
import VerifyButton from '../verify-button';
const FormSchema = z.object({
instance_name: z.string().min(1, {
message: t('setting.instanceNameMessage'),
}),
llm_name: z.string().min(1, {
message: t('setting.mineru.modelNameRequired'),
}),
@@ -71,6 +74,7 @@ const MinerUModal = ({
const form = useForm<MinerUFormValues>({
resolver: zodResolver(FormSchema),
defaultValues: {
instance_name: '',
mineru_backend: 'pipeline',
mineru_delete_output: true,
},
@@ -102,6 +106,14 @@ const MinerUModal = ({
className="space-y-6"
id="mineru-form"
>
<RAGFlowFormItem
name="instance_name"
label={t('setting.instanceName')}
tooltip={t('setting.instanceNameTip')}
required
>
<Input placeholder={t('setting.instanceNameMessage')} />
</RAGFlowFormItem>
<RAGFlowFormItem
name="llm_name"
label={t('setting.modelName')}

View File

@@ -7,9 +7,13 @@ import { Modal } from '@/components/ui/modal/modal';
import { useCommonTranslation, useTranslate } from '@/hooks/common-hooks';
import { useBuildModelTypeOptions } from '@/hooks/logic-hooks/use-build-options';
import { IModalProps } from '@/interfaces/common';
import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { VerifyResult } from '@/pages/user-setting/setting-model/hooks';
import { memo, useCallback } from 'react';
import { IAddProviderInstanceRequestBody } from '@/interfaces/request/llm';
import {
useFetchInstanceNameSet,
useHideWhenInstanceExists,
VerifyResult,
} from '@/pages/user-setting/setting-model/hooks';
import { memo, useCallback, useMemo } from 'react';
import { FieldValues } from 'react-hook-form';
import { LLMHeader } from '../../components/llm-header';
import VerifyButton from '../../modal/verify-button';
@@ -21,7 +25,7 @@ const TencentCloudModal = ({
onVerify,
loading,
llmFactory,
}: IModalProps<Omit<IAddLlmRequestBody, 'max_tokens'>> & {
}: IModalProps<Omit<IAddProviderInstanceRequestBody, 'max_tokens'>> & {
llmFactory: string;
onVerify?: (
postBody: any,
@@ -30,87 +34,100 @@ const TencentCloudModal = ({
const { t } = useTranslate('setting');
const { t: tc } = useCommonTranslation();
const { buildModelTypeOptions } = useBuildModelTypeOptions();
const { instanceNameSet } = useFetchInstanceNameSet(llmFactory);
const fields: FormFieldConfig[] = [
{
name: 'model_type',
label: t('modelType'),
type: FormFieldType.Select,
required: true,
options: buildModelTypeOptions(['speech2text']),
defaultValue: 'speech2text',
validation: {
message: t('modelTypeMessage'),
const hideWhenInstanceExists = useHideWhenInstanceExists(instanceNameSet);
const fields: FormFieldConfig[] = useMemo(
() => [
{
name: 'instance_name',
label: t('instanceName'),
type: FormFieldType.Text,
required: true,
placeholder: t('instanceNameMessage'),
tooltip: t('instanceNameTip'),
validation: { message: t('instanceNameMessage') },
},
},
{
name: 'llm_name',
label: t('modelName'),
type: FormFieldType.Select,
required: true,
options: [
{ label: '16k_zh', value: '16k_zh' },
{ label: '16k_zh_large', value: '16k_zh_large' },
{ label: '16k_multi_lang', value: '16k_multi_lang' },
{ label: '16k_zh_dialect', value: '16k_zh_dialect' },
{ label: '16k_en', value: '16k_en' },
{ label: '16k_yue', value: '16k_yue' },
{ label: '16k_zh-PY', value: '16k_zh-PY' },
{ label: '16k_ja', value: '16k_ja' },
{ label: '16k_ko', value: '16k_ko' },
{ label: '16k_vi', value: '16k_vi' },
{ label: '16k_ms', value: '16k_ms' },
{ label: '16k_id', value: '16k_id' },
{ label: '16k_fil', value: '16k_fil' },
{ label: '16k_th', value: '16k_th' },
{ label: '16k_pt', value: '16k_pt' },
{ label: '16k_tr', value: '16k_tr' },
{ label: '16k_ar', value: '16k_ar' },
{ label: '16k_es', value: '16k_es' },
{ label: '16k_hi', value: '16k_hi' },
{ label: '16k_fr', value: '16k_fr' },
{ label: '16k_zh_medical', value: '16k_zh_medical' },
{ label: '16k_de', value: '16k_de' },
],
defaultValue: '16k_zh',
validation: {
message: t('SparkModelNameMessage'),
{
name: 'model_type',
label: t('modelType'),
type: FormFieldType.MultiSelect,
required: true,
options: buildModelTypeOptions(['speech2text']),
defaultValue: ['speech2text'],
},
},
{
name: 'TencentCloud_sid',
label: t('addTencentCloudSID'),
type: FormFieldType.Text,
required: true,
placeholder: t('TencentCloudSIDMessage'),
validation: {
message: t('TencentCloudSIDMessage'),
{
name: 'llm_name',
label: t('modelName'),
type: FormFieldType.Select,
required: true,
options: [
{ label: '16k_zh', value: '16k_zh' },
{ label: '16k_zh_large', value: '16k_zh_large' },
{ label: '16k_multi_lang', value: '16k_multi_lang' },
{ label: '16k_zh_dialect', value: '16k_zh_dialect' },
{ label: '16k_en', value: '16k_en' },
{ label: '16k_yue', value: '16k_yue' },
{ label: '16k_zh-PY', value: '16k_zh-PY' },
{ label: '16k_ja', value: '16k_ja' },
{ label: '16k_ko', value: '16k_ko' },
{ label: '16k_vi', value: '16k_vi' },
{ label: '16k_ms', value: '16k_ms' },
{ label: '16k_id', value: '16k_id' },
{ label: '16k_fil', value: '16k_fil' },
{ label: '16k_th', value: '16k_th' },
{ label: '16k_pt', value: '16k_pt' },
{ label: '16k_tr', value: '16k_tr' },
{ label: '16k_ar', value: '16k_ar' },
{ label: '16k_es', value: '16k_es' },
{ label: '16k_hi', value: '16k_hi' },
{ label: '16k_fr', value: '16k_fr' },
{ label: '16k_zh_medical', value: '16k_zh_medical' },
{ label: '16k_de', value: '16k_de' },
],
defaultValue: '16k_zh',
validation: {
message: t('SparkModelNameMessage'),
},
},
},
{
name: 'TencentCloud_sk',
label: t('addTencentCloudSK'),
type: FormFieldType.Text,
required: true,
placeholder: t('TencentCloudSKMessage'),
validation: {
message: t('TencentCloudSKMessage'),
{
name: 'TencentCloud_sid',
label: t('addTencentCloudSID'),
type: FormFieldType.Text,
required: true,
placeholder: t('TencentCloudSIDMessage'),
validation: {
message: t('TencentCloudSIDMessage'),
},
shouldRender: hideWhenInstanceExists,
},
},
];
{
name: 'TencentCloud_sk',
label: t('addTencentCloudSK'),
type: FormFieldType.Text,
required: true,
placeholder: t('TencentCloudSKMessage'),
validation: {
message: t('TencentCloudSKMessage'),
},
shouldRender: hideWhenInstanceExists,
},
],
[t, buildModelTypeOptions, hideWhenInstanceExists],
);
const handleOk = async (values?: FieldValues) => {
if (!values) return;
const modelType = values.model_type;
const data = {
model_type: modelType,
instance_name: values.instance_name as string,
model_type: values.model_type,
llm_name: values.llm_name as string,
TencentCloud_sid: values.TencentCloud_sid as string,
TencentCloud_sk: values.TencentCloud_sk as string,
llm_factory: llmFactory,
} as Omit<IAddLlmRequestBody, 'max_tokens'>;
} as Omit<IAddProviderInstanceRequestBody, 'max_tokens'>;
await onOk?.(data);
};
@@ -143,7 +160,8 @@ const TencentCloudModal = ({
onSubmit={() => {}}
defaultValues={
{
model_type: 'speech2text',
instance_name: '',
model_type: ['speech2text'],
llm_name: '16k_zh',
} as FieldValues
}

View File

@@ -9,8 +9,12 @@ import { LLMFactory } from '@/constants/llm';
import { useCommonTranslation, useTranslate } from '@/hooks/common-hooks';
import { useBuildModelTypeOptions } from '@/hooks/logic-hooks/use-build-options';
import { IModalProps } from '@/interfaces/common';
import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { VerifyResult } from '@/pages/user-setting/setting-model/hooks';
import { IAddProviderInstanceRequestBody } from '@/interfaces/request/llm';
import {
useFetchInstanceNameSet,
useHideWhenInstanceExists,
VerifyResult,
} from '@/pages/user-setting/setting-model/hooks';
import { memo, useCallback, useMemo, useRef } from 'react';
import { FieldValues } from 'react-hook-form';
import { LLMHeader } from '../../components/llm-header';
@@ -47,7 +51,9 @@ const OllamaModal = ({
llmFactory,
editMode = false,
initialValues,
}: IModalProps<Partial<IAddLlmRequestBody> & { provider_order?: string }> & {
}: IModalProps<
Partial<IAddProviderInstanceRequestBody> & { provider_order?: string }
> & {
llmFactory: string;
editMode?: boolean;
onVerify?: (
@@ -58,6 +64,9 @@ const OllamaModal = ({
const { t: tc } = useCommonTranslation();
const { buildModelTypeOptions } = useBuildModelTypeOptions();
const formRef = useRef<DynamicFormRef>(null);
const { instanceNameSet } = useFetchInstanceNameSet(llmFactory);
const hideWhenInstanceExists = useHideWhenInstanceExists(instanceNameSet);
const optionsMap: Partial<
Record<LLMFactory, { label: string; value: string }[]>
@@ -118,15 +127,23 @@ const OllamaModal = ({
const defaultToolCallEnabled = initialValues?.is_tools ?? false;
const baseFields: FormFieldConfig[] = [
{
name: 'instance_name',
label: t('instanceName'),
type: FormFieldType.Text,
required: true,
placeholder: t('instanceNameMessage'),
tooltip: t('instanceNameTip'),
validation: {
message: t('instanceNameMessage'),
},
},
{
name: 'model_type',
label: t('modelType'),
type: FormFieldType.Select,
type: FormFieldType.MultiSelect,
required: true,
options: getOptions(llmFactory),
validation: {
message: t('modelTypeMessage'),
},
},
{
name: 'llm_name',
@@ -147,6 +164,7 @@ const OllamaModal = ({
validation: {
message: t('baseUrlNameMessage'),
},
shouldRender: hideWhenInstanceExists,
},
{
name: 'api_key',
@@ -154,6 +172,7 @@ const OllamaModal = ({
type: FormFieldType.Text,
required: false,
placeholder: t('apiKeyMessage'),
shouldRender: hideWhenInstanceExists,
},
{
name: 'max_tokens',
@@ -186,6 +205,9 @@ const OllamaModal = ({
dependencies: ['model_type'],
shouldRender: (formValues: any) => {
const modelType = formValues?.model_type;
if (Array.isArray(modelType)) {
return modelType.includes('chat') || modelType.includes('image2text');
}
return modelType === 'chat' || modelType === 'image2text';
},
tooltip: t('enableToolCallTip'),
@@ -212,18 +234,25 @@ const OllamaModal = ({
required: false,
dependencies: ['model_type'],
shouldRender: (formValues: any) => {
return formValues?.model_type === 'chat';
const modelType = formValues?.model_type;
if (Array.isArray(modelType)) {
return modelType.includes('chat');
}
return modelType === 'chat';
},
});
return baseFields;
}, [llmFactory, t]);
}, [llmFactory, t, hideWhenInstanceExists, initialValues?.is_tools]);
const defaultValues: FieldValues = useMemo(() => {
if (editMode && initialValues) {
return {
instance_name: initialValues.instance_name || '',
llm_name: initialValues.llm_name || '',
model_type: initialValues.model_type || 'chat',
model_type: initialValues.model_type
? initialValues.model_type.split(',').filter(Boolean)
: ['chat'],
api_base: initialValues.api_base || '',
max_tokens: initialValues.max_tokens || 8192,
api_key: '',
@@ -233,34 +262,42 @@ const OllamaModal = ({
};
}
return {
model_type:
instance_name: '',
model_type: [
llmFactory === LLMFactory.Ollama || llmFactory === LLMFactory.VLLM
? 'chat'
: llmFactory in optionsMap
? optionsMap[llmFactory as LLMFactory]?.at(0)?.value
: 'embedding',
],
vision: false,
is_tools: false,
max_tokens: 8192,
};
}, [editMode, initialValues, llmFactory]);
const handleOk = async (values?: FieldValues) => {
if (!values) return;
const modelType =
values.model_type === 'chat' && values.vision
? 'image2text'
: values.model_type;
const supportsToolCall = modelType === 'chat' || modelType === 'image2text';
const modelType = values.model_type.map((t: string) =>
t === 'chat' && values.vision ? 'image2text' : t,
);
const modelTypeArray: string[] = Array.isArray(values.model_type)
? values.model_type
: [values.model_type];
const supportsToolCall =
modelTypeArray.includes('chat') || modelTypeArray.includes('image2text');
const data: IAddLlmRequestBody & { provider_order?: string } = {
llm_factory: llmFactory,
llm_name: values.llm_name as string,
model_type: modelType,
api_base: values.api_base as string,
api_key: values.api_key as string,
max_tokens: values.max_tokens as number,
};
const data: IAddProviderInstanceRequestBody & { provider_order?: string } =
{
instance_name: values.instance_name as string,
llm_factory: llmFactory,
llm_name: values.llm_name as string,
model_type: modelType,
api_base: values.api_base as string,
api_key: values.api_key as string,
max_tokens: values.max_tokens as number,
};
if (supportsToolCall) {
data.is_tools = Boolean(values.is_tools);
}
@@ -275,13 +312,11 @@ const OllamaModal = ({
const verifyParamsFunc = useCallback(() => {
const values = formRef.current?.getValues();
const modelType =
values.model_type === 'chat' && values.vision
? 'image2text'
: values.model_type;
return {
llm_factory: llmFactory,
model_type: modelType,
model_type: values.model_type.map((t: string) =>
t === 'chat' && values.vision ? 'image2text' : t,
),
};
}, [llmFactory]);

View File

@@ -20,6 +20,7 @@ import { LLMHeader } from '../../components/llm-header';
import VerifyButton from '../verify-button';
export type OpenDataLoaderFormValues = {
instance_name: string;
llm_name: string;
opendataloader_apiserver: string;
opendataloader_api_key?: string;
@@ -47,6 +48,9 @@ const OpenDataLoaderModal = ({
const FormSchema = useMemo(
() =>
z.object({
instance_name: z.string().min(1, {
message: t('setting.instanceNameMessage'),
}),
llm_name: z.string().min(1, {
message: t('setting.modelNameMessage'),
}),
@@ -61,6 +65,7 @@ const OpenDataLoaderModal = ({
const form = useForm<OpenDataLoaderFormValues>({
resolver: zodResolver(FormSchema),
defaultValues: {
instance_name: '',
opendataloader_apiserver: '',
opendataloader_api_key: '',
},
@@ -87,6 +92,14 @@ const OpenDataLoaderModal = ({
className="space-y-6"
id="opendataloader-form"
>
<RAGFlowFormItem
name="instance_name"
label={t('setting.instanceName')}
tooltip={t('setting.instanceNameTip')}
required
>
<Input placeholder={t('setting.instanceNameMessage')} />
</RAGFlowFormItem>
<RAGFlowFormItem
name="llm_name"
label={t('setting.modelName')}

View File

@@ -22,6 +22,9 @@ import { LLMHeader } from '../../components/llm-header';
import VerifyButton from '../verify-button';
const FormSchema = z.object({
instance_name: z.string().min(1, {
message: t('setting.instanceNameMessage'),
}),
llm_name: z.string().min(1, {
message: t('setting.paddleocr.modelNameRequired'),
}),
@@ -63,6 +66,7 @@ const PaddleOCRModal = ({
const form = useForm<PaddleOCRFormValues>({
resolver: zodResolver(FormSchema),
defaultValues: {
instance_name: '',
paddleocr_algorithm: 'PaddleOCR-VL',
},
});
@@ -88,6 +92,14 @@ const PaddleOCRModal = ({
className="space-y-6"
id="paddleocr-form"
>
<RAGFlowFormItem
name="instance_name"
label={t('setting.instanceName')}
tooltip={t('setting.instanceNameTip')}
required
>
<Input placeholder={t('setting.instanceNameMessage')} />
</RAGFlowFormItem>
<RAGFlowFormItem
name="llm_name"
label={t('setting.modelName')}

View File

@@ -8,10 +8,13 @@ import { Modal } from '@/components/ui/modal/modal';
import { useCommonTranslation, useTranslate } from '@/hooks/common-hooks';
import { useBuildModelTypeOptions } from '@/hooks/logic-hooks/use-build-options';
import { IModalProps } from '@/interfaces/common';
import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { VerifyResult } from '@/pages/user-setting/setting-model/hooks';
import omit from 'lodash/omit';
import { memo, useCallback, useRef } from 'react';
import { IAddProviderInstanceRequestBody } from '@/interfaces/request/llm';
import {
useFetchInstanceNameSet,
useHideWhenInstanceExists,
VerifyResult,
} from '@/pages/user-setting/setting-model/hooks';
import { memo, useCallback, useMemo, useRef } from 'react';
import { FieldValues } from 'react-hook-form';
import { LLMHeader } from '../../components/llm-header';
import VerifyButton from '../../modal/verify-button';
@@ -23,7 +26,7 @@ const SparkModal = ({
onVerify,
loading,
llmFactory,
}: IModalProps<IAddLlmRequestBody> & {
}: IModalProps<IAddProviderInstanceRequestBody> & {
llmFactory: string;
onVerify?: (
postBody: any,
@@ -33,120 +36,140 @@ const SparkModal = ({
const { t: tc } = useCommonTranslation();
const { buildModelTypeOptions } = useBuildModelTypeOptions();
const formRef = useRef<DynamicFormRef>(null);
const fields: FormFieldConfig[] = [
{
name: 'model_type',
label: t('modelType'),
type: FormFieldType.Select,
required: true,
options: buildModelTypeOptions(['chat', 'tts']),
defaultValue: 'chat',
validation: {
message: t('modelTypeMessage'),
const { instanceNameSet } = useFetchInstanceNameSet(llmFactory);
const hideWhenInstanceExists = useHideWhenInstanceExists(instanceNameSet);
const fields: FormFieldConfig[] = useMemo(
() => [
{
name: 'instance_name',
label: t('instanceName'),
type: FormFieldType.Text,
required: true,
placeholder: t('instanceNameMessage'),
tooltip: t('instanceNameTip'),
validation: { message: t('instanceNameMessage') },
},
},
{
name: 'llm_name',
label: t('modelName'),
type: FormFieldType.Text,
required: true,
placeholder: t('modelNameMessage'),
validation: {
message: t('SparkModelNameMessage'),
{
name: 'model_type',
label: t('modelType'),
type: FormFieldType.MultiSelect,
required: true,
options: buildModelTypeOptions(['chat', 'tts']),
defaultValue: ['chat'],
},
},
{
name: 'spark_api_password',
label: t('addSparkAPIPassword'),
type: FormFieldType.Text,
required: true,
placeholder: t('SparkAPIPasswordMessage'),
validation: {
message: t('SparkAPIPasswordMessage'),
{
name: 'llm_name',
label: t('modelName'),
type: FormFieldType.Text,
required: true,
placeholder: t('modelNameMessage'),
validation: {
message: t('SparkModelNameMessage'),
},
},
},
{
name: 'spark_app_id',
label: t('addSparkAPPID'),
type: FormFieldType.Text,
required: true,
placeholder: t('SparkAPPIDMessage'),
validation: {
message: t('SparkAPPIDMessage'),
{
name: 'spark_api_password',
label: t('addSparkAPIPassword'),
type: FormFieldType.Text,
required: true,
placeholder: t('SparkAPIPasswordMessage'),
validation: {
message: t('SparkAPIPasswordMessage'),
},
shouldRender: hideWhenInstanceExists,
},
dependencies: ['model_type'],
shouldRender: (formValues: any) => {
return formValues?.model_type === 'tts';
{
name: 'spark_app_id',
label: t('addSparkAPPID'),
type: FormFieldType.Text,
required: true,
placeholder: t('SparkAPPIDMessage'),
validation: {
message: t('SparkAPPIDMessage'),
},
dependencies: ['model_type', 'instance_name'],
shouldRender: (formValues: any) => {
if (!hideWhenInstanceExists(formValues)) return false;
const modelType = formValues?.model_type;
if (Array.isArray(modelType)) {
return modelType.includes('tts');
}
return modelType === 'tts';
},
},
},
{
name: 'spark_api_secret',
label: t('addSparkAPISecret'),
type: FormFieldType.Text,
required: true,
placeholder: t('SparkAPISecretMessage'),
validation: {
message: t('SparkAPISecretMessage'),
{
name: 'spark_api_secret',
label: t('addSparkAPISecret'),
type: FormFieldType.Text,
required: true,
placeholder: t('SparkAPISecretMessage'),
validation: {
message: t('SparkAPISecretMessage'),
},
dependencies: ['model_type', 'instance_name'],
shouldRender: (formValues: any) => {
if (!hideWhenInstanceExists(formValues)) return false;
const modelType = formValues?.model_type;
if (Array.isArray(modelType)) {
return modelType.includes('tts');
}
return modelType === 'tts';
},
},
dependencies: ['model_type'],
shouldRender: (formValues: any) => {
return formValues?.model_type === 'tts';
{
name: 'spark_api_key',
label: t('addSparkAPIKey'),
type: FormFieldType.Text,
required: true,
placeholder: t('SparkAPIKeyMessage'),
validation: {
message: t('SparkAPIKeyMessage'),
},
dependencies: ['model_type', 'instance_name'],
shouldRender: (formValues: any) => {
if (!hideWhenInstanceExists(formValues)) return false;
const modelType = formValues?.model_type;
if (Array.isArray(modelType)) {
return modelType.includes('tts');
}
return modelType === 'tts';
},
},
},
{
name: 'spark_api_key',
label: t('addSparkAPIKey'),
type: FormFieldType.Text,
required: true,
placeholder: t('SparkAPIKeyMessage'),
validation: {
message: t('SparkAPIKeyMessage'),
{
name: 'max_tokens',
label: t('maxTokens'),
type: FormFieldType.Number,
required: true,
placeholder: t('maxTokensTip'),
validation: {
min: 0,
message: t('maxTokensInvalidMessage'),
},
},
dependencies: ['model_type'],
shouldRender: (formValues: any) => {
return formValues?.model_type === 'tts';
},
},
{
name: 'max_tokens',
label: t('maxTokens'),
type: FormFieldType.Number,
required: true,
placeholder: t('maxTokensTip'),
validation: {
min: 0,
message: t('maxTokensInvalidMessage'),
},
},
];
],
[t, buildModelTypeOptions, hideWhenInstanceExists],
);
const handleOk = async (values?: FieldValues) => {
if (!values) return;
const modelType =
values.model_type === 'chat' && values.vision
? 'image2text'
: values.model_type;
const data = {
...omit(values, ['vision']),
model_type: modelType,
instance_name: values.instance_name as string,
model_type: values.model_type,
llm_factory: llmFactory,
max_tokens: values.max_tokens,
};
await onOk?.(data as IAddLlmRequestBody);
await onOk?.(data as IAddProviderInstanceRequestBody);
};
const verifyParamsFunc = useCallback(() => {
const values = formRef.current?.getValues();
const modelType =
values.model_type === 'chat' && values.vision
? 'image2text'
: values.model_type;
return {
llm_factory: llmFactory,
model_type: modelType,
model_type: values.model_type,
};
}, [llmFactory]);
@@ -175,8 +198,9 @@ const SparkModal = ({
ref={formRef}
defaultValues={
{
model_type: 'chat',
vision: false,
instance_name: '',
model_type: ['chat'],
max_tokens: 8192,
} as FieldValues
}
labelClassName="font-normal"

View File

@@ -8,14 +8,18 @@ import { Modal } from '@/components/ui/modal/modal';
import { useCommonTranslation, useTranslate } from '@/hooks/common-hooks';
import { useBuildModelTypeOptions } from '@/hooks/logic-hooks/use-build-options';
import { IModalProps } from '@/interfaces/common';
import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { VerifyResult } from '@/pages/user-setting/setting-model/hooks';
import { memo, useCallback, useRef } from 'react';
import { IAddProviderInstanceRequestBody } from '@/interfaces/request/llm';
import {
useFetchInstanceNameSet,
useHideWhenInstanceExists,
VerifyResult,
} from '@/pages/user-setting/setting-model/hooks';
import { memo, useCallback, useMemo, useRef } from 'react';
import { FieldValues } from 'react-hook-form';
import { LLMHeader } from '../../components/llm-header';
import VerifyButton from '../../modal/verify-button';
type VolcEngineLlmRequest = IAddLlmRequestBody & {
type VolcEngineLlmRequest = IAddProviderInstanceRequestBody & {
endpoint_id: string;
ark_api_key: string;
};
@@ -27,7 +31,7 @@ const VolcEngineModal = ({
onVerify,
loading,
llmFactory,
}: IModalProps<IAddLlmRequestBody> & {
}: IModalProps<IAddProviderInstanceRequestBody> & {
llmFactory: string;
onVerify?: (
postBody: any,
@@ -37,79 +41,87 @@ const VolcEngineModal = ({
const { t: tc } = useCommonTranslation();
const { buildModelTypeOptions } = useBuildModelTypeOptions();
const formRef = useRef<DynamicFormRef>(null);
const fields: FormFieldConfig[] = [
{
name: 'model_type',
label: t('modelType'),
type: FormFieldType.Select,
required: true,
options: buildModelTypeOptions(['chat', 'embedding', 'image2text']),
defaultValue: 'chat',
},
{
name: 'llm_name',
label: t('modelName'),
type: FormFieldType.Text,
required: true,
placeholder: t('volcModelNameMessage'),
},
{
name: 'endpoint_id',
label: t('addEndpointID'),
type: FormFieldType.Text,
required: true,
placeholder: t('endpointIDMessage'),
},
{
name: 'ark_api_key',
label: t('addArkApiKey'),
type: FormFieldType.Text,
required: true,
placeholder: t('ArkApiKeyMessage'),
},
{
name: 'max_tokens',
label: t('maxTokens'),
type: FormFieldType.Number,
required: true,
placeholder: t('maxTokensTip'),
validation: {
min: 0,
const { instanceNameSet } = useFetchInstanceNameSet(llmFactory);
const hideWhenInstanceExists = useHideWhenInstanceExists(instanceNameSet);
const fields: FormFieldConfig[] = useMemo(
() => [
{
name: 'instance_name',
label: t('instanceName'),
type: FormFieldType.Text,
required: true,
placeholder: t('instanceNameMessage'),
tooltip: t('instanceNameTip'),
validation: { message: t('instanceNameMessage') },
},
},
];
{
name: 'model_type',
label: t('modelType'),
type: FormFieldType.MultiSelect,
required: true,
options: buildModelTypeOptions(['chat', 'embedding', 'image2text']),
defaultValue: ['chat'],
},
{
name: 'llm_name',
label: t('modelName'),
type: FormFieldType.Text,
required: true,
placeholder: t('volcModelNameMessage'),
},
{
name: 'endpoint_id',
label: t('addEndpointID'),
type: FormFieldType.Text,
required: true,
placeholder: t('endpointIDMessage'),
shouldRender: hideWhenInstanceExists,
},
{
name: 'ark_api_key',
label: t('addArkApiKey'),
type: FormFieldType.Text,
required: true,
placeholder: t('ArkApiKeyMessage'),
shouldRender: hideWhenInstanceExists,
},
{
name: 'max_tokens',
label: t('maxTokens'),
type: FormFieldType.Number,
required: true,
placeholder: t('maxTokensTip'),
validation: {
min: 0,
},
},
],
[t, buildModelTypeOptions, hideWhenInstanceExists],
);
const handleOk = async (values?: FieldValues) => {
if (!values) return;
const modelType =
values.model_type === 'chat' && values.vision
? 'image2text'
: values.model_type;
const data: VolcEngineLlmRequest = {
instance_name: values.instance_name as string,
llm_factory: llmFactory,
llm_name: values.llm_name as string,
model_type: modelType,
model_type: values.model_type,
endpoint_id: values.endpoint_id as string,
ark_api_key: values.ark_api_key as string,
max_tokens: values.max_tokens as number,
};
console.info(data);
await onOk?.(data);
};
const verifyParamsFunc = useCallback(() => {
const values = formRef.current?.getValues();
const modelType =
values.model_type === 'chat' && values.vision
? 'image2text'
: values.model_type;
return {
llm_factory: llmFactory,
model_type: modelType,
model_type: values.model_type,
};
}, [llmFactory]);
@@ -138,8 +150,9 @@ const VolcEngineModal = ({
ref={formRef}
defaultValues={
{
model_type: 'chat',
vision: false,
instance_name: '',
model_type: ['chat'],
max_tokens: 8192,
} as FieldValues
}
labelClassName="font-normal"

View File

@@ -8,8 +8,12 @@ import { Modal } from '@/components/ui/modal/modal';
import { useCommonTranslation, useTranslate } from '@/hooks/common-hooks';
import { useBuildModelTypeOptions } from '@/hooks/logic-hooks/use-build-options';
import { IModalProps } from '@/interfaces/common';
import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { VerifyResult } from '@/pages/user-setting/setting-model/hooks';
import { IAddProviderInstanceRequestBody } from '@/interfaces/request/llm';
import {
useFetchInstanceNameSet,
useHideWhenInstanceExists,
VerifyResult,
} from '@/pages/user-setting/setting-model/hooks';
import { memo, useCallback, useMemo, useRef } from 'react';
import { FieldValues } from 'react-hook-form';
import { LLMHeader } from '../../components/llm-header';
@@ -22,7 +26,7 @@ const YiyanModal = ({
onVerify,
loading,
llmFactory,
}: IModalProps<IAddLlmRequestBody> & {
}: IModalProps<IAddProviderInstanceRequestBody> & {
llmFactory: string;
onVerify?: (
postBody: any,
@@ -32,16 +36,28 @@ const YiyanModal = ({
const { t: tc } = useCommonTranslation();
const { buildModelTypeOptions } = useBuildModelTypeOptions();
const formRef = useRef<DynamicFormRef>(null);
const { instanceNameSet } = useFetchInstanceNameSet(llmFactory);
const hideWhenInstanceExists = useHideWhenInstanceExists(instanceNameSet);
const fields = useMemo<FormFieldConfig[]>(
() => [
{
name: 'instance_name',
label: t('instanceName'),
type: FormFieldType.Text,
required: true,
placeholder: t('instanceNameMessage'),
tooltip: t('instanceNameTip'),
validation: { message: t('instanceNameMessage') },
},
{
name: 'model_type',
label: t('modelType'),
type: FormFieldType.Select,
type: FormFieldType.MultiSelect,
required: true,
options: buildModelTypeOptions(['chat', 'embedding', 'rerank']),
defaultValue: 'chat',
defaultValue: ['chat'],
},
{
name: 'llm_name',
@@ -56,6 +72,7 @@ const YiyanModal = ({
type: FormFieldType.Text,
required: true,
placeholder: t('yiyanAKMessage'),
shouldRender: hideWhenInstanceExists,
},
{
name: 'yiyan_sk',
@@ -63,6 +80,7 @@ const YiyanModal = ({
type: FormFieldType.Text,
required: true,
placeholder: t('yiyanSKMessage'),
shouldRender: hideWhenInstanceExists,
},
{
name: 'max_tokens',
@@ -75,21 +93,17 @@ const YiyanModal = ({
},
},
],
[t, buildModelTypeOptions],
[t, buildModelTypeOptions, hideWhenInstanceExists],
);
const handleOk = async (values?: FieldValues) => {
if (!values) return;
const modelType =
values.model_type === 'chat' && values.vision
? 'image2text'
: values.model_type;
const data: IAddLlmRequestBody = {
const data: IAddProviderInstanceRequestBody = {
instance_name: values.instance_name as string,
llm_factory: llmFactory,
llm_name: values.llm_name as string,
model_type: modelType,
model_type: values.model_type,
api_key: {
yiyan_ak: values.yiyan_ak,
yiyan_sk: values.yiyan_sk,
@@ -97,21 +111,15 @@ const YiyanModal = ({
max_tokens: values.max_tokens as number,
};
console.info(data);
await onOk?.(data);
};
const verifyParamsFunc = useCallback(() => {
const values = formRef.current?.getValues();
const modelType =
values.model_type === 'chat' && values.vision
? 'image2text'
: values.model_type;
return {
llm_factory: llmFactory,
llm_name: values.llm_name as string,
model_type: modelType,
model_type: values.model_type,
api_key: {
yiyan_ak: values.yiyan_ak,
yiyan_sk: values.yiyan_sk,
@@ -148,8 +156,9 @@ const YiyanModal = ({
}}
defaultValues={
{
model_type: 'chat',
vision: false,
instance_name: '',
model_type: ['chat'],
max_tokens: 8192,
} as FieldValues
}
labelClassName="font-normal"

View File

@@ -0,0 +1,109 @@
import {
IAddInstanceModelRequestBody,
IAddProviderInstanceRequestBody,
} from '@/interfaces/request/llm';
const MODEL_RESERVED_KEYS = new Set([
'llm_name',
'model_name',
'model_type',
'max_tokens',
]);
const INSTANCE_RESERVED_KEYS = new Set([
'instance_name',
'llm_factory',
'provider_name',
'api_base',
'base_url',
'region',
'verify',
]);
export const MODEL_EXTRA_KEYS = new Set([
'is_tools',
'vision',
'provider_order',
'api_version',
]);
export const MODEL_FIELD_NAMES = new Set<string>([
...MODEL_RESERVED_KEYS,
...MODEL_EXTRA_KEYS,
]);
export const isModelField = (fieldName: string) =>
MODEL_FIELD_NAMES.has(fieldName);
type FlatPayload = Record<string, any>;
export type SplitResult = {
instancePayload: Omit<
IAddProviderInstanceRequestBody,
'llm_name' | 'model_type' | 'max_tokens'
> & {
base_url?: string;
region?: string;
};
modelPayload: IAddInstanceModelRequestBody;
};
const collectApiKeyExtras = (payload: FlatPayload) => {
const extras: Record<string, any> = {};
let apiKeyValue: any = undefined;
for (const [key, value] of Object.entries(payload)) {
if (value === undefined) continue;
if (key === 'api_key') {
apiKeyValue = value;
continue;
}
if (INSTANCE_RESERVED_KEYS.has(key)) continue;
if (MODEL_RESERVED_KEYS.has(key)) continue;
if (MODEL_EXTRA_KEYS.has(key)) continue;
extras[key] = value;
}
if (apiKeyValue && typeof apiKeyValue === 'object') {
return { ...apiKeyValue, ...extras };
}
if (Object.keys(extras).length === 0) {
return apiKeyValue ?? '';
}
if (apiKeyValue !== undefined && apiKeyValue !== '') {
return { api_key: apiKeyValue, ...extras };
}
return extras;
};
const collectModelExtras = (payload: FlatPayload) => {
const extras: Record<string, any> = {};
for (const key of MODEL_EXTRA_KEYS) {
if (payload[key] !== undefined && payload[key] !== '') {
extras[key] = payload[key];
}
}
return extras;
};
export const splitProviderPayload = (payload: FlatPayload): SplitResult => {
const instancePayload = {
instance_name: payload.instance_name as string,
llm_factory: payload.llm_factory as string,
api_key: collectApiKeyExtras(payload),
base_url: (payload.base_url ?? payload.api_base) as string | undefined,
region: (payload.region as string | undefined) || 'default',
};
const modelExtra = collectModelExtras(payload);
const modelPayload = {
model_name: (payload.model_name ?? payload.llm_name) as string,
model_type: payload.model_type,
max_tokens: payload.max_tokens as number,
...(Object.keys(modelExtra).length > 0 ? { extra: modelExtra } : {}),
};
return {
instancePayload: instancePayload as SplitResult['instancePayload'],
modelPayload,
};
};

View File

@@ -0,0 +1,71 @@
import api from '@/utils/api';
import { registerNextServer } from '@/utils/register-server';
const {
listAllAddedModels,
defaultModel,
listProviders,
addProvider,
addProviderInstance,
listProviderInstances,
listInstanceModels,
showProviderInstance,
addInstanceModel,
deleteProviderInstance,
updateModelStatus,
} = api;
const methods = {
listAllAddedModels: {
url: listAllAddedModels,
method: 'get',
},
listDefaultModels: {
url: defaultModel,
method: 'get',
},
setDefaultModel: {
url: defaultModel,
method: 'patch',
},
listProviders: {
url: listProviders,
method: 'get',
},
addProvider: {
url: addProvider,
method: 'put',
},
addProviderInstance: {
url: addProviderInstance,
method: 'post',
},
listProviderInstances: {
url: listProviderInstances,
method: 'get',
},
listInstanceModels: {
url: listInstanceModels,
method: 'get',
},
showProviderInstance: {
url: showProviderInstance,
method: 'get',
},
addInstanceModel: {
url: addInstanceModel,
method: 'post',
},
deleteProviderInstance: {
url: deleteProviderInstance,
method: 'delete',
},
updateModelStatus: {
url: updateModelStatus,
method: 'patch',
},
} as const;
const llmService = registerNextServer<keyof typeof methods>(methods);
export default llmService;

View File

@@ -9,15 +9,6 @@ const {
setting,
userInfo,
tenantInfo,
factoriesList,
llmList,
myLlm,
setApiKey,
setTenantInfo,
addLlm,
deleteLlm,
enableLlm,
deleteFactory,
getSystemVersion,
getSystemTokenList,
removeSystemToken,
@@ -51,46 +42,10 @@ const methods = {
url: tenantInfo,
method: 'get',
},
setTenantInfo: {
url: setTenantInfo,
method: 'patch',
},
factoriesList: {
url: factoriesList,
method: 'get',
},
llmList: {
url: llmList,
method: 'get',
},
myLlm: {
url: myLlm,
method: 'get',
},
setApiKey: {
url: setApiKey,
method: 'post',
},
addLlm: {
url: addLlm,
method: 'post',
},
deleteLlm: {
url: deleteLlm,
method: 'post',
},
enableLlm: {
url: enableLlm,
method: 'post',
},
getSystemVersion: {
url: getSystemVersion,
method: 'get',
},
deleteFactory: {
url: deleteFactory,
method: 'post',
},
listToken: {
url: getSystemTokenList,
method: 'get',

View File

@@ -11,7 +11,6 @@ export default {
setting: `${restAPIv1}/users/me`,
userInfo: `${restAPIv1}/users/me`,
tenantInfo: `${restAPIv1}/users/me/models`,
setTenantInfo: `${restAPIv1}/users/me/models`,
loginChannels: `${restAPIv1}/auth/login/channels`,
loginChannel: (channel: string) => `${restAPIv1}/auth/login/${channel}`,
@@ -25,14 +24,49 @@ export default {
agreeTenant: (tenantId: string) => `${restAPIv1}/tenants/${tenantId}`,
// llm model
factoriesList: `${webAPI}/llm/factories`,
llmList: `${webAPI}/llm/list`,
myLlm: `${webAPI}/llm/my_llms`,
setApiKey: `${webAPI}/llm/set_api_key`,
addLlm: `${webAPI}/llm/add_llm`,
deleteLlm: `${webAPI}/llm/delete_llm`,
enableLlm: `${webAPI}/llm/enable_llm`,
deleteFactory: `${webAPI}/llm/delete_factory`,
listAllAddedModels: `${restAPIv1}/models`,
defaultModel: `${restAPIv1}/models/default`,
listProviders: `${restAPIv1}/providers`,
addProvider: `${restAPIv1}/providers/`,
addProviderInstance: ({ llm_factory }: { llm_factory: string }) =>
`${restAPIv1}/providers/${llm_factory}/instances`,
listProviderInstances: ({ provider_name }: { provider_name: string }) =>
`${restAPIv1}/providers/${provider_name}/instances`,
listInstanceModels: ({
provider_name,
instance_name,
}: {
provider_name: string;
instance_name: string;
}) =>
`${restAPIv1}/providers/${provider_name}/instances/${instance_name}/models`,
showProviderInstance: ({
provider_name,
instance_name,
}: {
provider_name: string;
instance_name: string;
}) => `${restAPIv1}/providers/${provider_name}/instances/${instance_name}`,
addInstanceModel: ({
provider_name,
instance_name,
}: {
provider_name: string;
instance_name: string;
}) =>
`${restAPIv1}/providers/${provider_name}/instances/${instance_name}/models`,
deleteProviderInstance: ({ provider_name }: { provider_name: string }) =>
`${restAPIv1}/providers/${provider_name}/instances`,
updateModelStatus: ({
provider_name,
instance_name,
model_name,
}: {
provider_name: string;
instance_name: string;
model_name: string;
}) =>
`${restAPIv1}/providers/${provider_name}/instances/${instance_name}/models/${model_name}`,
// data source
dataSourceUpdate: (id: string) => `${restAPIv1}/connectors/${id}`,

View File

@@ -1,29 +1,10 @@
import { IThirdOAIModel } from '@/interfaces/database/llm';
import { getCachedLlmList } from './llm-cache';
export const getLLMIconName = (fid: string, llm_name: string) => {
if (fid === 'FastEmbed') {
return llm_name.split('/').at(0) ?? '';
}
return fid;
};
export const getLlmNameAndFIdByLlmId = (llmId?: string) => {
const [llmName, fId] = llmId?.split('@') || [];
return { fId, llmName };
};
// The names of the large models returned by the interface are similar to "deepseek-r1___OpenAI-API"
export function getRealModelName(llmName: string) {
return llmName.split('__').at(0) ?? '';
}
export function buildLlmUuid(llm: IThirdOAIModel) {
return `${llm.llm_name}@${llm.fid}`;
}
// Get tenant model ID from LLM list by model name and factory ID
export function getTenantModelId(
llmList: Record<string, any>,
@@ -53,12 +34,37 @@ export function getTenantModelId(
return '';
}
// Extract model name and factory ID from a model UUID (e.g., "model_name@factory_id")
/** Build "modelName@instanceName@providerName" */
export function buildModelValue(model: {
model_name: string;
model_instance: string;
model_provider: string;
}) {
return `${model.model_name}@${model.model_instance}@${model.model_provider}`;
}
/** Parse "modelName@instanceName@providerName" */
export function parseModelValue(val: string) {
if (!val) return null;
const firstAt = val.indexOf('@');
const lastAt = val.lastIndexOf('@');
if (firstAt === -1 || firstAt === lastAt) return null;
return {
model_name: val.substring(0, firstAt),
model_instance: val.substring(firstAt + 1, lastAt),
model_provider: val.substring(lastAt + 1),
};
}
// Extract model name and factory ID from a model UUID
// Supports both "model_name@factory_id" and "model_name@factory_id#instance_name"
export function parseModelUuid(uuid: string): {
modelName: string;
factoryId: string;
} {
const [modelName, factoryId] = uuid.split('@');
const hashIndex = uuid.indexOf('#');
const core = hashIndex === -1 ? uuid : uuid.slice(0, hashIndex);
const [modelName, factoryId] = core.split('@');
return { modelName, factoryId };
}

View File

@@ -148,8 +148,6 @@ request.interceptors.response.use(
return response;
},
function (error) {
console.log('🚀 ~ error:', error);
// Handle HTTP 401 (token expired / invalid)
const status = error?.response?.status;
if (status === 401) {