completion-kit 0.4.2 → 0.4.8
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/app/assets/stylesheets/completion_kit/application.css +850 -69
- data/app/controllers/completion_kit/runs_controller.rb +31 -18
- data/app/controllers/completion_kit/suggestions_controller.rb +24 -0
- data/app/jobs/completion_kit/generate_row_job.rb +7 -0
- data/app/jobs/completion_kit/judge_review_job.rb +2 -0
- data/app/jobs/completion_kit/model_discovery_job.rb +9 -4
- data/app/models/completion_kit/dataset.rb +9 -0
- data/app/models/completion_kit/provider_credential.rb +1 -1
- data/app/models/completion_kit/response.rb +7 -0
- data/app/models/completion_kit/run.rb +22 -1
- data/app/services/completion_kit/anthropic_client.rb +33 -14
- data/app/services/completion_kit/model_discovery_service.rb +35 -9
- data/app/services/completion_kit/ollama_client.rb +31 -10
- data/app/services/completion_kit/open_ai_client.rb +35 -13
- data/app/services/completion_kit/open_router_client.rb +34 -13
- data/app/services/completion_kit/worker_health.rb +4 -1
- data/app/views/completion_kit/datasets/index.html.erb +1 -1
- data/app/views/completion_kit/datasets/show.html.erb +47 -9
- data/app/views/completion_kit/metrics/_form.html.erb +1 -1
- data/app/views/completion_kit/metrics/index.html.erb +15 -2
- data/app/views/completion_kit/metrics/show.html.erb +1 -1
- data/app/views/completion_kit/prompts/index.html.erb +27 -8
- data/app/views/completion_kit/prompts/show.html.erb +6 -36
- data/app/views/completion_kit/provider_credentials/_discovery_status.html.erb +4 -2
- data/app/views/completion_kit/provider_credentials/_models_card.html.erb +1 -1
- data/app/views/completion_kit/provider_credentials/index.html.erb +1 -1
- data/app/views/completion_kit/runs/_actions.html.erb +3 -0
- data/app/views/completion_kit/runs/_form.html.erb +114 -20
- data/app/views/completion_kit/runs/_response_row.html.erb +58 -35
- data/app/views/completion_kit/runs/_row.html.erb +50 -0
- data/app/views/completion_kit/runs/_sort_toolbar.html.erb +5 -4
- data/app/views/completion_kit/runs/_status_header.html.erb +3 -2
- data/app/views/completion_kit/runs/_status_panel.html.erb +55 -21
- data/app/views/completion_kit/runs/index.html.erb +4 -16
- data/app/views/completion_kit/runs/show.html.erb +110 -16
- data/app/views/completion_kit/suggestions/show.html.erb +65 -0
- data/app/views/layouts/completion_kit/application.html.erb +71 -0
- data/config/routes.rb +8 -2
- data/db/migrate/20260507000001_add_discovery_error_to_provider_credentials.rb +5 -0
- data/db/migrate/20260507150000_add_temperature_ignored_to_runs.rb +5 -0
- data/lib/completion_kit/version.rb +1 -1
- metadata +8 -7
- data/app/views/completion_kit/runs/suggestion.html.erb +0 -47
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module CompletionKit
|
|
2
2
|
class RunsController < ApplicationController
|
|
3
|
-
before_action :set_run, only: [:show, :edit, :update, :destroy, :generate, :suggest, :
|
|
3
|
+
before_action :set_run, only: [:show, :edit, :update, :destroy, :generate, :suggest, :retry_failures, :rerun, :refresh_status]
|
|
4
4
|
before_action :load_form_collections, only: [:new, :edit, :create, :update]
|
|
5
5
|
|
|
6
6
|
def index
|
|
@@ -72,21 +72,44 @@ module CompletionKit
|
|
|
72
72
|
end
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
+
def rerun
|
|
76
|
+
new_run = Run.create!(
|
|
77
|
+
prompt_id: @run.prompt_id,
|
|
78
|
+
dataset_id: @run.dataset_id,
|
|
79
|
+
judge_model: @run.judge_model,
|
|
80
|
+
temperature: @run.temperature,
|
|
81
|
+
status: "pending"
|
|
82
|
+
)
|
|
83
|
+
new_run.replace_metrics!(@run.metric_ids)
|
|
84
|
+
if new_run.start!
|
|
85
|
+
redirect_to run_path(new_run), notice: "Re-running with the same configuration."
|
|
86
|
+
else
|
|
87
|
+
redirect_to run_path(new_run), alert: new_run.failure_summary || "Could not start the new run."
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def refresh_status
|
|
92
|
+
respond_to do |format|
|
|
93
|
+
format.turbo_stream do
|
|
94
|
+
render turbo_stream: turbo_stream.replace(
|
|
95
|
+
"run_status_header",
|
|
96
|
+
partial: "completion_kit/runs/status_header",
|
|
97
|
+
locals: { run: @run }
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
75
103
|
def suggest
|
|
76
104
|
service = PromptImprovementService.new(@run)
|
|
77
105
|
result = service.suggest
|
|
78
|
-
@run.suggestions.create!(
|
|
106
|
+
suggestion = @run.suggestions.create!(
|
|
79
107
|
prompt: @run.prompt,
|
|
80
108
|
reasoning: result["reasoning"],
|
|
81
109
|
suggested_template: result["suggested_template"],
|
|
82
110
|
original_template: result["original_template"]
|
|
83
111
|
)
|
|
84
|
-
redirect_to
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def suggestion
|
|
88
|
-
@suggestion = @run.suggestions.order(created_at: :desc).first
|
|
89
|
-
return redirect_to run_path(@run), alert: "No suggestion available. Generate one first." unless @suggestion
|
|
112
|
+
redirect_to suggestion_path(suggestion, from: "run")
|
|
90
113
|
end
|
|
91
114
|
|
|
92
115
|
def retry_failures
|
|
@@ -115,16 +138,6 @@ module CompletionKit
|
|
|
115
138
|
redirect_to run_path(@run)
|
|
116
139
|
end
|
|
117
140
|
|
|
118
|
-
def apply_suggestion
|
|
119
|
-
suggestion = @run.suggestions.order(created_at: :desc).first
|
|
120
|
-
return redirect_to run_path(@run), alert: "No suggestion to apply." unless suggestion
|
|
121
|
-
|
|
122
|
-
new_prompt = @run.prompt.clone_as_new_version(template: suggestion.suggested_template)
|
|
123
|
-
new_prompt.publish!
|
|
124
|
-
suggestion.update!(applied_at: Time.current)
|
|
125
|
-
redirect_to prompt_path(new_prompt), notice: "Suggestion applied."
|
|
126
|
-
end
|
|
127
|
-
|
|
128
141
|
private
|
|
129
142
|
|
|
130
143
|
def set_run
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module CompletionKit
|
|
2
|
+
class SuggestionsController < ApplicationController
|
|
3
|
+
before_action :set_suggestion
|
|
4
|
+
|
|
5
|
+
def show
|
|
6
|
+
@run = @suggestion.run
|
|
7
|
+
@from = params[:from] == "run" ? "run" : "prompt"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def apply
|
|
11
|
+
run = @suggestion.run
|
|
12
|
+
new_prompt = run.prompt.clone_as_new_version(template: @suggestion.suggested_template)
|
|
13
|
+
new_prompt.publish!
|
|
14
|
+
@suggestion.update!(applied_at: Time.current)
|
|
15
|
+
redirect_to prompt_path(new_prompt), notice: "Suggestion applied."
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def set_suggestion
|
|
21
|
+
@suggestion = Suggestion.find(params[:id])
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -49,6 +49,11 @@ module CompletionKit
|
|
|
49
49
|
raise ConfigurationError, client.configuration_errors.join(", ") unless client.configured?
|
|
50
50
|
|
|
51
51
|
text = client.generate_completion(rendered, model: prompt.llm_model, temperature: run.temperature)
|
|
52
|
+
raise StandardError, text.to_s.sub(/\AError:\s*/, "") if text.to_s.start_with?("Error:")
|
|
53
|
+
|
|
54
|
+
if client.respond_to?(:temperature_dropped?) && client.temperature_dropped? && !run.temperature_ignored?
|
|
55
|
+
run.update_columns(temperature_ignored: true)
|
|
56
|
+
end
|
|
52
57
|
|
|
53
58
|
response.update!(
|
|
54
59
|
status: "succeeded",
|
|
@@ -56,6 +61,7 @@ module CompletionKit
|
|
|
56
61
|
error_provider: nil, error_class: nil, error_status: nil, error_message: nil
|
|
57
62
|
)
|
|
58
63
|
run.send(:broadcast_response_update, response)
|
|
64
|
+
run.send(:broadcast_progress)
|
|
59
65
|
|
|
60
66
|
if run.judge_configured?
|
|
61
67
|
run.metrics.each do |metric|
|
|
@@ -88,6 +94,7 @@ module CompletionKit
|
|
|
88
94
|
error_message: error.message.to_s.truncate(2000)
|
|
89
95
|
)
|
|
90
96
|
response.run&.send(:broadcast_response_update, response)
|
|
97
|
+
response.run&.send(:broadcast_progress)
|
|
91
98
|
end
|
|
92
99
|
|
|
93
100
|
def provider_for(response)
|
|
@@ -71,6 +71,7 @@ module CompletionKit
|
|
|
71
71
|
review.save!
|
|
72
72
|
|
|
73
73
|
run.send(:broadcast_response_update, response)
|
|
74
|
+
run.send(:broadcast_progress)
|
|
74
75
|
enqueue_completion_check
|
|
75
76
|
end
|
|
76
77
|
|
|
@@ -93,6 +94,7 @@ module CompletionKit
|
|
|
93
94
|
)
|
|
94
95
|
review.save!(validate: false)
|
|
95
96
|
response.run&.send(:broadcast_response_update, response)
|
|
97
|
+
response.run&.send(:broadcast_progress)
|
|
96
98
|
end
|
|
97
99
|
|
|
98
100
|
def provider_for(response)
|
|
@@ -17,9 +17,9 @@ module CompletionKit
|
|
|
17
17
|
|
|
18
18
|
discard_on ActiveJob::DeserializationError
|
|
19
19
|
|
|
20
|
-
rescue_from(StandardError) do |
|
|
20
|
+
rescue_from(StandardError) do |error|
|
|
21
21
|
credential = ProviderCredential.find(arguments.first)
|
|
22
|
-
credential.update_columns(discovery_status: "failed")
|
|
22
|
+
credential.update_columns(discovery_status: "failed", discovery_error: error.message.to_s.truncate(500))
|
|
23
23
|
credential.reload
|
|
24
24
|
credential.broadcast_discovery_progress
|
|
25
25
|
end
|
|
@@ -28,7 +28,12 @@ module CompletionKit
|
|
|
28
28
|
credential = ProviderCredential.find_by(id: provider_credential_id)
|
|
29
29
|
return unless credential
|
|
30
30
|
|
|
31
|
-
credential.update_columns(
|
|
31
|
+
credential.update_columns(
|
|
32
|
+
discovery_status: "discovering",
|
|
33
|
+
discovery_current: 0,
|
|
34
|
+
discovery_total: 0,
|
|
35
|
+
discovery_error: nil
|
|
36
|
+
)
|
|
32
37
|
credential.reload
|
|
33
38
|
credential.broadcast_discovery_progress
|
|
34
39
|
|
|
@@ -39,7 +44,7 @@ module CompletionKit
|
|
|
39
44
|
credential.broadcast_discovery_progress
|
|
40
45
|
end
|
|
41
46
|
|
|
42
|
-
credential.update_columns(discovery_status: "completed", updated_at: Time.current)
|
|
47
|
+
credential.update_columns(discovery_status: "completed", discovery_error: nil, updated_at: Time.current)
|
|
43
48
|
credential.reload
|
|
44
49
|
credential.broadcast_discovery_complete
|
|
45
50
|
end
|
|
@@ -20,5 +20,14 @@ module CompletionKit
|
|
|
20
20
|
rescue ::CSV::MalformedCSVError
|
|
21
21
|
0
|
|
22
22
|
end
|
|
23
|
+
|
|
24
|
+
def headers
|
|
25
|
+
return [] if csv_data.blank?
|
|
26
|
+
|
|
27
|
+
require "csv"
|
|
28
|
+
::CSV.parse(csv_data.lines.first.to_s).first.to_a.map(&:to_s).map(&:strip)
|
|
29
|
+
rescue ::CSV::MalformedCSVError
|
|
30
|
+
[]
|
|
31
|
+
end
|
|
23
32
|
end
|
|
24
33
|
end
|
|
@@ -56,7 +56,7 @@ module CompletionKit
|
|
|
56
56
|
def judge_count
|
|
57
57
|
model_ids = Model.where(provider: provider).pluck(:model_id)
|
|
58
58
|
return 0 if model_ids.empty?
|
|
59
|
-
Run.where(judge_model: model_ids).count
|
|
59
|
+
Run.where(judge_model: model_ids).distinct.count(:judge_model)
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
def last_used_at
|
|
@@ -43,6 +43,13 @@ module CompletionKit
|
|
|
43
43
|
reviews.any? { |r| r.ai_score.present? }
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
+
def fully_reviewed?
|
|
47
|
+
metric_ids = run.metric_ids
|
|
48
|
+
return true if metric_ids.empty?
|
|
49
|
+
reviewed_metric_ids = reviews.where(status: Review::TERMINAL_STATUSES).pluck(:metric_id).uniq
|
|
50
|
+
(metric_ids - reviewed_metric_ids).empty?
|
|
51
|
+
end
|
|
52
|
+
|
|
46
53
|
def error_payload
|
|
47
54
|
return nil if error_class.blank?
|
|
48
55
|
{ provider: error_provider, class: error_class, status: error_status, message: error_message }
|
|
@@ -13,10 +13,20 @@ module CompletionKit
|
|
|
13
13
|
|
|
14
14
|
validates :name, presence: true
|
|
15
15
|
validates :status, inclusion: { in: STATUSES }
|
|
16
|
+
validate :dataset_supplies_prompt_variables
|
|
16
17
|
|
|
17
18
|
before_validation :set_default_status, on: :create
|
|
18
19
|
before_validation :set_auto_name, on: :create
|
|
19
20
|
|
|
21
|
+
def missing_dataset_variables
|
|
22
|
+
return [] unless prompt
|
|
23
|
+
vars = prompt.variables
|
|
24
|
+
return [] if vars.empty?
|
|
25
|
+
return vars if dataset.nil?
|
|
26
|
+
|
|
27
|
+
vars - dataset.headers
|
|
28
|
+
end
|
|
29
|
+
|
|
20
30
|
def mark_completed!
|
|
21
31
|
update!(status: "completed")
|
|
22
32
|
broadcast_ui
|
|
@@ -236,7 +246,7 @@ module CompletionKit
|
|
|
236
246
|
broadcast_replace_to(
|
|
237
247
|
"completion_kit_run_#{id}",
|
|
238
248
|
target: "run_responses",
|
|
239
|
-
html: '<
|
|
249
|
+
html: '<tbody id="run_responses"></tbody>'
|
|
240
250
|
)
|
|
241
251
|
end
|
|
242
252
|
|
|
@@ -267,5 +277,16 @@ module CompletionKit
|
|
|
267
277
|
count = Run.where(prompt_id: prompt_id).count + 1
|
|
268
278
|
self.name = "#{prompt.name} — v#{prompt.version_number} ##{count}"
|
|
269
279
|
end
|
|
280
|
+
|
|
281
|
+
def dataset_supplies_prompt_variables
|
|
282
|
+
missing = missing_dataset_variables
|
|
283
|
+
return if missing.empty?
|
|
284
|
+
|
|
285
|
+
if dataset.nil?
|
|
286
|
+
errors.add(:dataset_id, "is required: prompt uses #{missing.join(', ')}")
|
|
287
|
+
else
|
|
288
|
+
errors.add(:dataset_id, "is missing columns required by the prompt: #{missing.join(', ')}")
|
|
289
|
+
end
|
|
290
|
+
end
|
|
270
291
|
end
|
|
271
292
|
end
|
|
@@ -5,28 +5,25 @@ module CompletionKit
|
|
|
5
5
|
{ id: "claude-3-5-haiku-latest", name: "Claude 3.5 Haiku" }
|
|
6
6
|
].freeze
|
|
7
7
|
|
|
8
|
+
def temperature_dropped?
|
|
9
|
+
@temperature_dropped == true
|
|
10
|
+
end
|
|
11
|
+
|
|
8
12
|
def generate_completion(prompt, options = {})
|
|
13
|
+
@temperature_dropped = false
|
|
9
14
|
return "Error: API key not configured" unless configured?
|
|
10
15
|
|
|
11
16
|
model = options[:model] || "claude-3-7-sonnet-latest"
|
|
12
17
|
max_tokens = options[:max_tokens] || 1000
|
|
13
18
|
temperature = options[:temperature] || 0.7
|
|
14
19
|
|
|
15
|
-
response =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
req.body = {
|
|
21
|
-
model: model,
|
|
22
|
-
messages: [
|
|
23
|
-
{ role: "user", content: prompt }
|
|
24
|
-
],
|
|
25
|
-
max_tokens: max_tokens,
|
|
26
|
-
temperature: temperature
|
|
27
|
-
}.to_json
|
|
20
|
+
response = post_messages(model: model, prompt: prompt, max_tokens: max_tokens, temperature: temperature)
|
|
21
|
+
|
|
22
|
+
if response.status == 400 && temperature_unsupported?(response.body)
|
|
23
|
+
@temperature_dropped = true
|
|
24
|
+
response = post_messages(model: model, prompt: prompt, max_tokens: max_tokens, temperature: nil)
|
|
28
25
|
end
|
|
29
|
-
|
|
26
|
+
|
|
30
27
|
if response.status == 429
|
|
31
28
|
raise CompletionKit::RateLimitError.new(
|
|
32
29
|
response.body.to_s.truncate(500),
|
|
@@ -82,5 +79,27 @@ module CompletionKit
|
|
|
82
79
|
def api_key
|
|
83
80
|
@config[:api_key] || ENV["ANTHROPIC_API_KEY"]
|
|
84
81
|
end
|
|
82
|
+
|
|
83
|
+
def post_messages(model:, prompt:, max_tokens:, temperature:)
|
|
84
|
+
body = {
|
|
85
|
+
model: model,
|
|
86
|
+
messages: [{ role: "user", content: prompt }],
|
|
87
|
+
max_tokens: max_tokens
|
|
88
|
+
}
|
|
89
|
+
body[:temperature] = temperature unless temperature.nil?
|
|
90
|
+
|
|
91
|
+
build_connection("https://api.anthropic.com").post do |req|
|
|
92
|
+
req.url "/v1/messages"
|
|
93
|
+
req.headers["Content-Type"] = "application/json"
|
|
94
|
+
req.headers["x-api-key"] = api_key
|
|
95
|
+
req.headers["anthropic-version"] = "2023-06-01"
|
|
96
|
+
req.body = body.to_json
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def temperature_unsupported?(body)
|
|
101
|
+
s = body.to_s
|
|
102
|
+
s.include?("temperature") && (s.include?("deprecated") || s.include?("not supported"))
|
|
103
|
+
end
|
|
85
104
|
end
|
|
86
105
|
end
|
|
@@ -4,6 +4,8 @@ require "json"
|
|
|
4
4
|
|
|
5
5
|
module CompletionKit
|
|
6
6
|
class ModelDiscoveryService
|
|
7
|
+
class DiscoveryError < StandardError; end
|
|
8
|
+
|
|
7
9
|
def initialize(config:)
|
|
8
10
|
@provider = config[:provider]
|
|
9
11
|
@api_key = config[:api_key]
|
|
@@ -36,11 +38,31 @@ module CompletionKit
|
|
|
36
38
|
end
|
|
37
39
|
end
|
|
38
40
|
|
|
41
|
+
def raise_fetch_error!(response)
|
|
42
|
+
label = case response.status
|
|
43
|
+
when 401, 403 then "Invalid API key for #{@provider}"
|
|
44
|
+
when 429 then "Rate limited by #{@provider}"
|
|
45
|
+
when 500..599 then "#{@provider} returned #{response.status}"
|
|
46
|
+
else "#{@provider} model list request failed (#{response.status})"
|
|
47
|
+
end
|
|
48
|
+
detail = extract_provider_error_message(response.body)
|
|
49
|
+
raise DiscoveryError, detail.present? ? "#{label}: #{detail}" : label
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def extract_provider_error_message(body)
|
|
53
|
+
return nil if body.blank?
|
|
54
|
+
data = JSON.parse(body)
|
|
55
|
+
err = data["error"]
|
|
56
|
+
(err.is_a?(Hash) && err["message"]) || data["message"] || (err.is_a?(String) && err) || nil
|
|
57
|
+
rescue JSON::ParserError
|
|
58
|
+
body.to_s.truncate(200)
|
|
59
|
+
end
|
|
60
|
+
|
|
39
61
|
def fetch_openai_models
|
|
40
62
|
response = fetch_connection("https://api.openai.com").get("/v1/models") do |req|
|
|
41
63
|
req.headers["Authorization"] = "Bearer #{@api_key}"
|
|
42
64
|
end
|
|
43
|
-
|
|
65
|
+
raise_fetch_error!(response) unless response.success?
|
|
44
66
|
JSON.parse(response.body).fetch("data", []).map { |e| { id: e["id"], display_name: nil } }
|
|
45
67
|
end
|
|
46
68
|
|
|
@@ -49,7 +71,7 @@ module CompletionKit
|
|
|
49
71
|
req.headers["x-api-key"] = @api_key
|
|
50
72
|
req.headers["anthropic-version"] = "2023-06-01"
|
|
51
73
|
end
|
|
52
|
-
|
|
74
|
+
raise_fetch_error!(response) unless response.success?
|
|
53
75
|
JSON.parse(response.body).fetch("data", []).map { |e| { id: e["id"], display_name: e["display_name"] } }
|
|
54
76
|
end
|
|
55
77
|
|
|
@@ -59,7 +81,7 @@ module CompletionKit
|
|
|
59
81
|
req.headers["HTTP-Referer"] = "https://completionkit.com"
|
|
60
82
|
req.headers["X-Title"] = "CompletionKit"
|
|
61
83
|
end
|
|
62
|
-
|
|
84
|
+
raise_fetch_error!(response) unless response.success?
|
|
63
85
|
JSON.parse(response.body).fetch("data", []).filter_map do |entry|
|
|
64
86
|
next nil if entry["deprecated"] == true
|
|
65
87
|
context_length = entry["context_length"].to_i
|
|
@@ -69,12 +91,12 @@ module CompletionKit
|
|
|
69
91
|
end
|
|
70
92
|
|
|
71
93
|
def fetch_ollama_models
|
|
72
|
-
|
|
94
|
+
raise DiscoveryError, "Ollama endpoint URL is required" if @api_endpoint.blank?
|
|
73
95
|
base_url = @api_endpoint.to_s.delete_suffix("/")
|
|
74
96
|
response = fetch_connection(base_url).get("/models") do |req|
|
|
75
97
|
req.headers["Authorization"] = "Bearer #{@api_key}" if @api_key.present?
|
|
76
98
|
end
|
|
77
|
-
|
|
99
|
+
raise_fetch_error!(response) unless response.success?
|
|
78
100
|
JSON.parse(response.body).fetch("data", []).map { |e| { id: e["id"], display_name: e["id"] } }
|
|
79
101
|
end
|
|
80
102
|
|
|
@@ -147,14 +169,18 @@ module CompletionKit
|
|
|
147
169
|
end
|
|
148
170
|
|
|
149
171
|
def probe_generation(model)
|
|
150
|
-
|
|
172
|
+
probe_input = "Reply with exactly this token and nothing else: PING-OK"
|
|
173
|
+
response = send_probe(model.model_id, probe_input, 65536)
|
|
151
174
|
if response.success?
|
|
152
|
-
text = extract_text(response)
|
|
153
|
-
if text.
|
|
175
|
+
text = extract_text(response).to_s
|
|
176
|
+
if text.blank?
|
|
177
|
+
model.supports_generation = false
|
|
178
|
+
model.generation_error = "Empty response"
|
|
179
|
+
elsif text.include?("PING-OK")
|
|
154
180
|
model.supports_generation = true
|
|
155
181
|
else
|
|
156
182
|
model.supports_generation = false
|
|
157
|
-
model.generation_error = "
|
|
183
|
+
model.generation_error = "Did not follow text completion instruction (likely non-text-output model): #{text.truncate(200)}"
|
|
158
184
|
end
|
|
159
185
|
else
|
|
160
186
|
model.supports_generation = false
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
module CompletionKit
|
|
2
2
|
class OllamaClient < LlmClient
|
|
3
|
+
def temperature_dropped?
|
|
4
|
+
@temperature_dropped == true
|
|
5
|
+
end
|
|
6
|
+
|
|
3
7
|
def generate_completion(prompt, options = {})
|
|
8
|
+
@temperature_dropped = false
|
|
4
9
|
return "Error: API endpoint not configured" unless configured?
|
|
5
10
|
|
|
6
11
|
model = options[:model]
|
|
7
12
|
max_tokens = options[:max_tokens] || 1000
|
|
8
13
|
temperature = options[:temperature] || 0.7
|
|
9
14
|
|
|
10
|
-
response =
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
model: model,
|
|
16
|
-
prompt: prompt,
|
|
17
|
-
max_tokens: max_tokens,
|
|
18
|
-
temperature: temperature
|
|
19
|
-
}.to_json
|
|
15
|
+
response = post_completion(model: model, prompt: prompt, max_tokens: max_tokens, temperature: temperature)
|
|
16
|
+
|
|
17
|
+
if response.status == 400 && temperature_unsupported?(response.body)
|
|
18
|
+
@temperature_dropped = true
|
|
19
|
+
response = post_completion(model: model, prompt: prompt, max_tokens: max_tokens, temperature: nil)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
if response.status == 429
|
|
@@ -76,5 +76,26 @@ module CompletionKit
|
|
|
76
76
|
def api_endpoint
|
|
77
77
|
(@config[:api_endpoint] || ENV["OLLAMA_API_ENDPOINT"] || "http://localhost:11434/v1").to_s.delete_suffix("/")
|
|
78
78
|
end
|
|
79
|
+
|
|
80
|
+
def post_completion(model:, prompt:, max_tokens:, temperature:)
|
|
81
|
+
body = {
|
|
82
|
+
model: model,
|
|
83
|
+
prompt: prompt,
|
|
84
|
+
max_tokens: max_tokens
|
|
85
|
+
}
|
|
86
|
+
body[:temperature] = temperature unless temperature.nil?
|
|
87
|
+
|
|
88
|
+
build_connection(api_endpoint).post do |req|
|
|
89
|
+
req.url "/v1/completions"
|
|
90
|
+
req.headers["Content-Type"] = "application/json"
|
|
91
|
+
req.headers["Authorization"] = "Bearer #{api_key}" if api_key.present?
|
|
92
|
+
req.body = body.to_json
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def temperature_unsupported?(body)
|
|
97
|
+
s = body.to_s
|
|
98
|
+
s.include?("temperature") && (s.include?("deprecated") || s.include?("not supported") || s.include?("Unsupported parameter"))
|
|
99
|
+
end
|
|
79
100
|
end
|
|
80
101
|
end
|
|
@@ -6,25 +6,23 @@ module CompletionKit
|
|
|
6
6
|
{ id: "gpt-4o-mini", name: "GPT-4o Mini" }
|
|
7
7
|
].freeze
|
|
8
8
|
|
|
9
|
+
def temperature_dropped?
|
|
10
|
+
@temperature_dropped == true
|
|
11
|
+
end
|
|
12
|
+
|
|
9
13
|
def generate_completion(prompt, options = {})
|
|
14
|
+
@temperature_dropped = false
|
|
10
15
|
return "Error: API key not configured" unless configured?
|
|
11
16
|
|
|
12
17
|
model = options[:model] || "gpt-4.1-mini"
|
|
13
18
|
max_tokens = options[:max_tokens] || 1000
|
|
14
19
|
temperature = options[:temperature] || 0.7
|
|
15
20
|
|
|
16
|
-
response =
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
model: model,
|
|
22
|
-
input: prompt,
|
|
23
|
-
instructions: "You are a helpful assistant.",
|
|
24
|
-
max_output_tokens: max_tokens,
|
|
25
|
-
temperature: temperature,
|
|
26
|
-
store: false
|
|
27
|
-
}.to_json
|
|
21
|
+
response = post_responses(model: model, prompt: prompt, max_tokens: max_tokens, temperature: temperature)
|
|
22
|
+
|
|
23
|
+
if response.status == 400 && temperature_unsupported?(response.body)
|
|
24
|
+
@temperature_dropped = true
|
|
25
|
+
response = post_responses(model: model, prompt: prompt, max_tokens: max_tokens, temperature: nil)
|
|
28
26
|
end
|
|
29
27
|
|
|
30
28
|
if response.status == 429
|
|
@@ -38,7 +36,8 @@ module CompletionKit
|
|
|
38
36
|
|
|
39
37
|
if response.success?
|
|
40
38
|
data = JSON.parse(response.body)
|
|
41
|
-
data["output"][
|
|
39
|
+
message = Array(data["output"]).find { |o| o["type"] == "message" }
|
|
40
|
+
message&.dig("content", 0, "text").to_s.strip
|
|
42
41
|
else
|
|
43
42
|
"Error: #{response.status} - #{response.body}"
|
|
44
43
|
end
|
|
@@ -69,5 +68,28 @@ module CompletionKit
|
|
|
69
68
|
def api_key
|
|
70
69
|
@config[:api_key] || ENV["OPENAI_API_KEY"]
|
|
71
70
|
end
|
|
71
|
+
|
|
72
|
+
def post_responses(model:, prompt:, max_tokens:, temperature:)
|
|
73
|
+
body = {
|
|
74
|
+
model: model,
|
|
75
|
+
input: prompt,
|
|
76
|
+
instructions: "You are a helpful assistant.",
|
|
77
|
+
max_output_tokens: max_tokens,
|
|
78
|
+
store: false
|
|
79
|
+
}
|
|
80
|
+
body[:temperature] = temperature unless temperature.nil?
|
|
81
|
+
|
|
82
|
+
build_connection("https://api.openai.com").post do |req|
|
|
83
|
+
req.url "/v1/responses"
|
|
84
|
+
req.headers["Content-Type"] = "application/json"
|
|
85
|
+
req.headers["Authorization"] = "Bearer #{api_key}"
|
|
86
|
+
req.body = body.to_json
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def temperature_unsupported?(body)
|
|
91
|
+
s = body.to_s
|
|
92
|
+
s.include?("temperature") && (s.include?("deprecated") || s.include?("not supported") || s.include?("Unsupported parameter"))
|
|
93
|
+
end
|
|
72
94
|
end
|
|
73
95
|
end
|
|
@@ -1,28 +1,26 @@
|
|
|
1
1
|
module CompletionKit
|
|
2
2
|
class OpenRouterClient < LlmClient
|
|
3
|
-
BASE_URL = "https://openrouter.ai
|
|
3
|
+
BASE_URL = "https://openrouter.ai".freeze
|
|
4
4
|
REFERER = "https://completionkit.com".freeze
|
|
5
5
|
APP_TITLE = "CompletionKit".freeze
|
|
6
6
|
|
|
7
|
+
def temperature_dropped?
|
|
8
|
+
@temperature_dropped == true
|
|
9
|
+
end
|
|
10
|
+
|
|
7
11
|
def generate_completion(prompt, options = {})
|
|
12
|
+
@temperature_dropped = false
|
|
8
13
|
return "Error: API key not configured" unless configured?
|
|
9
14
|
|
|
10
15
|
model = options[:model] || "openai/gpt-4o-mini"
|
|
11
16
|
max_tokens = options[:max_tokens] || 1000
|
|
12
17
|
temperature = options[:temperature] || 0.7
|
|
13
18
|
|
|
14
|
-
response =
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
req.headers["X-Title"] = APP_TITLE
|
|
20
|
-
req.body = {
|
|
21
|
-
model: model,
|
|
22
|
-
messages: [{ role: "user", content: prompt }],
|
|
23
|
-
max_tokens: max_tokens,
|
|
24
|
-
temperature: temperature
|
|
25
|
-
}.to_json
|
|
19
|
+
response = post_chat(model: model, prompt: prompt, max_tokens: max_tokens, temperature: temperature)
|
|
20
|
+
|
|
21
|
+
if response.status == 400 && temperature_unsupported?(response.body)
|
|
22
|
+
@temperature_dropped = true
|
|
23
|
+
response = post_chat(model: model, prompt: prompt, max_tokens: max_tokens, temperature: nil)
|
|
26
24
|
end
|
|
27
25
|
|
|
28
26
|
if response.status == 429
|
|
@@ -67,5 +65,28 @@ module CompletionKit
|
|
|
67
65
|
def api_key
|
|
68
66
|
@config[:api_key] || ENV["OPENROUTER_API_KEY"]
|
|
69
67
|
end
|
|
68
|
+
|
|
69
|
+
def post_chat(model:, prompt:, max_tokens:, temperature:)
|
|
70
|
+
body = {
|
|
71
|
+
model: model,
|
|
72
|
+
messages: [{ role: "user", content: prompt }],
|
|
73
|
+
max_tokens: max_tokens
|
|
74
|
+
}
|
|
75
|
+
body[:temperature] = temperature unless temperature.nil?
|
|
76
|
+
|
|
77
|
+
build_connection(BASE_URL, timeout: 30, open_timeout: 5).post do |req|
|
|
78
|
+
req.url "/api/v1/chat/completions"
|
|
79
|
+
req.headers["Content-Type"] = "application/json"
|
|
80
|
+
req.headers["Authorization"] = "Bearer #{api_key}"
|
|
81
|
+
req.headers["HTTP-Referer"] = REFERER
|
|
82
|
+
req.headers["X-Title"] = APP_TITLE
|
|
83
|
+
req.body = body.to_json
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def temperature_unsupported?(body)
|
|
88
|
+
s = body.to_s
|
|
89
|
+
s.include?("temperature") && (s.include?("deprecated") || s.include?("not supported") || s.include?("Unsupported parameter"))
|
|
90
|
+
end
|
|
70
91
|
end
|
|
71
92
|
end
|
|
@@ -4,7 +4,10 @@ module CompletionKit
|
|
|
4
4
|
|
|
5
5
|
def self.healthy?
|
|
6
6
|
return true unless defined?(::SolidQueue::Process)
|
|
7
|
-
::SolidQueue::Process
|
|
7
|
+
::SolidQueue::Process
|
|
8
|
+
.where("kind LIKE 'Worker%'")
|
|
9
|
+
.where("last_heartbeat_at > ?", HEARTBEAT_THRESHOLD.ago)
|
|
10
|
+
.exists?
|
|
8
11
|
end
|
|
9
12
|
end
|
|
10
13
|
end
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
<td><strong><%= dataset.name %></strong></td>
|
|
26
26
|
<td><%= dataset.row_count %></td>
|
|
27
27
|
<td><%= dataset.runs.count %></td>
|
|
28
|
-
<td><%= dataset.created_at.strftime("%Y
|
|
28
|
+
<td class="ck-meta-copy"><time datetime="<%= dataset.created_at.iso8601 %>"><%= dataset.created_at.strftime("%b %-d, %Y") %></time></td>
|
|
29
29
|
<td class="ck-results-table__arrow">→</td>
|
|
30
30
|
</tr>
|
|
31
31
|
<% end %>
|