Skip to content

Commit b9f88c2

Browse files
committed
fix(model): validate workbench embedding selection
1 parent fe0740c commit b9f88c2

5 files changed

Lines changed: 83 additions & 18 deletions

File tree

src/backend/bisheng/llm/domain/services/llm.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@
1414
from bisheng.common.constants.enums.telemetry import ApplicationTypeEnum
1515
from bisheng.common.dependencies.user_deps import UserPayload
1616
from bisheng.common.errcode.http_error import NotFoundError, ServerError
17-
from bisheng.common.errcode.llm import ModelNameRepeatError, ServerAddAllError, ServerAddError, ServerExistError
17+
from bisheng.common.errcode.llm import (
18+
ModelNameRepeatError,
19+
ServerAddAllError,
20+
ServerAddError,
21+
ServerExistError,
22+
WorkbenchEmbeddingError,
23+
)
1824
from bisheng.common.errcode.llm_tenant import (
1925
LLMModelNotAccessibleError,
2026
LLMSystemConfigForbiddenError,
@@ -1339,6 +1345,12 @@ async def update_workbench_llm(
13391345
``None`` falls back to the admin-scope ContextVar then Root.
13401346
:return:
13411347
"""
1348+
if config_obj.embedding_model is not None:
1349+
embedding_model_id = _coerce_model_id(config_obj.embedding_model.id)
1350+
if embedding_model_id is None:
1351+
raise WorkbenchEmbeddingError()
1352+
config_obj.embedding_model.id = str(embedding_model_id)
1353+
13421354
# Delay imports to avoid looping imports
13431355
from bisheng.worker.knowledge.rebuild_knowledge_worker import rebuild_knowledge_celery
13441356

src/backend/test/llm/test_workbench_update_merge.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import pytest
1919

20+
from bisheng.common.errcode.llm import WorkbenchEmbeddingError
2021
from bisheng.llm.domain.schemas import WorkbenchModelConfig, WSModel
2122
from bisheng.llm.domain.services.llm import LLMService
2223

@@ -73,3 +74,22 @@ async def test_provided_models_replace_normally():
7374
incoming = WorkbenchModelConfig(models=[WSModel(id="840", name="qwen3.7-max")], embedding_model=None)
7475
persisted = await _run_and_capture(incoming)
7576
assert [m["id"] for m in persisted["models"]] == ["840"]
77+
78+
79+
@pytest.mark.parametrize("invalid_model_id", ["", "null", "undefined", "0", "-1"])
80+
@pytest.mark.asyncio
81+
async def test_invalid_embedding_model_id_returns_business_error(invalid_model_id: str):
82+
config = WorkbenchModelConfig(
83+
models=[WSModel(id="840", name="qwen3.7-max")],
84+
embedding_model=WSModel(id=invalid_model_id),
85+
)
86+
87+
with patch(
88+
"bisheng.llm.domain.services.llm.avalidate_system_model_refs",
89+
new=AsyncMock(),
90+
) as mock_validate:
91+
with pytest.raises(WorkbenchEmbeddingError) as exc_info:
92+
await LLMService.update_workbench_llm(1, config, MagicMock(), tenant_id=1)
93+
94+
assert exc_info.value.code == 10810
95+
mock_validate.assert_not_awaited()

src/frontend/platform/src/pages/ModelPage/manage/tabs/WorkbenchModel.tsx

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { useTranslation } from "react-i18next";
1414
import { useQuery } from "react-query";
1515
import { useModel } from "..";
1616
import { ModelManagement } from "@/pages/BuildPage/bench/ModelManagement";
17+
import { hasValidWorkbenchEmbeddingModelId } from "./workbenchModelValidation";
1718

1819
export const ModelSelect = ({ required = false, close = false, label, tooltipText = '', value, options, onChange, placeholder = '' }) => {
1920
const defaultValue = useMemo(() => {
@@ -87,20 +88,8 @@ export default function WorkbenchModel({ onBack }) {
8788
return '';
8889
};
8990

90-
const handleSave = async () => {
91+
const submitConfig = async () => {
9192
const { linsightDefaultModelId, sourceModelId, asrModelId, ttsModelId, chatTitleLlmId, models } = form;
92-
const errors = [];
93-
if (!models.length) {
94-
errors.push('请至少配置一个对话模型');
95-
}
96-
// Reject blank rows: a model without an id serializes to `{ id: '' }`,
97-
// which reaches the client model picker as <SelectItem value="">
98-
// (empty string) and crashes the whole page in Radix. Force the admin
99-
// to pick a model or remove the empty row before saving.
100-
if (models.some((m) => !m.id)) {
101-
errors.push('存在未选择模型的对话模型行,请选择模型或删除该行');
102-
}
103-
if (errors.length) return message({ variant: 'error', description: errors });
10493
setSaveLoad(true);
10594
try {
10695
const data = {
@@ -159,7 +148,26 @@ export default function WorkbenchModel({ onBack }) {
159148
return lastEmbeddingId !== currentEmbeddingId;
160149
};
161150

162-
const handleSaveWithConfirm = () => {
151+
const handleSave = () => {
152+
const errors = [];
153+
if (!form.models.length) {
154+
errors.push('请至少配置一个对话模型');
155+
}
156+
// Reject blank rows: a model without an id serializes to `{ id: '' }`,
157+
// which reaches the client model picker as <SelectItem value="">
158+
// (empty string) and crashes the whole page in Radix. Force the admin
159+
// to pick a model or remove the empty row before saving.
160+
if (form.models.some((model) => !model.id)) {
161+
errors.push('存在未选择模型的对话模型行,请选择模型或删除该行');
162+
}
163+
if (!hasValidWorkbenchEmbeddingModelId(form.sourceModelId)) {
164+
errors.push(t('model.workVectorModel') + t('bs:required'));
165+
}
166+
if (errors.length) {
167+
message({ variant: 'error', description: errors });
168+
return;
169+
}
170+
163171
if (checkEmbeddingModified()) {
164172
bsConfirm({
165173
title: t('model.tip'),
@@ -168,12 +176,12 @@ export default function WorkbenchModel({ onBack }) {
168176
okTxt: t('model.confirm'),
169177
canelTxt: t('model.cancel'),
170178
onOk(next) {
171-
handleSave().then(next);
179+
submitConfig().then(next);
172180
},
173181
onCancel() { }
174182
});
175183
} else {
176-
handleSave();
184+
submitConfig();
177185
}
178186
};
179187

@@ -308,7 +316,7 @@ export default function WorkbenchModel({ onBack }) {
308316
<Button
309317
className="px-10"
310318
disabled={saveload}
311-
onClick={handleSaveWithConfirm}
319+
onClick={handleSave}
312320
>
313321
{saveload && <LoadIcon className="mr-2" />}
314322
{t('model.save')}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export function hasValidWorkbenchEmbeddingModelId(value: unknown): boolean {
2+
if (typeof value === "number") {
3+
return Number.isInteger(value) && value > 0;
4+
}
5+
6+
if (typeof value !== "string") return false;
7+
8+
return /^[1-9]\d*$/.test(value.trim());
9+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { hasValidWorkbenchEmbeddingModelId } from "@/pages/ModelPage/manage/tabs/workbenchModelValidation";
4+
5+
describe("hasValidWorkbenchEmbeddingModelId", () => {
6+
it.each([null, undefined, "", "null", "undefined", "0", "-1", 0, -1])(
7+
"rejects an empty or invalid model id: %s",
8+
(value) => {
9+
expect(hasValidWorkbenchEmbeddingModelId(value)).toBe(false);
10+
},
11+
);
12+
13+
it.each([1, 42, "1", "42"])("accepts a positive model id: %s", (value) => {
14+
expect(hasValidWorkbenchEmbeddingModelId(value)).toBe(true);
15+
});
16+
});

0 commit comments

Comments
 (0)