@@ -452,6 +458,18 @@ export default function ModelConfigsPage() {
+
+ {() => form.getFieldValue('capability') === 'chat' && (
+
+
+
+ )}
+
diff --git a/migrations/versions/a3f9c2e71b48_add_analysis_default_to_model_configs.py b/migrations/versions/a3f9c2e71b48_add_analysis_default_to_model_configs.py
new file mode 100644
index 0000000..00142f0
--- /dev/null
+++ b/migrations/versions/a3f9c2e71b48_add_analysis_default_to_model_configs.py
@@ -0,0 +1,28 @@
+"""add analysis default flag to model configs
+
+Revision ID: a3f9c2e71b48
+Revises: e6c3d1a2f809
+Create Date: 2026-08-02
+"""
+
+from typing import Sequence, Union
+
+import sqlalchemy as sa
+from alembic import op
+
+revision: str = "a3f9c2e71b48"
+down_revision: Union[str, Sequence[str], None] = "e6c3d1a2f809"
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ with op.batch_alter_table("model_configs", schema=None) as batch_op:
+ batch_op.add_column(
+ sa.Column("is_analysis_default", sa.Boolean(), nullable=False, server_default=sa.false())
+ )
+
+
+def downgrade() -> None:
+ with op.batch_alter_table("model_configs", schema=None) as batch_op:
+ batch_op.drop_column("is_analysis_default")
diff --git a/tests/integration/test_model_configs_api.py b/tests/integration/test_model_configs_api.py
index 7e46976..ca933fe 100644
--- a/tests/integration/test_model_configs_api.py
+++ b/tests/integration/test_model_configs_api.py
@@ -201,3 +201,38 @@ def test_model_config_metadata_defaults_and_validation(model_client):
json=_payload(name="invalid-context", api_key=None, context_window=0),
)
assert invalid_context.status_code == 422
+
+
+def test_analysis_default_via_api(model_client):
+ client, _ = model_client
+
+ first = client.post(
+ "/api/model-configs",
+ json=_payload(name="分析模型一", api_key=None, is_default=False, is_analysis_default=True),
+ )
+ assert first.status_code == 200
+ assert first.json()["is_analysis_default"] is True
+
+ second = client.post(
+ "/api/model-configs",
+ json=_payload(name="分析模型二", api_key=None, is_default=False, is_analysis_default=True),
+ )
+ assert second.status_code == 200
+
+ listed = client.get("/api/model-configs").json()
+ flags = {item["name"]: item["is_analysis_default"] for item in listed}
+ assert flags == {"分析模型一": False, "分析模型二": True}
+
+ rejected = client.post(
+ "/api/model-configs",
+ json=_payload(
+ name="向量分析",
+ capability="embedding",
+ model_name="embed-model",
+ api_key=None,
+ is_default=False,
+ is_analysis_default=True,
+ ),
+ )
+ assert rejected.status_code == 400
+ assert "分析默认" in rejected.json()["detail"]
diff --git a/tests/unit/test_analysis_default_migration.py b/tests/unit/test_analysis_default_migration.py
new file mode 100644
index 0000000..d820956
--- /dev/null
+++ b/tests/unit/test_analysis_default_migration.py
@@ -0,0 +1,35 @@
+"""Verify the analysis-default migration backfills existing configurations."""
+
+import importlib
+
+import sqlalchemy as sa
+from alembic.migration import MigrationContext
+from alembic.operations import Operations
+
+
+def test_analysis_default_migration_backfills_false(tmp_path, monkeypatch):
+ engine = sa.create_engine(f"sqlite:///{tmp_path / 'analysis_default.db'}")
+ metadata = sa.MetaData()
+ legacy_table = sa.Table(
+ "model_configs",
+ metadata,
+ sa.Column("id", sa.String(), primary_key=True),
+ sa.Column("name", sa.String(), nullable=False),
+ )
+ metadata.create_all(engine)
+
+ with engine.begin() as connection:
+ connection.execute(legacy_table.insert().values(id="existing", name="现有模型"))
+ operations = Operations(MigrationContext.configure(connection))
+ migration = importlib.import_module("migrations.versions.a3f9c2e71b48_add_analysis_default_to_model_configs")
+ monkeypatch.setattr(migration, "op", operations)
+ migration.upgrade()
+
+ columns = {column["name"] for column in sa.inspect(connection).get_columns("model_configs")}
+ assert "is_analysis_default" in columns
+
+ value = connection.execute(
+ sa.text("SELECT is_analysis_default FROM model_configs WHERE id = :id"),
+ {"id": "existing"},
+ ).scalar_one()
+ assert value == 0
diff --git a/tests/unit/test_model_configs.py b/tests/unit/test_model_configs.py
index 0bff313..0f3c847 100644
--- a/tests/unit/test_model_configs.py
+++ b/tests/unit/test_model_configs.py
@@ -17,7 +17,15 @@ def _service(session):
return ModelConfigService(session, SecretCipher(Fernet.generate_key().decode("ascii")))
-def _create_config(service, *, name="评估模型", capability="chat", is_default=False, api_key="secret"):
+def _create_config(
+ service,
+ *,
+ name="评估模型",
+ capability="chat",
+ is_default=False,
+ is_analysis_default=False,
+ api_key="secret",
+):
return service.create(
name=name,
provider="openai_compatible",
@@ -27,6 +35,7 @@ def _create_config(service, *, name="评估模型", capability="chat", is_defaul
api_key=api_key,
enabled=True,
is_default=is_default,
+ is_analysis_default=is_analysis_default,
description="test",
)
@@ -199,3 +208,76 @@ def test_model_metadata_validation_and_runtime_snapshot(db_session):
description="",
documentation_url="not-a-url",
)
+
+
+def _update(service, config_id, **overrides):
+ params = {
+ "name": "updated",
+ "provider": "openai_compatible",
+ "capability": "chat",
+ "endpoint_url": "https://models.example.com/v1/chat/completions",
+ "model_name": "test-model",
+ "api_key": None,
+ "clear_api_key": False,
+ "enabled": True,
+ "is_default": False,
+ "is_analysis_default": False,
+ "description": "",
+ }
+ params.update(overrides)
+ return service.update(config_id, **params)
+
+
+def test_analysis_default_is_unique_across_configs(db_session):
+ service = _service(db_session)
+ first = _create_config(service, name="analysis-1", is_analysis_default=True)
+ second = _create_config(service, name="analysis-2", is_analysis_default=True)
+
+ assert service.require(first.id).is_analysis_default is False
+ assert service.require(second.id).is_analysis_default is True
+
+
+def test_analysis_default_update_clears_previous(db_session):
+ service = _service(db_session)
+ first = _create_config(service, name="analysis-1", is_analysis_default=True)
+ second = _create_config(service, name="analysis-2")
+
+ _update(service, second.id, is_analysis_default=True)
+
+ assert service.require(first.id).is_analysis_default is False
+ assert service.require(second.id).is_analysis_default is True
+
+
+def test_analysis_default_requires_chat_capability(db_session):
+ service = _service(db_session)
+ with pytest.raises(ModelConfigError, match="分析默认"):
+ _create_config(service, name="emb", capability="embedding", is_analysis_default=True)
+
+
+def test_analysis_default_requires_enabled(db_session):
+ service = _service(db_session)
+ with pytest.raises(ModelConfigError, match="分析默认"):
+ service.create(
+ name="disabled-analysis",
+ provider="openai_compatible",
+ capability="chat",
+ endpoint_url="https://models.example.com/v1/chat/completions",
+ model_name="test-model",
+ api_key="secret",
+ enabled=False,
+ is_default=False,
+ is_analysis_default=True,
+ description="",
+ )
+
+ config = _create_config(service, name="to-disable", is_analysis_default=True)
+ with pytest.raises(ModelConfigError, match="分析默认"):
+ _update(service, config.id, enabled=False, is_analysis_default=True)
+
+
+def test_scenario_binding_rejects_analysis_purpose(db_session):
+ service = _service(db_session)
+ config = _create_config(service)
+ with pytest.raises(ModelConfigError, match="不支持的模型用途"):
+ service.validate_bindings({ModelPurpose.ANALYSIS.value: config.id})
+