diff --git a/web/src/interfaces/database/llm.ts b/web/src/interfaces/database/llm.ts index 2b7c86eef6..1a6f7dd200 100644 --- a/web/src/interfaces/database/llm.ts +++ b/web/src/interfaces/database/llm.ts @@ -108,6 +108,14 @@ export interface IInstanceModel { * without relying solely on the (possibly unfetched) catalog. */ is_tools?: boolean; + /** + * Per-model extra config persisted in `tenant_model.extra`. + * Carries provider-specific fields such as SoMark's element-format + * selects and feature-config toggles. Echoed back by the backend's + * `_hybrid_get_instance_models` so the frontend can pre-fill the + * edit dialog. + */ + extra?: Record; } export interface IDefaultModel { diff --git a/web/src/interfaces/request/llm.ts b/web/src/interfaces/request/llm.ts index d0d30b5634..615deb31c4 100644 --- a/web/src/interfaces/request/llm.ts +++ b/web/src/interfaces/request/llm.ts @@ -138,6 +138,13 @@ export interface IProviderModelItem { max_tokens: number; model_types: string[]; features: string[] | null; + /** + * Per-model extra config forwarded through `model_info[].extra` + * (e.g. SoMark's element-format / feature-config fields). + * Catalog models typically omit this; it is populated by the + * edit dialog and the `useModelsDerived` echo path. + */ + extra?: Record; } /** diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index a8e9d20fc3..7ad38f99cc 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -2285,6 +2285,17 @@ Example: Virtual Hosted Style`, formulaFormat: 'Formula Format', tableFormat: 'Table Format', csFormat: 'Chemical Structural Formula Format', + formatOptions: { + url: 'URL', + base64: 'Base64', + none: 'None', + latex: 'LaTeX', + mathml: 'MathML', + ascii: 'ASCII', + html: 'HTML', + markdown: 'Markdown', + image: 'Image', + }, sectionFeatureConfig: 'Feature Config', enableInlineImage: 'Enable Inline Image', enableTableImage: 'Enable Table Image', diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index 1ea30be696..4835f95d62 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -1927,6 +1927,17 @@ NER:使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系 formulaFormat: '公式格式', tableFormat: '表格格式', csFormat: '化学结构式格式', + formatOptions: { + url: 'URL', + base64: 'Base64', + none: 'None', + latex: 'LaTeX', + mathml: 'MathML', + ascii: 'ASCII', + html: 'HTML', + markdown: 'Markdown', + image: 'Image', + }, sectionFeatureConfig: '特色功能配置', enableInlineImage: '返回文中图', enableTableImage: '返回表中图', diff --git a/web/src/pages/user-setting/setting-model/hooks.tsx b/web/src/pages/user-setting/setting-model/hooks.tsx index 173baf1335..1ef0167722 100644 --- a/web/src/pages/user-setting/setting-model/hooks.tsx +++ b/web/src/pages/user-setting/setting-model/hooks.tsx @@ -144,9 +144,9 @@ export const useVerifyConnection = () => { }; // ============ Hooks for retained special modals ============ -// Bedrock and SoMark have been migrated to inline instance cards -// (BedrockInstanceCard / SoMarkInstanceCard); these legacy modal -// hooks are kept only for backward-compat references. +// Bedrock has been migrated to an inline instance card +// (BedrockInstanceCard); these legacy modal hooks are kept only +// for backward-compat references. export const useSubmitBedrock = () => { const [saveLoading, setSaveLoading] = useState(false); diff --git a/web/src/pages/user-setting/setting-model/index.tsx b/web/src/pages/user-setting/setting-model/index.tsx index 4408ea05da..2f09bd6117 100644 --- a/web/src/pages/user-setting/setting-model/index.tsx +++ b/web/src/pages/user-setting/setting-model/index.tsx @@ -49,12 +49,13 @@ import SystemSetting from './layout/system-setting'; * Save flow: the top Save button validates every visible card through * the imperative ref API; if all are valid it collects each card's * payload (skipping non-dirty saved cards) and dispatches one API call - * per dirty card - `addProviderInstance` for drafts and Bedrock/SoMark + * per dirty card - `addProviderInstance` for drafts and Bedrock * saved cards, `updateProviderInstance` for generic saved cards. * * Special-case providers (handled inside `ProviderInstanceCard`): * - `Bedrock`: rendered inline via `BedrockInstanceCard`. - * - `SoMark`: rendered inline via `SoMarkInstanceCard`. + * - All other providers (including SoMark) use the generic + * `GenericProviderInstanceCard` path. */ const SettingModelV2: FC = () => { const { t: tSetting } = useTranslate('setting'); @@ -163,10 +164,10 @@ const SettingModelV2: FC = () => { // 3. If any card is invalid (or a draft has no name), abort the // whole batch - errors are surfaced in the form UI by `trigger()`. // 4. Dispatch one API call per dirty card, in order. Drafts and - // Bedrock/SoMark saved cards go through `addProviderInstance` - // (Bedrock/SoMark saved cards carry an `id` so the backend - // updates instead of creating); generic saved cards go through - // `updateProviderInstance`. + // Bedrock saved cards go through `addProviderInstance` + // (Bedrock saved cards carry an `id` so the backend + // updates instead of creating); generic and SoMark saved cards + // go through `updateProviderInstance`. // 5. On success: clear drafts (they're persisted now), mark each // saved card's baseline so the next save short-circuits, and // invalidate the instance query so the new/updated cards appear. diff --git a/web/src/pages/user-setting/setting-model/instance-card/add-custom-model-dialog.tsx b/web/src/pages/user-setting/setting-model/instance-card/add-custom-model-dialog.tsx index 494238b6ee..47c603229b 100644 --- a/web/src/pages/user-setting/setting-model/instance-card/add-custom-model-dialog.tsx +++ b/web/src/pages/user-setting/setting-model/instance-card/add-custom-model-dialog.tsx @@ -44,8 +44,15 @@ export interface AddCustomModelDialogFields { /** Display label */ label: string; /** Form field type */ - type: 'text' | 'number' | 'multi-select' | 'switch-group'; - /** Options for multi-select / switch-group types */ + type: + | 'text' + | 'number' + | 'multi-select' + | 'switch-group' + | 'select' + | 'switch' + | 'section'; + /** Options for multi-select / switch-group / select types */ options?: { label: string; value: string }[]; /** Whether the field is required */ required?: boolean; @@ -74,6 +81,15 @@ interface AddCustomModelDialogProps { loading?: boolean; /** Existing model names for uniqueness validation */ existingNames: string[]; + /** + * Whitelist of feature keys that are provider-specific (e.g. + * SoMark's `somark_enable_*`). On submit, these are moved from the + * `features` array into `extra` as boolean `true` values so each + * provider's payload stays self-describing. Standard features like + * `is_tools` stay in `features`. When omitted, no features are + * moved to `extra`. + */ + providerFeatureKeys?: string[]; /** Initial form values (overrides field-level defaults). Useful for edit mode. */ defaultValues?: Record; } @@ -95,6 +111,7 @@ export const AddCustomModelDialog = ({ loading = false, existingNames, defaultValues, + providerFeatureKeys, }: AddCustomModelDialogProps) => { const { t } = useTranslate('setting'); const { t: commonT } = useTranslate('common'); @@ -109,7 +126,53 @@ export const AddCustomModelDialog = ({ field.type === 'multi-select' || field.type === 'switch-group'; const defaultValue = field.defaultValue ?? - (field.type === 'number' ? 0 : isArrayType ? [] : ''); + (field.type === 'number' + ? 0 + : field.type === 'switch' + ? false + : isArrayType + ? [] + : ''); + + if (field.type === 'section') { + return { + name: `__section_${field.name}`, + label: field.label, + type: FormFieldType.Custom, + hideLabel: true, + schema: z.any().optional(), + render: () => ( +
+ {field.label} +
+ ), + }; + } + + if (field.type === 'switch') { + return { + name: field.name, + label: field.label, + type: FormFieldType.Switch, + required: field.required, + defaultValue: defaultValue ?? false, + disabled: field.disabled, + labelClassName: '!mb-0', + }; + } + + if (field.type === 'select') { + return { + name: field.name, + label: field.label, + type: FormFieldType.Select, + required: field.required, + defaultValue, + disabled: field.disabled, + options: field.options, + placeholder: field.label, + }; + } if (field.type === 'switch-group') { return { @@ -125,14 +188,16 @@ export const AddCustomModelDialog = ({ render: (fieldProps) => { const currentValues = (fieldProps.value as string[]) ?? []; return ( -
- {field.options?.map((opt) => { +
+ {field.options?.map((opt, index) => { const isChecked = currentValues.includes(opt.value); const switchId = `${field.name}-${opt.value}`; return (