completion-kit 0.25.3 → 0.26.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 522b556370ce964401d721cfca911d3eeed34c73d4f00a10fd817d72aea6ebd9
4
- data.tar.gz: 376659f68600f29d4b5a8d5a659b106154794b1c21fce13419115199061f99b5
3
+ metadata.gz: 4c870f728fb1ea2e6cb3eb0319a5ff3102b23d6aec1d2bf6023215e61d9e8896
4
+ data.tar.gz: 12dcb5ff03f4efa4d61ac761d213252f9eb29c12f90619147da2daec6e16876f
5
5
  SHA512:
6
- metadata.gz: 2f4d28f7545dee426f9ecf540a8249a5559c60c75624dae1649c0cf5a9a32eeeb37d3ce0e82d90c3749859bd593aec9c6f874a8284a0b3635ea7b60b4197baae
7
- data.tar.gz: 547840aee8d6b9d2ca7495ab9d716116ca5aba721503c6b4d71570789587af94ed0c7d5afe5c73d853362e1e6dbabd583e2042731002336ef63e1b703312db84
6
+ metadata.gz: 53238e3533ba8fb70bbca85287367a2caf409ab65fb544d04f78c00c5a98ce2938caabcb5ccc75e75f0dfa45a84959aaaabbcd69675f7c152936b125cbcf189f
7
+ data.tar.gz: 289b3034cf9a8dfc28792303790afffa37d89b852bed6009efc40184b42ab6e74d6f8095e42f4e9204babf45781ac5d5c07e7ccfdb4891ebb568b442ab351a5d
@@ -30,8 +30,11 @@ module CompletionKit
30
30
  end
31
31
 
32
32
  def destroy
33
- @credential.destroy!
34
- head :no_content
33
+ if @credential.destroy
34
+ head :no_content
35
+ else
36
+ render_validation_errors(@credential)
37
+ end
35
38
  end
36
39
 
37
40
  private
@@ -43,7 +46,7 @@ module CompletionKit
43
46
  end
44
47
 
45
48
  def credential_params
46
- params.permit(:provider, :api_key, :api_endpoint)
49
+ params.permit(:provider, :api_key, :api_endpoint, :api_version)
47
50
  end
48
51
  end
49
52
  end
@@ -1,6 +1,6 @@
1
1
  module CompletionKit
2
2
  class ProviderCredentialsController < ApplicationController
3
- before_action :set_provider_credential, only: [:edit, :update, :refresh]
3
+ before_action :set_provider_credential, only: [:edit, :update, :refresh, :destroy]
4
4
 
5
5
  def index
6
6
  @provider_credentials = ProviderCredential.order(:provider)
@@ -36,6 +36,14 @@ module CompletionKit
36
36
  end
37
37
  end
38
38
 
39
+ def destroy
40
+ if @provider_credential.destroy
41
+ redirect_to provider_credentials_path, notice: "#{@provider_credential.display_provider} provider was removed."
42
+ else
43
+ redirect_to provider_credentials_path, alert: @provider_credential.errors.full_messages.to_sentence
44
+ end
45
+ end
46
+
39
47
  def refresh
40
48
  @provider_credential.update_columns(discovery_status: "discovering", discovery_current: 0, discovery_total: 0)
41
49
  @provider_credential.reload
@@ -62,7 +70,7 @@ module CompletionKit
62
70
  end
63
71
 
64
72
  def provider_credential_params
65
- params.require(:provider_credential).permit(:provider, :api_key, :api_endpoint)
73
+ params.require(:provider_credential).permit(:provider, :api_key, :api_endpoint, :api_version)
66
74
  end
67
75
  end
68
76
  end
@@ -18,6 +18,9 @@ module CompletionKit
18
18
  discard_on ActiveJob::DeserializationError
19
19
 
20
20
  rescue_from(StandardError) do |error|
