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

@@ -49,6 +49,12 @@ def test_openai_parser(
monkeypatch.setattr(OpenAIService, "get_response", mock_get_response)
def mock_openai_init(self, repos):
self.repos = repos
self.custom_prompt_dir = None
monkeypatch.setattr(OpenAIService, "__init__", mock_openai_init)
with session_context() as session:
loop = asyncio.get_event_loop()
parser = get_parser(RegisteredParser.openai, unique_local_group_id, session, get_locale_provider())
@@ -69,7 +75,7 @@ def test_openai_parser_sanitize_output(
parsed_ingredient_data: tuple[list[IngredientFood], list[IngredientUnit]], # required so database is populated
monkeypatch: pytest.MonkeyPatch,
):
async def mock_get_raw_response(self, prompt: str, content: list[dict], response_schema) -> MagicMock:
async def mock_get_raw_response(self, prompt: str, content: list[dict], response_schema, provider) -> MagicMock:
# Create data with null character in JSON to test preprocessing
data = OpenAIIngredients(
ingredients=[
@@ -91,6 +97,17 @@ def test_openai_parser_sanitize_output(
# Mock the raw response here since we want to make sure our service executes processing before loading the model
monkeypatch.setattr(OpenAIService, "_get_raw_response", mock_get_raw_response)
def mock_openai_init(self, repos):
from unittest.mock import MagicMock
self.repos = repos
self.custom_prompt_dir = None
self.default_provider = MagicMock()
self.audio_provider = None
self.image_provider = None
monkeypatch.setattr(OpenAIService, "__init__", mock_openai_init)
with session_context() as session:
loop = asyncio.get_event_loop()
parser = get_parser(RegisteredParser.openai, unique_local_group_id, session, get_locale_provider())

View File

@@ -49,7 +49,7 @@ def test_html_with_recipe_data():
url = "https://www.bbc.co.uk/food/recipes/healthy_pasta_bake_60759"
translator = get_locale_provider()
open_graph_strategy = RecipeScraperOpenGraph(url, translator)
open_graph_strategy = RecipeScraperOpenGraph(url, translator, None) # type: ignore[arg-type]
recipe_data = open_graph_strategy.get_recipe_fields(path.read_text())
@@ -78,7 +78,7 @@ def test_clean_scraper_preserves_notes():
html = RecipeScraperPackage.ld_json_to_html(ld_json)
scraped = scrape_html(html, org_url="https://example.com", supported_only=False)
translator = get_locale_provider()
strategy = RecipeScraperPackage("https://example.com", translator)
strategy = RecipeScraperPackage("https://example.com", translator, None) # type: ignore[arg-type]
recipe, _ = strategy.clean_scraper(scraped, "https://example.com")

View File

@@ -1,23 +1,28 @@
from unittest.mock import MagicMock
from uuid import uuid4
import pytest
import mealie.services.openai.openai as openai_module
from mealie.services.openai.openai import OpenAIService
def _make_mock_repos() -> MagicMock:
provider_settings = MagicMock()
provider_settings.ai_enabled = True
provider_settings.default_provider_id = uuid4()
provider_settings.audio_provider_id = None
provider_settings.image_provider_id = None
repos = MagicMock()
repos.group_id = uuid4()
repos.group_ai_provider_settings.get_one.return_value = provider_settings
repos.group_ai_providers.get_one.return_value = MagicMock()
return repos
class _SettingsStub:
OPENAI_ENABLED = True
OPENAI_MODEL = "gpt-4o"
OPENAI_AUDIO_MODEL = "whisper-1"
OPENAI_WORKERS = 1
OPENAI_SEND_DATABASE_DATA = False
OPENAI_ENABLE_IMAGE_SERVICES = True
OPENAI_ENABLE_TRANSCRIPTION_SERVICES = True
OPENAI_CUSTOM_PROMPT_DIR: str | None = None
OPENAI_BASE_URL: str | None = None
OPENAI_API_KEY = "dummy"
OPENAI_REQUEST_TIMEOUT = 30
OPENAI_CUSTOM_HEADERS: dict = {}
OPENAI_CUSTOM_PARAMS: dict = {}
@pytest.fixture()
@@ -39,7 +44,7 @@ def settings_stub(tmp_path, monkeypatch):
def test_get_prompt_default_only(settings_stub):
svc = OpenAIService()
svc = OpenAIService(_make_mock_repos())
out = svc.get_prompt("recipes.parse-recipe-ingredients")
assert out == "DEFAULT PROMPT"
@@ -51,7 +56,7 @@ def test_get_prompt_custom_dir_used(settings_stub, tmp_path):
settings_stub.OPENAI_CUSTOM_PROMPT_DIR = str(custom_dir)
svc = OpenAIService()
svc = OpenAIService(_make_mock_repos())
out = svc.get_prompt("recipes.parse-recipe-ingredients")
assert out == "CUSTOM PROMPT"
@@ -62,7 +67,7 @@ def test_get_prompt_custom_empty_falls_back_to_default(settings_stub, tmp_path):
(custom_dir / "recipes" / "parse-recipe-ingredients.txt").write_text("")
settings_stub.OPENAI_CUSTOM_PROMPT_DIR = str(custom_dir)
svc = OpenAIService()
svc = OpenAIService(_make_mock_repos())
out = svc.get_prompt("recipes.parse-recipe-ingredients")
assert out == "DEFAULT PROMPT"
@@ -73,7 +78,7 @@ def test_get_prompt_raises_when_no_files(settings_stub, monkeypatch):
for p in prompts_dir.rglob("*.txt"):
p.unlink()
svc = OpenAIService()
svc = OpenAIService(_make_mock_repos())
with pytest.raises(OSError) as ei:
svc.get_prompt("recipes.parse-recipe-ingredients")
assert "Unable to load prompt" in str(ei.value)