completion-kit 0.26.8 → 0.27.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 42fae0f3b31530598ee43e1d39f3d8dbb1afd53e4c0af73aa2013a33433414f7
4
- data.tar.gz: 46ab6507a7181494383f4d960fbd985c4a04587d842d274151c1e639729a96fd
3
+ metadata.gz: 38c7acaadacc7cb636ee99e3eb7e4c52f68b89ed57122487eb1591be99435716
4
+ data.tar.gz: 3e5f9c36019c6635ae41b913eea74607f3b8c9f21a511c5ccc3f9ff75422282d
5
5
  SHA512:
6
- metadata.gz: 6f2be3fdcaa3c439159fbddf36718b85b300ffd67bd0a660ebc002de0fd6f70ac73383f0ae8efe14d6ee2df8f35421cf9712f75ec503c22c92a08a633685214c
7
- data.tar.gz: b3b371448f03e9a94d4d3ee52c47546d156b6e9b194cdec20cd4ee10e9f1f12b783f37e4eb1a1446b597f6e3c4e01b8c0f682c69f9710ec567d56d223ddc846a
6
+ metadata.gz: 80adc55bde36fa3ac782c213c4786e677cd27d1b85bef81c14fbd89e05a82e229681866390c34d7f17cd9d6a153bcb93da705d63a87f7aede39dcb26929e3629
7
+ data.tar.gz: 533ef4f6c4b29791a66ce9169ab4c980a38838f5e3a5184d0b07cf0a4276ad0fd5990fae69616a2ea43421daebab1ca1ed2f246b4a310a506403db7548935981
@@ -961,14 +961,6 @@ tr:hover .ck-chip--publish {
961
961
  max-width: 36rem;
962
962
  }
963
963
 
