Skip to content

Commit bc20faf

Browse files
committed
refactor: enhance model validation and loading logic in drawers
1 parent 1156fe1 commit bc20faf

4 files changed

Lines changed: 129 additions & 13 deletions

File tree

‎apps/models_provider/tests/test_provide_validation.py‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,58 @@ def test_model_list_returns_provider_models(self):
9797
api_base=None,
9898
credential=None,
9999
)
100+
101+
def test_model_list_post_requires_provider_credential_fields(self):
102+
provider = MagicMock()
103+
provider.get_model_type_list.return_value = [{"value": "EMBEDDING"}]
104+
provider.get_model_credential.return_value.to_form_list.return_value = [
105+
{"field": "api_base", "label": "API URL", "required": True},
106+
{"field": "api_key", "label": "API Key", "required": True},
107+
]
108+
request = SimpleNamespace(
109+
data={
110+
"provider": "test_provider",
111+
"model_type": "EMBEDDING",
112+
"credential": {"api_base": " ", "api_key": ""},
113+
}
114+
)
115+
116+
with patch("models_provider.views.provide.ModelProvideConstants", {"test_provider": provider}):
117+
with self.assertRaises(BadRequest400) as context:
118+
Provide.ModelList().post(request)
119+
120+
self.assertIn("API URL", str(context.exception.message))
121+
self.assertIn("API Key", str(context.exception.message))
122+
provider.get_model_list.assert_not_called()
123+
124+
def test_model_list_post_queries_with_provider_credential_fields(self):
125+
provider = MagicMock()
126+
provider.get_model_type_list.return_value = [{"value": "EMBEDDING"}]
127+
provider.get_model_credential.return_value.to_form_list.return_value = [
128+
{"field": "api_base", "label": "API URL", "required": True},
129+
{"field": "api_key", "label": "API Key", "required": True},
130+
]
131+
provider.get_model_list.return_value = [{"name": "text-embedding-3-small"}]
132+
request = SimpleNamespace(
133+
data={
134+
"provider": "test_provider",
135+
"model_type": "EMBEDDING",
136+
"credential": {"api_base": "https://api.example.com/v1", "api_key": "secret"},
137+
}
138+
)
139+
140+
with (
141+
patch("models_provider.views.provide.ModelProvideConstants", {"test_provider": provider}),
142+
patch("models_provider.views.provide.cache_base_model_list") as cache_model_list,
143+
):
144+
response = Provide.ModelList().post(request)
145+
146+
self.assertEqual(200, response.status_code)
147+
provider.get_model_list.assert_called_once_with(
148+
"EMBEDDING",
149+
api_key=None,
150+
api_base=None,
151+
credential={"api_base": "https://api.example.com/v1", "api_key": "secret"},
152+
allow_builtin_fallback=False,
153+
)
154+
cache_model_list.assert_called_once()

‎apps/models_provider/views/provide.py‎

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,38 @@ def _get_provider(provider):
3030
return ModelProvideConstants[provider]
3131

3232

33+
def _is_empty_credential_value(value):
34+
return value is None or (isinstance(value, str) and not value.strip())
35+
36+
37+
def _validate_model_type(provider_handler, model_type):
38+
if not model_type:
39+
raise BadRequest400(400, _("model_type is required"))
40+
model_type_list = [item.get("value") for item in provider_handler.get_model_type_list()]
41+
if model_type not in model_type_list:
42+
raise BadRequest400(400, _("model_type is invalid"))
43+
44+
45+
def _validate_required_credential(provider_handler, model_type, credential):
46+
"""按供应商自己的凭证表单校验模型列表查询所需的必填参数。"""
47+
model_credential = provider_handler.get_model_credential(model_type, "")
48+
if model_credential is None:
49+
return
50+
missing_fields = []
51+
for field in model_credential.to_form_list():
52+
if not field.get("required") or not _is_empty_credential_value(credential.get(field.get("field"))):
53+
continue
54+
label = field.get("label")
55+
if isinstance(label, dict):
56+
label = label.get("label")
57+
missing_fields.append(str(label or field.get("field")))
58+
if missing_fields:
59+
raise BadRequest400(
60+
400,
61+
_("The following fields are required: {keys}").format(keys=", ".join(missing_fields)),
62+
)
63+
64+
3365
class Provide(APIView):
3466
authentication_classes = [TokenAuth]
3567

