mistral_translator 0.1.0 → 0.3.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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -0
  3. data/CONTRIBUTING.md +70 -0
  4. data/LICENSE.txt +6 -6
  5. data/README.md +212 -119
  6. data/README_TESTING.md +33 -0
  7. data/SECURITY.md +157 -0
  8. data/docs/.nojekyll +2 -0
  9. data/docs/404.html +30 -0
  10. data/docs/README.md +153 -0
  11. data/docs/advanced-usage/batch-processing.md +158 -0
  12. data/docs/advanced-usage/concurrent-async.md +270 -0
  13. data/docs/advanced-usage/error-handling.md +106 -0
  14. data/docs/advanced-usage/monitoring.md +133 -0
  15. data/docs/advanced-usage/summarization.md +86 -0
  16. data/docs/advanced-usage/translations.md +141 -0
  17. data/docs/api-reference/callbacks.md +231 -0
  18. data/docs/api-reference/configuration.md +74 -0
  19. data/docs/api-reference/errors.md +673 -0
  20. data/docs/api-reference/methods.md +539 -0
  21. data/docs/getting-started.md +179 -0
  22. data/docs/index.html +27 -0
  23. data/docs/installation.md +142 -0
  24. data/docs/migration-0.1.0-to-0.2.0.md +61 -0
  25. data/docs/rails-integration/adapters.md +84 -0
  26. data/docs/rails-integration/controllers.md +107 -0
  27. data/docs/rails-integration/jobs.md +97 -0
  28. data/docs/rails-integration/setup.md +339 -0
  29. data/examples/basic_usage.rb +129 -102
  30. data/examples/batch-job.rb +511 -0
  31. data/examples/monitoring-setup.rb +499 -0
  32. data/examples/rails-model.rb +399 -0
  33. data/lib/mistral_translator/adapters.rb +261 -0
  34. data/lib/mistral_translator/client.rb +103 -100
  35. data/lib/mistral_translator/client_helpers.rb +191 -0
  36. data/lib/mistral_translator/configuration.rb +191 -1
  37. data/lib/mistral_translator/errors.rb +16 -0
  38. data/lib/mistral_translator/helpers.rb +292 -0
  39. data/lib/mistral_translator/helpers_extensions.rb +150 -0
  40. data/lib/mistral_translator/levenshtein_helpers.rb +40 -0
  41. data/lib/mistral_translator/logger.rb +39 -8
  42. data/lib/mistral_translator/prompt_builder.rb +93 -41
  43. data/lib/mistral_translator/prompt_helpers.rb +83 -0
  44. data/lib/mistral_translator/prompt_metadata_helpers.rb +42 -0
  45. data/lib/mistral_translator/response_parser.rb +194 -23
  46. data/lib/mistral_translator/security.rb +72 -0
  47. data/lib/mistral_translator/summarizer.rb +41 -2
  48. data/lib/mistral_translator/translator.rb +174 -98
  49. data/lib/mistral_translator/translator_helpers.rb +268 -0
  50. data/lib/mistral_translator/version.rb +1 -1
  51. data/lib/mistral_translator.rb +51 -25
  52. metadata +55 -3
@@ -4,144 +4,147 @@ require "net/http"
4
4
  require "json"
5
5
  require "uri"
6
6
  require_relative "logger"
7
+ require_relative "client_helpers"
8
+ require_relative "security"
7
9
 
8
10
  module MistralTranslator
9
11
  class Client
10
- def initialize(api_key: nil)
12
+ include ClientHelpers::RequestHandler
13
+ include ClientHelpers::ErrorHandler
14
+ include ClientHelpers::BatchHandler
15
+ include ClientHelpers::LoggingHelper
16
+
17
+ def initialize(api_key: nil, rate_limiter: nil)
11
18
  @api_key = api_key || MistralTranslator.configuration.api_key!
12
19
  @base_uri = MistralTranslator.configuration.api_url
13
20
  @model = MistralTranslator.configuration.model
14
21
  @retry_delays = MistralTranslator.configuration.retry_delays
22
+ @rate_limiter = rate_limiter || Security::BasicRateLimiter.new
15
23
  end
16
24
 
