diff --git a/web/src/pages/user-setting/setting-model/components/used-model.tsx b/web/src/pages/user-setting/setting-model/components/used-model.tsx
index 23d2f21d6a..fbc13ac1a6 100644
--- a/web/src/pages/user-setting/setting-model/components/used-model.tsx
+++ b/web/src/pages/user-setting/setting-model/components/used-model.tsx
@@ -74,6 +74,9 @@ function ProviderCard({
) => void;
}) {
const { data: instances } = useFetchProviderInstances(provider.name);
+ if (!instances || instances.length <= 0) {
+ return null;
+ }
return (
diff --git a/web/src/pages/user-setting/setting-model/modal/provider-modal/components/use-custom-model-fields.tsx b/web/src/pages/user-setting/setting-model/modal/provider-modal/components/use-custom-model-fields.tsx
index 01b1d05b69..e96833c192 100644
--- a/web/src/pages/user-setting/setting-model/modal/provider-modal/components/use-custom-model-fields.tsx
+++ b/web/src/pages/user-setting/setting-model/modal/provider-modal/components/use-custom-model-fields.tsx
@@ -1,127 +1,73 @@
import { useTranslate } from '@/hooks/common-hooks';
-import { IProviderModelItem } from '@/interfaces/request/llm';
import { useMemo } from 'react';
import type { AddCustomModelDialogFields } from './add-custom-model-dialog';
-/**
- * Allowed values for `IProviderModelItem['model_types']`. Kept in sync
- * with the backend model registry; `LabelKey` matches the i18n namespace
- * the descriptor below uses for each option.
- */
-const MODEL_TYPE_VALUES = [
- 'chat',
- 'embedding',
- 'rerank',
- 'tts',
- 'image2text',
- 'speech2text',
-] as const;
-
-/**
- * Allowed values for `IProviderModelItem['features']`.
- */
-const FEATURE_VALUES = ['tool_call'] as const;
-
-/**
- * Descriptor for a single IProviderModelItem property. Each descriptor
- * tells the dialog how to render the form input for that property.
- *
- * - `type` : the form input type
- * - `required` : whether the field must be non-empty
- * - `min` : for `number` types, the minimum allowed value
- * - `options` : for select-style types, the allowed value set
- * - `labelKey` : i18n key for the field label
- * - `optionLabelKey` : i18n key prefix for each option's label
- */
-type ModelFieldDescriptor = {
- type: AddCustomModelDialogFields['type'];
- required: boolean;
- min?: number;
- options?: readonly string[];
- labelKey: string;
- optionLabelKey?: (value: string) => string;
-};
-
/**
* Single source of truth for the custom-model dialog schema. Mirrors
* the shape of `IProviderModelItem` 1:1 — adding a new property to the
* interface means adding an entry here, and the dialog auto-adapts.
+ *
+ * `label` and each option's `label` are i18n keys (under the `setting`
+ * namespace). `useCustomModelFields` resolves them via `t()`.
*/
-const MODEL_FIELD_SCHEMA: Record<
- keyof IProviderModelItem,
- ModelFieldDescriptor
-> = {
- name: {
+export const MODEL_FIELD_SCHEMA: AddCustomModelDialogFields[] = [
+ {
+ name: 'name',
+ label: 'modelName',
type: 'text',
required: true,
- labelKey: 'modelName',
+ defaultValue: '',
},
- model_types: {
+ {
+ name: 'model_types',
+ label: 'modelType',
type: 'multi-select',
required: false,
- options: MODEL_TYPE_VALUES,
- labelKey: 'modelType',
- optionLabelKey: (v) => `setting.modelTypes.${v}`,
+ defaultValue: [],
+ options: [
+ { value: 'chat', label: 'modelTypes.chat' },
+ { value: 'embedding', label: 'modelTypes.embedding' },
+ { value: 'rerank', label: 'modelTypes.rerank' },
+ { value: 'tts', label: 'modelTypes.tts' },
+ { value: 'image2text', label: 'modelTypes.image2text' },
+ { value: 'speech2text', label: 'modelTypes.speech2text' },
+ ],
},
- max_tokens: {
+ {
+ name: 'max_tokens',
+ label: 'modelMaxTokens',
type: 'number',
required: false,
min: 0,
- labelKey: 'modelMaxTokens',
+ defaultValue: 0,
},
- features: {
+ {
+ name: 'features',
+ label: 'modelFeatures',
type: 'switch-group',
required: false,
- options: FEATURE_VALUES,
- labelKey: 'modelFeatures',
- optionLabelKey: (v) =>
- v === 'tool_call'
- ? 'modelFeatureToolCall'
- : v === 'function_call'
- ? 'modelFeatureFunctionCall'
- : v,
+ defaultValue: [],
+ options: [{ value: 'is_tools', label: 'modelFeatureToolCall' }],
},
-};
+];
/**
- * Dialog field schema for adding a custom model. Derived from
- * `IProviderModelItem` via `MODEL_FIELD_SCHEMA`, so the form is in
- * lockstep with the model interface.
+ * Dialog field schema for adding a custom model. Returns
+ * `MODEL_FIELD_SCHEMA` with i18n keys resolved.
*/
export const useCustomModelFields = (): AddCustomModelDialogFields[] => {
const { t } = useTranslate('setting');
- return useMemo
(() => {
- return (
- Object.entries(MODEL_FIELD_SCHEMA) as Array<
- [keyof IProviderModelItem, ModelFieldDescriptor]
- >
- ).map(([prop, desc]) => {
- const defaultValue =
- desc.type === 'number'
- ? 0
- : desc.type === 'multi-select' || desc.type === 'switch-group'
- ? []
- : '';
-
- return {
- name: String(prop),
- label: t(desc.labelKey),
- type: desc.type,
- required: desc.required,
- defaultValue,
- ...(desc.min !== undefined ? { min: desc.min } : {}),
- ...(desc.options
- ? {
- options: desc.options.map((value) => ({
- value,
- label: t(
- desc.optionLabelKey ? desc.optionLabelKey(value) : value,
- ),
- })),
- }
- : {}),
- };
- });
- }, [t]);
+ return useMemo(
+ () =>
+ MODEL_FIELD_SCHEMA.map((field) => ({
+ ...field,
+ label: t(field.label),
+ options: field.options?.map((opt) => ({
+ value: opt.value,
+ label: t(opt.label),
+ })),
+ })),
+ [t],
+ );
};
diff --git a/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-list-models-picker.ts b/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-list-models-picker.ts
index 15f5d2ca9f..004c7c16b0 100644
--- a/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-list-models-picker.ts
+++ b/web/src/pages/user-setting/setting-model/modal/provider-modal/hooks/use-list-models-picker.ts
@@ -18,7 +18,7 @@ const getIsToolsFromFeatures = (
features: IProviderModelItem['features'],
): boolean | undefined => {
if (!Array.isArray(features)) return undefined;
- return features.includes('tool_call') || features.includes('function_call');
+ return features.includes('is_tools');
};
// Map a fetched list-model item to the request-side IModelInfo shape.
diff --git a/web/src/pages/user-setting/setting-model/modal/provider-modal/index.tsx b/web/src/pages/user-setting/setting-model/modal/provider-modal/index.tsx
index dacb400f83..53f5cb3ea9 100644
--- a/web/src/pages/user-setting/setting-model/modal/provider-modal/index.tsx
+++ b/web/src/pages/user-setting/setting-model/modal/provider-modal/index.tsx
@@ -42,6 +42,7 @@ const ProviderModal = ({
const { instanceNameSet } = useFetchInstanceNameSet(llmFactory);
const hideWhenInstanceExists = useHideWhenInstanceExists(instanceNameSet);
const [verifyResult, setVerifyResult] = useState(null);
+ const scrollAnchorRef = useRef(null);
useEffect(() => {
setVerifyResult(null);
return () => {
@@ -49,6 +50,26 @@ const ProviderModal = ({
};
}, [visible]);
+ // When a verify result comes back, the VerifyButton renders new log
+ // content below the existing form. Scroll the modal's scrollable area
+ // to the bottom so the user actually sees the result. We walk up the
+ // DOM from a ref inside the scrollable container (the Modal renders
+ // it via a Radix Portal) and use rAF to wait for the new content to
+ // be laid out before measuring scrollHeight.
+ useEffect(() => {
+ if (!verifyResult || !scrollAnchorRef.current) {
+ return;
+ }
+ const scrollContainer =
+ scrollAnchorRef.current.closest('.overflow-y-auto');
+ if (!scrollContainer) {
+ return;
+ }
+ requestAnimationFrame(() => {
+ scrollContainer.scrollTop = scrollContainer.scrollHeight;
+ });
+ }, [verifyResult]);
+
// Field config, default values, doc link, and the LIST_MODEL_PROVIDERS
// flag are all derived from the current llmFactory / mode / initialValues.
// `baseUrlRegionMaps` is forwarded to the actions hook so the modal can
@@ -123,9 +144,7 @@ const ProviderModal = ({
max_tokens: item.max_tokens,
extra: item.features
? {
- is_tools:
- item.features.includes('tool_call') ||
- item.features.includes('function_call'),
+ is_tools: item.features.includes('is_tools'),
}
: undefined,
});
@@ -203,12 +222,18 @@ const ProviderModal = ({
/>
)}
- {
- setVerifyResult(result);
- }}
- />
+
+ {
+ setVerifyResult(result);
+ }}
+ className={cn({
+ '!flex flex-col ![position:inherit] ':
+ verifyResult && docLinkText && config.docLink,
+ })}
+ />
+