Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 1 addition & 22 deletions api/core/provider_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -847,7 +847,7 @@ def get_provider_model_bundle(self, tenant_id: str, provider: str, model_type: M

def get_default_model(self, tenant_id: str, model_type: ModelType) -> DefaultModelEntity | None:
"""
Get default model.
Get the configured default model, or None when no default is saved.

:param tenant_id: workspace id
:param model_type: model type
Expand All @@ -859,27 +859,6 @@ def get_default_model(self, tenant_id: str, model_type: ModelType) -> DefaultMod
)
default_model = db.session.scalar(stmt)

# If it does not exist, get the first available provider model from get_configurations
# and update the TenantDefaultModel record
if not default_model:
# Get provider configurations
provider_configurations = self.get_configurations(tenant_id)

# get available models from provider_configurations
available_models = provider_configurations.get_models(model_type=model_type, only_active=True)

if available_models:
available_model = available_models[0]

default_model = TenantDefaultModel(
tenant_id=tenant_id,
model_type=model_type,
provider_name=available_model.provider.provider,
model_name=available_model.model,
)
db.session.add(default_model)
db.session.commit()

if not default_model:
return None

Expand Down
44 changes: 44 additions & 0 deletions api/services/model_provider/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import TYPE_CHECKING, Any

from sqlalchemy import and_, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session

from services.credentials.query import CredentialQuery
Expand Down Expand Up @@ -881,6 +882,49 @@ def get_default_model_of_model_type(self, tenant_id: str, model_type: str) -> De
),
)

def initialize_default_llm(self, tenant_id: str) -> None:
"""Save the first active LLM during onboarding without replacing a configured default."""
saved_default = select(TenantDefaultModel.id).where(
TenantDefaultModel.tenant_id == tenant_id,
TenantDefaultModel.model_type == ModelType.LLM,
)
with session_factory.create_session() as session:
if session.scalar(saved_default) is not None:
return

configurations = self._get_provider_manager(tenant_id).get_configurations(tenant_id)
available_models = configurations.get_models(model_type=ModelType.LLM, only_active=True)
if not available_models:
logger.info("No active LLM available for default model initialization, tenant_id: %s", tenant_id)
return

model = available_models[0]
try:
with session_factory.create_session() as session, session.begin():
if session.scalar(saved_default) is not None:
return
session.add(
TenantDefaultModel(
tenant_id=tenant_id,
model_type=ModelType.LLM,
provider_name=model.provider.provider,
model_name=model.model,
)
)
except IntegrityError:
# A concurrent initializer or user selection may have saved the default first.
with session_factory.create_session() as session:
if session.scalar(saved_default) is None:
raise
return

logger.info(
"Initialized default LLM for tenant %s: provider=%s model=%s",
tenant_id,
model.provider.provider,
model.model,
)

def update_default_model_of_model_type(self, tenant_id: str, model_type: str, provider: str, model: str):
"""
update default model of model type.
Expand Down
25 changes: 15 additions & 10 deletions api/tasks/install_default_plugins_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,15 @@
from core.helper import marketplace
from core.plugin.entities.plugin_daemon import PluginInstallTaskStatus
from core.plugin.plugin_service import PluginService
from graphon.model_runtime.entities.model_entities import ModelType
from services.model_provider.service import ModelProviderService

logger = logging.getLogger(__name__)


@shared_task(queue="plugin", bind=True, max_retries=60, default_retry_delay=5)
def configure_default_models_task(self, tenant_id: str, plugin_install_task_id: str | None) -> None:
"""Set explicitly configured default models after default plugins finish installing."""
if not dify_config.NEW_USER_DEFAULT_MODELS:
return

"""Apply explicit defaults and initialize an unset LLM after default plugins finish installing."""
plugin_install_failed = False
if plugin_install_task_id:
try:
Expand Down Expand Up @@ -46,7 +44,8 @@ def configure_default_models_task(self, tenant_id: str, plugin_install_task_id:

model_provider_service = ModelProviderService()
failed_model_types: list[str] = []
for model_type, provider, model in dify_config.NEW_USER_DEFAULT_MODEL_LIST:
model_settings = dify_config.NEW_USER_DEFAULT_MODEL_LIST
for model_type, provider, model in model_settings:
try:
model_provider_service.update_default_model_of_model_type(
tenant_id=tenant_id,
Expand All @@ -64,6 +63,13 @@ def configure_default_models_task(self, tenant_id: str, plugin_install_task_id:
model,
)

if not any(model_type == ModelType.LLM for model_type, _, _ in model_settings):
try:
model_provider_service.initialize_default_llm(tenant_id)
except Exception:
failed_model_types.append(ModelType.LLM.value)
logger.exception("Failed to initialize default LLM for tenant %s", tenant_id)

if plugin_install_failed or failed_model_types:
raise RuntimeError(
f"Failed to initialize defaults for tenant {tenant_id}; "
Expand All @@ -89,11 +95,10 @@ def install_default_plugins_task(tenant_id: str, plugin_ids: list[str]) -> None:
return

response = PluginService.install_from_marketplace_pkg(tenant_id, plugin_identifiers)
if dify_config.NEW_USER_DEFAULT_MODELS:
configure_default_models_task.delay(
tenant_id,
None if response.all_installed else response.task_id,
)
configure_default_models_task.delay(
tenant_id,
None if response.all_installed else response.task_id,
)
except Exception:
logger.exception("Failed to install default plugins for tenant %s", tenant_id)
raise
105 changes: 28 additions & 77 deletions api/tests/unit_tests/core/test_provider_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
from sqlalchemy.orm import Session, sessionmaker

from core import provider_manager as provider_manager_module
from core.entities.model_entities import DefaultModelSetting
from core.entities.model_entities import (
DefaultModelSetting,
ModelStatus,
ModelWithProviderEntity,
SimpleModelProviderEntity,
)
from core.entities.provider_entities import (
CustomConfiguration,
CustomProviderConfiguration,
Expand All @@ -22,7 +27,7 @@
from core.provider_manager import ProviderConfigurationCacheSource, ProviderManager
from enums import DeploymentEdition
from graphon.model_runtime.entities.common_entities import I18nObject
from graphon.model_runtime.entities.model_entities import ModelType
from graphon.model_runtime.entities.model_entities import FetchFrom, ModelType
from graphon.model_runtime.entities.provider_entities import ConfigurateMethod
from models.base import TypeBase
from models.provider import (
Expand Down Expand Up @@ -512,80 +517,6 @@ def test__to_model_settings_lb_disabled(mock_provider_entity, provider_db: Sessi
assert len(result[0].load_balancing_configs) == 0


def test_get_default_model_uses_first_available_active_model(provider_db: Session):
other_tenant_default = TenantDefaultModel(
tenant_id="other-tenant",
provider_name="anthropic",
model_name="claude",
model_type=ModelType.LLM,
)
provider_db.add(other_tenant_default)
provider_db.commit()
provider_configurations = Mock()
provider_configurations.get_models.return_value = [
Mock(model="gpt-3.5-turbo", provider=Mock(provider="openai")),
Mock(model="gpt-4", provider=Mock(provider="openai")),
]

manager = _build_provider_manager()
with (
patch.object(manager, "get_configurations", return_value=provider_configurations),
patch("core.provider_manager.ModelProviderFactory") as mock_factory_cls,
):
mock_factory_cls.return_value.get_provider_schema.return_value = Mock(
provider="openai",
label=I18nObject(en_US="OpenAI", zh_Hans="OpenAI"),
icon_small=I18nObject(en_US="icon_small.png", zh_Hans="icon_small.png"),
supported_model_types=[ModelType.LLM],
)

result = manager.get_default_model("tenant-id", ModelType.LLM)

assert result is not None
assert result.model == "gpt-3.5-turbo"
assert result.provider.provider == "openai"
provider_configurations.get_models.assert_called_once_with(model_type=ModelType.LLM, only_active=True)
saved_default_model = provider_db.scalar(
select(TenantDefaultModel).where(TenantDefaultModel.tenant_id == "tenant-id")
)
assert saved_default_model is not None
assert saved_default_model.model_name == "gpt-3.5-turbo"
assert saved_default_model.provider_name == "openai"
assert (
provider_db.scalar(select(TenantDefaultModel).where(TenantDefaultModel.tenant_id == "other-tenant"))
is other_tenant_default
)


def test_get_default_model_returns_none_when_no_default_or_active_models(provider_db: Session):
provider_db.add(
TenantDefaultModel(
tenant_id="other-tenant",
provider_name="anthropic",
model_name="claude",
model_type=ModelType.LLM,
)
)
provider_db.commit()
provider_configurations = Mock()
provider_configurations.get_models.return_value = []
manager = _build_provider_manager()

with (
patch.object(manager, "get_configurations", return_value=provider_configurations),
patch("core.provider_manager.ModelProviderFactory") as mock_factory_cls,
):
result = manager.get_default_model("tenant-id", ModelType.LLM)

assert result is None
provider_configurations.get_models.assert_called_once_with(model_type=ModelType.LLM, only_active=True)
mock_factory_cls.assert_not_called()
assert provider_db.scalar(select(TenantDefaultModel).where(TenantDefaultModel.tenant_id == "tenant-id")) is None
assert (
provider_db.scalar(select(TenantDefaultModel).where(TenantDefaultModel.tenant_id == "other-tenant")) is not None
)


def test_get_default_model_uses_injected_runtime_for_existing_default_record(provider_db: Session):
existing_default_model = TenantDefaultModel(
tenant_id="tenant-id",
Expand Down Expand Up @@ -999,11 +930,23 @@ def test_replace_default_model_records_removes_omitted_types_only_for_current_te
)
provider_db.add_all([existing_default, omitted_default, other_tenant_default])
provider_db.commit()
embedding_provider = _build_plugin_provider_declaration(None)
embedding_provider.supported_model_types = [ModelType.TEXT_EMBEDDING]
available_embedding_model = ModelWithProviderEntity(
model="text-embedding-3-small",
label=I18nObject(en_US="text-embedding-3-small"),
model_type=ModelType.TEXT_EMBEDDING,
fetch_from=FetchFrom.PREDEFINED_MODEL,
model_properties={},
status=ModelStatus.ACTIVE,
provider=SimpleModelProviderEntity(embedding_provider),
)
configurations = MagicMock()
configurations.__contains__.return_value = True
configurations.get_models.side_effect = lambda model_type, **_kwargs: {
ModelType.LLM: [Mock(model="gpt-4")],
ModelType.RERANK: [Mock(model="rerank-v3")],
ModelType.TEXT_EMBEDDING: [available_embedding_model],
}[model_type]
settings = (
[]
Expand All @@ -1014,8 +957,16 @@ def test_replace_default_model_records_removes_omitted_types_only_for_current_te
]
)

with patch.object(manager, "get_configurations", return_value=configurations) as get_configurations:
with (
patch.object(manager, "get_configurations", return_value=configurations) as get_configurations,
patch("core.provider_manager.ModelProviderFactory") as mock_factory_cls,
):
manager.replace_default_model_records("tenant-id", settings)
provider_db.expire_all()
for _ in range(2):
assert manager.get_default_model("tenant-id", ModelType.TEXT_EMBEDDING) is None

mock_factory_cls.assert_not_called()

provider_db.expire_all()
defaults = list(provider_db.scalars(select(TenantDefaultModel)))
Expand Down
Loading
Loading