completion-kit 0.28.42 → 0.28.43

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9976b877524a1b4f64e96c78c3d5f158f1157c749902d27dcf37efe18c9bdb13
4
- data.tar.gz: bc2fd622095d7556b40fb2de3dcd3836ea47b77c4a784e9fd1e6c1fd0d5dc381
3
+ metadata.gz: 3cd88afe200e8524d2cad09e6214612f645d9edb42ca0300c8c72c3dd2edf77c
4
+ data.tar.gz: 8873756fb01f18fa8405dc486e2f8d4bfbe4a11b37c6f1288ec80012e0a6bd18
5
5
  SHA512:
6
- metadata.gz: 21b4460aea3386e6ecb936ef2a3303401929a9d26cba52e3fe9200efca47128625a4617f07f86539d43d9f700e3bba24d86f69a877921e128cf7ede75a5e1c7d
7
- data.tar.gz: 611a2a701c2db81edb16cee9ca7fa1d69e62b945ad07787cd2a979f33b762979abf94265150627c883a5bccd2c56b656b449b212eb1d0e3bde373c66cd5884c7
6
+ metadata.gz: 5b6f6806c9d3522221ecc2cb9249ed87082961712d1f8ed75860998e5774bc28693ea5dc0957d2a7ce371fb23f97578b87ca1b7ed181a8fe8d81e989682061c6
7
+ data.tar.gz: 57c412ed0623239f5f88a731ca46a70404ad4f4211bc7ff06ab612057522b23a6edd13ac6ae9dfde578e1ba22dc5eeda0ce5ae3b9ec3488391ba844475ff860d
@@ -4,7 +4,6 @@ module CompletionKit
4
4
  def generate_completion(prompt, options = {})
5
5
  @temperature_dropped = false
6
6
  return "Error: Azure provider is not fully configured" unless configured?
7
- return "Error: API endpoint resolves to a private address" unless ProviderEndpoint.safe?(api_endpoint)
8
7
 
9
8
  model = options[:model]
10
9
  max_tokens = options[:max_tokens] || 1000
@@ -47,13 +46,14 @@ module CompletionKit
47
46
  raise
48
47
  rescue Faraday::Error
49
48
  raise
49
+ rescue ProviderEndpoint::UnsafeEndpoint => e
50
+ "Error: API endpoint #{e.message}"
50
51
  rescue => e
51
52
  "Error: #{e.message}"
52
53
  end
53
54
 
54
55
  def available_models
55
56
  return [] unless configured?
56
- return [] unless ProviderEndpoint.safe?(api_endpoint)
57
57
 
58
58
  path = foundry_project? ? "#{azure_base_url}/deployments?api-version=v1" : models_path
59
59
  response = build_connection(azure_base_url).get(path) do |req|
@@ -88,6 +88,10 @@ module CompletionKit
88
88
 
89
89
  private
90
90
 
91
+ def attach_adapter(builder, url)
92
+ ProviderEndpoint.pin(builder, url)
93
+ end
94
+
91
95
  def api_key
92
96
  @config[:api_key]
93
97
  end
@@ -102,8 +102,12 @@ module CompletionKit
102
102
  f.options.timeout = timeout if timeout
103
103
  f.options.open_timeout = open_timeout if open_timeout
104
104
  f.request :retry, max: 2, interval: 0.5
105
- f.adapter Faraday.default_adapter
105
+ attach_adapter(f, url)
106
106
  end
107
107
  end
108
+
109
+ def attach_adapter(builder, _url)
110
+ builder.adapter Faraday.default_adapter
111
+ end
108
112
  end
109
113
  end
@@ -90,10 +90,7 @@ module CompletionKit
90
90
  end
91
91
 
92
92
  def self.create_from_url(args)
93
- issues = ProviderEndpoint.validate(args["url"])
94
- return error_result("URL is not allowed (#{issues.join(", ")}).") if issues.any?
95
-
96
- response = csv_connection.get(args["url"])
93
+ response = csv_connection(args["url"]).get(args["url"])
97
94
  return error_result("Could not download CSV (HTTP #{response.status}).") unless response.success?
98
95
 
99
96
  body = response.body.to_s
@@ -106,15 +103,17 @@ module CompletionKit
106
103
  else
107
104
  error_result(dataset.errors.full_messages.join(", "))
108
105
  end
106
+ rescue ProviderEndpoint::UnsafeEndpoint => e
107
+ error_result("URL is not allowed: it #{e.message}.")
109
108
  rescue Faraday::Error => e
110
109
  error_result("Could not download CSV: #{e.message}")
111
110
  end
112
111
 
113
- def self.csv_connection
112
+ def self.csv_connection(url)
114
113
  Faraday.new do |f|
115
114
  f.options.timeout = 30