964
- .ck-model-list__empty {
965
- display: flex;
966
- align-items: center;
967
- gap: 1rem;
968
- flex-wrap: wrap;
969
- padding: 0.35rem 0.25rem;
970
- }
971
-
972
964
  .ck-form-card__footer-header {
973
965
  display: flex;
974
966
  align-items: center;
@@ -1504,6 +1496,26 @@ tr:hover .ck-chip--publish {
1504
1496
  gap: 0.6rem;
1505
1497
  }
1506
1498
 
1499
+ .ck-model-list__catalog {
1500
+ margin-top: 0.75rem;
1501
+ padding-top: 0.6rem;
1502
+ border-top: 1px solid var(--ck-line);
1503
+ font-family: var(--ck-mono);
1504
+ font-size: 0.72rem;
1505
+ letter-spacing: 0.03em;
1506
+ }
1507
+
1508
+ .ck-model-list__catalog-link {
1509
+ color: var(--ck-accent);
1510
+ font-weight: 500;
1511
+ text-decoration: none;
1512
+ white-space: nowrap;
1513
+ }
1514
+
1515
+ .ck-model-list__catalog-link:hover {
1516
+ color: var(--ck-accent-hover);
1517
+ }
1518
+
1507
1519
  .ck-model-list__summary-stamp {
1508
1520
  font-family: var(--ck-mono);
1509
1521
  font-size: 0.7rem;
@@ -25,10 +25,17 @@ module CompletionKit
25
25
 
26
26
  def update
27
27
  if @prompt.runs.exists?
28
- new_prompt = @prompt.clone_as_new_version(prompt_params.except(:tag_names).to_h)
29
- new_prompt.publish!
30
- new_prompt.update!(tag_names: prompt_params[:tag_names]) if prompt_params.key?(:tag_names)
31
- render json: new_prompt.reload
28
+ new_prompt = @prompt.build_next_version(prompt_params.except(:tag_names).to_h)
29
+ if new_prompt.valid?
30
+ CompletionKit::ApplicationRecord.transaction do
31
+ new_prompt.save!
32
+ new_prompt.publish!
33
+ new_prompt.update!(tag_names: prompt_params[:tag_names]) if prompt_params.key?(:tag_names)
34
+ end
35
+ render json: new_prompt.reload
36
+ else
37
+ render_validation_errors(new_prompt)
38
+ end
32
39
  elsif @prompt.update(prompt_params)
33
40
  render json: @prompt
34
41
  else
@@ -33,10 +33,19 @@ module CompletionKit
33
33
 
34
34
  def update
35
35
  if @prompt.runs.exists?
36
- new_prompt = @prompt.clone_as_new_version(prompt_params.except(:tag_names).to_h)
37
- new_prompt.publish!
38
- new_prompt.update!(tag_names: prompt_params[:tag_names]) if prompt_params.key?(:tag_names)
39
- redirect_to prompt_path(new_prompt), notice: "Saved as #{new_prompt.version_label}."
36
+ new_prompt = @prompt.build_next_version(prompt_params.except(:tag_names).to_h)
37
+ if new_prompt.valid?
38
+ CompletionKit::ApplicationRecord.transaction do
39
+ new_prompt.save!
40
+ new_prompt.publish!
41
+ new_prompt.update!(tag_names: prompt_params[:tag_names]) if prompt_params.key?(:tag_names)
42
+ end
43
+ redirect_to prompt_path(new_prompt), notice: "Saved as #{new_prompt.version_label}."
44
+ else
45
+ @prompt.assign_attributes(prompt_params.except(:tag_names).to_h)
46
+ @prompt.errors.merge!(new_prompt.errors)
47
+ render :edit, status: :unprocessable_entity
48
+ end
40
49
  elsif @prompt.update(prompt_params)
41
50
  redirect_to prompt_path(@prompt), notice: "Prompt saved."
42
51
  else
@@ -48,7 +48,12 @@ module CompletionKit
48
48
  credential.broadcast_discovery_progress
49
49
  end
50
50
 
51
- credential.update_columns(discovery_status: "completed", discovery_error: nil, updated_at: Time.current)
51
+ credential.update_columns(
52
+ discovery_status: "completed",
53
+ discovery_error: nil,
54
+ catalog_model_count: service.catalog_model_count,
55
+ updated_at: Time.current
56
+ )
52
57
  credential.reload
53
58
  credential.broadcast_discovery_complete
54
59
  end
@@ -5,9 +5,12 @@ module CompletionKit
5
5
  has_many :runs, dependent: :destroy
6
6
  has_many :responses, through: :runs
7
7
 
8
+ attr_accessor :cloned_from_llm_model
9
+
8
10
  validates :name, presence: true
9
11
  validates :template, presence: true
10
12
  validates :llm_model, presence: true
13
+ validate :llm_model_usable_for_generation, if: :llm_model_newly_selected?
11
14
  validates :family_key, presence: true
12
15
  validates :version_number, presence: true, numericality: { only_integer: true, greater_than: 0 }
13
16
 
@@ -52,8 +55,8 @@ module CompletionKit
52
55
  self.class.where(family_key: family_key).order(version_number: :desc, created_at: :desc)
53
56
  end
54
57
 
55
- def clone_as_new_version(overrides = {})
56
- self.class.create!(
58
+ def build_next_version(overrides = {})
59
+ version = self.class.new(
57
60
  {
58
61
  name: name,
59
62
  description: description,
@@ -65,6 +68,14 @@ module CompletionKit
65
68
  published_at: nil
66
69
  }.merge(overrides.compact)
67
70
  )
71
+ version.cloned_from_llm_model = llm_model
72
+ version
73
+ end
74
+
75
+ def clone_as_new_version(overrides = {})
76
+ version = build_next_version(overrides)
77
+ version.save!
78
+ version
68
79
  end
69
80
 
70
81
  def publish!
@@ -86,6 +97,22 @@ module CompletionKit
86
97
 
87
98
  private
88
99
 
100
+ def llm_model_newly_selected?
101
+ return false if llm_model.blank?
102
+ return llm_model != cloned_from_llm_model if new_record? && cloned_from_llm_model.present?
103
+
104
+ llm_model_changed?
105
+ end
106
+
107
+ def llm_model_usable_for_generation
108
+ rows = Model.where(model_id: llm_model)
109
+ return if rows.empty?
110
+ return if rows.where(supports_generation: true, status: "active").exists?
111
+ return unless rows.where(supports_generation: false).exists?
112
+
113
+ errors.add(:llm_model, "is not available for generating responses")
114
+ end
115
+
89
116
  def assign_family_key
90
117
  self.family_key ||= SecureRandom.uuid
91
118
  end
@@ -61,6 +61,10 @@ module CompletionKit
61
61
  Model.where(provider: provider).active.count
62
62
  end
63
63
 
64
+ def foundry_catalog_available?
65
+ azure_foundry? && catalog_model_count.to_i.positive?
66
+ end
67
+
64
68
  def self.discovery_in_progress?
65
69
  where(discovery_status: "discovering").exists?
66
70
  end
@@ -58,16 +58,26 @@ module CompletionKit
58
58
  return [] unless configured?
59
59
  return [] unless ProviderEndpoint.safe?(api_endpoint)
60
60
 
61
- response = build_connection(azure_base_url).get(models_url) do |req|
61
+ path = foundry_project? ? "#{azure_base_url}/deployments?api-version=v1" : models_path
62
+ response = build_connection(azure_base_url).get(path) do |req|
62
63
  req.headers["api-key"] = api_key
63
64
  end
64
65
  return [] unless response.success?
65
66
 
66
- parse_models(response.body)
67
+ body = JSON.parse(response.body)
68
+ if foundry_project?
69
+ body.fetch("value", []).map { |d| { id: d["name"], name: d["name"] } }
70
+ else
71
+ body.fetch("data", []).map { |entry| { id: entry["id"], name: entry["id"] } }
72
+ end
67
73
  rescue StandardError
68
74
  []
69
75
  end
70
76
 
77
+ def foundry_project?
78
+ api_endpoint.to_s.include?("/api/projects/")
79
+ end
80
+
71
81
  def configured?
72
82
  configuration_errors.empty?
73
83
  end
@@ -101,27 +111,8 @@ module CompletionKit
101
111
  api_version.blank?
102
112
  end
103
113
 
104
- def foundry_project?
105
- api_endpoint.to_s.include?("/api/projects/")
106
- end
107
-
108
- def models_url
109
- if foundry_project?
110
- "#{azure_base_url}/deployments?api-version=v1"
111
- elsif v1_mode?
112
- "/openai/v1/models"
113
- else
114
- "/openai/deployments?api-version=#{api_version}"
115
- end
116
- end
117
-
118
- def parse_models(body)
119
- json = JSON.parse(body)
120
- if foundry_project?
121
- json.fetch("value", []).map { |d| { id: d["name"], name: d["name"] } }
122
- else
123
- json.fetch("data", []).map { |e| { id: e["id"], name: e["id"] } }
124
- end
114
+ def models_path
115
+ v1_mode? ? "/openai/v1/models" : "/openai/deployments?api-version=#{api_version}"
125
116
  end
126
117
 
127
118
  def post_chat(model:, prompt:, max_tokens:, temperature:, max_completion: false)
@@ -8,11 +8,14 @@ module CompletionKit
8
8
 
9
9
  AZURE_HOST_SUFFIXES = [".openai.azure.com", ".services.ai.azure.com"].freeze
10
10
 
11
+ attr_reader :catalog_model_count
12
+
11
13
  def initialize(config:)
12
14
  @provider = config[:provider]
13
15
  @api_key = config[:api_key]
14
16
  @api_endpoint = config[:api_endpoint]
15
17
  @api_version = config[:api_version]
18
+ @catalog_model_count = nil
16
19
  end
17
20
 
18
21
  def refresh!(force: false, &on_progress)
@@ -156,18 +159,32 @@ module CompletionKit
156
159
  def fetch_azure_foundry_models
157
160
  raise DiscoveryError, "An Azure endpoint URL is required." if @api_endpoint.blank?
158
161
 
159
- response = fetch_connection(azure_base_url).get(azure_foundry_project? ? "#{azure_base_url}/deployments?api-version=v1" : azure_models_path) do |req|
162
+ path = azure_foundry_project? ? "#{azure_base_url}/deployments?api-version=v1" : azure_models_path
163
+ response = fetch_connection(azure_base_url).get(path) do |req|
160
164
  req.headers["api-key"] = @api_key
161
165
  end
162
166
  raise DiscoveryError, azure_error_message(response) unless response.success?
167
+
163
168
  body = JSON.parse(response.body)
164
169
  if azure_foundry_project?
170
+ @catalog_model_count = fetch_azure_catalog_count
165
171
  body.fetch("value", []).map { |d| { id: d["name"], display_name: d["name"] } }
166
172
  else
167
173
  body.fetch("data", []).map { |e| { id: e["id"], display_name: e["id"] } }
168
174
  end
169
175
  end
170
176
 
177
+ def fetch_azure_catalog_count
178
+ response = fetch_connection(azure_base_url).get("/openai/v1/models") do |req|
179
+ req.headers["api-key"] = @api_key
180
+ end
181
+ return nil unless response.success?
182
+
183
+ JSON.parse(response.body).fetch("data", []).length
184
+ rescue StandardError
185
+ nil
186
+ end
187
+
171
188
  def azure_v1_mode?
172
189
  @api_version.blank?
173
190
  end
@@ -27,6 +27,8 @@ module CompletionKit
27
27
  metrics = import_metrics(config)
28
28
  Result.new(ok: true, error: nil, prompts: prompts, dataset: dataset, metrics: metrics, providers: providers)
29
29
  end
30
+ rescue ActiveRecord::RecordInvalid => e
31
+ failure(e.record.errors.full_messages.to_sentence.presence || e.message)
30
32
  end
31
33
 
32
34
  private
@@ -71,12 +71,19 @@
71
71
  </div>
72
72
  </details>
73
73
  <% elsif provider_credential.discovery_status != "discovering" %>
74
- <div class="ck-model-list__empty">
75
- <% unless provider_credential.discovery_status == "failed" %>
76
- <span class="ck-meta-copy">No models discovered yet.</span>
77
- <% end %>
78
- <button type="button" class="<%= ck_button_classes(:light, variant: :outline) %>" onclick="fetch('<%= refresh_provider_credential_path(provider_credential) %>', {method:'POST',headers:{'X-CSRF-Token':document.querySelector('meta[name=csrf-token]').content}});if(window.ckStartDiscoveryPolling)ckStartDiscoveryPolling(8000)">Discover models</button>
74
+ <div class="ck-model-list__summary">
75
+ <span class="ck-model-list__summary-label">Available models <span class="ck-model-list__summary-count">0</span></span>
76
+ <span class="ck-model-list__summary-meta">
77
+ <button type="button" class="ck-icon-btn ck-model-list__refresh" title="Refresh models" aria-label="Refresh available models" onclick="event.preventDefault();fetch('<%= refresh_provider_credential_path(provider_credential) %>', {method:'POST',headers:{'X-CSRF-Token':document.querySelector('meta[name=csrf-token]').content}});if(window.ckStartDiscoveryPolling)ckStartDiscoveryPolling(8000)"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" width="13" height="13" aria-hidden="true"><path fill-rule="evenodd" d="M13.836 2.477a.75.75 0 0 1 .75.75v3.182a.75.75 0 0 1-.75.75h-3.182a.75.75 0 0 1 0-1.5h1.37l-.84-.841a4.5 4.5 0 0 0-7.08.681.75.75 0 0 1-1.264-.808 6 6 0 0 1 9.44-.908l.84.84V3.227a.75.75 0 0 1 .75-.75Zm-.911 7.5A.75.75 0 0 1 13.199 11a6 6 0 0 1-9.44.908l-.84-.84v1.68a.75.75 0 0 1-1.5 0V9.567a.75.75 0 0 1 .75-.75h3.182a.75.75 0 0 1 0 1.5h-1.37l.84.841a4.5 4.5 0 0 0 7.08-.681.75.75 0 0 1 1.024-.274Z" clip-rule="evenodd"/></svg></button>
78
+ </span>
79
79
  </div>
80
80
  <% end %>
81
+
82
+ <% if provider_credential.foundry_catalog_available? %>
83
+ <p class="ck-meta-copy ck-model-list__catalog">
84
+ <%= pluralize(provider_credential.catalog_model_count, "model") %> available to deploy in
85
+ <a href="https://ai.azure.com/" target="_blank" rel="noopener" class="ck-model-list__catalog-link">Azure AI Foundry <span aria-hidden="true">&rarr;</span></a>
86
+ </p>
87
+ <% end %>
81
88
  </div>
82
89
  </div>
@@ -79,7 +79,9 @@
79
79
  </div>
80
80
  <% if @response.status == "failed" %>
81
81
  <% err = @response.error_payload || {} %>
82
- <div class="ck-code-scroll-wrap"><pre class="ck-code ck-code--error"><%= err[:provider]&.titleize %><%= " #{err[:status]}" if err[:status].present? %><%= "\n\n" if err[:provider].present? || err[:status].present? %><%= err[:message] %></pre></div>
82
+ <% head = [err[:provider]&.titleize, err[:status].presence].compact.join(" ").presence %>
83
+ <% error_text = [head, @response.error_message.presence].compact.join("\n\n").presence || "The provider returned no error detail." %>
84
+ <div class="ck-code-scroll-wrap"><pre class="ck-code ck-code--error"><%= error_text %></pre></div>
83
85
  <% else %>
84
86
  <div class="ck-code-scroll-wrap"><pre class="ck-code"><%= ck_format_maybe_json(@response.response_text) %></pre></div>
85
87
  <% end %>
@@ -0,0 +1,5 @@
1
+ class AddCatalogModelCountToCompletionKitProviderCredentials < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_column :completion_kit_provider_credentials, :catalog_model_count, :integer
4
+ end
5
+ end
@@ -1,3 +1,3 @@
1
1
  module CompletionKit
2
- VERSION = "0.26.8"
2
+ VERSION = "0.27.1"
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.26.8
4
+ version: 0.27.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Damien Bastin
@@ -479,6 +479,7 @@ files:
479
479
  - db/migrate/20260629000003_add_passed_to_completion_kit_reviews.rb
480
480
  - db/migrate/20260706000001_add_expected_column_to_completion_kit_runs.rb
481
481
  - db/migrate/20260708000001_add_api_version_to_completion_kit_provider_credentials.rb
482
+ - db/migrate/20260713000001_add_catalog_model_count_to_completion_kit_provider_credentials.rb
482
483
  - lib/completion-kit.rb
483
484
  - lib/completion_kit.rb
484
485
  - lib/completion_kit/concurrency_check.rb