17
- def complete(prompt, max_tokens: nil, temperature: nil)
18
- response = make_request_with_retry(prompt, max_tokens, temperature)
19
- parsed_response = JSON.parse(response.body)
25
+ def complete(prompt, max_tokens: nil, temperature: nil, context: {})
26
+ # Vérifier le rate limit avant de faire la requête
27
+ @rate_limiter.wait_and_record!
20
28
 
21
- content = parsed_response.dig("choices", 0, "message", "content")
22
- raise InvalidResponseError, "No content in API response" if content.nil? || content.empty?
29
+ ctx = (context || {}).merge(operation: :complete)
30
+ start_time = Time.now
31
+ trigger_translation_start_callback(ctx, prompt)
32
+
33
+ response = make_request_with_retry(prompt, max_tokens, temperature, ctx)
34
+ content = extract_content_from_response(response)
23
35
 
36
+ trigger_translation_complete_callback(ctx, prompt, content, start_time)
24
37
  content
25
38
  rescue JSON::ParserError => e
26
- raise InvalidResponseError, "Invalid JSON in API response: #{e.message}"
39
+ handle_json_parse_error(e, ctx)
27
40
  rescue NoMethodError => e
28
- raise InvalidResponseError, "Invalid response structure: #{e.message}"
41
+ handle_response_structure_error(e, ctx)
29
42
  end
30
43
 
31
- def chat(prompt, max_tokens: nil, temperature: nil)
32
- response = make_request_with_retry(prompt, max_tokens, temperature)
33
- parsed_response = JSON.parse(response.body)
44
+ def chat(prompt, max_tokens: nil, temperature: nil, context: {})
45
+ # Vérifier le rate limit avant de faire la requête
46
+ @rate_limiter.wait_and_record!
34
47
 
35
- parsed_response.dig("choices", 0, "message", "content")
48
+ ctx = (context || {}).merge(operation: :chat)
49
+ start_time = Time.now
50
+ trigger_translation_start_callback(ctx, prompt)
51
+
52
+ response = make_request_with_retry(prompt, max_tokens, temperature, ctx)
53
+ content = extract_chat_content_from_response(response)
54
+
55
+ trigger_translation_complete_callback(ctx, prompt, content, start_time)
56
+ content
36
57
  rescue JSON::ParserError => e
37
- raise InvalidResponseError, "JSON parse error: #{e.message}"
58
+ handle_json_parse_error(e, ctx)
38
59
  rescue NoMethodError => e
39
- raise InvalidResponseError, "Invalid response structure: #{e.message}"
60
+ handle_response_structure_error(e, ctx)
40
61
  end
41
62
 
42
- private
43
-
44
- def make_request_with_retry(prompt, max_tokens, temperature, attempt = 0)
45
- response = make_request(prompt, max_tokens, temperature)
46
-
47
- # Vérifier les erreurs dans la réponse
48
- check_response_for_errors(response)
49
-
50
- if rate_limit_exceeded?(response)
51
- handle_rate_limit(prompt, max_tokens, temperature, attempt)
52
- else
53
- response
63
+ # Nouvelle méthode pour traduction par batch optimisée
64
+ def translate_batch(requests, batch_size: 5)
65
+ start_time = Time.now
66
+ results = []
67
+ success_count = 0
68
+ error_count = 0
69
+
70
+ requests.each_slice(batch_size) do |batch|
71
+ batch_results = process_batch_slice(batch)
72
+ results.concat(batch_results[:results])
73
+ success_count += batch_results[:success_count]
74
+ error_count += batch_results[:error_count]
75
+
76
+ # Délai entre les batches pour éviter les rate limits
77
+ sleep(2) unless batch == requests.last(batch_size)
54
78
  end
55
- end
56
-
57
- def make_request(prompt, max_tokens, temperature)
58
- uri = URI("#{@base_uri}/v1/chat/completions")
59
-
60
- request_body = build_request_body(prompt, max_tokens, temperature)
61
79
 