@@ -116,11 +148,7 @@ def _filter_model_list(model_list, search):
116148
@staticmethod
117149
def _get_model_list(provider, model_type, api_key, api_base, credential, allow_builtin_fallback=True):
118150
provider_handler = _get_provider(provider)
119-
if not model_type:
120-
raise BadRequest400(400, _("model_type is required"))
121-
model_type_list = [item.get("value") for item in provider_handler.get_model_type_list()]
122-
if model_type not in model_type_list:
123-
raise BadRequest400(400, _("model_type is invalid"))
151+
_validate_model_type(provider_handler, model_type)
124152
if allow_builtin_fallback:
125153
return provider_handler.get_model_list(
126154
model_type, api_key=api_key, api_base=api_base, credential=credential
@@ -161,6 +189,15 @@ def post(self, request: Request):
161189
credential = data.get("credential")
162190
search = data.get("search")
163191

192+
provider_handler = _get_provider(provider)
193+
credential = dict(credential) if isinstance(credential, dict) else {}
194+
if api_key and not credential.get("api_key"):
195+
credential["api_key"] = api_key
196+
if api_base and not credential.get("api_base"):
197+
credential["api_base"] = api_base
198+
_validate_model_type(provider_handler, model_type)
199+
_validate_required_credential(provider_handler, model_type, credential)
200+
164201
if not (api_key or credential):
165202
# 未提供凭证:直接返回内置列表,无缓存语义。
166203
model_list = self._get_model_list(provider, model_type, api_key, api_base, credential)

‎ui/src/views/model/create-model/CreateModelDrawer.vue‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,23 @@ function applyBaseModelOptions(provider: string, modelType: string, credentialSn
9090
JSON.stringify(modelForm.credential) === credentialSnapshot
9191
) {
9292
baseModelOptions.value = options
93+
return true
9394
}
95+
return false
9496
}
9597
const baseModelLoading = ref(false)
9698
const credentialFields = ref<FormField[]>([])
9799
const modelParamsForm = ref<DynamicFormField[]>([])
98-
function fetchBaseModelOptions() {
100+
async function fetchBaseModelOptions() {
99101
if (!modelForm.provider || !modelForm.model_type) return
102+
const requiredFields = credentialFields.value.filter((field) => field.required !== false).map((field) => field.field)
103+
if (requiredFields.length) {
104+
const valid = await dynamicsFormRef.value?.ruleFormRef?.validateField(requiredFields).then(
105+
() => true,
106+
() => false,
107+
)
108+
if (!valid) return
109+
}
100110
baseModelLoading.value = true
101111
const requestProvider = modelForm.provider
102112
const requestModelType = modelForm.model_type
@@ -109,10 +119,12 @@ function fetchBaseModelOptions() {
109119
ProviderApi.getBaseModelList(requestProvider, requestModelType, undefined, modelForm.credential as Record<string, unknown>)
110120
.then((options) => {
111121
if (options?.length) {
112-
applyBaseModelOptions(requestProvider, requestModelType, credentialSnapshot, options)
113-
} else {
114-
return loadBuiltin()
122+
if (applyBaseModelOptions(requestProvider, requestModelType, credentialSnapshot, options)) {
123+
MsgSuccess('获取成功')
124+
}
125+
return
115126
}
127+
return loadBuiltin()
116128
})
117129
.catch(() => loadBuiltin())
118130
.finally(() => {

‎ui/src/views/model/model-card/action-dropdown/edit-model-action/EditModelDrawer.vue‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ function applyBaseModelOptions(provider: string, modelType: string, credentialSn
3434
JSON.stringify(modelForm.credential) === credentialSnapshot
3535
) {
3636
baseModelOptions.value = options
37+
return true
3738
}
39+
return false
3840
}
3941
const baseModelLoading = ref(false)
4042
const credentialFields = ref<FormField[]>([])
@@ -93,8 +95,16 @@ function open(provider: ModelProviderItem, model: ModelItem) {
9395
})
9496
}
9597
96-
function fetchBaseModelOptions() {
98+
async function fetchBaseModelOptions() {
9799
if (!modelForm.provider || !modelForm.model_type) return
100+
const requiredFields = credentialFields.value.filter((field) => field.required !== false).map((field) => field.field)
101+
if (requiredFields.length) {
102+
const valid = await dynamicsFormRef.value?.ruleFormRef?.validateField(requiredFields).then(
103+
() => true,
104+
() => false,
105+
)
106+
if (!valid) return
107+
}
98108
baseModelLoading.value = true
99109
const requestProvider = modelForm.provider
100110
const requestModelType = modelForm.model_type
@@ -107,10 +117,12 @@ function fetchBaseModelOptions() {
107117
ProviderApi.getBaseModelList(requestProvider, requestModelType, undefined, modelForm.credential as Record<string, unknown>)
108118
.then((options) => {
109119
if (options?.length) {
110-
applyBaseModelOptions(requestProvider, requestModelType, requestCredentialSnapshot, options)
111-
} else {
112-
return loadBuiltin()
120+
if (applyBaseModelOptions(requestProvider, requestModelType, requestCredentialSnapshot, options)) {
121+
MsgSuccess('获取成功')
122+
}
123+
return
113124
}
125+
return loadBuiltin()
114126
})
115127
.catch(() => loadBuiltin())
116128
.finally(() => {

0 commit comments

Comments
 (0)