mirror of
https://github.com/langgenius/dify.git
synced 2026-09-24 23:22:26 +08:00
feat(api): isolate credentials from local packages (#39957)
This commit is contained in:
@@ -4,6 +4,7 @@ from collections.abc import Generator, Sequence
|
||||
from decimal import Decimal
|
||||
from json import dumps
|
||||
|
||||
from core.plugin.entities.plugin import PluginInstallationSource
|
||||
from core.plugin.entities.plugin_daemon import PluginModelProviderEntity
|
||||
from core.plugin.impl.model import PluginModelClient
|
||||
|
||||
@@ -41,6 +42,7 @@ class MockModelClass(PluginModelClient):
|
||||
tenant_id=tenant_id,
|
||||
plugin_unique_identifier="langgenius/openai/openai",
|
||||
plugin_id="langgenius/openai",
|
||||
installation_source=PluginInstallationSource.Marketplace,
|
||||
declaration=ProviderEntity(
|
||||
provider="openai",
|
||||
label=I18nObject(
|
||||
|
||||
@@ -7,6 +7,7 @@ from unittest.mock import MagicMock, Mock, patch, sentinel
|
||||
|
||||
import pytest
|
||||
|
||||
from core.plugin.entities.plugin import PluginInstallationSource
|
||||
from core.plugin.entities.plugin_daemon import PluginModelProviderEntity
|
||||
from core.plugin.impl import model_runtime as model_runtime_module
|
||||
from core.plugin.impl.model import PluginModelClient
|
||||
@@ -93,6 +94,7 @@ def _build_plugin_model_provider(*, tenant_id: str, provider: str = "openai") ->
|
||||
tenant_id=tenant_id,
|
||||
plugin_unique_identifier=f"langgenius/{provider}/{provider}",
|
||||
plugin_id=f"langgenius/{provider}",
|
||||
installation_source=PluginInstallationSource.Marketplace,
|
||||
declaration=ProviderEntity(
|
||||
provider=provider,
|
||||
label=I18nObject(en_US=provider.title()),
|
||||
@@ -116,6 +118,7 @@ class TestPluginModelRuntime:
|
||||
tenant_id="tenant",
|
||||
plugin_unique_identifier="langgenius/openai/openai",
|
||||
plugin_id="langgenius/openai",
|
||||
installation_source=PluginInstallationSource.Marketplace,
|
||||
declaration=ProviderEntity(
|
||||
provider="openai",
|
||||
label=I18nObject(en_US="OpenAI"),
|
||||
@@ -145,6 +148,7 @@ class TestPluginModelRuntime:
|
||||
tenant_id="tenant",
|
||||
plugin_unique_identifier="acme/openai/openai",
|
||||
plugin_id="acme/openai",
|
||||
installation_source=PluginInstallationSource.Marketplace,
|
||||
declaration=ProviderEntity(
|
||||
provider="openai",
|
||||
label=I18nObject(en_US="Acme OpenAI"),
|
||||
@@ -160,6 +164,7 @@ class TestPluginModelRuntime:
|
||||
tenant_id="tenant",
|
||||
plugin_unique_identifier="langgenius/openai/openai",
|
||||
plugin_id="langgenius/openai",
|
||||
installation_source=PluginInstallationSource.Marketplace,
|
||||
declaration=ProviderEntity(
|
||||
provider="openai",
|
||||
label=I18nObject(en_US="OpenAI"),
|
||||
@@ -187,6 +192,7 @@ class TestPluginModelRuntime:
|
||||
tenant_id="tenant",
|
||||
plugin_unique_identifier="langgenius/gemini/google",
|
||||
plugin_id="langgenius/gemini",
|
||||
installation_source=PluginInstallationSource.Marketplace,
|
||||
declaration=ProviderEntity(
|
||||
provider="google",
|
||||
label=I18nObject(en_US="Google"),
|
||||
@@ -821,6 +827,7 @@ def test_get_provider_icon_reads_requested_variant_and_detects_svg_mime(monkeypa
|
||||
tenant_id="tenant",
|
||||
plugin_unique_identifier="langgenius/openai/openai",
|
||||
plugin_id="langgenius/openai",
|
||||
installation_source=PluginInstallationSource.Marketplace,
|
||||
declaration=ProviderEntity(
|
||||
provider="openai",
|
||||
label=I18nObject(en_US="OpenAI"),
|
||||
@@ -857,6 +864,7 @@ def test_get_provider_icon_rejects_unsupported_types_and_missing_variants() -> N
|
||||
tenant_id="tenant",
|
||||
plugin_unique_identifier="langgenius/openai/openai",
|
||||
plugin_id="langgenius/openai",
|
||||
installation_source=PluginInstallationSource.Marketplace,
|
||||
declaration=ProviderEntity(
|
||||
provider="openai",
|
||||
label=I18nObject(en_US="OpenAI"),
|
||||
@@ -989,6 +997,7 @@ def test_get_provider_schema_supports_short_alias_and_rejects_invalid_provider()
|
||||
tenant_id="tenant",
|
||||
plugin_unique_identifier="langgenius/openai/openai",
|
||||
plugin_id="langgenius/openai",
|
||||
installation_source=PluginInstallationSource.Marketplace,
|
||||
declaration=ProviderEntity(
|
||||
provider="openai",
|
||||
label=I18nObject(en_US="OpenAI"),
|
||||
|
||||
@@ -7,10 +7,19 @@ from sqlalchemy import Engine, event, select
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from core import provider_manager as provider_manager_module
|
||||
from core.entities.provider_entities import ModelSettings
|
||||
from core.entities.provider_entities import (
|
||||
CustomConfiguration,
|
||||
CustomProviderConfiguration,
|
||||
ModelSettings,
|
||||
ProviderQuotaType,
|
||||
)
|
||||
from core.hosting_configuration import HostingProvider, TrialHostingQuota
|
||||
from core.plugin.entities.plugin import PluginInstallationSource
|
||||
from core.plugin.entities.plugin_daemon import PluginModelProviderDeclaration
|
||||
from core.provider_manager import ProviderConfigurationCacheSource, ProviderManager
|
||||
from graphon.model_runtime.entities.common_entities import I18nObject
|
||||
from graphon.model_runtime.entities.model_entities import ModelType
|
||||
from graphon.model_runtime.entities.provider_entities import ConfigurateMethod
|
||||
from models.base import TypeBase
|
||||
from models.provider import (
|
||||
LoadBalancingModelConfig,
|
||||
@@ -71,6 +80,39 @@ def provider_db(sqlite_engine: Engine, monkeypatch: pytest.MonkeyPatch) -> Itera
|
||||
yield request_session
|
||||
|
||||
|
||||
def _build_plugin_provider_declaration(
|
||||
installation_source: PluginInstallationSource | None,
|
||||
) -> PluginModelProviderDeclaration:
|
||||
return PluginModelProviderDeclaration(
|
||||
provider="langgenius/openai/openai",
|
||||
plugin_unique_identifier="langgenius/openai:1.0.0@checksum",
|
||||
installation_source=installation_source,
|
||||
label=I18nObject(en_US="OpenAI"),
|
||||
supported_model_types=[ModelType.LLM],
|
||||
configurate_methods=[ConfigurateMethod.PREDEFINED_MODEL],
|
||||
)
|
||||
|
||||
|
||||
def _build_hosting_provider() -> HostingProvider:
|
||||
return HostingProvider(
|
||||
enabled=True,
|
||||
credentials={"api_key": "system-secret"},
|
||||
quotas=[TrialHostingQuota(quota_limit=100)],
|
||||
)
|
||||
|
||||
|
||||
def _build_trial_provider_record() -> Provider:
|
||||
return Provider(
|
||||
tenant_id="tenant-id",
|
||||
provider_name="openai",
|
||||
provider_type=ProviderType.SYSTEM,
|
||||
quota_type=ProviderQuotaType.TRIAL,
|
||||
quota_limit=100,
|
||||
quota_used=0,
|
||||
is_valid=True,
|
||||
)
|
||||
|
||||
|
||||
class _FakeRedis:
|
||||
def __init__(self) -> None:
|
||||
self.store: dict[str, str] = {}
|
||||
@@ -180,6 +222,140 @@ def test__to_model_settings(mock_provider_entity, provider_db: Session):
|
||||
assert result[0].load_balancing_configs[1].name == "first"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"installation_source",
|
||||
[
|
||||
None,
|
||||
PluginInstallationSource.Github,
|
||||
PluginInstallationSource.Package,
|
||||
PluginInstallationSource.Remote,
|
||||
],
|
||||
)
|
||||
def test_to_system_configuration_rejects_non_marketplace_provider(
|
||||
installation_source: PluginInstallationSource | None,
|
||||
) -> None:
|
||||
provider_entity = _build_plugin_provider_declaration(installation_source)
|
||||
manager = _build_provider_manager()
|
||||
|
||||
with (
|
||||
patch.object(manager, "_choice_current_using_quota_type") as choose_quota,
|
||||
patch(
|
||||
"core.provider_manager.ext_hosting_provider.hosting_configuration.provider_map",
|
||||
{provider_entity.provider: _build_hosting_provider()},
|
||||
),
|
||||
):
|
||||
configuration = manager._to_system_configuration("tenant-id", provider_entity, [])
|
||||
|
||||
assert configuration.enabled is False
|
||||
assert configuration.credentials is None
|
||||
choose_quota.assert_not_called()
|
||||
|
||||
|
||||
def test_to_system_configuration_rejects_unverified_marketplace_provider() -> None:
|
||||
provider_entity = _build_plugin_provider_declaration(PluginInstallationSource.Marketplace)
|
||||
manager = _build_provider_manager()
|
||||
|
||||
with (
|
||||
patch.object(manager, "_choice_current_using_quota_type") as choose_quota,
|
||||
patch(
|
||||
"core.provider_manager.ext_hosting_provider.hosting_configuration.provider_map",
|
||||
{provider_entity.provider: _build_hosting_provider()},
|
||||
),
|
||||
patch(
|
||||
"core.plugin.plugin_service.PluginService.is_plugin_verified",
|
||||
return_value=False,
|
||||
) as is_plugin_verified,
|
||||
):
|
||||
configuration = manager._to_system_configuration("tenant-id", provider_entity, [])
|
||||
|
||||
assert configuration.enabled is False
|
||||
assert configuration.credentials is None
|
||||
is_plugin_verified.assert_called_once_with("tenant-id", provider_entity.plugin_unique_identifier)
|
||||
choose_quota.assert_not_called()
|
||||
|
||||
|
||||
def test_to_system_configuration_never_returns_hosting_credentials_for_package_with_valid_quota() -> None:
|
||||
provider_entity = _build_plugin_provider_declaration(PluginInstallationSource.Package)
|
||||
manager = _build_provider_manager()
|
||||
|
||||
with patch(
|
||||
"core.provider_manager.ext_hosting_provider.hosting_configuration.provider_map",
|
||||
{provider_entity.provider: _build_hosting_provider()},
|
||||
):
|
||||
configuration = manager._to_system_configuration(
|
||||
"tenant-id",
|
||||
provider_entity,
|
||||
[_build_trial_provider_record()],
|
||||
)
|
||||
|
||||
assert configuration.enabled is False
|
||||
assert configuration.credentials is None
|
||||
|
||||
|
||||
def test_to_system_configuration_preserves_marketplace_behavior() -> None:
|
||||
provider_entity = _build_plugin_provider_declaration(PluginInstallationSource.Marketplace)
|
||||
manager = _build_provider_manager()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"core.provider_manager.ext_hosting_provider.hosting_configuration.provider_map",
|
||||
{provider_entity.provider: _build_hosting_provider()},
|
||||
),
|
||||
patch(
|
||||
"core.plugin.plugin_service.PluginService.is_plugin_verified",
|
||||
return_value=True,
|
||||
) as is_plugin_verified,
|
||||
):
|
||||
configuration = manager._to_system_configuration(
|
||||
"tenant-id",
|
||||
provider_entity,
|
||||
[_build_trial_provider_record()],
|
||||
)
|
||||
|
||||
assert configuration.enabled is True
|
||||
assert configuration.credentials == {"api_key": "system-secret"}
|
||||
assert configuration.current_quota_type == ProviderQuotaType.TRIAL
|
||||
is_plugin_verified.assert_called_once_with("tenant-id", provider_entity.plugin_unique_identifier)
|
||||
|
||||
|
||||
def test_package_provider_keeps_custom_configuration() -> None:
|
||||
provider_entity = _build_plugin_provider_declaration(PluginInstallationSource.Package)
|
||||
manager = _build_provider_manager()
|
||||
provider_factory = Mock()
|
||||
provider_factory.get_providers.return_value = [provider_entity]
|
||||
custom_configuration = CustomConfiguration(
|
||||
provider=CustomProviderConfiguration(credentials={"api_key": "user-secret"})
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(manager, "_get_all_providers", return_value={provider_entity.provider: []}),
|
||||
patch.object(
|
||||
manager,
|
||||
"_init_trial_provider_records",
|
||||
return_value={provider_entity.provider: []},
|
||||
),
|
||||
patch.object(manager, "_get_all_provider_models", return_value={}),
|
||||
patch.object(manager, "_get_all_preferred_model_providers", return_value={}),
|
||||
patch.object(manager, "_get_all_provider_model_settings", return_value={}),
|
||||
patch.object(manager, "_get_all_provider_load_balancing_configs", return_value={}),
|
||||
patch.object(manager, "_get_all_provider_model_credentials", return_value={}),
|
||||
patch.object(manager, "_get_all_provider_credentials", return_value={}),
|
||||
patch.object(manager, "_to_custom_configuration", return_value=custom_configuration),
|
||||
patch.object(manager, "_to_model_settings", return_value=[]),
|
||||
patch("core.provider_manager.ModelProviderFactory", return_value=provider_factory),
|
||||
patch(
|
||||
"core.provider_manager.ext_hosting_provider.hosting_configuration.provider_map",
|
||||
{provider_entity.provider: _build_hosting_provider()},
|
||||
),
|
||||
):
|
||||
configuration = manager.get_configurations("tenant-id").get(provider_entity.provider)
|
||||
|
||||
assert configuration is not None
|
||||
assert configuration.system_configuration.enabled is False
|
||||
assert configuration.custom_configuration.provider is not None
|
||||
assert configuration.custom_configuration.provider.credentials == {"api_key": "user-secret"}
|
||||
|
||||
|
||||
def test__to_model_settings_only_one_lb(mock_provider_entity, provider_db: Session):
|
||||
# Mocking the inputs
|
||||
|
||||
|
||||
@@ -11,8 +11,13 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from core.helper.model_provider_cache import ProviderCredentialsCacheType
|
||||
from core.plugin.entities.plugin import PluginCategory, PluginInstallationSource
|
||||
from core.plugin.entities.plugin_daemon import PluginInstallTask, PluginInstallTaskStatus, PluginModelProviderEntity
|
||||
from core.provider_manager import ProviderConfigurationCacheSource
|
||||
from core.plugin.entities.plugin_daemon import (
|
||||
PluginInstallTask,
|
||||
PluginInstallTaskStatus,
|
||||
PluginModelProviderDeclaration,
|
||||
PluginModelProviderEntity,
|
||||
)
|
||||
from core.provider_manager import ProviderConfigurationCacheSource, ProviderManager
|
||||
from graphon.model_runtime.entities.common_entities import I18nObject
|
||||
from graphon.model_runtime.entities.provider_entities import ConfigurateMethod, ProviderEntity
|
||||
from models.provider import Provider, ProviderCredential, ProviderType, TenantPreferredModelProvider
|
||||
@@ -24,24 +29,35 @@ OTHER_TENANT_ID = "22222222-2222-2222-2222-222222222222"
|
||||
USER_ID = "33333333-3333-3333-3333-333333333333"
|
||||
|
||||
|
||||
def _build_provider_entity(provider: str = "openai") -> ProviderEntity:
|
||||
return ProviderEntity(
|
||||
def _build_provider_entity(
|
||||
provider: str = "openai",
|
||||
installation_source: PluginInstallationSource | None = PluginInstallationSource.Marketplace,
|
||||
) -> PluginModelProviderDeclaration:
|
||||
return PluginModelProviderDeclaration(
|
||||
provider=f"langgenius/{provider}/{provider}",
|
||||
plugin_unique_identifier=f"langgenius/{provider}:1.0.0@checksum",
|
||||
installation_source=installation_source,
|
||||
label=I18nObject(en_US=provider.title()),
|
||||
supported_model_types=[],
|
||||
configurate_methods=[ConfigurateMethod.PREDEFINED_MODEL],
|
||||
)
|
||||
|
||||
|
||||
def _build_plugin_model_provider(*, tenant_id: str = "tenant-1", provider: str = "openai") -> PluginModelProviderEntity:
|
||||
def _build_plugin_model_provider(
|
||||
*,
|
||||
tenant_id: str = "tenant-1",
|
||||
provider: str = "openai",
|
||||
installation_source: PluginInstallationSource | None = PluginInstallationSource.Marketplace,
|
||||
) -> PluginModelProviderEntity:
|
||||
return PluginModelProviderEntity(
|
||||
id=uuid.uuid4().hex,
|
||||
created_at=datetime.datetime.now(),
|
||||
updated_at=datetime.datetime.now(),
|
||||
provider=provider,
|
||||
tenant_id=tenant_id,
|
||||
plugin_unique_identifier=f"langgenius/{provider}/{provider}",
|
||||
plugin_unique_identifier=f"langgenius/{provider}:1.0.0@checksum",
|
||||
plugin_id=f"langgenius/{provider}",
|
||||
installation_source=installation_source,
|
||||
declaration=ProviderEntity(
|
||||
provider=provider,
|
||||
label=I18nObject(en_US=provider.title()),
|
||||
@@ -137,7 +153,7 @@ class TestPluginModelProviderCache:
|
||||
"""Large provider metadata payloads are compressed before being stored in Redis."""
|
||||
large_provider = _build_provider_entity()
|
||||
large_provider.label = I18nObject(en_US="OpenAI " * 10000)
|
||||
raw_payload = TypeAdapter(list[ProviderEntity]).dump_json([large_provider])
|
||||
raw_payload = TypeAdapter(list[PluginModelProviderDeclaration]).dump_json([large_provider])
|
||||
cache_key = _provider_cache_key("tenant-1", 0)
|
||||
|
||||
with (
|
||||
@@ -164,7 +180,7 @@ class TestPluginModelProviderCache:
|
||||
"""Compressed tenant cache entries are decoded before provider schema validation."""
|
||||
cached_provider = _build_provider_entity()
|
||||
cached_provider.label = I18nObject(en_US="OpenAI " * 10000)
|
||||
cached_payload = TypeAdapter(list[ProviderEntity]).dump_json([cached_provider])
|
||||
cached_payload = TypeAdapter(list[PluginModelProviderDeclaration]).dump_json([cached_provider])
|
||||
generation_key = _provider_generation_key("tenant-1")
|
||||
cache_key = _provider_cache_key("tenant-1", 0)
|
||||
|
||||
@@ -182,6 +198,7 @@ class TestPluginModelProviderCache:
|
||||
result = PluginService.fetch_plugin_model_providers(tenant_id="tenant-1", client=client)
|
||||
|
||||
assert [provider.provider for provider in result] == ["langgenius/openai/openai"]
|
||||
assert result[0].plugin_unique_identifier == "langgenius/openai:1.0.0@checksum"
|
||||
assert result[0].label.en_us == "OpenAI " * 10000
|
||||
client.fetch_model_providers.assert_not_called()
|
||||
redis_client.setex.assert_not_called()
|
||||
@@ -189,9 +206,9 @@ class TestPluginModelProviderCache:
|
||||
redis_client.mget.assert_called_once_with([cache_key])
|
||||
|
||||
def test_fetch_plugin_model_providers_returns_cached_provider_without_calling_daemon(self) -> None:
|
||||
"""A valid tenant cache entry is reused across runtime calls without plugin daemon access."""
|
||||
cached_provider = _build_provider_entity()
|
||||
cached_payload = TypeAdapter(list[ProviderEntity]).dump_json([cached_provider])
|
||||
"""A cached package source remains available to the system configuration guard."""
|
||||
cached_provider = _build_provider_entity(installation_source=PluginInstallationSource.Package)
|
||||
cached_payload = TypeAdapter(list[PluginModelProviderDeclaration]).dump_json([cached_provider])
|
||||
generation_key = _provider_generation_key("tenant-1")
|
||||
cache_key = _provider_cache_key("tenant-1", 0)
|
||||
|
||||
@@ -205,13 +222,33 @@ class TestPluginModelProviderCache:
|
||||
result = PluginService.fetch_plugin_model_providers(tenant_id="tenant-1", client=client)
|
||||
|
||||
assert [provider.provider for provider in result] == ["langgenius/openai/openai"]
|
||||
assert result[0].installation_source == PluginInstallationSource.Package
|
||||
provider_manager = ProviderManager(model_runtime=Mock())
|
||||
with (
|
||||
patch(
|
||||
"core.provider_manager.ext_hosting_provider.hosting_configuration.provider_map",
|
||||
{result[0].provider: SimpleNamespace(enabled=True)},
|
||||
),
|
||||
patch("core.plugin.plugin_service.PluginService.is_plugin_verified") as is_plugin_verified,
|
||||
):
|
||||
system_configuration = provider_manager._to_system_configuration("tenant-1", result[0], [])
|
||||
|
||||
assert system_configuration.enabled is False
|
||||
is_plugin_verified.assert_not_called()
|
||||
client.fetch_model_providers.assert_not_called()
|
||||
redis_client.setex.assert_not_called()
|
||||
redis_client.get.assert_called_once_with(generation_key)
|
||||
redis_client.mget.assert_called_once_with([cache_key])
|
||||
|
||||
def test_fetch_plugin_model_providers_deletes_invalid_cache_and_refetches(self) -> None:
|
||||
"""Invalid generation-scoped cache payloads are removed before falling back to the daemon."""
|
||||
def test_fetch_plugin_model_providers_invalidates_legacy_cache_without_plugin_identity(self) -> None:
|
||||
"""Legacy provider cache entries are refreshed before they can reach system configuration."""
|
||||
legacy_provider = ProviderEntity(
|
||||
provider="langgenius/openai/openai",
|
||||
label=I18nObject(en_US="OpenAI"),
|
||||
supported_model_types=[],
|
||||
configurate_methods=[ConfigurateMethod.PREDEFINED_MODEL],
|
||||
)
|
||||
legacy_payload = TypeAdapter(list[ProviderEntity]).dump_json([legacy_provider])
|
||||
generation_key = _provider_generation_key("tenant-1")
|
||||
cache_key = _provider_cache_key("tenant-1", 0)
|
||||
with (
|
||||
@@ -219,7 +256,7 @@ class TestPluginModelProviderCache:
|
||||
patch(f"{MODULE}.dify_config") as mock_config,
|
||||
):
|
||||
redis_client.get.side_effect = [None, None, None]
|
||||
redis_client.mget.side_effect = [["not-json"], [None]]
|
||||
redis_client.mget.side_effect = [[legacy_payload], [None]]
|
||||
mock_config.PLUGIN_MODEL_PROVIDERS_CACHE_TTL = 86400
|
||||
client = Mock()
|
||||
client.fetch_model_providers.return_value = [_build_plugin_model_provider()]
|
||||
@@ -253,12 +290,37 @@ class TestPluginModelProviderCache:
|
||||
|
||||
assert [provider.provider for provider in first] == ["langgenius/openai/openai"]
|
||||
assert [provider.provider for provider in second] == ["langgenius/openai/openai"]
|
||||
assert first[0].plugin_unique_identifier == "langgenius/openai:1.0.0@checksum"
|
||||
assert client.fetch_model_providers.call_count == 2
|
||||
redis_client.get.assert_not_called()
|
||||
redis_client.mget.assert_not_called()
|
||||
redis_client.setex.assert_not_called()
|
||||
redis_client.lock.assert_not_called()
|
||||
|
||||
def test_fetch_plugin_model_providers_resolves_missing_installation_source(self) -> None:
|
||||
provider = _build_plugin_model_provider(installation_source=None)
|
||||
installation = SimpleNamespace(
|
||||
plugin_unique_identifier=provider.plugin_unique_identifier,
|
||||
source=PluginInstallationSource.Package,
|
||||
)
|
||||
|
||||
from core.plugin.plugin_service import PluginService
|
||||
|
||||
with (
|
||||
patch(f"{MODULE}.dify_config") as config,
|
||||
patch.object(
|
||||
PluginService, "list_installations_from_ids", return_value=[installation]
|
||||
) as list_installations,
|
||||
):
|
||||
config.PLUGIN_MODEL_PROVIDERS_CACHE_ENABLED = False
|
||||
client = Mock()
|
||||
client.fetch_model_providers.return_value = [provider]
|
||||
|
||||
result = PluginService.fetch_plugin_model_providers(tenant_id="tenant-1", client=client)
|
||||
|
||||
list_installations.assert_called_once_with("tenant-1", [provider.plugin_id])
|
||||
assert result[0].installation_source == PluginInstallationSource.Package
|
||||
|
||||
def test_fetch_plugin_model_providers_refetches_when_cache_read_fails(self) -> None:
|
||||
"""Redis read failures do not block provider discovery for the tenant."""
|
||||
with patch(f"{MODULE}.redis_client") as redis_client:
|
||||
@@ -309,7 +371,7 @@ class TestPluginModelProviderCache:
|
||||
def test_fetch_plugin_model_providers_waits_for_concurrent_refresh_cache_fill(self) -> None:
|
||||
"""A cache miss waits for the active tenant refresh instead of stampeding the daemon."""
|
||||
cached_provider = _build_provider_entity()
|
||||
cached_payload = TypeAdapter(list[ProviderEntity]).dump_json([cached_provider])
|
||||
cached_payload = TypeAdapter(list[PluginModelProviderDeclaration]).dump_json([cached_provider])
|
||||
cache_key = _provider_cache_key("tenant-1", 0)
|
||||
|
||||
with (
|
||||
@@ -636,7 +698,7 @@ class TestPluginModelProviderCache:
|
||||
|
||||
def test_fetch_plugin_model_providers_reuses_cached_empty_provider_list(self) -> None:
|
||||
"""A cached empty list should prevent repeated daemon fetches for tenants without plugin models."""
|
||||
empty_payload = TypeAdapter(list[ProviderEntity]).dump_json([])
|
||||
empty_payload = TypeAdapter(list[PluginModelProviderDeclaration]).dump_json([])
|
||||
cache_key = _provider_cache_key("tenant-1", 0)
|
||||
|
||||
with patch(f"{MODULE}.redis_client") as redis_client:
|
||||
|
||||
Reference in New Issue
Block a user