21
+ if error.is_a?(CompletionKit::ModelDiscoveryService::DiscoveryError)
22
+ Rails.error.report(error, handled: true, context: { job: self.class.name, provider_credential_id: arguments.first })
23
+ end
21
24
  credential = ProviderCredential.find(arguments.first)
22
25
  credential.update_columns(discovery_status: "failed", discovery_error: error.message.to_s.truncate(500))
23
26
  credential.reload
@@ -1,19 +1,20 @@
1
1
  module CompletionKit
2
2
  class ProviderCredential < ApplicationRecord
3
3
  include Turbo::Broadcastable
4
- PROVIDERS = %w[openai anthropic ollama openrouter].freeze
4
+ PROVIDERS = %w[openai anthropic ollama openrouter azure_foundry].freeze
5
5
  PROVIDER_LABELS = {
6
6
  "openai" => "OpenAI",
7
7
  "anthropic" => "Anthropic",
8
8
  "ollama" => "Ollama / OpenAI-compatible endpoint",
9
- "openrouter" => "OpenRouter"
9
+ "openrouter" => "OpenRouter",
10
+ "azure_foundry" => "Azure AI Foundry"
10
11
  }.freeze
11
12
 
12
13
  encrypts :api_key
13
14
 
14
15
  def as_json(options = {})
15
16
  {
16
- id: id, provider: provider, api_endpoint: api_endpoint,
17
+ id: id, provider: provider, api_endpoint: api_endpoint, api_version: api_version,
17
18
  created_at: created_at, updated_at: updated_at
18
19
  }
19
20
  end
@@ -24,15 +25,24 @@ module CompletionKit
24
25
 
25
26
  validates :provider, presence: true, inclusion: { in: PROVIDERS }
26
27
  validates :provider, tenant_scoped_uniqueness: true
28
+ validates :api_endpoint, presence: true, if: :azure_foundry?
29
+ validates :api_version, presence: true, if: :azure_foundry?
27
30
  validate :api_endpoint_not_internal
28
31
 
29
32
  after_save :enqueue_discovery
33
+ before_destroy :ensure_not_in_use, prepend: true
34
+ after_destroy :destroy_discovered_models
35
+
36
+ def azure_foundry?
37
+ provider == "azure_foundry"
38
+ end
30
39
 
31
40
  def config_hash
32
41
  {
33
42
  provider: provider,
34
43
  api_key: api_key,
35
- api_endpoint: api_endpoint
44
+ api_endpoint: api_endpoint,
45
+ api_version: api_version
36
46
  }.compact
37
47
  end
38
48
 
@@ -78,6 +88,17 @@ module CompletionKit
78
88
  .maximum(:created_at)
79
89
  end
80
90
 
91
+ def in_use?
92
+ prompt_count.positive? || judge_count.positive?
93
+ end
94
+
95
+ def in_use_message
96
+ parts = []
97
+ parts << "#{prompt_count} #{'prompt'.pluralize(prompt_count)}" if prompt_count.positive?
98
+ parts << "#{judge_count} judge #{'run'.pluralize(judge_count)}" if judge_count.positive?
99
+ "#{display_provider} is still in use by #{parts.to_sentence}. Remove those references before deleting it."
100
+ end
101
+
81
102
  def broadcast_discovery_progress
82
103
  safely_broadcast do
