feat: In-app AI Provider Configuration (#7650)

This commit is contained in:
Michael Genson
2026-05-23 11:13:10 -05:00
committed by GitHub
parent f6fe92b400
commit c3f87736d0
86 changed files with 3325 additions and 297 deletions

View File

@@ -0,0 +1,206 @@
from uuid import uuid4
import pytest
from pydantic import ValidationError
from mealie.schema.group.ai_providers import (
AIProviderCreate,
AIProviderSettingsOut,
AIProviderSummary,
)
class AIProviderCreateTests:
def test_valid_create(self):
provider = AIProviderCreate(name="test", api_key="key", model="gpt-4o")
assert provider.name == "test"
assert provider.model == "gpt-4o"
assert provider.timeout == 300
assert provider.base_url is None
@pytest.mark.parametrize("field", ["name", "api_key", "model"])
def test_empty_field_raises(self, field: str):
data: dict = {"name": "test", "api_key": "key", "model": "gpt-4o", field: ""}
with pytest.raises(ValidationError):
AIProviderCreate(**data)
@pytest.mark.parametrize("timeout", [-1, -100])
def test_negative_timeout_raises(self, timeout: int):
with pytest.raises(ValidationError):
AIProviderCreate(name="test", api_key="key", model="gpt-4o", timeout=timeout)
def test_zero_timeout_is_valid(self):
provider = AIProviderCreate(name="test", api_key="key", model="gpt-4o", timeout=0)
assert provider.timeout == 0
@pytest.mark.parametrize("base_url", ["", None])
def test_base_url_empty_becomes_none(self, base_url: str | None):
provider = AIProviderCreate(name="test", api_key="key", model="gpt-4o", base_url=base_url)
assert provider.base_url is None
def test_api_key_excluded_from_serialization(self):
provider = AIProviderCreate(name="test", api_key="secret", model="gpt-4o")
dumped = provider.model_dump()
assert "api_key" not in dumped
def test_api_key_excluded_from_json(self):
provider = AIProviderCreate(name="test", api_key="secret", model="gpt-4o")
json_str = provider.model_dump_json()
assert "api_key" not in json_str
assert "secret" not in json_str
class AIProviderSettingsOutTests:
def _make_settings(
self,
*,
default_provider_id=None,
audio_provider_id=None,
image_provider_id=None,
providers=None,
) -> AIProviderSettingsOut:
if providers is None:
providers = []
return AIProviderSettingsOut(
default_provider_id=default_provider_id,
audio_provider_id=audio_provider_id,
image_provider_id=image_provider_id,
providers=providers,
)
# --- ai_enabled ---
def test_ai_enabled_false_when_no_default(self):
s = self._make_settings()
assert not s.ai_enabled
def test_ai_enabled_true_when_default_set(self):
pid = uuid4()
s = self._make_settings(default_provider_id=pid, providers=[AIProviderSummary(id=pid, name="p")])
assert s.ai_enabled
# --- audio_provider_enabled ---
def test_audio_provider_disabled_when_no_default(self):
audio_id = uuid4()
s = self._make_settings(
audio_provider_id=audio_id,
providers=[AIProviderSummary(id=audio_id, name="audio")],
)
# audio_provider_id is valid, but validate_providers sets audio_provider_id to None
# because without default_provider_id, it would be fine; let's test audio_provider_enabled
# which requires ai_enabled to be True
assert not s.ai_enabled
assert not s.audio_provider_enabled
def test_audio_provider_disabled_when_only_default_set(self):
pid = uuid4()
s = self._make_settings(default_provider_id=pid, providers=[AIProviderSummary(id=pid, name="p")])
assert s.ai_enabled
assert not s.audio_provider_enabled
def test_audio_provider_enabled_when_both_set(self):
pid = uuid4()
audio_id = uuid4()
s = self._make_settings(
default_provider_id=pid,
audio_provider_id=audio_id,
providers=[AIProviderSummary(id=pid, name="p"), AIProviderSummary(id=audio_id, name="audio")],
)
assert s.ai_enabled
assert s.audio_provider_enabled
# --- image_provider_enabled ---
def test_image_provider_disabled_when_no_default(self):
image_id = uuid4()
s = self._make_settings(
image_provider_id=image_id,
providers=[AIProviderSummary(id=image_id, name="img")],
)
assert not s.ai_enabled
assert not s.image_provider_enabled
def test_image_provider_disabled_when_only_default_set(self):
pid = uuid4()
s = self._make_settings(default_provider_id=pid, providers=[AIProviderSummary(id=pid, name="p")])
assert s.ai_enabled
assert not s.image_provider_enabled
def test_image_provider_enabled_when_both_set(self):
pid = uuid4()
image_id = uuid4()
s = self._make_settings(
default_provider_id=pid,
image_provider_id=image_id,
providers=[AIProviderSummary(id=pid, name="p"), AIProviderSummary(id=image_id, name="img")],
)
assert s.ai_enabled
assert s.image_provider_enabled
# --- validate_providers model validator ---
def test_validate_providers_strips_unknown_default(self):
s = self._make_settings(default_provider_id=uuid4(), providers=[])
assert s.default_provider_id is None
assert not s.ai_enabled
def test_validate_providers_strips_unknown_audio(self):
pid = uuid4()
providers = [AIProviderSummary(id=pid, name="p")]
s = self._make_settings(default_provider_id=pid, audio_provider_id=uuid4(), providers=providers)
assert s.default_provider_id == pid
assert s.audio_provider_id is None
def test_validate_providers_strips_unknown_image(self):
pid = uuid4()
providers = [AIProviderSummary(id=pid, name="p")]
s = self._make_settings(default_provider_id=pid, image_provider_id=uuid4(), providers=providers)
assert s.default_provider_id == pid
assert s.image_provider_id is None
def test_validate_providers_keeps_valid_ids(self):
pid = uuid4()
audio_id = uuid4()
image_id = uuid4()
providers = [
AIProviderSummary(id=pid, name="p"),
AIProviderSummary(id=audio_id, name="audio"),
AIProviderSummary(id=image_id, name="img"),
]
s = self._make_settings(
default_provider_id=pid,
audio_provider_id=audio_id,
image_provider_id=image_id,
providers=providers,
)
assert s.default_provider_id == pid
assert s.audio_provider_id == audio_id
assert s.image_provider_id == image_id
def test_validate_providers_strips_all_if_empty_list(self):
pid = uuid4()
s = self._make_settings(
default_provider_id=pid,
audio_provider_id=uuid4(),
image_provider_id=uuid4(),
providers=[],
)
assert s.default_provider_id is None
assert s.audio_provider_id is None
assert s.image_provider_id is None
def test_validate_providers_partial_strip(self):
"""Only the IDs pointing to missing providers are stripped."""
pid = uuid4()
audio_id = uuid4()
providers = [AIProviderSummary(id=pid, name="p"), AIProviderSummary(id=audio_id, name="audio")]
s = self._make_settings(
default_provider_id=pid,
audio_provider_id=audio_id,
image_provider_id=uuid4(), # not in list → stripped
providers=providers,
)
assert s.default_provider_id == pid
assert s.audio_provider_id == audio_id
assert s.image_provider_id is None