116
115
  f.options.open_timeout = 5
117
- f.adapter Faraday.default_adapter
116
+ ProviderEndpoint.pin(f, url)
118
117
  end
119
118
  end
120
119
  end
@@ -40,14 +40,20 @@ module CompletionKit
40
40
  end
41
41
  end
42
42
 
43
- def fetch_connection(base_url)
43
+ def fetch_connection(base_url, pinned: false)
44
44
  Faraday.new(url: base_url) do |f|
45
45
  f.options.timeout = 15
46
46
  f.options.open_timeout = 5
47
- f.adapter Faraday.default_adapter
47
+ pinned ? pin_endpoint(f, base_url) : f.adapter(Faraday.default_adapter)
48
48
  end
49
49
  end
50
50
 
51
+ def pin_endpoint(builder, url)
52
+ ProviderEndpoint.pin(builder, url)
53
+ rescue ProviderEndpoint::UnsafeEndpoint => e
54
+ raise DiscoveryError, "The model endpoint #{e.message}."
55
+ end
56
+
51
57
  def raise_fetch_error!(response)
52
58
  label = case response.status
53
59
  when 401, 403 then "Invalid API key for #{@provider}"
@@ -122,7 +128,7 @@ module CompletionKit
122
128
  def fetch_ollama_models
123
129
  raise DiscoveryError, "A model endpoint URL is required." if @api_endpoint.blank?
124
130
  base_url = ollama_root_url
125
- response = fetch_connection(base_url).get("/v1/models") do |req|
131
+ response = fetch_connection(base_url, pinned: true).get("/v1/models") do |req|
126
132
  req.headers["Authorization"] = "Bearer #{@api_key}" if @api_key.present?
127
133
  end
128
134
  raise DiscoveryError, custom_endpoint_error_message(response) unless response.success?
@@ -154,7 +160,7 @@ module CompletionKit
154
160
  end
155
161
 
156
162
  def custom_endpoint_host
157
- ProviderEndpoint.parse(@api_endpoint)&.host || @api_endpoint.to_s
163
+ ProviderEndpoint.parse(@api_endpoint).host
158
164
  end
159
165
 
160
166
  def azure_custom_host?
@@ -170,7 +176,7 @@ module CompletionKit
170
176
  raise DiscoveryError, "An Azure endpoint URL is required." if @api_endpoint.blank?
171
177
 
172
178
  path = azure_foundry_project? ? "#{azure_base_url}/deployments?api-version=v1" : azure_models_path
173
- response = fetch_connection(azure_base_url).get(path) do |req|
179
+ response = fetch_connection(azure_base_url, pinned: true).get(path) do |req|
174
180
  req.headers["api-key"] = @api_key
175
181
  end
176
182
  raise DiscoveryError, azure_error_message(response) unless response.success?
@@ -185,7 +191,7 @@ module CompletionKit
185
191
  end
186
192
 
187
193
  def fetch_azure_catalog_count
188
- response = fetch_connection(azure_base_url).get("/openai/v1/models") do |req|
194
+ response = fetch_connection(azure_base_url, pinned: true).get("/openai/v1/models") do |req|
189
195
  req.headers["api-key"] = @api_key
190
196
  end
191
197
  return nil unless response.success?
@@ -464,7 +470,7 @@ module CompletionKit
464
470
  f.options.timeout = 60
465
471
  f.options.open_timeout = 5
466
472
  f.request :retry, max: 1, interval: 0.5
467
- f.adapter Faraday.default_adapter
473
+ pin_endpoint(f, ollama_root_url)
468
474
  end
469
475
  conn.post do |req|
470
476
  req.url "/v1/chat/completions"
@@ -479,7 +485,7 @@ module CompletionKit
479
485
  f.options.timeout = 60
480
486
  f.options.open_timeout = 5
481
487
  f.request :retry, max: 1, interval: 0.5
482
- f.adapter Faraday.default_adapter
488
+ pin_endpoint(f, azure_base_url)
483
489
  end
484
490
  response = azure_probe_post(conn, model_id, input, max_tokens, max_completion: false)
485
491
  if response.status == 400 && azure_max_tokens_unsupported?(response.body)
@@ -4,7 +4,6 @@ module CompletionKit
4
4
  def generate_completion(prompt, options = {})
5
5
  @temperature_dropped = false
6
6
  return "Error: API endpoint not configured" unless configured?
7
- return "Error: API endpoint resolves to a private address" unless ProviderEndpoint.safe?(api_endpoint)
8
7
 
9
8
  model = options[:model]
10
9
  max_tokens = options[:max_tokens] || 1000
@@ -36,13 +35,14 @@ module CompletionKit
36
35
  raise
37
36
  rescue Faraday::Error
38
37
  raise