83
104
  broadcast_replace_to(
@@ -108,6 +129,17 @@ module CompletionKit
108
129
 
109
130
  private
110
131
 
132
+ def ensure_not_in_use
133
+ return unless in_use?
134
+
135
+ errors.add(:base, in_use_message)
136
+ throw :abort
137
+ end
138
+
139
+ def destroy_discovered_models
140
+ Model.where(provider: provider).delete_all
141
+ end
142
+
111
143
  def enqueue_discovery
112
144
  update_columns(discovery_status: "discovering", discovery_current: 0, discovery_total: 0)
113
145
  ModelDiscoveryJob.perform_later(id)
@@ -1,6 +1,6 @@
1
1
  module CompletionKit
2
2
  class ApiConfig
3
- PROVIDERS = %w[openai anthropic ollama openrouter].freeze
3
+ PROVIDERS = %w[openai anthropic ollama openrouter azure_foundry].freeze
4
4
 
5
5
  def self.for_model(model_name)
6
6
  provider = provider_for_model(model_name)
@@ -0,0 +1,109 @@
1
+ module CompletionKit
2
+ class AzureFoundryClient < LlmClient
3
+ def temperature_dropped?
4
+ @temperature_dropped == true
5
+ end
6
+
7
+ def generate_completion(prompt, options = {})
8
+ @temperature_dropped = false
9
+ return "Error: Azure provider is not fully configured" unless configured?
10
+ return "Error: API endpoint resolves to a private address" unless ProviderEndpoint.safe?(api_endpoint)
11
+
12
+ model = options[:model]
13
+ max_tokens = options[:max_tokens] || 1000
14
+ temperature = options[:temperature] || 0.7
15
+
16
+ response = post_chat(model: model, prompt: prompt, max_tokens: max_tokens, temperature: temperature)
17
+
18
+ if response.status == 400 && temperature_unsupported?(response.body)
19
+ @temperature_dropped = true
20
+ response = post_chat(model: model, prompt: prompt, max_tokens: max_tokens, temperature: nil)
21
+ end
22
+
23
+ if response.status == 429
24
+ raise CompletionKit::RateLimitError.new(
25
+ response.body.to_s.truncate(500),
26
+ provider: "azure_foundry",
27
+ status: 429,
28
+ retry_after: response.headers["Retry-After"]&.to_i
29
+ )
30
+ end
31
+
32
+ if response.success?
33
+ data = JSON.parse(response.body)
34
+ content = data.dig("choices", 0, "message", "content").to_s.strip
35
+ return "Error: model returned empty content" if content.empty?
36
+ content
37
+ else
38
+ "Error: #{response.status} - #{response.body}"
39
+ end
40
+ rescue CompletionKit::RateLimitError
41
+ raise
42
+ rescue Faraday::Error
43
+ raise
44
+ rescue => e
45
+ "Error: #{e.message}"
46
+ end
47
+
48
+ def available_models
49
+ return [] unless configured?
50
+ return [] unless ProviderEndpoint.safe?(api_endpoint)
51
+
52
+ response = build_connection(azure_base_url).get("/openai/deployments?api-version=#{api_version}") do |req|
53
+ req.headers["api-key"] = api_key
54
+ end
55
+ return [] unless response.success?
56
+
57
+ JSON.parse(response.body).fetch("data", []).map { |entry| { id: entry["id"], name: entry["id"] } }
58
+ rescue StandardError
59
+ []
60
+ end
61
+
62
+ def configured?
63
+ configuration_errors.empty?
64
+ end
65
+
66
+ def configuration_errors
67
+ errors = []
68
+ errors << "Azure endpoint is not configured" if api_endpoint.blank?
69
+ errors << "Azure API key is not configured" if api_key.blank?
70
+ errors << "Azure api-version is not configured" if api_version.blank?
71
+ errors
72
+ end
73
+
74
+ private
75
+
76
+ def api_key
77
+ @config[:api_key]
78
+ end
79
+
80
+ def api_endpoint
81
+ @config[:api_endpoint]
82
+ end
83
+
84
+ def api_version
85
+ @config[:api_version]
86
+ end
87
+
88
+ def azure_base_url
89
+ api_endpoint.to_s.strip.delete_suffix("/")
90
+ end
91
+
92
+ def post_chat(model:, prompt:, max_tokens:, temperature:)
93
+ body = { messages: [{ role: "user", content: prompt }], max_tokens: max_tokens }
94
+ body[:temperature] = temperature unless temperature.nil?
95
+
96
+ build_connection(azure_base_url, timeout: 30, open_timeout: 5).post do |req|
97
+ req.url "/openai/deployments/#{model}/chat/completions?api-version=#{api_version}"
98
+ req.headers["Content-Type"] = "application/json"
99
+ req.headers["api-key"] = api_key
100
+ req.body = body.to_json
101
+ end
102
+ end
103
+
104
+ def temperature_unsupported?(body)
105
+ s = body.to_s
106
+ s.include?("temperature") && (s.include?("deprecated") || s.include?("not supported") || s.include?("Unsupported parameter"))
107
+ end
108
+ end
109
+ end
@@ -34,6 +34,8 @@ module CompletionKit
34
34
  OllamaClient.new(config)
35
35
  when "openrouter"
36
36
  OpenRouterClient.new(config)
37
+ when "azure_foundry"
38
+ AzureFoundryClient.new(config)
37
39
  else
38
40
  raise ArgumentError, "Unsupported provider: #{provider_name}"
39
41
  end
@@ -19,9 +19,10 @@ module CompletionKit
19
19
  inputSchema: {
20
20
  type: "object",
21
21
  properties: {
22
- provider: {type: "string", enum: ["openai", "anthropic", "ollama", "openrouter"]},
22
+ provider: {type: "string", enum: ["openai", "anthropic", "ollama", "openrouter", "azure_foundry"]},
23
23
  api_key: {type: "string"},
24
- api_endpoint: {type: "string"}
24
+ api_endpoint: {type: "string"},
25
+ api_version: {type: "string"}
25
26
  },
26
27
  required: ["provider", "api_key"]
27
28
  },
@@ -33,7 +34,7 @@ module CompletionKit
33
34
  type: "object",
34
35
  properties: {
35
36
  id: {type: "integer"}, provider: {type: "string"},
36
- api_key: {type: "string"}, api_endpoint: {type: "string"}
37
+ api_key: {type: "string"}, api_endpoint: {type: "string"}, api_version: {type: "string"}
37
38
  },
38
39
  required: ["id"]
39
40
  },
