From b0bbf9fa7f9986ba0b8917e2ad6b8b469b178f52 Mon Sep 17 00:00:00 2001 From: chanx <1243304602@qq.com> Date: Wed, 29 Jul 2026 16:20:31 +0800 Subject: [PATCH] fix: support provider-specific verify payload for per-model verify (#17522) --- .../components/draft-mode-card.tsx | 2 + .../components/saved-mode-card.tsx | 2 + .../setting-model/instance-card/hooks.tsx | 1 - .../setting-model/instance-card/interface.ts | 17 +++++ .../instance-card/models-section/hooks.ts | 71 ++++++++++++++++--- .../instance-card/models-section/index.tsx | 3 + .../instance-card/models-section/interface.ts | 19 +++++ .../instance-card/provider-instance-card.tsx | 2 + 8 files changed, 107 insertions(+), 10 deletions(-) diff --git a/web/src/pages/user-setting/setting-model/instance-card/components/draft-mode-card.tsx b/web/src/pages/user-setting/setting-model/instance-card/components/draft-mode-card.tsx index 3cc43f5ee0..5ad71ffbda 100644 --- a/web/src/pages/user-setting/setting-model/instance-card/components/draft-mode-card.tsx +++ b/web/src/pages/user-setting/setting-model/instance-card/components/draft-mode-card.tsx @@ -46,6 +46,7 @@ export function DraftModeCard({ modelInfoRef, draftName, setDraftName, + verifyTransform, }: DraftModeCardProps) { // On success, fold the OAuth-issued key into the current form values so it // lands in the (editable) api_key field without clobbering other inputs. @@ -94,6 +95,7 @@ export function DraftModeCard({ hideActions={false} hideIfEmpty={false} getFormValues={() => formRef.current?.getValues?.() ?? {}} + verifyTransform={verifyTransform} onInstanceModelsChange={(info) => { modelInfoRef.current = info; }} diff --git a/web/src/pages/user-setting/setting-model/instance-card/components/saved-mode-card.tsx b/web/src/pages/user-setting/setting-model/instance-card/components/saved-mode-card.tsx index ec11527c5b..cc4cac0e01 100644 --- a/web/src/pages/user-setting/setting-model/instance-card/components/saved-mode-card.tsx +++ b/web/src/pages/user-setting/setting-model/instance-card/components/saved-mode-card.tsx @@ -65,6 +65,7 @@ export function SavedModeCard({ draftName, open, setOpen, + verifyTransform, }: SavedModeCardProps) { const { t } = useTranslation(); const { t: tSetting } = useTranslate('setting'); @@ -215,6 +216,7 @@ export function SavedModeCard({ hideIfEmpty={false} instanceDetailsLoaded={instanceDetailsLoaded} getFormValues={() => formRef.current?.getValues?.() ?? {}} + verifyTransform={verifyTransform} onInstanceModelsChange={(info) => { modelInfoRef.current = info; }} diff --git a/web/src/pages/user-setting/setting-model/instance-card/hooks.tsx b/web/src/pages/user-setting/setting-model/instance-card/hooks.tsx index 36f0f89604..71ff157192 100644 --- a/web/src/pages/user-setting/setting-model/instance-card/hooks.tsx +++ b/web/src/pages/user-setting/setting-model/instance-card/hooks.tsx @@ -542,7 +542,6 @@ export function useInstanceSaveState({ }, [ isDraft, providerName, - instanceName, instanceId, instanceDetails?.id, formRef, diff --git a/web/src/pages/user-setting/setting-model/instance-card/interface.ts b/web/src/pages/user-setting/setting-model/instance-card/interface.ts index b1aa2f41d3..2d65feac47 100644 --- a/web/src/pages/user-setting/setting-model/instance-card/interface.ts +++ b/web/src/pages/user-setting/setting-model/instance-card/interface.ts @@ -19,6 +19,19 @@ import { IProviderInstance } from '@/interfaces/database/llm'; import { IModelInfo } from '@/interfaces/request/llm'; import { RefObject } from 'react'; +/** + * Provider-specific transform that maps form values onto the verify + * payload's `api_key` / `base_url` / `region`. Shared by the saved and + * draft mode cards so per-model verify can forward structured + * credentials (e.g. PaddleOCR's nested config) the backend expects. + */ +export type VerifyTransform = (values: Record) => { + apiKey: string | object | Record; + baseUrl?: string; + region?: string; + modelInfo?: IModelInfo[]; +}; + /** * Imperative save API exposed by each instance card (generic, Bedrock, * SoMark) so the parent page can drive a single batch save from the @@ -152,6 +165,8 @@ export interface SavedModeCardProps { draftName: string; open: boolean; setOpen: (open: boolean) => void; + /** Provider-specific transform forwarded to ModelsSection for per-model verify. */ + verifyTransform?: VerifyTransform; } /** Props for the draft-mode card (instance name + form fields). */ @@ -168,4 +183,6 @@ export interface DraftModeCardProps { modelInfoRef: React.MutableRefObject; draftName: string; setDraftName: (name: string) => void; + /** Provider-specific transform forwarded to ModelsSection for per-model verify. */ + verifyTransform?: VerifyTransform; } diff --git a/web/src/pages/user-setting/setting-model/instance-card/models-section/hooks.ts b/web/src/pages/user-setting/setting-model/instance-card/models-section/hooks.ts index 1b837d3e47..891f67a33d 100644 --- a/web/src/pages/user-setting/setting-model/instance-card/models-section/hooks.ts +++ b/web/src/pages/user-setting/setting-model/instance-card/models-section/hooks.ts @@ -391,6 +391,26 @@ interface UseModelVerifyArgs { resolveCreds: () => ResolvedCreds; instanceModels: IInstanceModel[] | undefined; instance?: IProviderInstance; + /** + * Host card's current form values. Required when `verifyTransform` is + * supplied so the transform can map provider-specific field names + * (e.g. PaddleOCR's `paddleocr_api_url`) onto the structured `api_key` + * object the verify endpoint expects. + */ + getFormValues?: () => Record; + /** + * Provider-specific transform that maps form values onto the verify + * payload's `api_key` / `base_url` / `region`. When present it takes + * precedence over the generic `resolveCreds()` mapping. The + * `modelInfo` it returns is ignored - per-model verify always sends + * only the single model being verified. + */ + verifyTransform?: (values: Record) => { + apiKey: string | object | Record; + baseUrl?: string; + region?: string; + modelInfo?: IModelInfo[]; + }; } export function useModelVerify({ @@ -398,6 +418,8 @@ export function useModelVerify({ resolveCreds, instanceModels, instance, + getFormValues, + verifyTransform, }: UseModelVerifyArgs) { const { verifyProviderConnection } = useVerifyProviderConnection(); const [verify, setVerify] = useState>({}); @@ -426,18 +448,49 @@ export function useModelVerify({ const handleVerify = async (model: IProviderModelItem) => { setVerify((prev) => ({ ...prev, [model.name]: 'loading' })); try { - const { apiKey, baseUrl } = resolveCreds(); + // Per-model verify always sends only the model being verified - + // even when `verifyTransform` returns a `modelInfo` array, it is + // intentionally overridden here. + const modelInfo: IModelInfo[] = [ + { + model_name: model.name, + model_type: model.model_types ?? [], + max_tokens: model.max_tokens ?? 0, + }, + ]; + + let apiKey: string | object; + let baseUrl: string | undefined; + let region: string | undefined; + + if (verifyTransform) { + // Provider-specific field mapping (e.g. PaddleOCR's nested + // `paddleocr_api_url` / `paddleocr_access_token` / + // `paddleocr_algorithm` -> structured `api_key` object). Run the + // host card's current form values through the transform so the + // user can verify with values they are still editing. + const formValues = getFormValues?.() ?? {}; + const transformed = verifyTransform(formValues); + apiKey = transformed.apiKey; + baseUrl = transformed.baseUrl; + region = transformed.region; + } else { + const creds = resolveCreds(); + apiKey = creds.apiKey; + baseUrl = creds.baseUrl; + } + + // `api_key` is typed `string` on the service signature, but + // providers with a `verifyTransform` may legitimately produce an + // object (e.g. PaddleOCR's nested config). The backend accepts + // both shapes, so cast to `any` to match the existing card-level + // verify path in `useVerifyProvider`. const ret = await verifyProviderConnection({ provider_name: providerName, - api_key: apiKey, + api_key: apiKey as any, base_url: baseUrl, - model_info: [ - { - model_name: model.name, - model_type: model.model_types ?? [], - max_tokens: model.max_tokens ?? 0, - }, - ], + model_info: modelInfo, + ...(region ? { region } : {}), ...(instance?.id ? { instance_id: instance.id } : {}), }); setVerify((prev) => ({ diff --git a/web/src/pages/user-setting/setting-model/instance-card/models-section/index.tsx b/web/src/pages/user-setting/setting-model/instance-card/models-section/index.tsx index a8c41c1dee..202ba696bf 100644 --- a/web/src/pages/user-setting/setting-model/instance-card/models-section/index.tsx +++ b/web/src/pages/user-setting/setting-model/instance-card/models-section/index.tsx @@ -50,6 +50,7 @@ export function ModelsSection(props: ModelsSectionProps) { hideActions = false, hideIfEmpty = false, getFormValues, + verifyTransform, onBlurSuppressChange, onInstanceModelsChange, onInstanceModelsEdited, @@ -154,6 +155,8 @@ export function ModelsSection(props: ModelsSectionProps) { resolveCreds, instanceModels, instance, + getFormValues, + verifyTransform, }); // 7. Add / remove / batch toggle / custom add. diff --git a/web/src/pages/user-setting/setting-model/instance-card/models-section/interface.ts b/web/src/pages/user-setting/setting-model/instance-card/models-section/interface.ts index f97ce0c2ea..d7327e7941 100644 --- a/web/src/pages/user-setting/setting-model/instance-card/models-section/interface.ts +++ b/web/src/pages/user-setting/setting-model/instance-card/models-section/interface.ts @@ -54,6 +54,25 @@ export interface ModelsSectionProps { * blur-save persists them to the backend). */ getFormValues?: () => Record; + /** + * Optional provider-specific transform used to build the verify + * payload's `api_key` / `base_url` / `region` from the host card's + * current form values. Providers whose credential field names don't + * map directly onto `api_key` / `base_url` (e.g. PaddleOCR's nested + * `paddleocr_api_url` / `paddleocr_access_token` / `paddleocr_algorithm`) + * supply this so the per-model verify can forward the structured + * `api_key` object the backend expects. When absent the generic + * `values.api_key` / `values.base_url` mapping is used. + * + * `modelInfo` returned by the transform is ignored for per-model + * verify - the single model being verified always overrides it. + */ + verifyTransform?: (values: Record) => { + apiKey: string | object | Record; + baseUrl?: string; + region?: string; + modelInfo?: IModelInfo[]; + }; /** * Notifies the host that ModelsSection has opened (or closed) a modal * dialog whose contents live in a React Portal outside the host's diff --git a/web/src/pages/user-setting/setting-model/instance-card/provider-instance-card.tsx b/web/src/pages/user-setting/setting-model/instance-card/provider-instance-card.tsx index 8df788c869..ab8a145cc3 100644 --- a/web/src/pages/user-setting/setting-model/instance-card/provider-instance-card.tsx +++ b/web/src/pages/user-setting/setting-model/instance-card/provider-instance-card.tsx @@ -229,6 +229,7 @@ const GenericProviderInstanceCard = forwardRef< modelInfoRef={modelInfoRef} draftName={draftName} setDraftName={setDraftName} + verifyTransform={providerConfig.verifyTransform} /> ) : ( )}