mistral_translator 0.3.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +75 -1
- data/README.md +14 -2
- data/docs/api-reference/configuration.md +44 -4
- data/docs/api-reference/errors.md +15 -24
- data/docs/api-reference/methods.md +10 -4
- data/docs/getting-started.md +10 -2
- data/docs/installation.md +0 -2
- data/docs/rails-integration/setup.md +0 -1
- data/lib/mistral_translator/client.rb +18 -19
- data/lib/mistral_translator/client_helpers.rb +41 -23
- data/lib/mistral_translator/configuration.rb +34 -4
- data/lib/mistral_translator/data/languages.json +912 -0
- data/lib/mistral_translator/deprecated_options.rb +45 -0
- data/lib/mistral_translator/helpers.rb +15 -17
- data/lib/mistral_translator/helpers_extensions.rb +1 -59
- data/lib/mistral_translator/locale_helper.rb +19 -31
- data/lib/mistral_translator/prompt.rb +48 -0
- data/lib/mistral_translator/prompt_builder.rb +30 -144
- data/lib/mistral_translator/prompt_helpers.rb +21 -23
- data/lib/mistral_translator/prompt_metadata_helpers.rb +15 -17
- data/lib/mistral_translator/proxy.rb +75 -0
- data/lib/mistral_translator/response_parser.rb +138 -215
- data/lib/mistral_translator/retries.rb +50 -0
- data/lib/mistral_translator/summarizer.rb +33 -146
- data/lib/mistral_translator/translator.rb +33 -95
- data/lib/mistral_translator/translator_helpers.rb +21 -149
- data/lib/mistral_translator/version.rb +1 -1
- data/lib/mistral_translator.rb +1 -1
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 22b02e4c378ffdd992bbb525c57a03fd15c4c82e2ee916c43a3753b09398c8e0
|
|
4
|
+
data.tar.gz: 1bdf19928ac211fbbc7e7fe064f600d53639c3b0aa57063b14b74fe3b95384c0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 67bb96d4142aa345f2feb2f1cf35ef30e131d529ad22338e9bd31c0c237d165a446b74e8ed75aa7715d903e486657250fb823e4add5994edade18d763fa50e2f
|
|
7
|
+
data.tar.gz: 39fcfd92f88aa10bb1ca930a8c9539a9d3e12ce7a8b4d1564d2cd0336ac5f349d9903334abec4a7f0d8fe8876a90d82738d8e6bc2a024bc91df5303c8f096588
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,78 @@
|
|
|
1
|
-
## [
|
|
1
|
+
## [0.5.0] - 2026-10-29
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- `config.read_timeout`: how long to wait for the API's answer, in seconds. Defaults to 60, the value the gem used until now; anything but a positive number raises `ConfigurationError` when it is set.
|
|
6
|
+
- `config.open_timeout`: how long to wait for the connection to open (TCP, then TLS), in seconds. Defaults to 60, validated the same way.
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
|
|
10
|
+
No change to translation results:
|
|
11
|
+
|
|
12
|
+
- Translations and summaries share one retry policy, with the same delays and counts as before. Their retry log lines now share one format, such as `translation retry 1/3 for fr->en in 2s: …`.
|
|
13
|
+
- The `Summarizer` no longer prints to stdout when `MISTRAL_TRANSLATOR_TEST_OUTPUT` is set.
|
|
14
|
+
- A response is parsed once instead of twice.
|
|
15
|
+
|
|
16
|
+
### Deprecated
|
|
17
|
+
|
|
18
|
+
These options still work, print a warning, and will be removed in 1.0:
|
|
19
|
+
|
|
20
|
+
- `config.ssl_timeout`, the former name of `open_timeout`, which it still sets. It is now validated like `open_timeout`, so `nil` (no limit) is rejected.
|
|
21
|
+
- `config.default_max_tokens` and `config.default_temperature`, which the gem never read.
|
|
22
|
+
|
|
23
|
+
### Removed
|
|
24
|
+
|
|
25
|
+
Undocumented code that the gem never called:
|
|
26
|
+
|
|
27
|
+
- the `HelpersExtensions` copies of `translate_with_progress`, `translate_multi_style` and `find_locale_suggestions`, which `Helpers` redefines;
|
|
28
|
+
- `TranslatorHelpers::RetryHandler`, `TranslatorHelpers::LoggingHelper` and `InputValidator#validate_batch_inputs!`;
|
|
29
|
+
- `PromptBuilder.bulk_translation_prompt`, `PromptBuilder.tiered_summary_prompt`, `PromptHelpers::FormatInstructions`, `ResponseParser.parse_bulk_translation_response` and `ResponseParser::JsonParserState`.
|
|
30
|
+
|
|
31
|
+
### Security
|
|
32
|
+
|
|
33
|
+
- When the scan for a JSON object inside text reached its limit, the error kept the `JSON::ParserError` as its `cause`, and that error quotes the response.
|
|
34
|
+
|
|
35
|
+
## [0.4.0] - 2026-10-28
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
These changes alter behaviour that callers may rely on:
|
|
40
|
+
|
|
41
|
+
- Unusable responses now raise instead of being returned. A response without JSON, with a target that is not text, or with a blank target raises `InvalidResponseError` or `EmptyTranslationError` after the usual retries. Previously the raw response text could be stored as the translation or the summary.
|
|
42
|
+
- `summarize_and_translate` no longer falls back to an untranslated summary in the source language; it retries like the other summaries, then raises.
|
|
43
|
+
- `translate_auto` raises `InvalidResponseError` when language detection is unreadable or names an unsupported language. It used to assume English, which returned the source text untranslated when the target was English.
|
|
44
|
+
- `PromptBuilder` methods return a `MistralTranslator::Prompt` (`instructions` and `text`) instead of a String. `Prompt#to_s` gives the previous single-text form.
|
|
45
|
+
- Prompts are sent as two messages: instructions in the system message, the text alone in the user message, between delimiters, with a rule telling the model to treat it as data.
|
|
46
|
+
- `Client#complete` takes `json: false`. The gem's own calls pass `json: true`, which sends `response_format: json_object`.
|
|
47
|
+
- 182 languages instead of 13: every ISO 639-1 code named by CLDR. `SUPPORTED_LANGUAGES` keeps the native names, unchanged for the original 13, and `supported_languages` returns a much longer string. Quality depends on the model and is weaker for rare languages.
|
|
48
|
+
- In `translate_batch` and the batch path of `translate_to_multiple`, an unreadable response fails its own item (`nil`) instead of the whole batch.
|
|
49
|
+
- `translate_auto`, `translate_to_multiple` and the summaries enforce the same 50,000 character limit as `translate`.
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- `config.proxy`: an `http://` proxy URL or URI, or `:env` to read `HTTPS_PROXY`, then `HTTP_PROXY`, honouring `NO_PROXY`. Disabled by default.
|
|
54
|
+
- `preserve_html:` in `translate_batch` and `translate_auto`.
|
|
55
|
+
- `context:` and `style:` in every summary method.
|
|
56
|
+
- English and French language names are accepted in place of codes (`"German"`, `"allemand"`), and locale suggestions are based on language names.
|
|
57
|
+
- `bin/generate_languages` regenerates `lib/mistral_translator/data/languages.json` from CLDR (macOS).
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
|
|
61
|
+
- `Helpers.smart_summarize` raised `ArgumentError` on every call.
|
|
62
|
+
- A translated text mentioning "rate limit" or "quota exceeded" was taken for a rate limit, retried, then failed. Only the 429 status counts now.
|
|
63
|
+
- Context and glossary were formatted three different ways; an empty glossary printed `GLOSSAIRE : {}` in quality check prompts, and the `has_context` / `has_glossary` metadata were never set.
|
|
64
|
+
- `translate_batch` silently ignored misspelled options.
|
|
65
|
+
- Locale suggestions returned unrelated codes once every ISO 639-1 language was supported.
|
|
66
|
+
|
|
67
|
+
### Security
|
|
68
|
+
|
|
69
|
+
- User text and API responses no longer reach logs or error messages: the verbose request log records sizes only, error messages carry no response content, errors no longer keep the original Ruby error as `cause`, and the error callback receives the sanitized error. The response stays available through `InvalidResponseError#raw_response`.
|
|
70
|
+
- Delimiters found in the text are neutralised so it cannot close its block, and `Prompt#inspect` shows lengths only.
|
|
71
|
+
- `https://` proxies are rejected: net-http-persistent talks to the proxy in clear, so their credentials would travel unencrypted.
|
|
72
|
+
|
|
73
|
+
### Removed
|
|
74
|
+
|
|
75
|
+
- `TranslatorHelpers::PromptHandler` and the test-only `make_request` fallback in `perform_client_request`.
|
|
2
76
|
|
|
3
77
|
## [0.3.2] - 2026-10-28
|
|
4
78
|
|
data/README.md
CHANGED
|
@@ -30,14 +30,16 @@ MistralTranslator.translate("Bonjour le monde", from: "fr", to: "en")
|
|
|
30
30
|
- **Native Rails integration** (Mobility, Globalize, I18n attributes)
|
|
31
31
|
- **Batch processing** for high-volume translations
|
|
32
32
|
- **Multi-level summarization** with translation
|
|
33
|
-
- **Robust error handling** with automatic
|
|
33
|
+
- **Robust error handling** with automatic retries; unusable responses raise instead of being returned
|
|
34
34
|
- **Complete monitoring** with metrics and callbacks
|
|
35
35
|
- **Thread-safe & concurrent** with connection pooling ([docs](https://peyochanchan.github.io/mistral_translator/#/advanced-usage/concurrent-async))
|
|
36
36
|
- **Built-in rate limiting** and security
|
|
37
37
|
|
|
38
38
|
## Supported Languages
|
|
39
39
|
|
|
40
|
-
`fr
|
|
40
|
+
All 182 ISO 639-1 languages named by CLDR (`fr`, `en`, `es`, `de`, `ja`, `br`, `wo`, `mg`…). `MistralTranslator.supported_locales` lists them, and English or French names are accepted in place of codes (`"German"`, `"allemand"`). The list and the names come from the CLDR data of macOS 27.2, through `bin/generate_languages` (2026-10-28).
|
|
41
|
+
|
|
42
|
+
Translation quality depends on the Mistral model: excellent for widely used languages, weaker for rare ones. Check the output before relying on a rare language.
|
|
41
43
|
|
|
42
44
|
## Installation
|
|
43
45
|
|
|
@@ -162,6 +164,11 @@ MistralTranslator.configure do |config|
|
|
|
162
164
|
config.model = "mistral-small-latest" # AI model to use (an alias that follows the current Mistral Small)
|
|
163
165
|
config.retry_delays = [1, 2, 4, 8, 16] # Retry delays in seconds
|
|
164
166
|
config.enable_metrics = true # Enable performance metrics
|
|
167
|
+
config.proxy = :env # HTTP proxy: nil (default), "http://host:port", or :env
|
|
168
|
+
# to read HTTPS_PROXY / HTTP_PROXY and NO_PROXY
|
|
169
|
+
config.open_timeout = 60 # Seconds to open the connection (default 60)
|
|
170
|
+
config.read_timeout = 60 # Seconds to wait for the API's answer (default 60);
|
|
171
|
+
# raise it for long texts or summaries
|
|
165
172
|
|
|
166
173
|
# Callbacks for monitoring
|
|
167
174
|
config.on_translation_complete = ->(from, to, orig_len, trans_len, duration) {
|
|
@@ -174,6 +181,8 @@ MistralTranslator.configure do |config|
|
|
|
174
181
|
end
|
|
175
182
|
```
|
|
176
183
|
|
|
184
|
+
Configure the gem before the first call. The module-level methods (`MistralTranslator.translate`, `summarize`…) share one client, which reads `api_key`, `model`, `api_url`, `retry_delays` and the connection settings (`read_timeout`, `proxy`, SSL) on the first call. To change them afterwards, call `MistralTranslator.reset_configuration!` and configure again, or create a `MistralTranslator::Translator.new` (or `Summarizer.new`), which builds its own client from the current configuration.
|
|
185
|
+
|
|
177
186
|
## Error Handling
|
|
178
187
|
|
|
179
188
|
```ruby
|
|
@@ -188,6 +197,9 @@ rescue MistralTranslator::AuthenticationError
|
|
|
188
197
|
rescue MistralTranslator::UnsupportedLanguageError => e
|
|
189
198
|
# Language not supported
|
|
190
199
|
Rails.logger.error "Language '#{e.language}' not supported"
|
|
200
|
+
rescue MistralTranslator::InvalidResponseError, MistralTranslator::EmptyTranslationError
|
|
201
|
+
# The model kept returning an unusable answer after the retries: nothing is returned
|
|
202
|
+
Rails.logger.error "Translation unavailable"
|
|
191
203
|
rescue MistralTranslator::Error => e
|
|
192
204
|
# General error handling
|
|
193
205
|
Rails.logger.error "Translation failed: #{e.message}"
|
|
@@ -13,13 +13,53 @@ MistralTranslator.configure do |config|
|
|
|
13
13
|
config.api_key = ENV['MISTRAL_API_KEY'] # Obligatoire
|
|
14
14
|
config.model = "mistral-small-latest" # Défaut
|
|
15
15
|
config.api_url = "https://api.mistral.ai" # URL API
|
|
16
|
-
config.default_max_tokens = 4000 # Limite tokens
|
|
17
|
-
config.default_temperature = 0.3 # Créativité (0-1)
|
|
18
16
|
config.retry_delays = [2, 4, 8, 16, 32] # Délais retry (secondes)
|
|
19
17
|
config.enable_metrics = false # Suivi des stats
|
|
18
|
+
config.proxy = nil # Proxy HTTP (désactivé par défaut)
|
|
19
|
+
config.open_timeout = 60 # Ouverture de la connexion (secondes)
|
|
20
|
+
config.read_timeout = 60 # Attente maximale de la réponse (secondes)
|
|
20
21
|
end
|
|
21
22
|
```
|
|
22
23
|
|
|
24
|
+
Configurez la gem avant le premier appel. Les méthodes du module (`MistralTranslator.translate`, `summarize`…) partagent un client, qui lit `api_key`, `model`, `api_url`, `retry_delays` et les réglages de connexion (`read_timeout`, `proxy`, SSL) lors du premier appel. Pour les changer ensuite, appelez `MistralTranslator.reset_configuration!` puis configurez à nouveau, ou créez un `MistralTranslator::Translator.new` (ou `Summarizer.new`), qui construit son propre client avec la configuration du moment.
|
|
25
|
+
|
|
26
|
+
## Délais
|
|
27
|
+
|
|
28
|
+
Deux délais, en secondes, 60 par défaut :
|
|
29
|
+
|
|
30
|
+
- `open_timeout` : ouverture de la connexion à l'API (TCP puis TLS) ;
|
|
31
|
+
- `read_timeout` : attente de la réponse. Un long texte ou un résumé détaillé peut demander plus au modèle ; augmentez-le dans ce cas.
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
config.read_timeout = 180
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- Seul un nombre strictement positif est accepté (entier ou décimal) ; toute autre valeur lève `ConfigurationError` dès l'affectation.
|
|
38
|
+
- Délai de réponse dépassé : la requête lève `ApiError` (« Request timeout »).
|
|
39
|
+
|
|
40
|
+
## Options obsolètes
|
|
41
|
+
|
|
42
|
+
Elles fonctionnent encore, avec un avertissement, et seront retirées en 1.0 :
|
|
43
|
+
|
|
44
|
+
- `ssl_timeout` : ancien nom d'`open_timeout`, qu'il règle toujours ;
|
|
45
|
+
- `default_max_tokens` et `default_temperature` : jamais lues par la gem, elles n'ont aucun effet. Pour régler ces paramètres, passez `max_tokens:` et `temperature:` à `Client#chat` ou `Client#complete`.
|
|
46
|
+
|
|
47
|
+
## Proxy
|
|
48
|
+
|
|
49
|
+
Désactivé par défaut. Trois valeurs possibles :
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
config.proxy = nil # Aucun proxy (défaut)
|
|
53
|
+
config.proxy = "http://user:password@proxy:3128" # Ce proxy (String ou URI)
|
|
54
|
+
config.proxy = :env # HTTPS_PROXY, puis HTTP_PROXY ; NO_PROXY respecté
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- Seuls les proxys `http://` sont acceptés. La bibliothèque HTTP utilisée parle toujours en clair au proxy : avec `https://`, les identifiants du proxy circuleraient sans chiffrement. Le flux vers l'API Mistral reste chiffré de bout en bout (tunnel `CONNECT` puis TLS).
|
|
58
|
+
- Avec `:env`, `NO_PROXY` suit les règles de curl : `*`, un domaine et ses sous-domaines (`mistral.ai`, `.mistral.ai`, `*.mistral.ai`), éventuellement avec le port 443.
|
|
59
|
+
- Une valeur invalide lève `ConfigurationError` dès l'affectation ; le message ne reprend jamais l'URL, pour ne pas exposer les identifiants.
|
|
60
|
+
- `api_url` reste verrouillée sur `https://api.mistral.ai`.
|
|
61
|
+
- Définissez le proxy avant la première traduction : le client et sa connexion sont créés une fois puis réutilisés.
|
|
62
|
+
|
|
23
63
|
## Callbacks
|
|
24
64
|
|
|
25
65
|
```ruby
|
|
@@ -63,9 +103,9 @@ config.setup_rails_logging # Active les logs Rails standard
|
|
|
63
103
|
MistralTranslator.health_check
|
|
64
104
|
# => { status: :ok, message: "API connection successful" }
|
|
65
105
|
|
|
66
|
-
# Langues supportées
|
|
106
|
+
# Langues supportées : les 182 codes ISO 639-1
|
|
67
107
|
MistralTranslator.supported_locales
|
|
68
|
-
# => ["
|
|
108
|
+
# => ["aa", "ab", "ae", "af", ..., "zh", "zu"]
|
|
69
109
|
```
|
|
70
110
|
|
|
71
111
|
---
|
|
@@ -210,8 +210,10 @@ end
|
|
|
210
210
|
|
|
211
211
|
**Attributs:**
|
|
212
212
|
|
|
213
|
-
- `message` (String) - Description de l'erreur
|
|
214
|
-
- `raw_response` (String, optionnel) - Réponse brute reçue
|
|
213
|
+
- `message` (String) - Description de l'erreur. Elle ne contient jamais la réponse de l'API, pour qu'aucun texte traduit ne finisse dans les journaux.
|
|
214
|
+
- `raw_response` (String, optionnel) - Réponse brute reçue, accessible au code appelant uniquement
|
|
215
|
+
|
|
216
|
+
L'erreur Ruby d'origine (`JSON::ParserError`, `NoMethodError`…) n'est pas conservée dans `cause` : son message cite la réponse.
|
|
215
217
|
|
|
216
218
|
**Causes courantes:**
|
|
217
219
|
|
|
@@ -321,13 +323,13 @@ end
|
|
|
321
323
|
|
|
322
324
|
### `MistralTranslator::SecurityError`
|
|
323
325
|
|
|
324
|
-
**
|
|
326
|
+
**Statut:** définie pour usage futur ; la gem ne la lève actuellement nulle part. Inutile de la rattraper.
|
|
325
327
|
|
|
326
|
-
|
|
328
|
+
Les contrôles de sécurité existants lèvent d'autres erreurs :
|
|
327
329
|
|
|
328
|
-
- Texte
|
|
329
|
-
-
|
|
330
|
-
-
|
|
330
|
+
- Texte de plus de 50 000 caractères : `ArgumentError` (traductions, détection automatique et résumés)
|
|
331
|
+
- Proxy invalide : `ConfigurationError`
|
|
332
|
+
- Instructions glissées dans le texte à traduire : rien n'est levé. Le texte est envoyé à part des consignes (message `user`), entre délimiteurs, avec une règle demandant au modèle de le traiter comme une donnée.
|
|
331
333
|
|
|
332
334
|
**Exemple:**
|
|
333
335
|
|
|
@@ -344,44 +346,33 @@ end
|
|
|
344
346
|
**Solutions:**
|
|
345
347
|
|
|
346
348
|
- Diviser les textes longs en plusieurs parties
|
|
347
|
-
- Valider le contenu avant traduction
|
|
348
349
|
- Utiliser `translate_batch` pour les lots importants
|
|
349
350
|
|
|
350
351
|
---
|
|
351
352
|
|
|
352
353
|
### `MistralTranslator::RateLimitExceededError`
|
|
353
354
|
|
|
354
|
-
**
|
|
355
|
+
**Statut:** définie pour usage futur ; la gem ne la lève actuellement nulle part.
|
|
355
356
|
|
|
356
357
|
**Attributs:**
|
|
357
358
|
|
|
358
359
|
- `wait_time` (Integer, optionnel) - Temps d'attente suggéré
|
|
359
360
|
- `retry_after` (Integer, optionnel) - Timestamp de retry
|
|
360
361
|
|
|
361
|
-
**
|
|
362
|
+
**Comportement réel des limites de débit:**
|
|
362
363
|
|
|
363
|
-
- `
|
|
364
|
-
-
|
|
365
|
-
|
|
366
|
-
**Exemple:**
|
|
364
|
+
- Limite locale (`Security::BasicRateLimiter`) : la requête **attend** qu'un créneau se libère, sans lever d'erreur.
|
|
365
|
+
- Limite de l'API Mistral (HTTP 429) : le client relance avec les délais de `config.retry_delays`, puis lève `RateLimitError`.
|
|
367
366
|
|
|
368
367
|
```ruby
|
|
369
|
-
# Configuration du rate limiter local
|
|
370
368
|
rate_limiter = MistralTranslator::Security::BasicRateLimiter.new(
|
|
371
369
|
max_requests: 10,
|
|
372
370
|
window_seconds: 60
|
|
373
371
|
)
|
|
374
|
-
|
|
375
372
|
client = MistralTranslator::Client.new(rate_limiter: rate_limiter)
|
|
376
373
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
15.times { client.complete("Hello") }
|
|
380
|
-
rescue MistralTranslator::RateLimitExceededError => e
|
|
381
|
-
puts "Local rate limit exceeded"
|
|
382
|
-
sleep(e.wait_time) if e.wait_time
|
|
383
|
-
retry
|
|
384
|
-
end
|
|
374
|
+
# Au-delà de 10 requêtes par minute, les appels attendent leur tour
|
|
375
|
+
15.times { client.complete("Hello") }
|
|
385
376
|
```
|
|
386
377
|
|
|
387
378
|
---
|
|
@@ -62,8 +62,13 @@ Traduit un texte avec détection automatique de la langue source.
|
|
|
62
62
|
- `to` (String|Symbol) - Code de langue cible
|
|
63
63
|
- `context` (String, optionnel) - Contexte pour la traduction
|
|
64
64
|
- `glossary` (Hash, optionnel) - Dictionnaire de termes
|
|
65
|
+
- `preserve_html` (Boolean, optionnel) - Préserve les balises HTML (défaut: false)
|
|
65
66
|
|
|
66
|
-
**Retour:** String - Le texte traduit
|
|
67
|
+
**Retour:** String - Le texte traduit (le texte d'origine si la langue détectée est déjà la langue cible)
|
|
68
|
+
|
|
69
|
+
**Exceptions:**
|
|
70
|
+
|
|
71
|
+
- `InvalidResponseError` - Détection illisible ou langue non supportée : aucune langue n'est supposée
|
|
67
72
|
|
|
68
73
|
**Exemple:**
|
|
69
74
|
|
|
@@ -118,8 +123,9 @@ Traduit plusieurs textes en une fois pour optimiser les performances.
|
|
|
118
123
|
- `to` (String|Symbol) - Code de langue cible
|
|
119
124
|
- `context` (String, optionnel) - Contexte commun pour tous les textes
|
|
120
125
|
- `glossary` (Hash, optionnel) - Dictionnaire de termes
|
|
126
|
+
- `preserve_html` (Boolean, optionnel) - Préserve les balises HTML (défaut: false)
|
|
121
127
|
|
|
122
|
-
**Retour:** Hash - Hash avec les index comme clés et traductions comme valeurs
|
|
128
|
+
**Retour:** Hash - Hash avec les index comme clés et traductions comme valeurs. Un élément dont la requête ou la réponse échoue vaut `nil`, sans faire échouer le lot.
|
|
123
129
|
|
|
124
130
|
**Exemple:**
|
|
125
131
|
|
|
@@ -292,7 +298,7 @@ Remet la configuration aux valeurs par défaut.
|
|
|
292
298
|
|
|
293
299
|
Retourne la liste des codes de langue supportés.
|
|
294
300
|
|
|
295
|
-
**Retour:** Array<String> - Liste des codes (ex: ["
|
|
301
|
+
**Retour:** Array<String> - Liste des 182 codes ISO 639-1 (ex: ["aa", "ab", ..., "zu"])
|
|
296
302
|
|
|
297
303
|
### `supported_languages`
|
|
298
304
|
|
|
@@ -399,7 +405,7 @@ Client HTTP pour l'API Mistral.
|
|
|
399
405
|
|
|
400
406
|
**Méthodes d'instance:**
|
|
401
407
|
|
|
402
|
-
- `complete(prompt, max_tokens: nil, temperature: nil, context: {})` - Complétion
|
|
408
|
+
- `complete(prompt, max_tokens: nil, temperature: nil, context: {}, json: false)` - Complétion ; `json: true` demande un objet JSON à l'API (`response_format: json_object`), le prompt devant décrire le JSON attendu
|
|
403
409
|
- `chat(prompt, max_tokens: nil, temperature: nil, context: {})` - Chat
|
|
404
410
|
- `translate_batch(requests, batch_size: 5)` - Traduction par lots
|
|
405
411
|
|
data/docs/getting-started.md
CHANGED
|
@@ -39,15 +39,23 @@ MistralTranslator.translate_auto("Guten Tag", to: "fr")
|
|
|
39
39
|
## 🌍 Langues Supportées
|
|
40
40
|
|
|
41
41
|
```ruby
|
|
42
|
-
#
|
|
42
|
+
# Les 182 langues ISO 639-1 sont disponibles
|
|
43
43
|
MistralTranslator::LocaleHelper.supported_locales
|
|
44
|
-
# => ["
|
|
44
|
+
# => ["aa", "ab", "ae", "af", ..., "zh", "zu"]
|
|
45
|
+
|
|
46
|
+
# Les noms anglais et français sont acceptés à la place du code
|
|
47
|
+
MistralTranslator::LocaleHelper.language_to_locale("allemand")
|
|
48
|
+
# => "de"
|
|
45
49
|
|
|
46
50
|
# Vérifier une langue spécifique
|
|
47
51
|
MistralTranslator::LocaleHelper.locale_supported?("ja")
|
|
48
52
|
# => true
|
|
49
53
|
```
|
|
50
54
|
|
|
55
|
+
La liste et les noms viennent des données CLDR de macOS 27.2, via `bin/generate_languages` (2026-10-28).
|
|
56
|
+
|
|
57
|
+
La qualité de traduction dépend du modèle Mistral : excellente pour les langues les plus parlées, plus faible pour les langues rares. Vérifiez le résultat avant de vous appuyer sur une langue rare.
|
|
58
|
+
|
|
51
59
|
## 🚨 Gestion d'Erreurs Simple
|
|
52
60
|
|
|
53
61
|
```ruby
|
data/docs/installation.md
CHANGED
|
@@ -79,8 +79,6 @@ MistralTranslator.configure do |config|
|
|
|
79
79
|
config.model = "mistral-small-latest" # Modèle par défaut
|
|
80
80
|
config.retry_delays = [2, 4, 8, 16] # Délais retry
|
|
81
81
|
config.enable_metrics = true # Suivi des stats
|
|
82
|
-
config.default_max_tokens = 4000 # Limite tokens
|
|
83
|
-
config.default_temperature = 0.3 # Créativité
|
|
84
82
|
end
|
|
85
83
|
```
|
|
86
84
|
|
|
@@ -272,7 +272,6 @@ MistralTranslator.configure do |config|
|
|
|
272
272
|
|
|
273
273
|
# Optimisation Rails
|
|
274
274
|
config.enable_metrics = true
|
|
275
|
-
config.default_max_tokens = 4000
|
|
276
275
|
|
|
277
276
|
# Cache intelligent des traductions
|
|
278
277
|
config.on_translation_complete = ->(from, to, orig_len, trans_len, duration) {
|
|
@@ -22,7 +22,9 @@ module MistralTranslator
|
|
|
22
22
|
@rate_limiter = rate_limiter || Security::BasicRateLimiter.new
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
# json: true demande un objet JSON à l'API (response_format json_object) ;
|
|
26
|
+
# le prompt doit alors décrire le JSON attendu.
|
|
27
|
+
def complete(prompt, max_tokens: nil, temperature: nil, context: {}, json: false)
|
|
26
28
|
# Vérifier le rate limit avant de faire la requête
|
|
27
29
|
@rate_limiter.wait_and_record!
|
|
28
30
|
|
|
@@ -30,14 +32,15 @@ module MistralTranslator
|
|
|
30
32
|
start_time = Time.now
|
|
31
33
|
trigger_translation_start_callback(ctx, prompt)
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
request_options = { max_tokens: max_tokens, temperature: temperature, json: json }
|
|
36
|
+
response = make_request_with_retry(prompt, request_options, ctx)
|
|
34
37
|
content = extract_content_from_response(response)
|
|
35
38
|
|
|
36
39
|
trigger_translation_complete_callback(ctx, prompt, content, start_time)
|
|
37
40
|
content
|
|
38
41
|
rescue JSON::ParserError => e
|
|
39
42
|
handle_json_parse_error(e, ctx)
|
|
40
|
-
rescue NoMethodError => e
|
|
43
|
+
rescue NoMethodError, TypeError => e
|
|
41
44
|
handle_response_structure_error(e, ctx)
|
|
42
45
|
end
|
|
43
46
|
|
|
@@ -49,14 +52,14 @@ module MistralTranslator
|
|
|
49
52
|
start_time = Time.now
|
|
50
53
|
trigger_translation_start_callback(ctx, prompt)
|
|
51
54
|
|
|
52
|
-
response = make_request_with_retry(prompt, max_tokens, temperature, ctx)
|
|
55
|
+
response = make_request_with_retry(prompt, { max_tokens: max_tokens, temperature: temperature }, ctx)
|
|
53
56
|
content = extract_chat_content_from_response(response)
|
|
54
57
|
|
|
55
58
|
trigger_translation_complete_callback(ctx, prompt, content, start_time)
|
|
56
59
|
content
|
|
57
60
|
rescue JSON::ParserError => e
|
|
58
61
|
handle_json_parse_error(e, ctx)
|
|
59
|
-
rescue NoMethodError => e
|
|
62
|
+
rescue NoMethodError, TypeError => e
|
|
60
63
|
handle_response_structure_error(e, ctx)
|
|
61
64
|
end
|
|
62
65
|
|
|
@@ -122,29 +125,25 @@ module MistralTranslator
|
|
|
122
125
|
)
|
|
123
126
|
end
|
|
124
127
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
error,
|
|
130
|
-
context[:attempt] || 0
|
|
131
|
-
)
|
|
132
|
-
message = if context[:operation] == :chat
|
|
133
|
-
"JSON parse error: #{error.message}"
|
|
134
|
-
else
|
|
135
|
-
"Invalid JSON in API response: #{error.message}"
|
|
136
|
-
end
|
|
137
|
-
raise InvalidResponseError, message
|
|
128
|
+
# Le message de JSON::ParserError, NoMethodError ou TypeError cite la réponse de l'API :
|
|
129
|
+
# ni lui ni l'erreur d'origine (cause) ne sont repris, et le callback reçoit l'erreur nettoyée.
|
|
130
|
+
def handle_json_parse_error(_error, context)
|
|
131
|
+
raise_invalid_response("Invalid JSON in API response", context)
|
|
138
132
|
end
|
|
139
133
|
|
|
140
134
|
def handle_response_structure_error(error, context)
|
|
135
|
+
raise_invalid_response("Invalid response structure (#{error.class})", context)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def raise_invalid_response(message, context)
|
|
139
|
+
error = InvalidResponseError.new(message)
|
|
141
140
|
MistralTranslator.configuration.trigger_translation_error(
|
|
142
141
|
context[:from_locale],
|
|
143
142
|
context[:to_locale],
|
|
144
143
|
error,
|
|
145
144
|
context[:attempt] || 0
|
|
146
145
|
)
|
|
147
|
-
raise
|
|
146
|
+
raise error, cause: nil
|
|
148
147
|
end
|
|
149
148
|
end
|
|
150
149
|
end
|
|
@@ -6,10 +6,10 @@ module MistralTranslator
|
|
|
6
6
|
module ClientHelpers
|
|
7
7
|
# Helper pour la gestion des requêtes HTTP
|
|
8
8
|
module RequestHandler
|
|
9
|
-
def make_request(prompt, max_tokens, temperature)
|
|
9
|
+
def make_request(prompt, max_tokens, temperature, json: false)
|
|
10
10
|
uri = URI("#{@base_uri}/v1/chat/completions")
|
|
11
11
|
|
|
12
|
-
request_body = build_request_body(prompt, max_tokens, temperature)
|
|
12
|
+
request_body = build_request_body(prompt, max_tokens, temperature, json: json)
|
|
13
13
|
|
|
14
14
|
request = Net::HTTP::Post.new(uri.path, headers)
|
|
15
15
|
request.body = request_body.to_json
|
|
@@ -22,18 +22,22 @@ module MistralTranslator
|
|
|
22
22
|
raise ApiError, "Request timeout: #{e.message}"
|
|
23
23
|
rescue Net::HTTPError => e
|
|
24
24
|
raise ApiError, "HTTP error: #{e.message}"
|
|
25
|
+
rescue Net::HTTP::Persistent::Error => e
|
|
26
|
+
# Proxy ou API injoignable ; le message d'origine peut citer l'adresse du proxy
|
|
27
|
+
raise ApiError, "Connection failed (#{e.class})", cause: nil
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
def http_pool
|
|
28
31
|
@http_pool ||= begin
|
|
29
32
|
pool = Net::HTTP::Persistent.new(name: "mistral_translator")
|
|
30
|
-
pool.read_timeout =
|
|
33
|
+
pool.read_timeout = MistralTranslator.configuration.read_timeout
|
|
31
34
|
pool.idle_timeout = 30
|
|
32
|
-
pool.open_timeout = MistralTranslator.configuration.
|
|
35
|
+
pool.open_timeout = MistralTranslator.configuration.open_timeout
|
|
33
36
|
pool.max_requests = 100
|
|
34
37
|
|
|
35
38
|
# SSL/TLS configuration
|
|
36
39
|
configure_ssl(pool)
|
|
40
|
+
configure_proxy(pool)
|
|
37
41
|
|
|
38
42
|
pool
|
|
39
43
|
end
|
|
@@ -56,18 +60,32 @@ module MistralTranslator
|
|
|
56
60
|
pool.ca_path = config.ssl_ca_path if config.ssl_ca_path
|
|
57
61
|
end
|
|
58
62
|
|
|
59
|
-
def
|
|
63
|
+
def configure_proxy(pool)
|
|
64
|
+
proxy_uri = Proxy.resolve(MistralTranslator.configuration.proxy, host: URI(@base_uri).host)
|
|
65
|
+
pool.proxy = proxy_uri if proxy_uri
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def build_request_body(prompt, max_tokens, temperature, json: false)
|
|
60
69
|
body = {
|
|
61
70
|
model: @model,
|
|
62
|
-
messages:
|
|
71
|
+
messages: request_messages(prompt)
|
|
63
72
|
}
|
|
64
73
|
|
|
65
74
|
body[:max_tokens] = max_tokens if max_tokens
|
|
66
75
|
body[:temperature] = temperature if temperature
|
|
76
|
+
# L'API garantit alors un objet JSON syntaxiquement valide
|
|
77
|
+
body[:response_format] = { type: "json_object" } if json
|
|
67
78
|
|
|
68
79
|
body
|
|
69
80
|
end
|
|
70
81
|
|
|
82
|
+
# Un Prompt sépare consignes et texte ; une String part en un seul message.
|
|
83
|
+
def request_messages(prompt)
|
|
84
|
+
return prompt.messages if prompt.respond_to?(:messages)
|
|
85
|
+
|
|
86
|
+
[{ role: "user", content: prompt }]
|
|
87
|
+
end
|
|
88
|
+
|
|
71
89
|
def headers
|
|
72
90
|
{
|
|
73
91
|
"Authorization" => "Bearer #{@api_key}",
|
|
@@ -79,15 +97,17 @@ module MistralTranslator
|
|
|
79
97
|
|
|
80
98
|
# Helper pour la gestion des erreurs et retry
|
|
81
99
|
module ErrorHandler
|
|
82
|
-
|
|
100
|
+
# request_options : max_tokens, temperature, json
|
|
101
|
+
def make_request_with_retry(prompt, request_options, context, attempt = 0)
|
|
83
102
|
context[:attempt] = attempt
|
|
84
|
-
response = make_request(prompt, max_tokens, temperature
|
|
103
|
+
response = make_request(prompt, request_options[:max_tokens], request_options[:temperature],
|
|
104
|
+
json: request_options.fetch(:json, false))
|
|
85
105
|
|
|
86
106
|
# Vérifier les erreurs dans la réponse
|
|
87
107
|
check_response_for_errors(response)
|
|
88
108
|
|
|
89
109
|
if rate_limit_exceeded?(response)
|
|
90
|
-
handle_rate_limit(prompt,
|
|
110
|
+
handle_rate_limit(prompt, request_options, context, attempt)
|
|
91
111
|
else
|
|
92
112
|
response
|
|
93
113
|
end
|
|
@@ -107,18 +127,13 @@ module MistralTranslator
|
|
|
107
127
|
end
|
|
108
128
|
end
|
|
109
129
|
|
|
130
|
+
# Seul le statut compte : le corps d'une réponse 200 contient le texte traduit,
|
|
131
|
+
# qui peut très bien parler de « rate limit ».
|
|
110
132
|
def rate_limit_exceeded?(response)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return false unless response.code.to_i == 200
|
|
114
|
-
|
|
115
|
-
body_content = response.body.to_s
|
|
116
|
-
return false if body_content.length > 1000
|
|
117
|
-
|
|
118
|
-
body_content.match?(/rate.?limit|quota.?exceeded/i)
|
|
133
|
+
response.code.to_i == 429
|
|
119
134
|
end
|
|
120
135
|
|
|
121
|
-
def handle_rate_limit(prompt,
|
|
136
|
+
def handle_rate_limit(prompt, request_options, context, attempt)
|
|
122
137
|
unless attempt < @retry_delays.length
|
|
123
138
|
raise RateLimitError, "API rate limit exceeded after #{@retry_delays.length} retries"
|
|
124
139
|
end
|
|
@@ -135,7 +150,7 @@ module MistralTranslator
|
|
|
135
150
|
|
|
136
151
|
log_rate_limit_retry(wait_time, attempt)
|
|
137
152
|
sleep(wait_time)
|
|
138
|
-
make_request_with_retry(prompt,
|
|
153
|
+
make_request_with_retry(prompt, request_options, context, attempt + 1)
|
|
139
154
|
end
|
|
140
155
|
end
|
|
141
156
|
|
|
@@ -153,7 +168,7 @@ module MistralTranslator
|
|
|
153
168
|
attempt: 0
|
|
154
169
|
}
|
|
155
170
|
|
|
156
|
-
result = complete(request[:prompt], context: context)
|
|
171
|
+
result = complete(request[:prompt], context: context, json: true)
|
|
157
172
|
results << {
|
|
158
173
|
success: true,
|
|
159
174
|
result: result,
|
|
@@ -175,10 +190,13 @@ module MistralTranslator
|
|
|
175
190
|
|
|
176
191
|
# Helper pour le logging
|
|
177
192
|
module LoggingHelper
|
|
193
|
+
# Jamais le contenu des messages : seulement le modèle et les tailles
|
|
178
194
|
def log_request_response(request_body, response)
|
|
179
|
-
|
|
180
|
-
Logger.debug_if_verbose(
|
|
181
|
-
|
|
195
|
+
sizes = request_body[:messages].map { |message| "#{message[:role]}=#{message[:content].to_s.length}" }
|
|
196
|
+
Logger.debug_if_verbose(
|
|
197
|
+
"Request sent to API: model=#{request_body[:model]} #{sizes.join(" ")}, response #{response.code}",
|
|
198
|
+
sensitive: false
|
|
199
|
+
)
|
|
182
200
|
end
|
|
183
201
|
|
|
184
202
|
def log_rate_limit_retry(wait_time, attempt)
|