@@ -55,7 +56,7 @@ module CompletionKit
55
56
  end
56
57
 
57
58
  def self.create(args)
58
- credential = ProviderCredential.new(args.slice("provider", "api_key", "api_endpoint"))
59
+ credential = ProviderCredential.new(args.slice("provider", "api_key", "api_endpoint", "api_version"))
59
60
  if credential.save
60
61
  text_result(credential.as_json)
61
62
  else
@@ -65,7 +66,7 @@ module CompletionKit
65
66
 
66
67
  def self.update(args)
67
68
  credential = ProviderCredential.find(args["id"])
68
- if credential.update(args.except("id").slice("provider", "api_key", "api_endpoint"))
69
+ if credential.update(args.except("id").slice("provider", "api_key", "api_endpoint", "api_version"))
69
70
  text_result(credential.as_json)
70
71
  else
71
72
  error_result(credential.errors.full_messages.join(", "))
@@ -73,8 +74,12 @@ module CompletionKit
73
74
  end
74
75
 
75
76
  def self.delete(args)
76
- ProviderCredential.find(args["id"]).destroy!
77
- text_result("Provider credential #{args["id"]} deleted")
77
+ credential = ProviderCredential.find(args["id"])
78
+ if credential.destroy
79
+ text_result("Provider credential #{args["id"]} deleted")
80
+ else
81
+ error_result(credential.errors.full_messages.join(", "))
82
+ end
78
83
  end
79
84
  end
80
85
  end
@@ -6,18 +6,18 @@ module CompletionKit
6
6
  class ModelDiscoveryService
7
7
  class DiscoveryError < StandardError; end
8
8
 
9
+ AZURE_HOST_SUFFIXES = [".openai.azure.com", ".services.ai.azure.com"].freeze
10
+
9
11
  def initialize(config:)
10
12
  @provider = config[:provider]
11
13
  @api_key = config[:api_key]
