completion-kit 0.26.7 → 0.27.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: 6841ace15da954da1ff736152f3d123fbfd3ae37ddde6e8df6b67bdf44270648
4
- data.tar.gz: 8be224e89d2c82d09f420f70cec115b8831be8f82d29742d2de36a56a8c3c370
3
+ metadata.gz: f0fa2b7edcc463d34ce7dbed34a522119f840778bd75fa9912124069564420dd
4
+ data.tar.gz: 16e1aa2310b4efd26c67fcf70ac55dedb0bd760e97201f41bdb9c73f98247070
5
5
  SHA512:
6
- metadata.gz: 6426ca4cc6bf8fd6b5abac3b149afbba83b674e5162d6041ed853ff0ffbc7bf2d2293f9238b301ec1b6e83cfd8638fc3c3cd798558ba13f04c59e3fe153ce86e
7
- data.tar.gz: 921ec6833c902ee93592d75d4091d294880c01442e5f3c3ef38891409d364c8f3541d2d0f86cff9226942ec75dbcc4bb6dc377b55c0e8d0802509c95a48da7dc
6
+ metadata.gz: '0572479a9194f494f34f4982c563dc5b3ffae3c1e430c0b39786ea05a6eac15359d7f51946810c8208bf60eb43274554b152d981b492d9fe9a04a01c7c8cb23b'
7
+ data.tar.gz: 931853452dac7e901d2e8eed066011c7f428aed7290d68d86787b0c029a9d4f7e0e1b0888ed2adefe015093a6f423487b433731e6a4826ac9dcf5ca255943e62
@@ -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;
@@ -1562,6 +1574,11 @@ tr:hover .ck-chip--publish {
1562
1574
  color: var(--ck-accent);
1563
1575
  }
1564
1576
 
1577
+ .ck-code--error {
1578
+ background: var(--ck-danger-soft);
1579
+ border-color: rgba(248, 113, 113, 0.35);
1580
+ }
1581
+
1565
1582
  /* the main prompt template block on prompts/show — bigger padding + a
1566
1583
  touch more line-height since this is the page's primary content */
1567
1584
  .ck-code--prompt {
@@ -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
@@ -8,6 +8,7 @@ module CompletionKit
8
8
  validates :name, presence: true
9
9
  validates :template, presence: true
10
10
  validates :llm_model, presence: true
11
+ validate :llm_model_usable_for_generation, if: :llm_model_changed?
11
12
  validates :family_key, presence: true
12
13
  validates :version_number, presence: true, numericality: { only_integer: true, greater_than: 0 }
13
14
 
@@ -86,6 +87,17 @@ module CompletionKit
86
87
 
87
88
  private
88
89
 
90
+ def llm_model_usable_for_generation
91
+ return if llm_model.blank?
92
+
93
+ rows = Model.where(model_id: llm_model)
94
+ return if rows.empty?
95
+ return if rows.where(supports_generation: true, status: "active").exists?
96
+ return unless rows.where(supports_generation: false).exists?
97
+
98
+ errors.add(:llm_model, "is not available for generating responses")
99
+ end
100
+
89
101
  def assign_family_key
90
102
  self.family_key ||= SecureRandom.uuid
91
103
  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
@@ -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>
@@ -72,12 +72,17 @@
72
72
 
73
73
  <section class="ck-card--spaced">
74
74
  <div class="ck-prompt-preview__header">
75
- <p class="ck-kicker">Response</p>
75
+ <p class="ck-kicker"><%= @response.status == "failed" ? "Error" : "Response" %></p>
76
76
  <% if @run.prompt %>
77
77
  <span class="ck-chip ck-chip--soft" style="text-transform: none;"><%= @run.prompt.llm_model %></span>
78
78
  <% end %>
79
79
  </div>
80
- <div class="ck-code-scroll-wrap"><pre class="ck-code"><%= ck_format_maybe_json(@response.response_text) %></pre></div>
80
+ <% if @response.status == "failed" %>
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>
83
+ <% else %>
84
+ <div class="ck-code-scroll-wrap"><pre class="ck-code"><%= ck_format_maybe_json(@response.response_text) %></pre></div>
85
+ <% end %>
81
86
  </section>
82
87
 
83
88
  <% if @response.expected_output.present? %>
@@ -1,4 +1,4 @@
1
- <% clickable = response.succeeded? %>
1
+ <% clickable = response.terminal? %>
2
2
  <tr id="response_<%= response.id %>"<% if clickable %> onclick="window.location='<%= run_response_path(run, response, sort: params[:sort]) %>'" style="cursor: pointer;"<% end %>>
3
3
  <td class="ck-response-cell__index"><% if clickable %><%= link_to index, run_response_path(run, response, sort: params[:sort]), class: "ck-record-name" %><% else %><%= index %><% end %></td>
4
4
  <td class="ck-response-cell__text">
@@ -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.7"
2
+ VERSION = "0.27.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.26.7
4
+ version: 0.27.0
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