62
- http = Net::HTTP.new(uri.host, uri.port)
63
- http.use_ssl = (uri.scheme == "https")
64
- http.read_timeout = 60 # 60 secondes de timeout
80
+ total_duration = Time.now - start_time
81
+ MistralTranslator.configuration.trigger_batch_complete(
82
+ requests.size,
83
+ total_duration,
84
+ success_count,
85
+ error_count
86
+ )
65
87
 
66
- request = Net::HTTP::Post.new(uri.path, headers)
67
- request.body = request_body.to_json
68
-
69
- response = http.request(request)
70
- log_request_response(request_body, response)
71
-
72
- response
73
- rescue Net::ReadTimeout, Timeout::Error => e
74
- raise ApiError, "Request timeout: #{e.message}"
75
- rescue Net::HTTPError => e
76
- raise ApiError, "HTTP error: #{e.message}"
88
+ results
77
89
  end
78
90
 
79
- def build_request_body(prompt, max_tokens, temperature)
80
- body = {
81
- model: @model,
82
- messages: [{ role: "user", content: prompt }]
83
- }
84
-
85
- body[:max_tokens] = max_tokens if max_tokens
86
- body[:temperature] = temperature if temperature
91
+ private
87
92
 
88
- body
93
+ def trigger_translation_start_callback(context, prompt)
94
+ MistralTranslator.configuration.trigger_translation_start(
95
+ context[:from_locale],
96
+ context[:to_locale],
97
+ prompt&.length || 0
98
+ )
89
99
  end
90
100
 
91
- def headers
92
- {
93
- "Authorization" => "Bearer #{@api_key}",
94
- "Content-Type" => "application/json",
95
- "User-Agent" => "mistral-translator-gem/#{MistralTranslator::VERSION}"
96
- }
97
- end
101
+ def extract_content_from_response(response)
102
+ parsed_response = JSON.parse(response.body)
103
+ content = parsed_response.dig("choices", 0, "message", "content")
104
+ raise InvalidResponseError, "No content in API response" if content.nil? || content.empty?
98
105
 
99
- def check_response_for_errors(response)
100
- case response.code.to_i
101
- when 401
102
- raise AuthenticationError, "Invalid API key"
103
- when 429
104
- # Rate limit sera géré séparément
105
- nil
106
- when 400..499
107
- raise ApiError, "Client error (#{response.code})}"
108
- when 500..599
109
- raise ApiError, "Server error (#{response.code})}"
110
- end
106
+ content
111
107
  end
112
108
 
113
- def rate_limit_exceeded?(response)
114
- return true if response.code.to_i == 429
115
-
116
- return false unless response.code.to_i == 200
117
-
118
- body_content = response.body.to_s
119
- return false if body_content.length > 1000
120
-
121
- body_content.match?(/rate.?limit|quota.?exceeded/i)
109
+ def extract_chat_content_from_response(response)
110
+ parsed_response = JSON.parse(response.body)
111
+ parsed_response.dig("choices", 0, "message", "content")
122
112
  end
123
113
 
124
- def handle_rate_limit(prompt, max_tokens, temperature, attempt)
125
- unless attempt < @retry_delays.length
126
- raise RateLimitError, "API rate limit exceeded after #{@retry_delays.length} retries"
127
- end
128
-
129
- wait_time = @retry_delays[attempt]
130
- log_rate_limit_retry(wait_time, attempt)
131
- sleep(wait_time)
132
- make_request_with_retry(prompt, max_tokens, temperature, attempt + 1)
114
+ def trigger_translation_complete_callback(context, prompt, content, start_time)
115
+ duration = Time.now - start_time
116
+ MistralTranslator.configuration.trigger_translation_complete(
117
+ context[:from_locale],
118
+ context[:to_locale],
119
+ prompt&.length || 0,
120
+ content&.length || 0,
121
+ duration
122
+ )
133
123
  end
134
124
 
