mistral_translator 0.3.1 → 0.4.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.
@@ -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,6 +22,9 @@ 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
@@ -34,6 +37,7 @@ module MistralTranslator
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 build_request_body(prompt, max_tokens, temperature)
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: [{ role: "user", content: prompt }]
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
- def make_request_with_retry(prompt, max_tokens, temperature, context, attempt = 0)
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, max_tokens, temperature, context, attempt)
110
+ handle_rate_limit(prompt, request_options, context, attempt)
91
111
  else
92
112
  response
93
113
  end
@@ -101,24 +121,19 @@ module MistralTranslator
101
121
  # Rate limit sera géré séparément
102
122
  nil
103
123
  when 400..499
104
- raise ApiError, "Client error (#{response.code})}"
124
+ raise ApiError, "Client error (#{response.code})"
105
125
  when 500..599
106
- raise ApiError, "Server error (#{response.code})}"
126
+ raise ApiError, "Server error (#{response.code})"
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
- 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)
133
+ response.code.to_i == 429
119
134
  end
120
135
 
121
- def handle_rate_limit(prompt, max_tokens, temperature, context, attempt)
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, max_tokens, temperature, context, attempt + 1)
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
- # 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)
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)
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "proxy"
4
+
3
5
  module MistralTranslator
4
6
  class Configuration
5
7
  attr_accessor :api_key, :model, :default_max_tokens, :default_temperature, :retry_delays,
6
8
  :on_translation_start, :on_translation_complete, :on_translation_error,
7
9
  :on_rate_limit, :on_batch_complete, :enable_metrics,
8
10
  :ssl_verify_mode, :ssl_ca_file, :ssl_ca_path, :ssl_timeout
9
- attr_reader :api_url
11
+ attr_reader :api_url, :proxy
10
12
 
11
13
  def initialize
12
14
  @api_key = nil
@@ -30,6 +32,9 @@ module MistralTranslator
30
32
  @ssl_ca_path = nil # Path to CA certificates directory
31
33
  @ssl_timeout = 60 # SSL handshake timeout in seconds
32
34
 
35
+ # Proxy : nil (aucun), URI ou String, ou :env (HTTPS_PROXY, HTTP_PROXY, NO_PROXY)
36
+ @proxy = nil
37
+
33
38
  # Métriques intégrées (thread-safe avec mutex)
34
39
  @metrics_mutex = Mutex.new
35
40
  @metrics = {
@@ -56,6 +61,11 @@ module MistralTranslator
56
61
  @api_url = url
57
62
  end
58
63
 
64
+ def proxy=(value)
65
+ Proxy.validate!(value)
66
+ @proxy = value
67
+ end
68
+
59
69
  # Méthodes pour les callbacks
60
70
  def trigger_translation_start(from_locale, to_locale, text_length)
61
71
  @on_translation_start&.call(from_locale, to_locale, text_length, Time.now)