12
14
  @api_endpoint = config[:api_endpoint]
15
+ @api_version = config[:api_version]
13
16
  end
14
17
 
15
18
  def refresh!(force: false, &on_progress)
16
19
  discovered = fetch_models
17
20
  reconcile(discovered)
18
- # OpenRouter publishes capability metadata (output modalities, etc.), so we
19
- # derive everything from the model list and skip live probing entirely.
20
- # Judging stays unknown ("?") until a real run proves it.
21
21
  return if @provider == "openrouter"
22
22
 
23
23
  reset_failed_generation if force
@@ -32,6 +32,7 @@ module CompletionKit
32
32
  when "anthropic" then fetch_anthropic_models
33
33
  when "openrouter" then fetch_openrouter_models
34
34
  when "ollama" then fetch_ollama_models
35
+ when "azure_foundry" then fetch_azure_foundry_models
35
36
  else []
36
37
  end
37
38
  end
@@ -106,19 +107,72 @@ module CompletionKit
106
107
  end
107
108
 
108
109
  def fetch_ollama_models
109
- raise DiscoveryError, "Ollama endpoint URL is required" if @api_endpoint.blank?
110
+ raise DiscoveryError, "A model endpoint URL is required." if @api_endpoint.blank?
110
111
  base_url = ollama_root_url
111
112
  response = fetch_connection(base_url).get("/v1/models") do |req|
112
113
  req.headers["Authorization"] = "Bearer #{@api_key}" if @api_key.present?
113
114
  end
114
- raise_fetch_error!(response) unless response.success?
115
+ raise DiscoveryError, custom_endpoint_error_message(response) unless response.success?
115
116
  JSON.parse(response.body).fetch("data", []).map { |e| { id: e["id"], display_name: e["id"] } }
116
117
  end
117
118
 
119
+ def custom_endpoint_error_message(response)
120
+ detail = extract_provider_error_message(response.body)
121
+ case response.status
122
+ when 401, 403
123
+ with_detail("The endpoint rejected the API key (#{response.status}).", detail)
124
+ when 404
125
+ custom_endpoint_404_message
126
+ when 429
127
+ "The endpoint rate-limited the model-list request (429). Try again shortly."
128
+ else
129
+ with_detail("The endpoint at #{custom_endpoint_host} did not return an OpenAI-compatible model list at /v1/models (#{response.status}).", detail)
130
+ end
131
+ end
132
+
133
+ def custom_endpoint_404_message
134
+ message = "No OpenAI-compatible model list was found at #{custom_endpoint_host}/v1/models (404). Check that the base URL is correct."
135
+ return message unless azure_custom_host?
136
+ "#{message} This looks like an Azure endpoint; add it with the Azure AI Foundry provider, which uses an api-version and an api-key header."
137
+ end
138
+
139
+ def with_detail(message, detail)
140
+ detail.present? ? "#{message} #{detail}" : message
141
+ end
142
+
143
+ def custom_endpoint_host
144
+ ProviderEndpoint.parse(@api_endpoint)&.host || @api_endpoint.to_s
145
+ end
146
+
147
+ def azure_custom_host?
148
+ host = custom_endpoint_host.to_s.downcase
149
+ AZURE_HOST_SUFFIXES.any? { |suffix| host.end_with?(suffix) }
150
+ end
151
+
118
152
  def ollama_root_url
119
153
  @api_endpoint.to_s.strip.delete_suffix("/").delete_suffix("/v1")
120
154
  end
121
155
 
