mistral_translator 0.1.0 → 0.2.1

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