completion-kit 0.28.45 → 0.28.46

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.
Files changed (26) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/completion_kit/application.css +14 -3
  3. data/app/controllers/completion_kit/onboarding_controller.rb +1 -1
  4. data/app/controllers/completion_kit/runs_controller.rb +14 -5
  5. data/app/helpers/completion_kit/application_helper.rb +11 -7
  6. data/app/models/completion_kit/prompt.rb +1 -1
  7. data/app/models/completion_kit/run.rb +17 -10
  8. data/app/services/completion_kit/mcp_tools/metric_versions.rb +1 -1
  9. data/app/services/completion_kit/mcp_tools/prompts.rb +1 -1
  10. data/app/services/completion_kit/prompt_improvement_service.rb +1 -1
  11. data/app/views/completion_kit/agreements/_buttons.html.erb +1 -1
  12. data/app/views/completion_kit/api_reference/_body.html.erb +2 -2
  13. data/app/views/completion_kit/metric_groups/_form.html.erb +13 -2
  14. data/app/views/completion_kit/metric_groups/edit.html.erb +1 -1
  15. data/app/views/completion_kit/onboarding/show.html.erb +2 -2
  16. data/app/views/completion_kit/prompts/_form.html.erb +2 -1
  17. data/app/views/completion_kit/provider_credentials/_models_card.html.erb +2 -2
  18. data/app/views/completion_kit/runs/_form.html.erb +14 -10
  19. data/app/views/completion_kit/runs/_response_row.html.erb +2 -1
  20. data/app/views/completion_kit/runs/edit.html.erb +1 -1
  21. data/app/views/completion_kit/runs/new.html.erb +1 -1
  22. data/app/views/layouts/completion_kit/application.html.erb +1 -1
  23. data/db/migrate/20261013000001_drop_completion_kit_mcp_sessions.rb +16 -0
  24. data/lib/completion_kit/errors.rb +2 -2
  25. data/lib/completion_kit/version.rb +1 -1
  26. metadata +2 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7c6a9a433f596def84c665f6b6fbe2b5aa4ed1422ac923f20a2a2d246aad02c4
4
- data.tar.gz: 3a0ad110587d51bad3b41f89439da3efa79c199887d58b76703b0d46d16d7f6d
3
+ metadata.gz: f620811d2032fa4aad51754dcee5aa92657cc193c890587845cb0c185060ef18
4
+ data.tar.gz: ea6426877ef769569438a48f3f2cc7848569e02c395db53e629898cda4c01545
5
5
  SHA512:
6
- metadata.gz: 5f17ea2b059df5b6e2656c9a80a60d87ccd5a55b7d1d2a2599ca3b913227eca49d19a983015765b52accc0287a8e07e60dedb3e1d92d2744e81f5803a32f5b19
7
- data.tar.gz: 673b44c3f1eb7edaf963acb85a0451067ed1f6fa9ee73fee156518c031e8fb2b74786aa119c88d39281cc36e4a5e56da3ab8354a3c0411fe1eca800ed96b275e
6
+ metadata.gz: c6f1a8afbf36d89f1dbd42919fc870732e6f5aeb14164303e62d68644489837f845fc50b19d5ea9735e55b5e1876e743f67ced221e9d3d720f08726d24b4f4f8
7
+ data.tar.gz: 4cf7b53081569d22ddb471e6b40418baeba32e3fcd11e284df14cd582810603f7e132165603e169cb19f3655fefc7b5f69a9fe437ee687be181d7dc4ced95447
@@ -2311,6 +2311,15 @@ label.ck-checkbox[hidden] {
2311
2311
  flex: 1;
2312
2312
  }
2313
2313
 
