completion-kit 0.28.44 → 0.28.45
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/README.md +31 -6
- data/app/assets/stylesheets/completion_kit/application.css +5 -0
- data/app/controllers/completion_kit/agreements_controller.rb +1 -5
- data/app/controllers/completion_kit/api/v1/base_controller.rb +5 -0
- data/app/controllers/completion_kit/api/v1/metric_groups_controller.rb +6 -4
- data/app/controllers/completion_kit/api/v1/metrics_controller.rb +1 -1
- data/app/controllers/completion_kit/api/v1/runs_controller.rb +9 -5
- data/app/controllers/completion_kit/application_controller.rb +13 -0
- data/app/controllers/completion_kit/mcp_controller.rb +12 -4
- data/app/controllers/completion_kit/metric_groups_controller.rb +6 -4
- data/app/controllers/completion_kit/provider_credentials_controller.rb +19 -1
- data/app/controllers/completion_kit/runs_controller.rb +13 -8
- data/app/jobs/completion_kit/generate_row_job.rb +2 -1
- data/app/jobs/completion_kit/judge_review_job.rb +1 -0
- data/app/jobs/completion_kit/metric_suggestion_job.rb +1 -2
- data/app/jobs/completion_kit/model_discovery_job.rb +28 -13
- data/app/models/completion_kit/metric_group.rb +4 -2
- data/app/models/completion_kit/metric_group_membership.rb +1 -1
- data/app/models/completion_kit/prompt.rb +7 -6
- data/app/models/completion_kit/provider_credential.rb +19 -2
- data/app/models/completion_kit/run.rb +77 -14
- data/app/models/completion_kit/run_metric.rb +1 -1
- data/app/models/concerns/completion_kit/metric_selection.rb +35 -0
- data/app/services/completion_kit/api_config.rb +23 -7
- data/app/services/completion_kit/azure_foundry_client.rb +4 -2
- data/app/services/completion_kit/csv_processor.rb +4 -6
- data/app/services/completion_kit/json_object.rb +14 -0
- data/app/services/completion_kit/llm_client.rb +1 -1
- data/app/services/completion_kit/mcp_dispatcher.rb +1 -1
- data/app/services/completion_kit/mcp_tools/base.rb +2 -0
- data/app/services/completion_kit/mcp_tools/datasets.rb +1 -1
- data/app/services/completion_kit/mcp_tools/judges.rb +2 -2
- data/app/services/completion_kit/mcp_tools/metric_groups.rb +2 -4
- data/app/services/completion_kit/mcp_tools/provider_credentials.rb +3 -2
- data/app/services/completion_kit/mcp_tools/runs.rb +6 -6
- data/app/services/completion_kit/model_discovery_service.rb +10 -10
- data/app/services/completion_kit/ollama_client.rb +4 -2
- data/app/services/completion_kit/promptfoo_importer.rb +1 -1
- data/app/services/completion_kit/provider_endpoint.rb +18 -9
- data/app/views/completion_kit/agreements/_buttons.html.erb +1 -1
- data/app/views/completion_kit/agreements/_trust_panel.html.erb +1 -2
- data/app/views/completion_kit/api_reference/_body.html.erb +4 -3
- data/app/views/completion_kit/dashboard/show.html.erb +1 -1
- data/app/views/completion_kit/datasets/show.html.erb +5 -0
- data/app/views/completion_kit/metric_groups/_form.html.erb +4 -1
- data/app/views/completion_kit/prompts/show.html.erb +5 -0
- data/app/views/completion_kit/provider_credentials/_form.html.erb +24 -1
- data/app/views/completion_kit/provider_credentials/edit.html.erb +1 -1
- data/app/views/completion_kit/provider_credentials/index.html.erb +1 -2
- data/app/views/completion_kit/responses/show.html.erb +1 -1
- data/app/views/completion_kit/runs/_form.html.erb +1 -1
- data/app/views/completion_kit/runs/index.html.erb +1 -1
- data/config/locales/en.yml +12 -0
- data/lib/completion_kit/errors.rb +30 -0
- data/lib/completion_kit/version.rb +1 -1
- data/lib/completion_kit.rb +1 -1
- data/lib/generators/completion_kit/templates/initializer.rb +9 -0
- data/lib/tasks/completion_kit_runs.rake +2 -2
- metadata +4 -2
- data/app/models/completion_kit/mcp_session.rb +0 -54
|
@@ -2,6 +2,7 @@ module CompletionKit
|
|
|
2
2
|
class Run < ApplicationRecord
|
|
3
3
|
include Turbo::Broadcastable
|
|
4
4
|
include CompletionKit::Taggable
|
|
5
|
+
include CompletionKit::MetricSelection
|
|
5
6
|
|
|
6
7
|
STATUSES = %w[pending running completed failed].freeze
|
|
7
8
|
INSERT_BATCH_SIZE = 1000
|
|
@@ -21,14 +22,17 @@ module CompletionKit
|
|
|
21
22
|
has_many :dashboard_dismissals, as: :dismissable, dependent: :destroy
|
|
22
23
|
|
|
23
24
|
attr_writer :avg_score, :check_pass_rate, :metric_averages, :response_count
|
|
25
|
+
attr_accessor :cloned_from_judge_model
|
|
24
26
|
|
|
25
27
|
validates :name, presence: true
|
|
26
28
|
validates :status, inclusion: { in: STATUSES }
|
|
27
29
|
validates :max_tokens, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true
|
|
28
30
|
validates :judge_temperature, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 1 }, allow_nil: true
|
|
29
|
-
validate :
|
|
30
|
-
validate :
|
|
31
|
+
validate :references_in_scope
|
|
32
|
+
validate :dataset_supplies_prompt_variables, unless: :reference_out_of_scope?
|
|
33
|
+
validate :judge_only_run_supplies_output_column, unless: :reference_out_of_scope?
|
|
31
34
|
validate :dataset_supplies_expected_column
|
|
35
|
+
validate :judge_model_servable, if: :judge_model_newly_selected?
|
|
32
36
|
|
|
33
37
|
before_validation :set_default_status, on: :create
|
|
34
38
|
before_validation :set_auto_name, on: :create
|
|
@@ -221,9 +225,10 @@ module CompletionKit
|
|
|
221
225
|
end
|
|
222
226
|
|
|
223
227
|
def replace_metrics!(metric_ids)
|
|
224
|
-
return
|
|
228
|
+
return if metric_ids.nil?
|
|
229
|
+
|
|
225
230
|
run_metrics.delete_all
|
|
226
|
-
|
|
231
|
+
normalize_metric_ids(metric_ids).each_with_index do |metric_id, index|
|
|
227
232
|
run_metrics.create!(metric_id: metric_id, position: index + 1)
|
|
228
233
|
end
|
|
229
234
|
end
|
|
@@ -329,12 +334,13 @@ module CompletionKit
|
|
|
329
334
|
return fail_with_summary!("Dataset has no \"#{column}\" column")
|
|
330
335
|
end
|
|
331
336
|
else
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
return fail_with_summary!("LLM API not configured: #{client.configuration_errors.join(', ')}")
|
|
335
|
-
end
|
|
337
|
+
generation_error = generation_client_error
|
|
338
|
+
return fail_with_summary!(generation_error) if generation_error
|
|
336
339
|
end
|
|
337
340
|
|
|
341
|
+
judge_error = judge_model_error
|
|
342
|
+
return fail_with_summary!(judge_error) if judge_error
|
|
343
|
+
|
|
338
344
|
begin
|
|
339
345
|
transaction do
|
|
340
346
|
responses.destroy_all
|
|
@@ -395,7 +401,7 @@ module CompletionKit
|
|
|
395
401
|
|
|
396
402
|
response_ids = responses.order(:row_index).pluck(:id)
|
|
397
403
|
if judge
|
|
398
|
-
judge_metrics =
|
|
404
|
+
judge_metrics = judge_model.present? ? llm_metrics.to_a : []
|
|
399
405
|
chk_metrics = check_metrics.to_a
|
|
400
406
|
review_jobs = response_ids.flat_map do |rid|
|
|
401
407
|
judge_metrics.map { |m| JudgeReviewJob.new(rid, m.id, id) } +
|
|
@@ -424,6 +430,11 @@ module CompletionKit
|
|
|
424
430
|
end
|
|
425
431
|
|
|
426
432
|
def regrade!
|
|
433
|
+
judge_error = judge_model_error
|
|
434
|
+
if judge_error
|
|
435
|
+
errors.add(:base, judge_error)
|
|
436
|
+
return false
|
|
437
|
+
end
|
|
427
438
|
return false if metrics.empty? || !gradable?
|
|
428
439
|
|
|
429
440
|
eligible_responses = responses.where(status: "succeeded")
|
|
@@ -447,7 +458,7 @@ module CompletionKit
|
|
|
447
458
|
update!(status: "running", failure_summary: nil, error_message: nil, judge_temperature_ignored: false)
|
|
448
459
|
|
|
449
460
|
response_ids.each do |rid|
|
|
450
|
-
llm_metrics.each { |m| JudgeReviewJob.perform_later(rid, m.id, id) } if
|
|
461
|
+
llm_metrics.each { |m| JudgeReviewJob.perform_later(rid, m.id, id) } if judge_model.present?
|
|
451
462
|
check_metrics.each { |m| CheckReviewJob.perform_later(rid, m.id, id) }
|
|
452
463
|
end
|
|
453
464
|
RunCompletionCheckJob.perform_later(id)
|
|
@@ -479,7 +490,8 @@ module CompletionKit
|
|
|
479
490
|
end
|
|
480
491
|
|
|
481
492
|
def rerun!
|
|
482
|
-
|
|
493
|
+
ensure_models_resolve!
|
|
494
|
+
Run.create!(
|
|
483
495
|
prompt_id: prompt_id,
|
|
484
496
|
dataset_id: dataset_id,
|
|
485
497
|
judge_model: judge_model,
|
|
@@ -489,10 +501,10 @@ module CompletionKit
|
|
|
489
501
|
output_column: output_column,
|
|
490
502
|
expected_column: expected_column,
|
|
491
503
|
tag_names: tag_names,
|
|
492
|
-
status: "pending"
|
|
504
|
+
status: "pending",
|
|
505
|
+
cloned_from_judge_model: judge_model,
|
|
506
|
+
selected_metric_ids: run_metrics.pluck(:metric_id)
|
|
493
507
|
)
|
|
494
|
-
new_run.replace_metrics!(metric_ids)
|
|
495
|
-
new_run
|
|
496
508
|
end
|
|
497
509
|
|
|
498
510
|
def retry_failures!(only: nil)
|
|
@@ -686,6 +698,40 @@ module CompletionKit
|
|
|
686
698
|
Rails.error.report(e, handled: true, context: { hook: "on_run_started", run_id: id })
|
|
687
699
|
end
|
|
688
700
|
|
|
701
|
+
def generation_client_error
|
|
702
|
+
client = LlmClient.for_model(prompt.llm_model, ApiConfig.for_model(prompt.llm_model))
|
|
703
|
+
"LLM API not configured: #{client.configuration_errors.join(', ')}" unless client.configured?
|
|
704
|
+
rescue UnknownModelError => e
|
|
705
|
+
e.message
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
def judge_model_error
|
|
709
|
+
return unless judge_model.present? && llm_metrics.exists?
|
|
710
|
+
return if ApiConfig.provider_for_model(judge_model)
|
|
711
|
+
|
|
712
|
+
%(No configured provider serves judge model "#{judge_model}")
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
def ensure_models_resolve!
|
|
716
|
+
raise UnknownModelError, prompt.llm_model unless judge_only? || ApiConfig.provider_for_model(prompt.llm_model)
|
|
717
|
+
|
|
718
|
+
judge_error = judge_model_error
|
|
719
|
+
raise ConfigurationError, judge_error if judge_error
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
def judge_model_newly_selected?
|
|
723
|
+
return false if judge_model.blank?
|
|
724
|
+
return judge_model != cloned_from_judge_model if new_record? && cloned_from_judge_model.present?
|
|
725
|
+
|
|
726
|
+
judge_model_changed?
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
def judge_model_servable
|
|
730
|
+
return if ApiConfig.servable_model?(judge_model)
|
|
731
|
+
|
|
732
|
+
errors.add(:judge_model, %("#{judge_model}" is not served by any configured provider))
|
|
733
|
+
end
|
|
734
|
+
|
|
689
735
|
def fail_with_summary!(message)
|
|
690
736
|
errors.add(:base, message)
|
|
691
737
|
if persisted?
|
|
@@ -736,6 +782,23 @@ module CompletionKit
|
|
|
736
782
|
end
|
|
737
783
|
end
|
|
738
784
|
|
|
785
|
+
def prompt_out_of_scope?
|
|
786
|
+
will_save_change_to_prompt_id? && prompt_id.present? && prompt.nil?
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
def dataset_out_of_scope?
|
|
790
|
+
will_save_change_to_dataset_id? && dataset_id.present? && dataset.nil?
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
def reference_out_of_scope?
|
|
794
|
+
prompt_out_of_scope? || dataset_out_of_scope?
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
def references_in_scope
|
|
798
|
+
errors.add(:prompt_id, "not found") if prompt_out_of_scope?
|
|
799
|
+
errors.add(:dataset_id, "not found") if dataset_out_of_scope?
|
|
800
|
+
end
|
|
801
|
+
|
|
739
802
|
def dataset_supplies_prompt_variables
|
|
740
803
|
missing = missing_dataset_variables
|
|
741
804
|
return if missing.empty?
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module CompletionKit
|
|
2
|
+
module MetricSelection
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
validate :selected_metrics_in_scope, if: -> { @metric_selection_assigned }
|
|
7
|
+
after_save :attach_selected_metrics, if: -> { @metric_selection_assigned }
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def selected_metric_ids=(ids)
|
|
11
|
+
@metric_selection_assigned = !ids.nil?
|
|
12
|
+
@selected_metric_ids = ids
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def selected_metric_ids
|
|
16
|
+
@metric_selection_assigned ? normalize_metric_ids(@selected_metric_ids) : metric_ids
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def normalize_metric_ids(ids)
|
|
22
|
+
Array(ids).reject(&:blank?).map(&:to_i).uniq
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def selected_metrics_in_scope
|
|
26
|
+
wanted = selected_metric_ids
|
|
27
|
+
errors.add(:metric_ids, "include one that isn't available") if CompletionKit::Metric.where(id: wanted).count < wanted.size
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def attach_selected_metrics
|
|
31
|
+
replace_metrics!(@selected_metric_ids)
|
|
32
|
+
@metric_selection_assigned = false
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -39,14 +39,14 @@ module CompletionKit
|
|
|
39
39
|
available_match = available_models.find { |model| model[:id] == model_name.to_s }
|
|
40
40
|
return available_match[:provider] if available_match
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
when /\Aclaude-/ then "anthropic"
|
|
45
|
-
end
|
|
46
|
-
configured = ProviderCredential.distinct.pluck(:provider)
|
|
47
|
-
return guess if configured.empty?
|
|
42
|
+
guessed_provider(model_name)
|
|
43
|
+
end
|
|
48
44
|
|
|
49
|
-
|
|
45
|
+
def self.servable_model?(model_name)
|
|
46
|
+
return true unless Model.for_generation.exists?
|
|
47
|
+
return true if Model.active.exists?(model_id: model_name.to_s)
|
|
48
|
+
|
|
49
|
+
guessed_provider(model_name).present?
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
def self.default_judge_model
|
|
@@ -58,11 +58,15 @@ module CompletionKit
|
|
|
58
58
|
def self.valid_for_model?(model_name)
|
|
59
59
|
client = LlmClient.for_model(model_name, for_model(model_name))
|
|
60
60
|
client.configured?
|
|
61
|
+
rescue UnknownModelError
|
|
62
|
+
false
|
|
61
63
|
end
|
|
62
64
|
|
|
63
65
|
def self.errors_for_model(model_name)
|
|
64
66
|
client = LlmClient.for_model(model_name, for_model(model_name))
|
|
65
67
|
client.configuration_errors
|
|
68
|
+
rescue UnknownModelError => e
|
|
69
|
+
[e.message]
|
|
66
70
|
end
|
|
67
71
|
|
|
68
72
|
def self.available_models(provider: nil, scope: :generation)
|
|
@@ -90,5 +94,17 @@ module CompletionKit
|
|
|
90
94
|
[]
|
|
91
95
|
end.uniq { |model| model[:id] }
|
|
92
96
|
end
|
|
97
|
+
|
|
98
|
+
def self.guessed_provider(model_name)
|
|
99
|
+
guess = case model_name.to_s
|
|
100
|
+
when /\Agpt-/ then "openai"
|
|
101
|
+
when /\Aclaude-/ then "anthropic"
|
|
102
|
+
end
|
|
103
|
+
configured = ProviderCredential.distinct.pluck(:provider)
|
|
104
|
+
return guess if configured.empty?
|
|
105
|
+
|
|
106
|
+
guess if guess && configured.include?(guess)
|
|
107
|
+
end
|
|
108
|
+
private_class_method :guessed_provider
|
|
93
109
|
end
|
|
94
110
|
end
|
|
@@ -35,7 +35,7 @@ module CompletionKit
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
if response.success?
|
|
38
|
-
data =
|
|
38
|
+
data = JsonObject.parse(response.body)
|
|
39
39
|
content = data.dig("choices", 0, "message", "content").to_s.strip
|
|
40
40
|
return "Error: model returned empty content" if content.empty?
|
|
41
41
|
content
|
|
@@ -48,6 +48,8 @@ module CompletionKit
|
|
|
48
48
|
raise
|
|
49
49
|
rescue ProviderEndpoint::UnsafeEndpoint => e
|
|
50
50
|
"Error: API endpoint #{e.message}"
|
|
51
|
+
rescue JSON::ParserError => e
|
|
52
|
+
"Error: #{EndpointError.from(e).message}"
|
|
51
53
|
rescue => e
|
|
52
54
|
"Error: #{e.message}"
|
|
53
55
|
end
|
|
@@ -61,7 +63,7 @@ module CompletionKit
|
|
|
61
63
|
end
|
|
62
64
|
return [] unless response.success?
|
|
63
65
|
|
|
64
|
-
body =
|
|
66
|
+
body = JsonObject.parse(response.body)
|
|
65
67
|
if foundry_project?
|
|
66
68
|
body.fetch("value", []).map { |d| { id: d["name"], name: d["name"] } }
|
|
67
69
|
else
|
|
@@ -53,13 +53,11 @@ module CompletionKit
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def self.apply_variables(prompt, variables)
|
|
56
|
-
|
|
56
|
+
values = variables.to_h.transform_keys(&:to_s)
|
|
57
|
+
return prompt.template.dup if values.empty?
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
result
|
|
59
|
+
names = values.keys.map { |name| Regexp.escape(name) }.join("|")
|
|
60
|
+
prompt.template.gsub(/\{\{\s*(#{names})\s*\}\}/) { values[Regexp.last_match(1)].to_s }
|
|
63
61
|
end
|
|
64
62
|
end
|
|
65
63
|
end
|
|
@@ -79,7 +79,7 @@ module CompletionKit
|
|
|
79
79
|
|
|
80
80
|
def self.for_model(model_name, config = {})
|
|
81
81
|
provider = ApiConfig.provider_for_model(model_name)
|
|
82
|
-
raise
|
|
82
|
+
raise UnknownModelError, model_name unless provider
|
|
83
83
|
|
|
84
84
|
for_provider(provider, config)
|
|
85
85
|
end
|
|
@@ -7,7 +7,7 @@ module CompletionKit
|
|
|
7
7
|
|
|
8
8
|
def self.initialize_session
|
|
9
9
|
{
|
|
10
|
-
session_id:
|
|
10
|
+
session_id: SecureRandom.uuid,
|
|
11
11
|
protocolVersion: PROTOCOL_VERSION,
|
|
12
12
|
serverInfo: {name: "CompletionKit", version: CompletionKit::VERSION},
|
|
13
13
|
capabilities: {tools: {listChanged: false}}
|
|
@@ -106,7 +106,7 @@ module CompletionKit
|
|
|
106
106
|
rescue ProviderEndpoint::UnsafeEndpoint => e
|
|
107
107
|
error_result("URL is not allowed: it #{e.message}.")
|
|
108
108
|
rescue Faraday::Error => e
|
|
109
|
-
error_result("Could not download CSV
|
|
109
|
+
error_result("Could not download CSV. #{EndpointError.from(e).message}")
|
|
110
110
|
end
|
|
111
111
|
|
|
112
112
|
def self.csv_connection(url)
|
|
@@ -41,10 +41,10 @@ module CompletionKit
|
|
|
41
41
|
name: args["name"],
|
|
42
42
|
dataset: dataset,
|
|
43
43
|
judge_model: args["judge_model"],
|
|
44
|
-
output_column: args["output_column"].presence || "actual_output"
|
|
44
|
+
output_column: args["output_column"].presence || "actual_output",
|
|
45
|
+
selected_metric_ids: [metric.id]
|
|
45
46
|
)
|
|
46
47
|
if run.save
|
|
47
|
-
run.replace_metrics!([metric.id])
|
|
48
48
|
text_result(run.reload.as_json)
|
|
49
49
|
else
|
|
50
50
|
error_result(run.errors.full_messages.join(", "))
|
|
@@ -56,10 +56,9 @@ module CompletionKit
|
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
def self.create(args)
|
|
59
|
-
metric_group = CompletionKit::MetricGroup.new(args.slice("name", "description"))
|
|
59
|
+
metric_group = CompletionKit::MetricGroup.new(args.slice("name", "description").merge("selected_metric_ids" => args["metric_ids"]))
|
|
60
60
|
metric_group.tag_names = args["tag_names"] if args.key?("tag_names")
|
|
61
61
|
if metric_group.save
|
|
62
|
-
metric_group.replace_metrics!(args["metric_ids"])
|
|
63
62
|
text_result(metric_group.reload.as_json)
|
|
64
63
|
else
|
|
65
64
|
error_result(metric_group.errors.full_messages.join(", "))
|
|
@@ -68,8 +67,7 @@ module CompletionKit
|
|
|
68
67
|
|
|
69
68
|
def self.update(args)
|
|
70
69
|
metric_group = CompletionKit::MetricGroup.find(args["id"])
|
|
71
|
-
if metric_group.update(args.
|
|
72
|
-
metric_group.replace_metrics!(args["metric_ids"]) if args.key?("metric_ids")
|
|
70
|
+
if metric_group.update(args.slice("name", "description").merge("selected_metric_ids" => args["metric_ids"]))
|
|
73
71
|
metric_group.update!(tag_names: args["tag_names"]) if args.key?("tag_names")
|
|
74
72
|
text_result(metric_group.reload.as_json)
|
|
75
73
|
else
|
|
@@ -29,12 +29,13 @@ module CompletionKit
|
|
|
29
29
|
handler: :create
|
|
30
30
|
},
|
|
31
31
|
"provider_credentials_update" => {
|
|
32
|
-
description: "Update a provider credential",
|
|
32
|
+
description: "Update a provider credential. Changing provider or api_endpoint on a credential with a stored key requires api_key in the same call, so a stored key never moves to a new provider or endpoint.",
|
|
33
33
|
inputSchema: {
|
|
34
34
|
type: "object",
|
|
35
35
|
properties: {
|
|
36
36
|
id: {type: "integer"}, provider: {type: "string"},
|
|
37
|
-
api_key: {type: "string"
|
|
37
|
+
api_key: {type: "string", description: "Required when provider or api_endpoint changes and a key is stored"},
|
|
38
|
+
api_endpoint: {type: "string"}, api_version: {type: "string"}
|
|
38
39
|
},
|
|
39
40
|
required: ["id"]
|
|
40
41
|
},
|
|
@@ -109,9 +109,8 @@ module CompletionKit
|
|
|
109
109
|
end
|
|
110
110
|
|
|
111
111
|
def self.create(args)
|
|
112
|
-
run = Run.new(args.slice("name", "prompt_id", "dataset_id", "judge_model", "output_column", "expected_column", *GENERATION_FIELDS))
|
|
112
|
+
run = Run.new(args.slice("name", "prompt_id", "dataset_id", "judge_model", "output_column", "expected_column", *GENERATION_FIELDS).merge("selected_metric_ids" => resolve_metric_ids(args)))
|
|
113
113
|
if run.save
|
|
114
|
-
run.replace_metrics!(resolve_metric_ids(args))
|
|
115
114
|
run.update!(tag_names: args["tag_names"]) if args.key?("tag_names")
|
|
116
115
|
text_result(run_payload(run.reload))
|
|
117
116
|
else
|
|
@@ -121,8 +120,7 @@ module CompletionKit
|
|
|
121
120
|
|
|
122
121
|
def self.update(args)
|
|
123
122
|
run = Run.find(args["id"])
|
|
124
|
-
if run.update(args.
|
|
125
|
-
run.replace_metrics!(resolve_metric_ids(args)) if args.key?("metric_ids") || args["metric_group_id"].present?
|
|
123
|
+
if run.update(args.slice("name", "dataset_id", "judge_model", "output_column", "expected_column", *GENERATION_FIELDS).merge("selected_metric_ids" => resolve_metric_ids(args)))
|
|
126
124
|
run.update!(tag_names: args["tag_names"]) if args.key?("tag_names")
|
|
127
125
|
text_result(run_payload(run.reload))
|
|
128
126
|
else
|
|
@@ -140,7 +138,7 @@ module CompletionKit
|
|
|
140
138
|
if run.start!
|
|
141
139
|
text_result(run_payload(run.reload))
|
|
142
140
|
else
|
|
143
|
-
|
|
141
|
+
error_result(run.failure_summary || run.errors.full_messages.to_sentence)
|
|
144
142
|
end
|
|
145
143
|
end
|
|
146
144
|
|
|
@@ -149,7 +147,7 @@ module CompletionKit
|
|
|
149
147
|
if run.regrade!
|
|
150
148
|
text_result(run_payload(run.reload))
|
|
151
149
|
else
|
|
152
|
-
error_result("Nothing to re-grade. The run has no succeeded responses or no metrics attached.")
|
|
150
|
+
error_result(run.errors.full_messages.to_sentence.presence || "Nothing to re-grade. The run has no succeeded responses or no metrics attached.")
|
|
153
151
|
end
|
|
154
152
|
end
|
|
155
153
|
|
|
@@ -160,6 +158,8 @@ module CompletionKit
|
|
|
160
158
|
else
|
|
161
159
|
error_result(new_run.failure_summary || "Could not start the new run.")
|
|
162
160
|
end
|
|
161
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
162
|
+
error_result(e.record.errors.full_messages.join(", "))
|
|
163
163
|
end
|
|
164
164
|
|
|
165
165
|
def self.retry_failures(args)
|
|
@@ -67,7 +67,7 @@ module CompletionKit
|
|
|
67
67
|
|
|
68
68
|
def extract_provider_error_message(body)
|
|
69
69
|
return nil if body.blank?
|
|
70
|
-
data =
|
|
70
|
+
data = JsonObject.parse(body)
|
|
71
71
|
err = data["error"]
|
|
72
72
|
message = (err.is_a?(Hash) && err["message"]) || data["message"] || (err.is_a?(String) && err) || nil
|
|
73
73
|
redact_secrets(message)
|
|
@@ -89,7 +89,7 @@ module CompletionKit
|
|
|
89
89
|
req.headers["Authorization"] = "Bearer #{@api_key}"
|
|
90
90
|
end
|
|
91
91
|
raise_fetch_error!(response) unless response.success?
|
|
92
|
-
|
|
92
|
+
JsonObject.parse(response.body).fetch("data", []).map { |e| { id: e["id"], display_name: nil } }
|
|
93
93
|
end
|
|
94
94
|
|
|
95
95
|
def fetch_anthropic_models
|
|
@@ -98,7 +98,7 @@ module CompletionKit
|
|
|
98
98
|
req.headers["anthropic-version"] = "2023-06-01"
|
|
99
99
|
end
|
|
100
100
|
raise_fetch_error!(response) unless response.success?
|
|
101
|
-
|
|
101
|
+
JsonObject.parse(response.body).fetch("data", []).map { |e| { id: e["id"], display_name: e["display_name"] } }
|
|
102
102
|
end
|
|
103
103
|
|
|
104
104
|
def fetch_openrouter_models
|
|
@@ -108,7 +108,7 @@ module CompletionKit
|
|
|
108
108
|
req.headers["X-Title"] = "CompletionKit"
|
|
109
109
|
end
|
|
110
110
|
raise_fetch_error!(response) unless response.success?
|
|
111
|
-
|
|
111
|
+
JsonObject.parse(response.body).fetch("data", []).filter_map do |entry|
|
|
112
112
|
next nil if entry["deprecated"] == true
|
|
113
113
|
context_length = entry["context_length"].to_i
|
|
114
114
|
next nil if context_length < 8192
|
|
@@ -132,7 +132,7 @@ module CompletionKit
|
|
|
132
132
|
req.headers["Authorization"] = "Bearer #{@api_key}" if @api_key.present?
|
|
133
133
|
end
|
|
134
134
|
raise DiscoveryError, custom_endpoint_error_message(response) unless response.success?
|
|
135
|
-
|
|
135
|
+
JsonObject.parse(response.body).fetch("data", []).map { |e| { id: e["id"], display_name: e["id"] } }
|
|
136
136
|
end
|
|
137
137
|
|
|
138
138
|
def custom_endpoint_error_message(response)
|
|
@@ -181,7 +181,7 @@ module CompletionKit
|
|
|
181
181
|
end
|
|
182
182
|
raise DiscoveryError, azure_error_message(response) unless response.success?
|
|
183
183
|
|
|
184
|
-
body =
|
|
184
|
+
body = JsonObject.parse(response.body)
|
|
185
185
|
if azure_foundry_project?
|
|
186
186
|
@catalog_model_count = fetch_azure_catalog_count
|
|
187
187
|
body.fetch("value", []).map { |d| { id: d["name"], display_name: d["name"] } }
|
|
@@ -196,7 +196,7 @@ module CompletionKit
|
|
|
196
196
|
end
|
|
197
197
|
return nil unless response.success?
|
|
198
198
|
|
|
199
|
-
|
|
199
|
+
JsonObject.parse(response.body).fetch("data", []).length
|
|
200
200
|
rescue StandardError
|
|
201
201
|
nil
|
|
202
202
|
end
|
|
@@ -356,7 +356,7 @@ module CompletionKit
|
|
|
356
356
|
end
|
|
357
357
|
rescue StandardError => e
|
|
358
358
|
model.supports_generation = false
|
|
359
|
-
model.generation_error = redact_secrets(e.message)
|
|
359
|
+
model.generation_error = redact_secrets(EndpointError.from(e).message)
|
|
360
360
|
end
|
|
361
361
|
|
|
362
362
|
def probe_judging(model)
|
|
@@ -384,7 +384,7 @@ module CompletionKit
|
|
|
384
384
|
end
|
|
385
385
|
rescue StandardError => e
|
|
386
386
|
model.supports_judging = false
|
|
387
|
-
model.judging_error = redact_secrets(e.message)
|
|
387
|
+
model.judging_error = redact_secrets(EndpointError.from(e).message)
|
|
388
388
|
end
|
|
389
389
|
|
|
390
390
|
OPENAI_REASONING_PROBE_BUDGET = 65_536
|
|
@@ -405,7 +405,7 @@ module CompletionKit
|
|
|
405
405
|
end
|
|
406
406
|
|
|
407
407
|
def extract_text(response)
|
|
408
|
-
data =
|
|
408
|
+
data = JsonObject.parse(response.body)
|
|
409
409
|
case @provider
|
|
410
410
|
when "openai"
|
|
411
411
|
message = Array(data["output"]).find { |o| o["type"] == "message" }
|
|
@@ -26,7 +26,7 @@ module CompletionKit
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
if response.success?
|
|
29
|
-
data =
|
|
29
|
+
data = JsonObject.parse(response.body)
|
|
30
30
|
data["choices"][0]["text"].strip
|
|
31
31
|
else
|
|
32
32
|
"Error: #{response.status} - #{response.body}"
|
|
@@ -37,6 +37,8 @@ module CompletionKit
|
|
|
37
37
|
raise
|
|
38
38
|
rescue ProviderEndpoint::UnsafeEndpoint => e
|
|
39
39
|
"Error: API endpoint #{e.message}"
|
|
40
|
+
rescue JSON::ParserError => e
|
|
41
|
+
"Error: #{EndpointError.from(e).message}"
|
|
40
42
|
rescue => e
|
|
41
43
|
"Error: #{e.message}"
|
|
42
44
|
end
|
|
@@ -50,7 +52,7 @@ module CompletionKit
|
|
|
50
52
|
|
|
51
53
|
return [] unless response.success?
|
|
52
54
|
|
|
53
|
-
models =
|
|
55
|
+
models = JsonObject.parse(response.body).fetch("data", []).map { |entry| entry["id"] }.sort
|
|
54
56
|
models.map { |id| { id: id, name: id } }
|
|
55
57
|
rescue StandardError
|
|
56
58
|
[]
|
|
@@ -86,7 +86,7 @@ module CompletionKit
|
|
|
86
86
|
next
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
-
prompt = Prompt.create!(name: "Imported prompt #{index + 1}", template: template, llm_model: model)
|
|
89
|
+
prompt = Prompt.create!(name: "Imported prompt #{index + 1}", template: template, llm_model: model, allow_unconfigured_provider: true)
|
|
90
90
|
created << prompt.name
|
|
91
91
|
end
|
|
92
92
|
{ created: created, skipped: skipped }
|
|
@@ -5,11 +5,17 @@ module CompletionKit
|
|
|
5
5
|
module ProviderEndpoint
|
|
6
6
|
class UnsafeEndpoint < StandardError; end
|
|
7
7
|
|
|
8
|
-
BLOCKED_NETS = [
|
|
8
|
+
BLOCKED_NETS = %w[
|
|
9
|
+
0.0.0.0/8 10.0.0.0/8 100.64.0.0/10 169.254.0.0/16 172.16.0.0/12 192.0.0.0/24 192.0.2.0/24
|
|
10
|
+
192.88.99.0/24 192.168.0.0/16 198.18.0.0/15 198.51.100.0/24 203.0.113.0/24 224.0.0.0/4 240.0.0.0/4
|
|
11
|
+
2001::/23 2001:db8::/32 2002::/16 3fff::/20
|
|
12
|
+
].map { |net| IPAddr.new(net) }.freeze
|
|
13
|
+
GLOBAL_UNICAST = IPAddr.new("2000::/3")
|
|
14
|
+
NAT64 = IPAddr.new("64:ff9b::/96")
|
|
9
15
|
ISSUE_MESSAGES = {
|
|
10
16
|
invalid_url: "is not a valid http or https URL",
|
|
11
|
-
unresolvable: "
|
|
12
|
-
unsafe_host: "
|
|
17
|
+
unresolvable: "does not resolve to a public address",
|
|
18
|
+
unsafe_host: "does not resolve to a public address"
|
|
13
19
|
}.freeze
|
|
14
20
|
|
|
15
21
|
module_function
|
|
@@ -66,12 +72,15 @@ module CompletionKit
|
|
|
66
72
|
|
|
67
73
|
def unsafe?(ip)
|
|
68
74
|
ip = ip.native
|
|
69
|
-
return
|
|
70
|
-
return
|
|
71
|
-
return true if ip.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
return !CompletionKit.config.allow_loopback_endpoints if ip.loopback?
|
|
76
|
+
return nat64_unsafe?(ip) if NAT64.include?(ip)
|
|
77
|
+
return true if ip.ipv6? && !GLOBAL_UNICAST.include?(ip)
|
|
78
|
+
BLOCKED_NETS.any? { |net| net.include?(ip) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def nat64_unsafe?(ip)
|
|
82
|
+
target = IPAddr.new(ip.to_i & 0xffff_ffff, Socket::AF_INET)
|
|
83
|
+
target.loopback? || unsafe?(target)
|
|
75
84
|
end
|
|
76
85
|
end
|
|
77
86
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<% pending_verdict = local_assigns[:pending_verdict] %>
|
|
4
4
|
<% active_verdict = pending_verdict || current_verdict %>
|
|
5
5
|
<% error = local_assigns[:error] %>
|
|
6
|
-
<% me =
|
|
6
|
+
<% me = ck_current_reviewer %>
|
|
7
7
|
<% other_agreements = CompletionKit::Agreement
|
|
8
8
|
.where(response_id: response_row.id, metric_id: metric.id)
|
|
9
9
|
.where.not(created_by: me)
|