156
+ def fetch_azure_foundry_models
157
+ raise DiscoveryError, "An Azure endpoint URL is required." if @api_endpoint.blank?
158
+ raise DiscoveryError, "An Azure api-version is required." if @api_version.blank?
159
+
160
+ response = fetch_connection(azure_base_url).get("/openai/deployments?api-version=#{@api_version}") do |req|
161
+ req.headers["api-key"] = @api_key
162
+ end
163
+ raise DiscoveryError, azure_error_message(response) unless response.success?
164
+ JSON.parse(response.body).fetch("data", []).map { |e| { id: e["id"], display_name: e["id"] } }
165
+ end
166
+
167
+ def azure_base_url
168
+ @api_endpoint.to_s.strip.delete_suffix("/")
169
+ end
170
+
171
+ def azure_error_message(response)
172
+ detail = extract_provider_error_message(response.body)
173
+ with_detail("Azure did not return a deployments list at /openai/deployments (#{response.status}). Check the endpoint base URL and api-version.", detail)
174
+ end
175
+
122
176
  def reconcile(discovered)
123
177
  api_model_ids = discovered.map { |m| m[:id] }
124
178
  meta_by_id = discovered.index_by { |m| m[:id] }
@@ -150,7 +204,7 @@ module CompletionKit
150
204
  supports_generation = meta[:supports_generation] != false
151
205
  attrs.merge!(
152
206
  supports_generation: supports_generation,
153
- supports_judging: nil,
207
+ supports_judging: supports_generation,
154
208
  probed_at: Time.current,
155
209
  status: supports_generation ? "active" : "failed"
156
210
  )
@@ -163,14 +217,13 @@ module CompletionKit
163
217
 
164
218
  def reconcile_existing_model(model, meta)
165
219
  if @provider == "openrouter"
166
- # Re-derive generation capability from the published metadata every refresh
167
- # (fixes models discovered before capability metadata was used). Leave
168
- # supports_judging alone — it's "learned" from successful runs.
169
220
  supports_generation = meta[:supports_generation] != false
