fix: support provider-specific verify payload for per-model verify (#17522)

This commit is contained in:
chanx
2026-07-29 16:20:31 +08:00
committed by GitHub
parent 06562cb03d
commit b0bbf9fa7f
8 changed files with 107 additions and 10 deletions

View File

@@ -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;
}}

View File

@@ -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;
}}

View File

@@ -542,7 +542,6 @@ export function useInstanceSaveState({
}, [
isDraft,
providerName,
instanceName,
instanceId,
instanceDetails?.id,
formRef,

View File

@@ -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<string, any>) => {
apiKey: string | object | Record<string, any>;
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<IModelInfo[]>;
draftName: string;
setDraftName: (name: string) => void;
/** Provider-specific transform forwarded to ModelsSection for per-model verify. */
verifyTransform?: VerifyTransform;
}

View File

@@ -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<string, any>;
/**
* 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<string, any>) => {
apiKey: string | object | Record<string, any>;
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<Record<string, VerifyStatus>>({});
@@ -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) => ({

View File

@@ -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.

View File

@@ -54,6 +54,25 @@ export interface ModelsSectionProps {
* blur-save persists them to the backend).
*/
getFormValues?: () => Record<string, any>;
/**
* 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<string, any>) => {
apiKey: string | object | Record<string, any>;
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

View File

@@ -229,6 +229,7 @@ const GenericProviderInstanceCard = forwardRef<
modelInfoRef={modelInfoRef}
draftName={draftName}
setDraftName={setDraftName}
verifyTransform={providerConfig.verifyTransform}
/>
) : (
<SavedModeCard
@@ -248,6 +249,7 @@ const GenericProviderInstanceCard = forwardRef<
draftName={draftName}
open={open}
setOpen={setOpen}
verifyTransform={providerConfig.verifyTransform}
/>
)}
</div>