135
- def log_request_response(_request_body, response)
136
- # Log seulement si mode verbose activé
137
- Logger.debug_if_verbose("Request sent to API", sensitive: true)
138
- Logger.debug_if_verbose("Response received: #{response.code}", sensitive: false)
125
+ def handle_json_parse_error(error, context)
126
+ MistralTranslator.configuration.trigger_translation_error(
127
+ context[:from_locale],
128
+ context[:to_locale],
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
139
138
  end
140
139
 
141
- def log_rate_limit_retry(wait_time, attempt)
142
- message = "Rate limit exceeded, retrying in #{wait_time} seconds (attempt #{attempt + 1})"
143
- # Log une seule fois par session pour éviter le spam
144
- Logger.warn_once(message, key: "rate_limit_retry", sensitive: false, ttl: 60)
140
+ def handle_response_structure_error(error, context)
141
+ MistralTranslator.configuration.trigger_translation_error(
142
+ context[:from_locale],
143
+ context[:to_locale],
144
+ error,
145
+ context[:attempt] || 0
146
+ )
147
+ raise InvalidResponseError, "Invalid response structure: #{error.message}"
145
148
  end
146
149
  end
147
150
  end
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http/persistent"
4
+
5
+ module MistralTranslator
6
+ module ClientHelpers
7
+ # Helper pour la gestion des requêtes HTTP
8
+ module RequestHandler
9
+ def make_request(prompt, max_tokens, temperature)
10
+ uri = URI("#{@base_uri}/v1/chat/completions")
11
+
12
+ request_body = build_request_body(prompt, max_tokens, temperature)
13
+
14
+ request = Net::HTTP::Post.new(uri.path, headers)
15
+ request.body = request_body.to_json
16
+
17
+ response = http_pool.request(uri, request)
18
+ log_request_response(request_body, response)
19
+
20
+ response
21
+ rescue Net::ReadTimeout, Timeout::Error => e
22
+ raise ApiError, "Request timeout: #{e.message}"
23
+ rescue Net::HTTPError => e
24
+ raise ApiError, "HTTP error: #{e.message}"
25
+ end
26
+
27
+ def http_pool
28
+ @http_pool ||= begin
29
+ pool = Net::HTTP::Persistent.new(name: "mistral_translator")
30
+ pool.read_timeout = 60
31
+ pool.idle_timeout = 30
32
+ pool.open_timeout = MistralTranslator.configuration.ssl_timeout
33
+ pool.max_requests = 100
34
+
35
+ # SSL/TLS configuration
36
+ configure_ssl(pool)
37
+
38
+ pool
39
+ end
40
+ end
41
+
42
+ def configure_ssl(pool)
43
+ config = MistralTranslator.configuration
44
+
45
+ # SSL verify mode
46
+ pool.verify_mode = case config.ssl_verify_mode
47
+ when :peer then OpenSSL::SSL::VERIFY_PEER
48
+ when :none then OpenSSL::SSL::VERIFY_NONE
49
+ else config.ssl_verify_mode
50
+ end
51
+
52
+ # Custom CA certificate file
53
+ pool.ca_file = config.ssl_ca_file if config.ssl_ca_file
54
+
55
+ # Custom CA certificates directory
56
+ pool.ca_path = config.ssl_ca_path if config.ssl_ca_path
57
+ end
58
+
59
+ def build_request_body(prompt, max_tokens, temperature)
60
+ body = {
61
+ model: @model,
62
+ messages: [{ role: "user", content: prompt }]
63
+ }
64
+
65
+ body[:max_tokens] = max_tokens if max_tokens
66
+ body[:temperature] = temperature if temperature
67
+
68
+ body
69
+ end
70
+
71
+ def headers
72
+ {
73
+ "Authorization" => "Bearer #{@api_key}",
74
+ "Content-Type" => "application/json",
75
+ "User-Agent" => "mistral-translator-gem/#{MistralTranslator::VERSION}"
76
+ }
77
+ end
78
+ end
79
+
80
+ # Helper pour la gestion des erreurs et retry
81
+ module ErrorHandler
82
+ def make_request_with_retry(prompt, max_tokens, temperature, context, attempt = 0)
83
+ context[:attempt] = attempt
84
+ response = make_request(prompt, max_tokens, temperature)
85
+
86
+ # Vérifier les erreurs dans la réponse
87
+ check_response_for_errors(response)
88
+
89
+ if rate_limit_exceeded?(response)
90
+ handle_rate_limit(prompt, max_tokens, temperature, context, attempt)
91
+ else
92
+ response
93
+ end
94
+ end
95
+
96
+ def check_response_for_errors(response)
97
+ case response.code.to_i
98
+ when 401
99
+ raise AuthenticationError, "Invalid API key"
100
+ when 429
101
+ # Rate limit sera géré séparément
102
+ nil
103
+ when 400..499
104
+ raise ApiError, "Client error (#{response.code})}"
105
+ when 500..599
106
+ raise ApiError, "Server error (#{response.code})}"
107
+ end
108
+ end
109
+
110
+ def rate_limit_exceeded?(response)
111
+ return true if response.code.to_i == 429
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)
119
+ end
120
+
121
+ def handle_rate_limit(prompt, max_tokens, temperature, context, attempt)
122
+ unless attempt < @retry_delays.length
123
+ raise RateLimitError, "API rate limit exceeded after #{@retry_delays.length} retries"
124
+ end
125
+
126
+ wait_time = @retry_delays[attempt]
127
+
128
+ # Trigger callback pour rate limit
129
+ MistralTranslator.configuration.trigger_rate_limit(
130
+ context[:from_locale],
131
+ context[:to_locale],
132
+ wait_time,
133
+ attempt + 1
134
+ )
135
+
136
+ log_rate_limit_retry(wait_time, attempt)
137
+ sleep(wait_time)
138
+ make_request_with_retry(prompt, max_tokens, temperature, context, attempt + 1)
139
+ end
140
+ end
141
+
142
+ # Helper pour la gestion des batches
143
+ module BatchHandler
144
+ def process_batch_slice(batch)
145
+ results = []
146
+ success_count = 0
147
+ error_count = 0
148
+
149
+ batch.each do |request|
150
+ context = {
151
+ from_locale: request[:from],
152
+ to_locale: request[:to],
153
+ attempt: 0
154
+ }
155
+
156
+ result = complete(request[:prompt], context: context)
157
+ results << {
158
+ success: true,
159
+ result: result,
160
+ original_request: request
161
+ }
162
+ success_count += 1
163
+ rescue StandardError => e
164
+ results << {
165
+ success: false,
166
+ error: e.message,
167
+ original_request: request
168
+ }
169
+ error_count += 1
170
+ end
171
+
172
+ { results: results, success_count: success_count, error_count: error_count }
173
+ end
174
+ end
175
+
176
+ # Helper pour le logging
177
+ module LoggingHelper
178
+ def log_request_response(request_body, response)
179
+ # Log seulement si mode verbose activé
180
+ Logger.debug_if_verbose("Request sent to API: #{request_body.to_json}", sensitive: true)
181
+ Logger.debug_if_verbose("Response received: #{response.code}", sensitive: false)
182
+ end
183
+
184
+ def log_rate_limit_retry(wait_time, attempt)
185
+ message = "Rate limit exceeded, retrying in #{wait_time} seconds (attempt #{attempt + 1})"
186
+ # Log une seule fois par session pour éviter le spam
187
+ Logger.warn_once(message, key: "rate_limit_retry", sensitive: false, ttl: 60)
188
+ end
189
+ end
190
+ end
191
+ end
@@ -2,7 +2,11 @@
2
2
 