170
221
  model.update!(
171
222
  display_name: meta[:display_name].presence || model.display_name,
172
223
  supports_generation: supports_generation,
224
+ supports_judging: supports_generation,
173
225
  generation_error: nil,
226
+ judging_error: nil,
174
227
  probed_at: Time.current,
175
228
  status: supports_generation ? "active" : "failed",
176
229
  retired_at: nil
@@ -287,6 +340,7 @@ module CompletionKit
287
340
  when "openai" then openai_probe(model_id, input, max_tokens)
288
341
  when "anthropic" then anthropic_probe(model_id, input, max_tokens)
289
342
  when "ollama" then ollama_probe(model_id, input, max_tokens)
343
+ when "azure_foundry" then azure_foundry_probe(model_id, input, max_tokens)
290
344
  else raise ArgumentError, "Unsupported probe provider: #{@provider}"
291
345
  end
292
346
  end
@@ -366,5 +420,20 @@ module CompletionKit
366
420
  req.body = { model: model_id, messages: [{ role: "user", content: input }], max_tokens: max_tokens }.to_json
367
421
  end
368
422
  end
423
+
424
+ def azure_foundry_probe(model_id, input, max_tokens)
425
+ conn = Faraday.new(url: azure_base_url) do |f|
426
+ f.options.timeout = 60
427
+ f.options.open_timeout = 5
428
+ f.request :retry, max: 1, interval: 0.5
429
+ f.adapter Faraday.default_adapter
430
+ end
431
+ conn.post do |req|
432
+ req.url "/openai/deployments/#{model_id}/chat/completions?api-version=#{@api_version}"
433
+ req.headers["Content-Type"] = "application/json"
434
+ req.headers["api-key"] = @api_key
435
+ req.body = { messages: [{ role: "user", content: input }], max_tokens: max_tokens }.to_json
436
+ end
437
+ end
369
438
  end
370
439
  end
@@ -24,11 +24,31 @@
24
24
 
25
25
  <div class="ck-field">
26
26
  <%= form.label :api_endpoint, "API endpoint", class: "ck-label" %>
27
- <%= form.text_field :api_endpoint, class: "ck-input", placeholder: "Only needed for Ollama or custom OpenAI-compatible endpoints", **ck_field_aria(form, :api_endpoint) %>
27
+ <%= form.text_field :api_endpoint, class: "ck-input", placeholder: "Needed for Ollama, Azure AI Foundry, or custom OpenAI-compatible endpoints", **ck_field_aria(form, :api_endpoint) %>
28
28
  <%= ck_field_error(form, :api_endpoint) %>
29
29
  </div>
30
30
 
31
+ <div class="ck-field">
32
+ <%= form.label :api_version, "API version", class: "ck-label" %>
33
+ <%= form.text_field :api_version, class: "ck-input", placeholder: "Only needed for Azure AI Foundry (e.g. 2024-10-21)", **ck_field_aria(form, :api_version) %>
34
+ <%= ck_field_error(form, :api_version) %>
35
+ </div>
36
+
37
+ <% if provider_credential.persisted? && provider_credential.in_use? %>
38
+ <p class="ck-field-hint"><%= provider_credential.in_use_message %></p>
39
+ <% end %>
40
+
31
41
  <div class="ck-actions">
42
+ <% if provider_credential.persisted? && !provider_credential.in_use? %>
43
+ <%= button_to provider_credential_path(provider_credential), method: :delete,
44
+ form_class: "inline-block",
45
+ class: "ck-icon-btn",
46
+ title: "Delete provider",
47
+ "aria-label": "Delete provider",
48
+ data: { turbo_confirm: "Delete the #{provider_credential.display_provider} provider and its discovered models? This can't be undone." } do %>
49
+ <%= heroicon_tag "trash", variant: :outline, size: 16, "aria-hidden": "true" %>
50
+ <% end %>
51
+ <% end %>
32
52
  <%= link_to "Cancel", provider_credentials_path, class: ck_button_classes(:light, variant: :outline), tabindex: "0" %>
33
53
  <%= form.submit(provider_credential.persisted? ? "Save provider" : "Create provider", class: ck_button_classes(:dark)) %>
34
54
  </div>
data/config/routes.rb CHANGED
@@ -51,7 +51,7 @@ CompletionKit::Engine.routes.draw do
51
51
  end
52
52
  end
53
53
 
54
- resources :provider_credentials, only: [:index, :new, :create, :edit, :update] do
54
+ resources :provider_credentials, only: [:index, :new, :create, :edit, :update, :destroy] do
55
55
  post :refresh, on: :member
56
56
  get :statuses, on: :collection
57
57
  end
@@ -0,0 +1,5 @@
1
+ class AddApiVersionToCompletionKitProviderCredentials < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :completion_kit_provider_credentials, :api_version, :string
4
+ end
5
+ end
@@ -1,3 +1,3 @@
1
1
  module CompletionKit
2
- VERSION = "0.25.3"
2
+ VERSION = "0.26.0"
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.25.3
4
+ version: 0.26.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Damien Bastin
@@ -300,6 +300,7 @@ files:
300
300
  - app/services/completion_kit/agreement_math.rb
301
301
  - app/services/completion_kit/anthropic_client.rb
302
302
  - app/services/completion_kit/api_config.rb
303
+ - app/services/completion_kit/azure_foundry_client.rb
303
304
  - app/services/completion_kit/checks/contains.rb
304
305
  - app/services/completion_kit/checks/equals.rb
305
306
  - app/services/completion_kit/checks/expected_resolver.rb
@@ -476,6 +477,7 @@ files:
476
477
  - db/migrate/20260629000002_add_check_type_to_completion_kit_metric_versions.rb
477
478
  - db/migrate/20260629000003_add_passed_to_completion_kit_reviews.rb
478
479
  - db/migrate/20260706000001_add_expected_column_to_completion_kit_runs.rb
480
+ - db/migrate/20260708000001_add_api_version_to_completion_kit_provider_credentials.rb
479
481
  - lib/completion-kit.rb
480
482
  - lib/completion_kit.rb
481
483
  - lib/completion_kit/concurrency_check.rb