From c28e9cebda7b2cf2d0f5208f62bb425f1d6085de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C4=8Ciha=C5=99?= Date: Thu, 11 Jun 2026 16:49:41 +0200 Subject: [PATCH 1/2] feat(machinery): add Mistral integration This is just a thin wrapper on top of existing OpenAI integration as the API is compatible. See #11700 --- docs/changes.rst | 2 + docs/snippets/machines-autogenerated.rst | 50 +++++++++ weblate/machinery/defaults.py | 1 + weblate/machinery/forms.py | 52 ++++++++++ weblate/machinery/mistral.py | 28 +++++ weblate/machinery/tests.py | 127 +++++++++++++++++++++-- 6 files changed, 253 insertions(+), 7 deletions(-) create mode 100644 weblate/machinery/mistral.py diff --git a/docs/changes.rst b/docs/changes.rst index ae5463571c2f..3414d417e0c6 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -5,6 +5,8 @@ Weblate 2026.7 .. rubric:: New features +* Added :ref:`mt-mistral` machinery integration for Mistral LLM automatic suggestions. + .. rubric:: Improvements * Management interface access control is now more fine-grained with dedicated site-wide permissions. diff --git a/docs/snippets/machines-autogenerated.rst b/docs/snippets/machines-autogenerated.rst index 56f34291ef76..937705a2f7e0 100644 --- a/docs/snippets/machines-autogenerated.rst +++ b/docs/snippets/machines-autogenerated.rst @@ -689,6 +689,56 @@ You can also specify a custom category to use `custom translator `_ +.. AUTOGENERATED START: mistral +.. This section is automatically generated by `./manage.py list_machinery`. Do not edit manually. + +.. _mt-mistral: + +Mistral +------- + +.. versionadded:: 2026.7 + +:Service ID: ``mistral`` +:Maximal score: 90 +:Advanced features: * :ref:`glossary-mt` + * :ref:`llm-translation-context` +:Configuration: +---------------------------+--------------------------------+---------------------------------------------------------------------------------------------------------------------------+ + | ``source_language`` | Source language selection | Available choices: | + | | | | + | | | ``auto`` -- Automatic selection | + | | | | + | | | ``source`` -- Component source language | + | | | | + | | | ``secondary`` -- Secondary language defined in project or component | + +---------------------------+--------------------------------+---------------------------------------------------------------------------------------------------------------------------+ + | ``base_url`` | Mistral API base URL | Base URL of the Mistral API, if it differs from the Mistral default URL | + +---------------------------+--------------------------------+---------------------------------------------------------------------------------------------------------------------------+ + | ``model`` | Mistral model | Available choices: | + | | | | + | | | ``auto`` -- Automatic selection | + | | | | + | | | ``mistral-small-latest`` -- Mistral Small | + | | | | + | | | ``mistral-medium-latest`` -- Mistral Medium | + | | | | + | | | ``mistral-large-latest`` -- Mistral Large | + | | | | + | | | ``custom`` -- Custom model | + +---------------------------+--------------------------------+---------------------------------------------------------------------------------------------------------------------------+ + | ``persona`` | Translator persona | Describe the persona of translator to improve the accuracy of the translation. For example: “You are a squirrel breeder.” | + +---------------------------+--------------------------------+---------------------------------------------------------------------------------------------------------------------------+ + | ``style`` | Translator style | Describe the style of translation. For example: “Use informal language.” | + +---------------------------+--------------------------------+---------------------------------------------------------------------------------------------------------------------------+ + | ``language_instructions`` | Language-specific instructions | JSON object mapping existing target language codes to extra instructions, up to 1000 characters each. | + +---------------------------+--------------------------------+---------------------------------------------------------------------------------------------------------------------------+ + | ``key`` | API key | | + +---------------------------+--------------------------------+---------------------------------------------------------------------------------------------------------------------------+ + | ``custom_model`` | Custom model name | Only needed when model is set to 'Custom model' | + +---------------------------+--------------------------------+---------------------------------------------------------------------------------------------------------------------------+ + + +.. AUTOGENERATED END: mistral .. AUTOGENERATED START: modernmt .. This section is automatically generated by `./manage.py list_machinery`. Do not edit manually. diff --git a/weblate/machinery/defaults.py b/weblate/machinery/defaults.py index 8e5c0fe1fe45..b04212436e72 100644 --- a/weblate/machinery/defaults.py +++ b/weblate/machinery/defaults.py @@ -27,6 +27,7 @@ "weblate.machinery.youdao.YoudaoTranslation", "weblate.machinery.systran.SystranTranslation", "weblate.machinery.openai.OpenAITranslation", + "weblate.machinery.mistral.MistralTranslation", "weblate.machinery.ollama.OllamaTranslation", "weblate.machinery.openai.AzureOpenAITranslation", "weblate.machinery.weblatetm.WeblateTranslation", diff --git a/weblate/machinery/forms.py b/weblate/machinery/forms.py index fbe12a981ceb..a9b2296437af 100644 --- a/weblate/machinery/forms.py +++ b/weblate/machinery/forms.py @@ -585,6 +585,58 @@ def clean(self) -> None: super().clean() +class MistralMachineryForm(BaseOpenAIMachineryForm): + # Ordering choices here defines priority for automatic selection + MODEL_CHOICES = ( + ("auto", pgettext_lazy("Mistral model selection", "Automatic selection")), + ("mistral-small-latest", "Mistral Small"), + ("mistral-medium-latest", "Mistral Medium"), + ("mistral-large-latest", "Mistral Large"), + ("custom", pgettext_lazy("Mistral model selection", "Custom model")), + ) + base_url = WeblateServiceURLField( + label=pgettext_lazy( + "Automatic suggestion service configuration", + "Mistral API base URL", + ), + widget=forms.TextInput, + help_text=gettext_lazy( + "Base URL of the Mistral API, if it differs from the Mistral default URL" + ), + required=False, + ) + model = forms.ChoiceField( + label=pgettext_lazy( + "Automatic suggestion service configuration", + "Mistral model", + ), + initial="auto", + choices=MODEL_CHOICES, + ) + custom_model = forms.CharField( + label=pgettext_lazy( + "Mistral model selection", + "Custom model name", + ), + help_text=gettext_lazy("Only needed when model is set to 'Custom model'"), + required=False, + ) + + def clean(self) -> None: + """Validate custom_model and model fields.""" + has_custom_model = bool(self.cleaned_data.get("custom_model")) + model = self.cleaned_data.get("model") + if model == "custom" and not has_custom_model: + raise ValidationError( + {"custom_model": gettext("Missing custom model name.")} + ) + if model != "custom" and has_custom_model: + raise ValidationError( + {"model": gettext("Choose custom model here to enable it.")} + ) + super().clean() + + class AzureOpenAIMachineryForm(BaseOpenAIMachineryForm): azure_endpoint = WeblateServiceURLField( label=pgettext_lazy( diff --git a/weblate/machinery/mistral.py b/weblate/machinery/mistral.py new file mode 100644 index 000000000000..393d91fa6484 --- /dev/null +++ b/weblate/machinery/mistral.py @@ -0,0 +1,28 @@ +# Copyright © Michal Čihař +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations + +from typing import ClassVar + +from .forms import MistralMachineryForm +from .openai import OpenAITranslation + + +class MistralTranslation(OpenAITranslation): + """ + Mistral machine translation integration. + + Configurable machine translation interface that uses Mistral language + models through its OpenAI-compatible API. + """ + + name = "Mistral" + trusted_error_hosts: ClassVar[set[str]] = {"api.mistral.ai"} + + settings_form = MistralMachineryForm + version_added = "2026.7" + + def get_runtime_base_url(self) -> str: + return self.settings.get("base_url") or "https://api.mistral.ai/v1" diff --git a/weblate/machinery/tests.py b/weblate/machinery/tests.py index 93aa6d2d0be1..5271b70c867f 100644 --- a/weblate/machinery/tests.py +++ b/weblate/machinery/tests.py @@ -72,6 +72,7 @@ PROMPT, ) from weblate.machinery.microsoft import MicrosoftCognitiveTranslation +from weblate.machinery.mistral import MistralTranslation from weblate.machinery.modernmt import ModernMTTranslation from weblate.machinery.mymemory import MyMemoryTranslation from weblate.machinery.netease import NETEASE_API_ROOT, NeteaseSightTranslation @@ -3238,10 +3239,10 @@ def mock_models() -> None: "object": "list", "data": [ { - "id": "gpt-5-nano", + "id": OpenAITranslationTest.TRACE_MODEL, "object": "model", "created": 1686935002, - "owned_by": "openai", + "owned_by": "test", } ], }, @@ -5183,10 +5184,10 @@ def mock_response(self, content: str = '["Ahoj světe"]') -> None: "object": "list", "data": [ { - "id": "gpt-5-nano", + "id": self.TRACE_MODEL, "object": "model", "created": 1686935002, - "owned_by": "openai", + "owned_by": "test", } ], }, @@ -5274,10 +5275,10 @@ def test_runtime_url_validation_uses_proxy_settings( "object": "list", "data": [ { - "id": "gpt-5-nano", + "id": self.TRACE_MODEL, "object": "model", "created": 1686935002, - "owned_by": "openai", + "owned_by": "test", } ], }, @@ -5294,12 +5295,123 @@ def test_runtime_url_validation_uses_proxy_settings( }, ), ): - self.assertEqual(machine.get_model(), "gpt-5-nano") + self.assertEqual(machine.get_model(), self.TRACE_MODEL) mocked_getaddrinfo.assert_not_called() self.assertEqual(len(responses.calls), 1) +class MistralTranslationTest(OpenAITranslationTest): + MACHINE_CLS: type[BatchMachineTranslation] = MistralTranslation + CONFIGURATION: ClassVar[SettingsDict] = { + "key": "x", + "model": "auto", + "persona": "", + "style": "", + } + TRACE_MODEL: ClassVar[str] = "mistral-small-latest" + + @staticmethod + def mock_models() -> None: + responses.add( + responses.GET, + "https://api.mistral.ai/v1/models", + json={ + "object": "list", + "data": [ + { + "id": "mistral-small-latest", + "object": "model", + "created": 1686935002, + "owned_by": "mistral", + } + ], + }, + ) + + def mock_response(self, content: str = '["Ahoj světe"]') -> None: + self.mock_models() + responses.add( + responses.POST, + "https://api.mistral.ai/v1/chat/completions", + json={ + "id": "chatcmpl-123", + "object": "chat.completion", + "created": 1677652288, + "model": "mistral-small-latest", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": content, + }, + "finish_reason": "stop", + } + ], + "usage": { + "prompt_tokens": 9, + "completion_tokens": 12, + "total_tokens": 21, + }, + }, + ) + + +class MistralCustomTranslationTest(OpenAICustomTranslationTest): + MACHINE_CLS: type[BatchMachineTranslation] = MistralTranslation + CONFIGURATION: ClassVar[SettingsDict] = { + "key": "x", + "model": "auto", + "persona": "", + "style": "", + "base_url": "https://custom.example.com/", + } + TRACE_MODEL: ClassVar[str] = "mistral-small-latest" + + def mock_response(self, content: str = '["Ahoj světe"]') -> None: + responses.add( + responses.GET, + "https://custom.example.com/models", + json={ + "object": "list", + "data": [ + { + "id": "mistral-small-latest", + "object": "model", + "created": 1686935002, + "owned_by": "mistral", + } + ], + }, + ) + responses.add( + responses.POST, + "https://custom.example.com/chat/completions", + json={ + "id": "chatcmpl-123", + "object": "chat.completion", + "created": 1677652288, + "model": "mistral-small-latest", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": content, + }, + "finish_reason": "stop", + } + ], + "usage": { + "prompt_tokens": 9, + "completion_tokens": 12, + "total_tokens": 21, + }, + }, + ) + + class AzureOpenAITranslationTest(OpenAITranslationTest): MACHINE_CLS: type[BatchMachineTranslation] = AzureOpenAITranslation CONFIGURATION: ClassVar[SettingsDict] = { @@ -6327,6 +6439,7 @@ def test_machinery_third_party_data_annotations(self) -> None: LibreTranslateTranslation, LTEngineTranslation, MicrosoftCognitiveTranslation, + MistralTranslation, ModernMTTranslation, MyMemoryTranslation, NeteaseSightTranslation, From 45a58bad07bb6290167174dfa0a44b60f2245a80 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 15:01:03 +0000 Subject: [PATCH 2/2] docs: Documentation snippets update --- docs/snippets/addon-parameters-autogenerated.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/snippets/addon-parameters-autogenerated.rst b/docs/snippets/addon-parameters-autogenerated.rst index 51db6a67dbc8..5a59eb0ec187 100644 --- a/docs/snippets/addon-parameters-autogenerated.rst +++ b/docs/snippets/addon-parameters-autogenerated.rst @@ -41,6 +41,8 @@ Machine translation engines - LTEngine * - ``libretranslate`` - LibreTranslate + * - ``mistral`` + - Mistral * - ``modernmt`` - ModernMT * - ``mymemory``