3
3
  module MistralTranslator
4
4
  class Configuration
5
- attr_accessor :api_key, :api_url, :model, :default_max_tokens, :default_temperature, :retry_delays
5
+ attr_accessor :api_key, :model, :default_max_tokens, :default_temperature, :retry_delays,
6
+ :on_translation_start, :on_translation_complete, :on_translation_error,
7
+ :on_rate_limit, :on_batch_complete, :enable_metrics,
8
+ :ssl_verify_mode, :ssl_ca_file, :ssl_ca_path, :ssl_timeout
9
+ attr_reader :api_url
6
10
 
7
11
  def initialize
8
12
  @api_key = nil
@@ -11,6 +15,31 @@ module MistralTranslator
11
15
  @default_max_tokens = nil
12
16
  @default_temperature = nil
13
17
  @retry_delays = [2, 4, 8, 16, 32, 64, 128, 256, 512, 1024]
18
+
19
+ # Callbacks pour le monitoring et la customisation
20
+ @on_translation_start = nil
21
+ @on_translation_complete = nil
22
+ @on_translation_error = nil
23
+ @on_rate_limit = nil
24
+ @on_batch_complete = nil
25
+ @enable_metrics = false
26
+
27
+ # SSL/TLS configuration
28
+ @ssl_verify_mode = :peer # :peer, :none, or OpenSSL constant
29
+ @ssl_ca_file = nil # Path to CA certificate file
30
+ @ssl_ca_path = nil # Path to CA certificates directory
31
+ @ssl_timeout = 60 # SSL handshake timeout in seconds
32
+
33
+ # Métriques intégrées (thread-safe avec mutex)
34
+ @metrics_mutex = Mutex.new
35
+ @metrics = {
36
+ total_translations: 0,
37
+ total_characters: 0,
38
+ total_duration: 0.0,
39
+ rate_limits_hit: 0,
40
+ errors_count: 0,
41
+ translations_by_language: Hash.new(0)
42
+ }
14
43
  end