38
+ rescue ProviderEndpoint::UnsafeEndpoint => e
39
+ "Error: API endpoint #{e.message}"
39
40
  rescue => e
40
41
  "Error: #{e.message}"
41
42
  end
42
43
 
43
44
  def available_models
44
45
  return [] unless configured?
45
- return [] unless ProviderEndpoint.safe?(api_endpoint)
46
46
 
47
47
  response = build_connection(api_endpoint).get("/v1/models") do |req|
48
48
  req.headers["Authorization"] = "Bearer #{api_key}" if api_key.present?
@@ -68,6 +68,10 @@ module CompletionKit
68
68
 
69
69
  private
70
70
 
71
+ def attach_adapter(builder, url)
72
+ ProviderEndpoint.pin(builder, url)
73
+ end
74
+
71
75
  def api_key
72
76
  @config[:api_key] || ENV["OLLAMA_API_KEY"]
73
77
  end
@@ -1,24 +1,41 @@
1
1
  require "ipaddr"
2
- require "resolv"
2
+ require "socket"
3
3
 
4
4
  module CompletionKit
5
5
  module ProviderEndpoint
6
- ZERO_NET = IPAddr.new("0.0.0.0/8").freeze
6
+ class UnsafeEndpoint < StandardError; end
7
+
8
+ BLOCKED_NETS = [IPAddr.new("0.0.0.0/8"), IPAddr.new("100.64.0.0/10")].freeze
9
+ ISSUE_MESSAGES = {
10
+ invalid_url: "is not a valid http or https URL",
11
+ unresolvable: "could not be resolved",
12
+ unsafe_host: "resolves to a private address"
13
+ }.freeze
7
14
 
8
15
  module_function
9
16
 
10
17
  def validate(url)
11
18
  uri = parse(url)
12
19
  return [:invalid_url] unless uri
13
- addrs = addresses(uri.host)
14
- return [:unresolvable] if addrs.empty?
15
- return [:unsafe_host] if addrs.any? { |ip| unsafe?(ip) }
16
- []
20
+ issues_for(addresses(uri.host))
17
21
  end
18
22
 
19
23
  def safe?(url)
20
- errors = validate(url)
21
- errors.empty? || errors == [:unresolvable]
24
+ validate(url).empty?
25
+ end
26
+
27
+ def pinned_address(url)
28
+ uri = parse(url)
29
+ raise UnsafeEndpoint, ISSUE_MESSAGES[:invalid_url] unless uri
30
+ addrs = addresses(uri.host)
31
+ issue = issues_for(addrs).first
32
+ raise UnsafeEndpoint, ISSUE_MESSAGES[issue] if issue
33
+ (addrs.find(&:ipv4?) || addrs.first).to_s
34
+ end
35
+
36
+ def pin(builder, url)
37
+ address = pinned_address(url)
38
+ builder.adapter(:net_http) { |http| http.ipaddr = address }
22
39
  end
23
40
 
24
41
  def parse(value)
@@ -28,18 +45,31 @@ module CompletionKit
28
45
  nil
29
46
  end
30
47
 
48
+ def issues_for(addrs)
49
+ return [:unresolvable] if addrs.empty?
50
+ return [:unsafe_host] if addrs.any? { |ip| unsafe?(ip) }
51
+ []
52
+ end
53
+
31
54
  def addresses(host)
32
- bare = host.delete_prefix("[").delete_suffix("]")
55
+ bare = host.delete_prefix("[").delete_suffix("]").delete_suffix(".")
33
56
  [IPAddr.new(bare)]
34
57
  rescue IPAddr::InvalidAddressError
35
- Resolv.getaddresses(host).map { |addr| IPAddr.new(addr) }
58
+ resolve(bare).map { |addr| IPAddr.new(addr) }
59
+ end
60
+
61
+ def resolve(host)
62
+ Addrinfo.getaddrinfo(host, nil, nil, :STREAM).map(&:ip_address).uniq
63
+ rescue SocketError
64
+ []
36
65
  end
37
66
 
38
67
  def unsafe?(ip)
68
+ ip = ip.native
39
69
  return true if ip.private?
40
70
  return true if ip.link_local?
41
71
  return true if ip.to_i.zero?
42
- return true if ip.ipv4? && ZERO_NET.include?(ip)
72
+ return true if ip.ipv4? && BLOCKED_NETS.any? { |net| net.include?(ip) }
43
73
  return true if ip.loopback? && !CompletionKit.config.allow_loopback_endpoints
44
74
  false
45
75
  end
@@ -1,3 +1,3 @@
1
1
  module CompletionKit
2
- VERSION = "0.28.42"
2
+ VERSION = "0.28.43"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: completion-kit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.28.42
4
+ version: 0.28.43
5
5
  platform: ruby
6
6
  authors:
7
7
  - Damien Bastin