From 8ad533cbdba4ed4a0568b37c55c7beb4f1127278 Mon Sep 17 00:00:00 2001 From: wxg0103 <727495428@qq.com> Date: Wed, 11 Nov 2026 18:01:16 +0800 Subject: [PATCH] refactor: enhance model validation and loading logic in drawers --- .../tests/test_provide_validation.py | 55 +++++++++++++++++++ apps/models_provider/views/provide.py | 47 ++++++++++++++-- .../model/create-model/CreateModelDrawer.vue | 20 +++++-- .../edit-model-action/EditModelDrawer.vue | 20 +++++-- 4 files changed, 129 insertions(+), 13 deletions(-) diff --git a/apps/models_provider/tests/test_provide_validation.py b/apps/models_provider/tests/test_provide_validation.py index 93767153654..07bd4495fae 100644 --- a/apps/models_provider/tests/test_provide_validation.py +++ b/apps/models_provider/tests/test_provide_validation.py @@ -97,3 +97,58 @@ def test_model_list_returns_provider_models(self): api_base=None, credential=None, ) + + def test_model_list_post_requires_provider_credential_fields(self): + provider = MagicMock() + provider.get_model_type_list.return_value = [{"value": "EMBEDDING"}] + provider.get_model_credential.return_value.to_form_list.return_value = [ + {"field": "api_base", "label": "API URL", "required": True}, + {"field": "api_key", "label": "API Key", "required": True}, + ] + request = SimpleNamespace( + data={ + "provider": "test_provider", + "model_type": "EMBEDDING", + "credential": {"api_base": " ", "api_key": ""}, + } + ) + + with patch("models_provider.views.provide.ModelProvideConstants", {"test_provider": provider}): + with self.assertRaises(BadRequest400) as context: + Provide.ModelList().post(request) + + self.assertIn("API URL", str(context.exception.message)) + self.assertIn("API Key", str(context.exception.message)) + provider.get_model_list.assert_not_called() + + def test_model_list_post_queries_with_provider_credential_fields(self): + provider = MagicMock() + provider.get_model_type_list.return_value = [{"value": "EMBEDDING"}] + provider.get_model_credential.return_value.to_form_list.return_value = [ + {"field": "api_base", "label": "API URL", "required": True}, + {"field": "api_key", "label": "API Key", "required": True}, + ] + provider.get_model_list.return_value = [{"name": "text-embedding-3-small"}] + request = SimpleNamespace( + data={ + "provider": "test_provider", + "model_type": "EMBEDDING", + "credential": {"api_base": "https://api.example.com/v1", "api_key": "secret"}, + } + ) + + with ( + patch("models_provider.views.provide.ModelProvideConstants", {"test_provider": provider}), + patch("models_provider.views.provide.cache_base_model_list") as cache_model_list, + ): + response = Provide.ModelList().post(request) + + self.assertEqual(200, response.status_code) + provider.get_model_list.assert_called_once_with( + "EMBEDDING", + api_key=None, + api_base=None, + credential={"api_base": "https://api.example.com/v1", "api_key": "secret"}, + allow_builtin_fallback=False, + ) + cache_model_list.assert_called_once() diff --git a/apps/models_provider/views/provide.py b/apps/models_provider/views/provide.py index 9b0711273ac..9d15e1c5dcf 100644 --- a/apps/models_provider/views/provide.py +++ b/apps/models_provider/views/provide.py @@ -30,6 +30,38 @@ def _get_provider(provider): return ModelProvideConstants[provider] +def _is_empty_credential_value(value): + return value is None or (isinstance(value, str) and not value.strip()) + + +def _validate_model_type(provider_handler, model_type): + if not model_type: + raise BadRequest400(400, _("model_type is required")) + model_type_list = [item.get("value") for item in provider_handler.get_model_type_list()] + if model_type not in model_type_list: + raise BadRequest400(400, _("model_type is invalid")) + + +def _validate_required_credential(provider_handler, model_type, credential): + """按供应商自己的凭证表单校验模型列表查询所需的必填参数。""" + model_credential = provider_handler.get_model_credential(model_type, "") + if model_credential is None: + return + missing_fields = [] + for field in model_credential.to_form_list(): + if not field.get("required") or not _is_empty_credential_value(credential.get(field.get("field"))): + continue + label = field.get("label") + if isinstance(label, dict): + label = label.get("label") + missing_fields.append(str(label or field.get("field"))) + if missing_fields: + raise BadRequest400( + 400, + _("The following fields are required: {keys}").format(keys=", ".join(missing_fields)), + ) + + class Provide(APIView): authentication_classes = [TokenAuth] @@ -116,11 +148,7 @@ def _filter_model_list(model_list, search): @staticmethod def _get_model_list(provider, model_type, api_key, api_base, credential, allow_builtin_fallback=True): provider_handler = _get_provider(provider) - if not model_type: - raise BadRequest400(400, _("model_type is required")) - model_type_list = [item.get("value") for item in provider_handler.get_model_type_list()] - if model_type not in model_type_list: - raise BadRequest400(400, _("model_type is invalid")) + _validate_model_type(provider_handler, model_type) if allow_builtin_fallback: return provider_handler.get_model_list( model_type, api_key=api_key, api_base=api_base, credential=credential @@ -161,6 +189,15 @@ def post(self, request: Request): credential = data.get("credential") search = data.get("search") + provider_handler = _get_provider(provider) + credential = dict(credential) if isinstance(credential, dict) else {} + if api_key and not credential.get("api_key"): + credential["api_key"] = api_key + if api_base and not credential.get("api_base"): + credential["api_base"] = api_base + _validate_model_type(provider_handler, model_type) + _validate_required_credential(provider_handler, model_type, credential) + if not (api_key or credential): # 未提供凭证:直接返回内置列表,无缓存语义。 model_list = self._get_model_list(provider, model_type, api_key, api_base, credential) diff --git a/ui/src/views/model/create-model/CreateModelDrawer.vue b/ui/src/views/model/create-model/CreateModelDrawer.vue index 7b471c87600..a486a34f44c 100644 --- a/ui/src/views/model/create-model/CreateModelDrawer.vue +++ b/ui/src/views/model/create-model/CreateModelDrawer.vue @@ -90,13 +90,23 @@ function applyBaseModelOptions(provider: string, modelType: string, credentialSn JSON.stringify(modelForm.credential) === credentialSnapshot ) { baseModelOptions.value = options + return true } + return false } const baseModelLoading = ref(false) const credentialFields = ref([]) const modelParamsForm = ref([]) -function fetchBaseModelOptions() { +async function fetchBaseModelOptions() { if (!modelForm.provider || !modelForm.model_type) return + const requiredFields = credentialFields.value.filter((field) => field.required !== false).map((field) => field.field) + if (requiredFields.length) { + const valid = await dynamicsFormRef.value?.ruleFormRef?.validateField(requiredFields).then( + () => true, + () => false, + ) + if (!valid) return + } baseModelLoading.value = true const requestProvider = modelForm.provider const requestModelType = modelForm.model_type @@ -109,10 +119,12 @@ function fetchBaseModelOptions() { ProviderApi.getBaseModelList(requestProvider, requestModelType, undefined, modelForm.credential as Record) .then((options) => { if (options?.length) { - applyBaseModelOptions(requestProvider, requestModelType, credentialSnapshot, options) - } else { - return loadBuiltin() + if (applyBaseModelOptions(requestProvider, requestModelType, credentialSnapshot, options)) { + MsgSuccess('获取成功') + } + return } + return loadBuiltin() }) .catch(() => loadBuiltin()) .finally(() => { diff --git a/ui/src/views/model/model-card/action-dropdown/edit-model-action/EditModelDrawer.vue b/ui/src/views/model/model-card/action-dropdown/edit-model-action/EditModelDrawer.vue index 2610240971e..5d3fdb84f37 100644 --- a/ui/src/views/model/model-card/action-dropdown/edit-model-action/EditModelDrawer.vue +++ b/ui/src/views/model/model-card/action-dropdown/edit-model-action/EditModelDrawer.vue @@ -34,7 +34,9 @@ function applyBaseModelOptions(provider: string, modelType: string, credentialSn JSON.stringify(modelForm.credential) === credentialSnapshot ) { baseModelOptions.value = options + return true } + return false } const baseModelLoading = ref(false) const credentialFields = ref([]) @@ -93,8 +95,16 @@ function open(provider: ModelProviderItem, model: ModelItem) { }) } -function fetchBaseModelOptions() { +async function fetchBaseModelOptions() { if (!modelForm.provider || !modelForm.model_type) return + const requiredFields = credentialFields.value.filter((field) => field.required !== false).map((field) => field.field) + if (requiredFields.length) { + const valid = await dynamicsFormRef.value?.ruleFormRef?.validateField(requiredFields).then( + () => true, + () => false, + ) + if (!valid) return + } baseModelLoading.value = true const requestProvider = modelForm.provider const requestModelType = modelForm.model_type @@ -107,10 +117,12 @@ function fetchBaseModelOptions() { ProviderApi.getBaseModelList(requestProvider, requestModelType, undefined, modelForm.credential as Record) .then((options) => { if (options?.length) { - applyBaseModelOptions(requestProvider, requestModelType, requestCredentialSnapshot, options) - } else { - return loadBuiltin() + if (applyBaseModelOptions(requestProvider, requestModelType, requestCredentialSnapshot, options)) { + MsgSuccess('获取成功') + } + return } + return loadBuiltin() }) .catch(() => loadBuiltin()) .finally(() => {