15
44
 
16
45
  def api_key!
@@ -21,6 +50,158 @@ module MistralTranslator
21
50
 
22
51
  @api_key
23
52
  end
53
+
54
+ def api_url=(url)
55
+ validate_api_url(url)
56
+ @api_url = url
57
+ end
58
+
59
+ # Méthodes pour les callbacks
60
+ def trigger_translation_start(from_locale, to_locale, text_length)
61
+ @on_translation_start&.call(from_locale, to_locale, text_length, Time.now)
62
+
63
+ return unless @enable_metrics
64
+
65
+ @metrics_mutex.synchronize do
66
+ @metrics[:total_translations] += 1
67
+ @metrics[:total_characters] += text_length
68
+ @metrics[:translations_by_language]["#{from_locale}->#{to_locale}"] += 1
69
+ end
70
+ end
71
+
72
+ def trigger_translation_complete(from_locale, to_locale, original_length, translated_length, duration)
73
+ @on_translation_complete&.call(from_locale, to_locale, original_length, translated_length, duration)
74
+
75
+ return unless @enable_metrics
76
+
77
+ @metrics_mutex.synchronize do
78
+ @metrics[:total_duration] += duration
79
+ end
80
+ end
81
+
82
+ def trigger_translation_error(from_locale, to_locale, error, attempt)
83
+ @on_translation_error&.call(from_locale, to_locale, error, attempt, Time.now)
84
+
85
+ return unless @enable_metrics
86
+
87
+ @metrics_mutex.synchronize do
88
+ @metrics[:errors_count] += 1
89
+ end
90
+ end
91
+
92
+ def trigger_rate_limit(from_locale, to_locale, wait_time, attempt)
93
+ @on_rate_limit&.call(from_locale, to_locale, wait_time, attempt, Time.now)
94
+
95
+ return unless @enable_metrics
96
+
97
+ @metrics_mutex.synchronize do
98
+ @metrics[:rate_limits_hit] += 1
99
+ end
100
+ end
101
+
102
+ def trigger_batch_complete(batch_size, total_duration, success_count, error_count)
103
+ @on_batch_complete&.call(batch_size, total_duration, success_count, error_count)
104
+ end
105
+
106
+ # Métriques
107
+ def metrics
108
+ return {} unless @enable_metrics
109
+
110
+ @metrics_mutex.synchronize do
111
+ @metrics.merge({
112
+ average_translation_time: if @metrics[:total_translations].positive?
113
+ (@metrics[:total_duration] / @metrics[:total_translations]).round(3)
114
+ else
115
+ 0
116
+ end,
117
+ average_characters_per_translation: if @metrics[:total_translations].positive?
118
+ (@metrics[:total_characters] /
119
+ @metrics[:total_translations]).round(0)
120
+ else
121
+ 0
122
+ end,
123
+ error_rate: if @metrics[:total_translations].positive?
124
+ ((@metrics[:errors_count].to_f / @metrics[:total_translations]) * 100).round(2)
125
+ else
126
+ 0
127
+ end
128
+ })
129
+ end
130
+ end
131
+
132
+ def reset_metrics!
133
+ @metrics_mutex.synchronize do
134
+ @metrics = {
135
+ total_translations: 0,
136
+ total_characters: 0,
137
+ total_duration: 0.0,
138
+ rate_limits_hit: 0,
139
+ errors_count: 0,
140
+ translations_by_language: Hash.new(0)
141
+ }
142
+ end
143
+ end
144
+
145
+ # Configuration helpers pour les callbacks les plus communs
146
+ def setup_rails_logging
147
+ return unless defined?(Rails)
148
+
149
+ @on_translation_start = lambda { |from, to, length, _timestamp|
150
+ Rails.logger.info "[MistralTranslator] Starting translation #{from}->#{to} (#{length} chars)"
151
+ }
152
+
153
+ @on_translation_complete = lambda { |from, to, _orig_len, _trans_len, duration|
154
+ Rails.logger.info "[MistralTranslator] Completed #{from}->#{to} in #{duration.round(2)}s"
155
+ }
156
+
157
+ @on_translation_error = lambda { |from, to, error, attempt, _timestamp|
158
+ Rails.logger.error "[MistralTranslator] Error #{from}->#{to} (attempt #{attempt}): #{error.message}"
159
+ }
160
+
161
+ @on_rate_limit = lambda { |from, to, wait_time, attempt, _timestamp|
162
+ Rails.logger.warn "[MistralTranslator] Rate limit #{from}->#{to}, waiting #{wait_time}s (attempt #{attempt})"
163
+ }
164
+ end
165
+
166
+ private
167
+
168
+ def validate_api_url(url)
169
+ uri = parse_uri(url)
170
+ validate_uri_scheme(uri, url)
171
+ validate_uri_host(uri)
172
+ validate_uri_path(uri)
173
+ validate_uri_port(uri)
174
+ end
175
+
176
+ def parse_uri(url)
177
+ require "uri"
178
+ URI.parse(url)
179
+ rescue URI::InvalidURIError
180
+ raise ConfigurationError, "Invalid API URL format: #{url}"
181
+ end
182
+
183
+ def validate_uri_scheme(uri, url)
184
+ raise ConfigurationError, "Invalid API URL format: #{url}" if uri.scheme.nil?
185
+ raise ConfigurationError, "API URL must use HTTPS protocol. Got: #{uri.scheme}" unless uri.scheme == "https"
186
+ end
187
+
188
+ def validate_uri_host(uri)
189
+ return if uri.host == "api.mistral.ai"
190
+
191
+ raise ConfigurationError, "Invalid API host. Expected 'api.mistral.ai', got: #{uri.host}"
192
+ end
193
+
194
+ def validate_uri_path(uri)
195
+ return if uri.path.nil? || uri.path.empty? || uri.path == "/"
196
+
197
+ raise ConfigurationError, "Invalid API path. Expected root path, got: #{uri.path}"
198
+ end
199
+
200
+ def validate_uri_port(uri)
201
+ return if uri.port.nil? || uri.port == 443
202
+
203
+ raise ConfigurationError, "Invalid API port. Expected 443 or default, got: #{uri.port}"
204
+ end
24
205
  end
25
206
 
26
207
  class << self
@@ -35,5 +216,14 @@ module MistralTranslator
35
216
  def reset_configuration!
36
217
  @configuration = Configuration.new
37
218
  end
219
+
220
+ # Méthodes utilitaires pour les métriques
221
+ def metrics
222
+ configuration.metrics
223
+ end
224
+
225
+ def reset_metrics!
226
+ configuration.reset_metrics!
227
+ end
38
228
  end
39
229
  end
@@ -50,4 +50,20 @@ module MistralTranslator
50
50
  super("Unsupported language: #{language}")
51
51
  end
52
52
  end
53
+
54
+ class SecurityError < Error
55
+ def initialize(message = "Security violation detected")
56
+ super
57
+ end
58
+ end
59
+
60
+ class RateLimitExceededError < Error
61
+ attr_reader :wait_time, :retry_after
62
+
63
+ def initialize(message = "Rate limit exceeded", wait_time: nil, retry_after: nil)
64
+ super(message)
65
+ @wait_time = wait_time
66
+ @retry_after = retry_after
67
+ end
68
+ end
53
69
  end