2314
+ .ck-select-with-action > .field_with_errors {
2315
+ flex: 1;
2316
+ min-width: 0;
2317
+ }
2318
+
2319
+ .ck-select-with-action > .field_with_errors > .ck-input {
2320
+ width: 100%;
2321
+ }
2322
+
2314
2323
  .ck-icon-btn:hover {
2315
2324
  background: rgba(255, 255, 255, 0.05);
2316
2325
  border-color: var(--ck-line-strong);
@@ -5175,11 +5184,13 @@ a.tag-mark {
5175
5184
  }
5176
5185
 
5177
5186
 
5178
- .ck-input--error {
5187
+ .ck-input--error,
5188
+ .ck-input[aria-invalid="true"] {
5179
5189
  border-color: var(--ck-danger);
5180
- background: rgba(248, 113, 113, 0.05);
5190
+ background-color: rgba(248, 113, 113, 0.05);
5181
5191
  }
5182
- .ck-input--error:focus {
5192
+ .ck-input--error:focus,
5193
+ .ck-input[aria-invalid="true"]:focus {
5183
5194
  box-shadow: 0 0 0 3px rgba(248, 113, 113, 0.15);
5184
5195
  border-color: var(--ck-danger);
5185
5196
  }
@@ -21,7 +21,7 @@ module CompletionKit
21
21
 
22
22
  def sample_data
23
23
  Onboarding::SampleData.install!
24
- redirect_to onboarding_path, notice: "Loaded a sample dataset and prompt — edit or delete them whenever."
24
+ redirect_to onboarding_path, notice: "Loaded a sample dataset and prompt. Edit or delete them whenever."
25
25
  end
26
26
  end
27
27
  end
@@ -40,6 +40,7 @@ module CompletionKit
40
40
  end
41
41
 
42
42
  def create
43
+ @judge_only = judge_only_chosen?
43
44
  @run = Run.new(run_attributes)
44
45
  if @run.save
45
46
  redirect_to run_path(@run), notice: "Run was successfully created."
@@ -50,10 +51,11 @@ module CompletionKit
50
51
  end
51
52
 
52
53
  def update
54
+ @judge_only = judge_only_chosen?
53
55
  if @run.responses.any? && run_generation_changed?
54
56
  attrs = run_attributes.to_h
55
57
  attrs.delete("name") if attrs["name"].to_s == @run.name.to_s
56
- new_run = Run.new(attrs.merge(status: "pending"))
58
+ new_run = Run.new(attrs.merge(status: "pending", cloned_from_judge_model: @run.judge_model))
57
59
  if new_run.save
58
60
  redirect_to run_path(new_run), notice: "Saved as a new run. The previous run and its results are preserved."
59
61
  else
@@ -224,11 +226,16 @@ module CompletionKit
224
226
  end
225
227
 
226
228
  def run_params
227
- params.require(:run).permit(:name, :prompt_id, :dataset_id, :judge_model, :temperature, :max_tokens, :judge_temperature, :output_column, :expected_column, metric_ids: [], tag_names: [])
229
+ params.require(:run).permit(:name, :prompt_id, :dataset_id, :judge_model, :temperature, :max_tokens, :judge_temperature, :output_column, :expected_column, :judge_only, metric_ids: [], tag_names: [])
228
230
  end
229
231
 
230
232
  def run_attributes
231
- run_params.except(:metric_ids).merge(selected_metric_ids: run_params[:metric_ids])
233
+ attributes = run_params.except(:metric_ids, :judge_only).merge(selected_metric_ids: run_params[:metric_ids])
234
+ judge_only_chosen? ? attributes.merge(prompt_id: nil) : attributes
235
+ end
236
+
237
+ def judge_only_chosen?
238
+ run_params[:judge_only] == "1"
232
239
  end
233
240
 
234
241
  # Editing a run that already has results forks a new run — but only when a
@@ -237,9 +244,10 @@ module CompletionKit
237
244
  GENERATION_RUN_FIELDS = %i[prompt_id dataset_id judge_model temperature max_tokens judge_temperature output_column expected_column].freeze
238
245
 
239
246
  def run_generation_changed?
247
+ attributes = run_attributes
240
248
  GENERATION_RUN_FIELDS.each do |field|
241
- next unless run_params.key?(field)
242
- return true if normalize_run_field(field, run_params[field]) != normalize_run_field(field, @run.public_send(field))
249
+ next unless attributes.key?(field)
250
+ return true if normalize_run_field(field, attributes[field]) != normalize_run_field(field, @run.public_send(field))
243
251
  end
244
252
  return false unless params[:run].key?(:metric_ids)
245
253
  Array(params[:run][:metric_ids]).map(&:to_i).reject(&:zero?).sort != @run.metric_ids.sort
@@ -247,6 +255,7 @@ module CompletionKit
247
255
 
248
256
  def normalize_run_field(field, value)
249
257
  s = value.to_s.strip
258
+ return s.presence || "actual_output" if field == :output_column
250
259
  return nil if s.empty?
251
260
  case field
252
261
  when :temperature, :judge_temperature then s.to_f
@@ -151,18 +151,22 @@ module CompletionKit
151
151
  model[:name]
152
152
  end
153
153
 
154
- def ck_grouped_models(models, selected = nil)
154
+ def ck_grouped_models(models, selected = nil, rejected: false)
155
+ kept = nil
155
156
  if selected.present? && models.none? { |m| m[:id] == selected }
156
- retired = CompletionKit::Model.find_by(model_id: selected)
157
+ retired = CompletionKit::Model.find_by(model_id: selected) unless rejected
157
158
  if retired
158
159
  models = models + [{ id: retired.model_id, name: "#{retired.display_name || retired.model_id} (retired)", provider: retired.provider }]
160
+ else
161
+ kept = [["#{selected} (unavailable)", selected]]
159
162
  end
160
163
  end
161
164
 
162
165
  groups = models.group_by { |m| ck_model_optgroup_label(m) }
163
166
  ordered_keys = groups.keys.sort_by { |label| ck_model_optgroup_sort_key(label) }
164
167
  grouped = ordered_keys.map { |label| [label, groups[label].map { |m| [ck_model_option_label(m), m[:id]] }] }
165
- grouped_options_for_select(grouped, selected)
168
+ options = grouped_options_for_select(grouped, selected)
169
+ kept ? options_for_select(kept, selected) + options : options
166
170
  end
167
171
 
168
172
  # Optgroup label for the model select — mirrors the provider models table:
@@ -170,15 +174,15 @@ module CompletionKit
170
174
  # else is a single group.
171
175
  def ck_model_optgroup_label(model)
172
176
  case model[:provider]
173
- when "openrouter" then "OpenRouter — #{model[:id].to_s.split("/", 2).first.delete_prefix("~")}"
174
- when "openai" then "OpenAI — #{ck_openai_model_family(model[:id])}"
177
+ when "openrouter" then "OpenRouter · #{model[:id].to_s.split("/", 2).first.delete_prefix("~")}"
178
+ when "openai" then "OpenAI · #{ck_openai_model_family(model[:id])}"
175
179
  else ck_provider_label(model[:provider])
176
180
  end
177
181
  end
178
182
 
179
183
  def ck_model_optgroup_sort_key(label)
180
- if label.start_with?("OpenAI — ")
181
- [0, OPENAI_MODEL_FAMILY_ORDER.index(label.delete_prefix("OpenAI — ")), label]
184
+ if label.start_with?("OpenAI · ")
185
+ [0, OPENAI_MODEL_FAMILY_ORDER.index(label.delete_prefix("OpenAI · ")), label]
182
186
  elsif label.start_with?("OpenRouter")
183
187
  [2, 0, label]
184
188
  else
@@ -44,7 +44,7 @@ module CompletionKit
44
44
  end
45
45
 
46
46
  def display_name
47
- "#{name} — #{version_label}"
47
+ "#{name} · #{version_label}"
48
48
  end
49
49
 
50
50
  def llm_model_provider
@@ -333,13 +333,10 @@ module CompletionKit
333
333
  unless judge_only_input_data_checks? || (dataset && dataset.headers.include?(column))
334
334
  return fail_with_summary!("Dataset has no \"#{column}\" column")
335
335
  end
336
- else
337
- generation_error = generation_client_error
338
- return fail_with_summary!(generation_error) if generation_error
339
336
  end
340
337
 
341
- judge_error = judge_model_error
342
- return fail_with_summary!(judge_error) if judge_error
338
+ model_error = model_start_error
339
+ return fail_with_summary!(model_error) if model_error
343
340
 
344
341
  begin
345
342
  transaction do
@@ -490,7 +487,8 @@ module CompletionKit
490
487
  end
491
488
 
492
489
  def rerun!
493
- ensure_models_resolve!
490
+ ensure_models_ready!
491
+
494
492
  Run.create!(
495
493
  prompt_id: prompt_id,
496
494
  dataset_id: dataset_id,
@@ -712,11 +710,20 @@ module CompletionKit
712
710
  %(No configured provider serves judge model "#{judge_model}")
713
711
  end
714
712
 
715
- def ensure_models_resolve!
716
- raise UnknownModelError, prompt.llm_model unless judge_only? || ApiConfig.provider_for_model(prompt.llm_model)
713
+ def model_start_error
714
+ (generation_client_error unless judge_only?) || judge_model_error
715
+ end
716
+
717
+ def ensure_models_ready!
718
+ unless judge_only?
719
+ raise UnknownModelError, prompt.llm_model unless ApiConfig.provider_for_model(prompt.llm_model)
720
+
721
+ client_error = generation_client_error
722
+ raise ConfigurationError, client_error if client_error
723
+ end
717
724
 
718
725
  judge_error = judge_model_error
719
- raise ConfigurationError, judge_error if judge_error
726
+ raise UnknownModelError.new(judge_model, judge_error) if judge_error
720
727
  end
721
728
 
722
729
  def judge_model_newly_selected?
@@ -775,7 +782,7 @@ module CompletionKit
775
782
 
776
783
  if prompt.present?
777
784
  count = Run.where(prompt_id: prompt_id).count + 1
778
- self.name = "#{prompt.name} — v#{prompt.version_number} ##{count}"
785
+ self.name = "#{prompt.name} · v#{prompt.version_number} ##{count}"
779
786
  elsif dataset.present?
780
787
  count = Run.where(prompt_id: nil, dataset_id: dataset.id).count + 1
781
788
  self.name = "#{dataset.name} scoring ##{count}"
@@ -27,7 +27,7 @@ module CompletionKit
27
27
  handler: :publish
28
28
  },
29
29
  "metric_versions_dismiss" => {
30
- description: "Destroy a draft MetricVersion (use for either source: 'edit' or source: 'suggestion'). Published versions are refused — to demote a published version, publish a different one as current instead.",
30
+ description: "Destroy a draft MetricVersion (use for either source: 'edit' or source: 'suggestion'). Published versions are refused. To demote a published version, publish a different one as current instead.",
31
31
  inputSchema: {
32
32
  type: "object",
33
33
  properties: {
@@ -28,7 +28,7 @@ module CompletionKit
28
28
  handler: :create
29
29
  },
30
30
  "prompts_update" => {
31
- description: "Update a prompt. If the prompt already has runs, this creates a new DRAFT version (current=false) rather than editing in place or publishing — promote it with prompts_publish — so an agent's edits don't go live without a gate. If it has no runs, it is updated in place.",
31
+ description: "Update a prompt. If the prompt already has runs, this creates a new DRAFT version (current=false) rather than editing in place or publishing. Promote it with prompts_publish, so an agent's edits don't go live without a gate. If it has no runs, it is updated in place.",
32
32
  inputSchema: {
33
33
  type: "object",
34
34
  properties: {
@@ -40,7 +40,7 @@ module CompletionKit
40
40
  if review.check?
41
41
  sections << " #{review.metric_name}: #{review.passed ? "PASS" : "FAIL"}"
42
42
  else
43
- sections << " #{review.metric_name}: #{review.ai_score}/5 — #{review.ai_feedback}"
43
+ sections << " #{review.metric_name}: #{review.ai_score}/5. #{review.ai_feedback}"
44
44
  end
45
45
  end
46
46
  sections << ""
@@ -21,7 +21,7 @@
21
21
  <% verdict_icons = { "agree" => "hand-thumb-up", "disagree" => "hand-thumb-down", "borderline" => "scale" } %>
22
22
  <% verdict_hints = {
23
23
  "agree" => "The score looks right.",
24
- "disagree" => "The score is wrong — you'll pick the right one.",
24
+ "disagree" => "The score is wrong. You'll pick the right one.",
25
25
  "borderline" => "The rubric is unclear here; either score could be defensible."
26
26
  } %>
27
27
  <% CompletionKit::Agreement::VERDICTS.each do |verdict| %>
@@ -196,7 +196,7 @@
196
196
  </div>
197
197
  <%= render "completion_kit/api_reference/resource_list", title: "Your responses",
198
198
  items: recent_runs.first(1).map { |r|
199
- { name: "#{r.name} — responses",
199
+ { name: "#{r.name} · responses",
200
200
  url: "#{base_url}/api/v1/runs/#{r.id}/responses", dom_id: "responses_ep_#{r.id}" }
201
201
  } %>
202
202
  </div>
@@ -329,7 +329,7 @@
329
329
  </div>
330
330
  <div class="ck-api-endpoint">
331
331
  <p class="ck-api-method"><span class="ck-chip ck-chip--soft">GET</span> /api/v1/runs/:run_id/responses/:response_id/metrics/:metric_id/agreements</p>
332
- <p class="ck-meta-copy">List agreements scoped to a specific run, response, and metric &mdash; the nested equivalent of the top-level index filtered by those three ids.</p>
332
+ <p class="ck-meta-copy">List agreements scoped to a specific run, response, and metric. It is the nested equivalent of the top-level index filtered by those three ids.</p>
333
333
  </div>
334
334
  <div class="ck-api-endpoint">
335
335
  <p class="ck-api-method"><span class="ck-chip" style="color: var(--ck-danger);">DELETE</span> /api/v1/agreements/:id</p>
@@ -2,10 +2,21 @@
2
2
  <% metric_group_delete_id = "ck_delete_metric_group_#{metric_group.id}" %>
3
3
  <% members_n = metric_group.metrics.count %>
4
4
  <% metric_group_delete_confirm = members_n.zero? ?
5
- "Delete \"#{metric_group.name}\"? It has no members." :
6
- "Delete \"#{metric_group.name}\"? Removes this grouping. #{pluralize(members_n, 'member metric')} #{members_n == 1 ? 'is' : 'are'} kept." %>
5
+ "Delete \"#{metric_group.name_was}\"? It has no members." :
6
+ "Delete \"#{metric_group.name_was}\"? Removes this grouping. #{pluralize(members_n, 'member metric')} #{members_n == 1 ? 'is' : 'are'} kept." %>
7
7
  <% end %>
8
8
  <%= form_with(model: metric_group, url: metric_group.persisted? ? metric_group_path(metric_group) : metric_groups_path, local: true) do |form| %>
9
+ <% if metric_group.errors.any? %>
10
+ <div class="ck-flash ck-flash--alert" role="alert">
11
+ <p class="ck-flash__title"><%= pluralize(metric_group.errors.count, "problem") %> prevented this metric group from being saved.</p>
12
+ <ul class="ck-error-list">
13
+ <% metric_group.errors.full_messages.each do |message| %>
14
+ <li><%= message %></li>
15
+ <% end %>
16
+ </ul>
17
+ </div>
18
+ <% end %>
19
+
9
20
  <div class="ck-card ck-form-card">
10
21
  <div class="ck-field">
11
22
  <%= form.label :name, "Metric group name", class: "ck-label" %>
@@ -1,7 +1,7 @@
1
1
  <% content_for :ck_title, "Edit metric group" %>
2
2
  <ol class="ck-breadcrumb">
3
3
  <li><%= link_to "Metrics", metrics_path %></li>
4
- <li><%= link_to @metric_group.name, metric_group_path(@metric_group) %></li>
4
+ <li><%= link_to @metric_group.name_was, metric_group_path(@metric_group) %></li>
5
5
  <li>Edit</li>
6
6
  </ol>
7
7
 
@@ -72,7 +72,7 @@
72
72
 
73
73
  <% if @checklist.sample_loadable? %>
74
74
  <div class="ck-launch__sample">
75
- <p class="ck-launch__sample-copy">Just exploring? Drop in a sample dataset and prompt to poke around — they're labelled <code>Sample:</code>, and you can edit or delete them whenever.</p>
75
+ <p class="ck-launch__sample-copy">Just exploring? Drop in a sample dataset and prompt to poke around. They're labelled <code>Sample:</code>, and you can edit or delete them whenever.</p>
76
76
  <%= button_to onboarding_sample_data_path, method: :post, class: "ck-launch__sample-cta", form: { style: "display:inline" } do %>
77
77
  Load sample data <span aria-hidden="true">&rarr;</span>
78
78
  <% end %>
@@ -81,7 +81,7 @@
81
81
 
82
82
  <div class="ck-launch__panel-footer">
83
83
  <%= button_to dismiss_onboarding_path, method: :post, class: "ck-launch__dismiss", form: { style: "display:inline" } do %>
84
- Skip setup &mdash; go to the app <span aria-hidden="true">&rarr;</span>
84
+ Skip setup and go to the app <span aria-hidden="true">&rarr;</span>
85
85
  <% end %>
86
86
  <p class="ck-launch__dismiss-hint">You can come back to this from <strong>Settings &rarr; Getting started</strong> any time.</p>
87
87
  </div>
@@ -47,13 +47,14 @@
47
47
  <% available = CompletionKit::ApiConfig.available_models(scope: :generation) %>
48
48
  <% if available.any? %>
49
49
  <div class="ck-select-with-action">
50
- <%= form.select :llm_model, ck_grouped_models(available, prompt.llm_model), { include_blank: "Select a model" }, { class: "ck-input", id: "prompt_llm_model" } %>
50
+ <%= form.select :llm_model, ck_grouped_models(available, prompt.llm_model, rejected: prompt.errors[:llm_model].any?), { include_blank: "Select a model" }, { class: "ck-input", id: "prompt_llm_model", **ck_field_aria(form, :llm_model) } %>
51
51
  <% ck_refreshing = CompletionKit::ProviderCredential.discovery_in_progress? %>
52
52
  <button type="button" class="ck-icon-btn<%= ' ck-icon-btn--spinning' if ck_refreshing %>" title="Refresh models" <%= 'disabled' if ck_refreshing %> onclick="ckRefreshModels()"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" width="16" height="16"><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>
53
53
  </div>
54
54
  <% else %>
55
55
  <p class="ck-meta-copy">No models available. <%= link_to "Add a provider", provider_credentials_path, class: "ck-link" %> or click refresh after configuring a provider.</p>
56
56
  <% end %>
57
+ <%= ck_field_error(form, :llm_model) %>
57
58
  <div hidden data-refresh-progress-carriers data-ck-refresh-url="<%= refresh_models_path %>" data-ck-statuses-url="<%= statuses_provider_credentials_path %>">
58
59
  <% CompletionKit::ProviderCredential.find_each do |pc| %>
59
60
  <%= turbo_stream_from "completion_kit_provider_#{pc.id}" %>
@@ -35,7 +35,7 @@
35
35
  Gen<span class="ck-info-toggle" tabindex="0">?</span><span class="ck-info-popup ck-info-popup--right">Generation models produce the responses your prompts ask for. Pick one when creating a prompt.</span>
36
36
  </th>
37
37
  <th scope="col" class="ck-model-table__cap">
38
- Judge<span class="ck-info-toggle" tabindex="0">?</span><span class="ck-info-popup ck-info-popup--right">Judge models score generated responses against your metrics. Pick one when configuring a run. A <strong>?</strong> means we haven't confirmed this model works as a judge — it's still selectable, and a successful run promotes it to ✓.</span>
38
+ Judge<span class="ck-info-toggle" tabindex="0">?</span><span class="ck-info-popup ck-info-popup--right">Judge models score generated responses against your metrics. Pick one when configuring a run. A <strong>?</strong> means we haven't confirmed this model works as a judge. It's still selectable, and a successful run promotes it to ✓.</span>
39
39
  </th>
40
40
  </tr>
41
41
  </thead>
@@ -58,7 +58,7 @@
58
58
  <% if m.supports_judging %>
59
59
  <span class="ck-model-table__tick" aria-label="Supports judging">✓</span>
60
60
  <% elsif m.supports_judging.nil? %>
61
- <span class="ck-model-table__unknown" aria-label="Untested as judge" title="Untested as a judge — selectable; a successful run confirms it">?</span>
61
+ <span class="ck-model-table__unknown" aria-label="Untested as judge" title="Untested as a judge but selectable. A successful run confirms it.">?</span>
62
62
  <% else %>
63
63
  <span class="ck-model-table__dash" aria-label="Not usable as judge">—</span>
64
64
  <% end %>
@@ -19,7 +19,7 @@
19
19
 
20
20
  <div class="ck-field">
21
21
  <label class="ck-checkbox-label">
22
- <%= check_box_tag "run[judge_only]", "1", run.persisted? && run.judge_only?, id: "run_judge_only", class: "ck-checkbox" %>
22
+ <%= check_box_tag "run[judge_only]", "1", local_assigns[:judge_only] || (run.persisted? && run.judge_only?), id: "run_judge_only", class: "ck-checkbox" %>
23
23
  <span class="ck-checkbox-label__box" aria-hidden="true"></span>
24
24
  <span class="ck-checkbox-label__body">
25
25
  <span class="ck-checkbox-label__text">Score existing outputs</span>
@@ -98,10 +98,11 @@
98
98
  <% available = CompletionKit::ApiConfig.available_models(scope: :judging) %>
99
99
  <% if available.any? %>
100
100
  <div class="ck-select-with-action">
101
- <%= form.select :judge_model, ck_grouped_models(available, run.judge_model), { include_blank: "None" }, { class: "ck-input", id: "run_judge_model" } %>
101
+ <%= form.select :judge_model, ck_grouped_models(available, run.judge_model, rejected: run.errors[:judge_model].any?), { include_blank: "None" }, { class: "ck-input", id: "run_judge_model", **ck_field_aria(form, :judge_model) } %>
102
102
  <% ck_refreshing = CompletionKit::ProviderCredential.discovery_in_progress? %>
103
103
  <button type="button" class="ck-icon-btn<%= ' ck-icon-btn--spinning' if ck_refreshing %>" title="Refresh models" <%= 'disabled' if ck_refreshing %> onclick="ckRefreshModels()"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" width="16" height="16"><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>
104
104
  </div>
105
+ <%= ck_field_error(form, :judge_model) %>
105
106
  <p class="ck-field-hint" id="judge-hint"></p>
106
107
  <div hidden data-refresh-progress-carriers data-ck-refresh-url="<%= refresh_models_path %>" data-ck-statuses-url="<%= statuses_provider_credentials_path %>">
107
108
  <% CompletionKit::ProviderCredential.find_each do |pc| %>
@@ -112,7 +113,7 @@
112
113
  <p class="ck-field-hint" id="refresh-status" role="status" aria-live="polite" style="min-height: 1.2em; margin-top: -0.25rem; font-size: 0.75rem;">&nbsp;</p>
113
114
  <% else %>
114
115
  <p class="ck-field-hint" style="color: var(--ck-warning);">No judge models available.&ensp;<%= link_to "Add a provider", provider_credentials_path, class: "ck-link" %></p>
115
- <%= form.hidden_field :judge_model, value: nil, id: "run_judge_model" %>
116
+ <%= form.hidden_field :judge_model, id: "run_judge_model" %>
116
117
  <% end %>
117
118
  </div>
118
119
 
@@ -157,6 +158,7 @@
157
158
  </div>
158
159
  <% end %>
159
160
 
161
+ <%= hidden_field_tag "run[metric_ids][]", "", id: nil %>
160
162
  <div class="ck-metric-checkboxes" data-metric-checkboxes role="group" aria-labelledby="run_metrics_label">
161
163
  <% @all_metrics.each do |metric| %>
162
164
  <label class="ck-checkbox-label" data-metric-tags="<%= metric.tag_names.join(",") %>" data-compare-expected="<%= metric.check? && metric.check_config.to_h["compare_to"] == "expected" ? "1" : "0" %>">
@@ -194,6 +196,7 @@ function updateRunForm() {
194
196
 
195
197
  var prompt = promptEl ? promptEl.value : '';
196
198
  var judge = judgeEl ? judgeEl.value : '';
199
+ var judgeListed = !!judgeEl && judgeEl.type !== 'hidden';
197
200
  var metrics = document.querySelectorAll('input[name="run[metric_ids][]"]:checked');
198
201
  var judgeField = document.getElementById('judge-field');
199
202
  var metricsField = document.getElementById('metrics-field');
@@ -206,13 +209,15 @@ function updateRunForm() {
206
209
  if (judgeHint) judgeHint.textContent = '';
207
210
  if (metricsHint) metricsHint.textContent = '';
208
211
 
209
- if (judge && metrics.length > 0) {
210
- if (judgeHint) judgeHint.textContent = 'Responses will be generated then judged automatically.';
211
- } else if (judge && metrics.length === 0) {
212
+ var judgeUnpaired = judgeListed && !!judge && metrics.length === 0;
213
+ var metricsUnpaired = judgeListed && !judge && metrics.length > 0;
214
+ if (judgeListed && judge && metrics.length > 0) {
215
+ if (judgeHint) judgeHint.textContent = judgeOnly ? 'Existing outputs will be judged automatically.' : 'Responses will be generated then judged automatically.';
216
+ } else if (judgeUnpaired) {
212
217
  if (judgeField) judgeField.className = 'ck-field ck-field--info';
213
218
  if (metricsField) metricsField.className = 'ck-field ck-field--info';
214
219
  if (metricsHint) metricsHint.textContent = 'Select at least one metric or group to enable judging.';
215
- } else if (!judge && metrics.length > 0) {
220
+ } else if (metricsUnpaired) {
216
221
  if (judgeField) judgeField.className = 'ck-field ck-field--info';
217
222
  if (judgeHint) judgeHint.textContent = 'Select a judge model to enable judging.';
218
223
  }
@@ -286,7 +291,7 @@ function updateRunForm() {
286
291
  if (col === '' || headersJudge.indexOf(col) === -1) {
287
292
  valid = false;
288
293
  if (datasetField) datasetField.className = 'ck-field ck-field--error';
289
- if (datasetHint) datasetHint.textContent = "Dataset has no \"" + col + "\" column — pick a different output column or dataset.";
294
+ if (datasetHint) datasetHint.textContent = "Dataset has no \"" + col + "\" column. Pick a different output column or dataset.";
290
295
  }
291
296
  } else if (!dataset) {
292
297
  if (datasetField) datasetField.className = 'ck-field ck-field--info';
@@ -297,8 +302,7 @@ function updateRunForm() {
297
302
  if (hasVars && !dataset) valid = false;
298
303
  if (missingVars.length > 0) valid = false;
299
304
  }
300
- if (judge && metrics.length === 0) valid = false;
301
- if (!judge && metrics.length > 0) valid = false;
305
+ if (judgeUnpaired || metricsUnpaired) valid = false;
302
306
  if (submitBtn) submitBtn.disabled = !valid;
303
307
 
304
308
  ckUpdateMetricGroupsState();
@@ -4,7 +4,8 @@
4
4
  <td class="ck-response-cell__text">
5
5
  <% if response.status == "failed" %>
6
6
  <% err = response.error_payload %>
7
- <span class="ck-response-cell__preview ck-response-cell__error"><%= err && err[:provider]&.titleize %><%= " #{err[:status]}" if err && err[:status] %> — <%= truncate(err && err[:message].to_s, length: 600) %></span>
7
+ <% err_prefix = [err && err[:provider]&.titleize, err && err[:status]].compact.join(" ") %>
8
+ <span class="ck-response-cell__preview ck-response-cell__error"><%= "#{err_prefix}: " if err_prefix.present? %><%= truncate(err && err[:message].to_s, length: 600) %></span>
8
9
  <% else %>
9
10
  <span class="ck-response-cell__preview"><%= truncate(response.response_text.to_s, length: 600) %></span>
10
11
  <% end %>
@@ -16,4 +16,4 @@
16
16
  </div>
17
17
  </section>
18
18
 
19
- <%= render "form", run: @run %>
19
+ <%= render "form", run: @run, judge_only: @judge_only %>
@@ -10,4 +10,4 @@
10
10
  </div>
11
11
  </section>
12
12
 
13
- <%= render "form", run: @run %>
13
+ <%= render "form", run: @run, judge_only: @judge_only %>
@@ -1,7 +1,7 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
- <title><%= [content_for(:ck_title).presence, "CompletionKit"].compact.join(" — ") %></title>
4
+ <title><%= [content_for(:ck_title).presence, "CompletionKit"].compact.join(" · ") %></title>
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <%= csrf_meta_tags %>
7
7
  <%= csp_meta_tag %>
@@ -0,0 +1,16 @@
1
+ class DropCompletionKitMcpSessions < ActiveRecord::Migration[8.1]
2
+ def up
3
+ drop_table :completion_kit_mcp_sessions, if_exists: true
4
+ end
5
+
6
+ def down
7
+ create_table :completion_kit_mcp_sessions do |t|
8
+ t.string :session_id, null: false
9
+ t.datetime :expires_at, null: false
10
+ t.timestamps
11
+ end
12
+
13
+ add_index :completion_kit_mcp_sessions, :session_id, unique: true
14
+ add_index :completion_kit_mcp_sessions, :expires_at
15
+ end
16
+ end
@@ -9,9 +9,9 @@ module CompletionKit
9
9
  class UnknownModelError < ConfigurationError
10
10
  attr_reader :model_name
11
11
 
12
- def initialize(model_name = nil)
12
+ def initialize(model_name = nil, message = nil)
13
13
  @model_name = model_name
14
- super(%(No configured provider serves model "#{model_name}"))
14
+ super(message || %(No configured provider serves model "#{model_name}"))
15
15
  end
16
16
  end
17
17
 
@@ -1,3 +1,3 @@
1
1
  module CompletionKit
2
- VERSION = "0.28.45"
2
+ VERSION = "0.28.46"
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.45
4
+ version: 0.28.46
5
5
  platform: ruby
6
6
  authors:
7
7
  - Damien Bastin
@@ -452,6 +452,7 @@ files:
452
452
  - db/migrate/20260730000002_create_completion_kit_prompt_serves.rb
453
453
  - db/migrate/20260804000001_add_judge_temperature_ignored_to_runs.rb
454
454
  - db/migrate/20260804000002_default_runs_to_no_temperature.rb
455
+ - db/migrate/20261013000001_drop_completion_kit_mcp_sessions.rb
455
456
  - lib/completion-kit.rb
456
457
  - lib/completion_kit.rb
457
458
  - lib/completion_kit/concurrency_check.rb