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 +4 -4
- data/app/services/completion_kit/azure_foundry_client.rb +6 -2
- data/app/services/completion_kit/llm_client.rb +5 -1
- data/app/services/completion_kit/mcp_tools/datasets.rb +5 -6
- data/app/services/completion_kit/model_discovery_service.rb +14 -8
- data/app/services/completion_kit/ollama_client.rb +6 -2
- data/app/services/completion_kit/provider_endpoint.rb +41 -11
- data/lib/completion_kit/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3cd88afe200e8524d2cad09e6214612f645d9edb42ca0300c8c72c3dd2edf77c
|
|
4
|
+
data.tar.gz: 8873756fb01f18fa8405dc486e2f8d4bfbe4a11b37c6f1288ec80012e0a6bd18
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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 "
|
|
2
|
+
require "socket"
|
|
3
3
|
|
|
4
4
|
module CompletionKit
|
|
5
5
|
module ProviderEndpoint
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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? &&
|
|
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
|