rubik 0.3.2 → 0.3.4

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.
@@ -34,173 +34,207 @@ module Rubik
34
34
  when "list", "ls", "status"
35
35
  render_provider_catalog(ctx)
36
36
  when "add"
37
- p_name = clean_args[1]&.downcase
38
- arg2 = clean_args[2]
39
- arg3 = clean_args[3]
40
-
41
- p_type = "openai_compatible"
42
- p_endpoint = nil
43
-
44
- if arg2 && (arg2.start_with?("http://") || arg2.start_with?("https://"))
45
- p_endpoint = arg2
46
- elsif arg2 && arg3
47
- p_type = arg2.downcase
48
- p_endpoint = arg3
49
- end
50
-
51
- if p_name.nil? || p_name.empty? || p_endpoint.nil? || p_endpoint.empty?
52
- ctx.out.puts "Usage: !provider add <name> [type] <endpoint_url> " \
53
- "[--key=API_KEY | --key-env=ENV_VAR] [--model=NAME] [--context=LIMIT] [--project|--global]"
54
- elsif !p_endpoint.start_with?("http://") && !p_endpoint.start_with?("https://")
55
- ctx.out.puts "Error: Endpoint must be a valid HTTP/HTTPS URL (starting with http:// or https://). " \
56
- "Received: '#{p_endpoint}'\nUsage: !provider add <name> [type] <endpoint_url> " \
57
- "[--key=API_KEY | --key-env=ENV_VAR] [--model=NAME] [--context=LIMIT] [--project|--global]"
58
- else
59
- key_arg = clean_args.find { |a| a.start_with?("--key=") || a.start_with?("--api-key=") }
60
- key_env_arg = clean_args.find { |a| a.start_with?("--key-env=") }
61
- model_arg = clean_args.find { |a| a.start_with?("--model=") }
62
- context_arg = clean_args.find { |a| a.start_with?("--context=") }
63
-
64
- api_key = key_arg&.split("=", 2)&.last
65
- key_env = key_env_arg&.split("=", 2)&.last
66
- m_name = model_arg ? model_arg.split("=", 2).last : "default"
67
- c_limit = context_arg ? context_arg.split("=", 2).last.to_i : 32_768
68
-
69
- normalized_endpoint = if ["openai_compatible", "openai"].include?(p_type)
70
- Rubik::LLM::OpenAICompatibleProvider.normalize_endpoint(p_endpoint)
71
- else
72
- p_endpoint
73
- end
74
-
75
- p_spec = {
76
- "type" => p_type,
77
- "endpoint" => normalized_endpoint,
78
- "api_key" => api_key,
79
- "api_key_env" => key_env,
80
- "models" => [
81
- { "name" => m_name, "context_limit" => c_limit }
82
- ]
83
- }.compact
84
-
85
- res = ctx.config.set("providers.#{p_name}", p_spec, persist: scope)
86
- ctx.refresh_llm_registry!
87
- next if Base.report_persist_failure(ctx, res, scope)
88
-
89
- ctx.out.puts "Successfully added provider '#{p_name}' (#{p_type} -> #{normalized_endpoint}) " \
90
- "with model '#{m_name}' [#{Base.scope_label(scope)}]."
91
- end
37
+ handle_add_provider(clean_args, ctx, scope)
92
38
  when "add-model", "model-add"
93
- p_name = clean_args[1]&.downcase
94
- m_name = clean_args[2]
95
- if p_name.nil? || p_name.empty? || m_name.nil? || m_name.empty?
96
- ctx.out.puts "Usage: !provider add-model <provider_name> <model_name> [--context=LIMIT] [--project|--global]"
97
- else
98
- context_arg = clean_args.find { |a| a.start_with?("--context=") }
99
- c_limit = context_arg ? context_arg.split("=", 2).last.to_i : 32_768
100
-
101
- res = ctx.config.upsert_provider_model(p_name, m_name, context_limit: c_limit, persist: scope)
102
- ctx.refresh_llm_registry!
103
- if res[:ok]
104
- ctx.out.puts "Added model '#{m_name}' (#{c_limit} tokens) to provider '#{p_name}' [#{Base.scope_label(scope)}]."
105
- elsif res[:reason] == :unknown_provider
106
- ctx.out.puts "Provider '#{p_name}' not found. Add it first via !provider add."
107
- else
108
- Base.report_persist_failure(ctx, res, scope)
109
- end
110
- end
39
+ handle_add_provider_model(clean_args, ctx, scope)
111
40
  when "remove-model", "model-remove", "model-rm"
112
- p_name = clean_args[1]&.downcase
113
- m_name = clean_args[2]
114
- if p_name.nil? || p_name.empty? || m_name.nil? || m_name.empty?
115
- ctx.out.puts "Usage: !provider remove-model <provider_name> <model_name> [--project|--global]"
116
- else
117
- res = ctx.config.remove_provider_model(p_name, m_name, persist: scope)
118
- ctx.refresh_llm_registry!
119
- if res[:ok]
120
- ctx.out.puts "Removed model '#{m_name}' from provider '#{p_name}' [#{Base.scope_label(scope)}]."
121
- elsif res[:reason] == :unknown_provider
122
- ctx.out.puts "Provider '#{p_name}' not found."
123
- else
124
- Base.report_persist_failure(ctx, res, scope)
125
- end
126
- end
41
+ handle_remove_provider_model(clean_args, ctx, scope)
127
42
  when "set"
128
- p_name = clean_args[1]&.downcase
129
- field = clean_args[2]&.downcase
130
- val = clean_args[3]
131
- if p_name.nil? || p_name.empty? || field.nil? || val.nil?
132
- ctx.out.puts "Usage: !provider set <name> <endpoint|key|key-env|type|stream|stream_timeout|open_timeout|tool_native|num_ctx> <value> [--project|--global]"
133
- else
134
- p_spec = ctx.config.provider_spec(p_name)
135
- if p_spec.nil?
136
- ctx.out.puts "Provider '#{p_name}' not found."
137
- else
138
- case field
139
- when "key", "api_key"
140
- p_spec["api_key"] = val
141
- when "key-env", "key_env", "api_key_env"
142
- p_spec["api_key_env"] = val
143
- when "endpoint", "url", "host"
144
- if !val.start_with?("http://") && !val.start_with?("https://")
145
- ctx.out.puts "Error: Endpoint must be a valid HTTP/HTTPS URL (starting with http:// or https://). Received: '#{val}'"
146
- next
147
- end
148
- p_spec["endpoint"] = val
149
- when "type"
150
- p_spec["type"] = val
151
- else
152
- # Free-form fields (stream, tool_native, num_ctx, ...) are
153
- # typed through parse_scalar_value so "false" becomes a
154
- # real false — a raw "false" String is truthy in Ruby and
155
- # would silently re-enable the flag it means to disable.
156
- p_spec[field] = ctx.config.parse_scalar_value(val)
157
- end
158
-
159
- res = ctx.config.set("providers.#{p_name}", p_spec, persist: scope)
160
- ctx.refresh_llm_registry!
161
- next if Base.report_persist_failure(ctx, res, scope)
162
-
163
- ctx.out.puts "Updated provider '#{p_name}' field '#{field}' to '#{val}' [#{Base.scope_label(scope)}]."
164
- end
165
- end
43
+ handle_set_provider_field(clean_args, ctx, scope)
166
44
  when "remove", "rm", "delete"
167
- p_name = clean_args[1]&.downcase
168
- if p_name.nil? || p_name.empty?
169
- ctx.out.puts "Usage: !provider remove <name> [--project|--global]"
170
- else
171
- res = ctx.config.remove_provider(p_name, persist: scope)
172
- ctx.refresh_llm_registry!
173
- if res[:ok]
174
- ctx.out.puts "Removed provider '#{p_name}' [#{Base.scope_label(scope)}]."
175
- elsif res[:reason] == :unknown_provider
176
- ctx.out.puts "Provider '#{p_name}' not found."
177
- else
178
- Base.report_persist_failure(ctx, res, scope)
179
- end
180
- end
45
+ handle_remove_provider(clean_args, ctx, scope)
181
46
  when "set-context", "context", "limit"
182
- p_name = clean_args[1]&.downcase
183
- m_name = clean_args[2]
184
- limit_val = clean_args[3]&.to_i
185
- if p_name.nil? || p_name.empty? || m_name.nil? || m_name.empty? || limit_val.nil? || limit_val <= 0
186
- ctx.out.puts "Usage: !provider set-context <provider> <model> <limit_in_tokens> [--project|--global]"
187
- else
188
- res = ctx.config.upsert_provider_model(p_name, m_name, context_limit: limit_val, persist: scope)
189
- ctx.refresh_llm_registry!
190
- if res[:ok]
191
- ctx.out.puts "Updated context limit for model '#{m_name}' on provider '#{p_name}' to #{limit_val} tokens [#{Base.scope_label(scope)}]."
192
- elsif res[:reason] == :unknown_provider
193
- ctx.out.puts "Provider '#{p_name}' not found."
194
- else
195
- Base.report_persist_failure(ctx, res, scope)
196
- end
197
- end
47
+ handle_set_provider_context(clean_args, ctx, scope)
198
48
  else
199
49
  ctx.out.puts USAGE_PROVIDER
200
50
  end
201
51
  end
202
52
  end
203
53
 
54
+ private_class_method def self.handle_add_provider(clean_args, ctx, scope)
55
+ p_name = clean_args[1]&.downcase
56
+ arg2 = clean_args[2]
57
+ arg3 = clean_args[3]
58
+
59
+ p_type = "openai_compatible"
60
+ p_endpoint = nil
61
+
62
+ if arg2 && (arg2.start_with?("http://") || arg2.start_with?("https://"))
63
+ p_endpoint = arg2
64
+ elsif arg2 && arg3
65
+ p_type = arg2.downcase
66
+ p_endpoint = arg3
67
+ end
68
+
69
+ if p_name.nil? || p_name.empty? || p_endpoint.nil? || p_endpoint.empty?
70
+ ctx.out.puts "Usage: !provider add <name> [type] <endpoint_url> " \
71
+ "[--key=API_KEY | --key-env=ENV_VAR] [--model=NAME] [--context=LIMIT] [--project|--global]"
72
+ return
73
+ end
74
+
75
+ unless p_endpoint.start_with?("http://") || p_endpoint.start_with?("https://")
76
+ ctx.out.puts "Error: Endpoint must be a valid HTTP/HTTPS URL (starting with http:// or https://). " \
77
+ "Received: '#{p_endpoint}'\nUsage: !provider add <name> [type] <endpoint_url> " \
78
+ "[--key=API_KEY | --key-env=ENV_VAR] [--model=NAME] [--context=LIMIT] [--project|--global]"
79
+ return
80
+ end
81
+
82
+ key_arg = clean_args.find { |a| a.start_with?("--key=") || a.start_with?("--api-key=") }
83
+ key_env_arg = clean_args.find { |a| a.start_with?("--key-env=") }
84
+ model_arg = clean_args.find { |a| a.start_with?("--model=") }
85
+ context_arg = clean_args.find { |a| a.start_with?("--context=") }
86
+
87
+ api_key = key_arg&.split("=", 2)&.last
88
+ key_env = key_env_arg&.split("=", 2)&.last
89
+ m_name = model_arg ? model_arg.split("=", 2).last : "default"
90
+ c_limit = context_arg ? context_arg.split("=", 2).last.to_i : 32_768
91
+
92
+ normalized_endpoint = if ["openai_compatible", "openai"].include?(p_type)
93
+ Rubik::LLM::OpenAICompatibleProvider.normalize_endpoint(p_endpoint)
94
+ else
95
+ p_endpoint
96
+ end
97
+
98
+ p_spec = {
99
+ "type" => p_type,
100
+ "endpoint" => normalized_endpoint,
101
+ "api_key" => api_key,
102
+ "api_key_env" => key_env,
103
+ "models" => [
104
+ { "name" => m_name, "context_limit" => c_limit }
105
+ ]
106
+ }.compact
107
+
108
+ res = ctx.config.set("providers.#{p_name}", p_spec, persist: scope)
109
+ ctx.refresh_llm_registry!
110
+ return if Base.report_persist_failure(ctx, res, scope)
111
+
112
+ ctx.out.puts "Successfully added provider '#{p_name}' (#{p_type} -> #{normalized_endpoint}) " \
113
+ "with model '#{m_name}' [#{Base.scope_label(scope)}]."
114
+ end
115
+
116
+ private_class_method def self.handle_add_provider_model(clean_args, ctx, scope)
117
+ p_name = clean_args[1]&.downcase
118
+ m_name = clean_args[2]
119
+ if p_name.nil? || p_name.empty? || m_name.nil? || m_name.empty?
120
+ ctx.out.puts "Usage: !provider add-model <provider_name> <model_name> [--context=LIMIT] [--project|--global]"
121
+ return
122
+ end
123
+
124
+ context_arg = clean_args.find { |a| a.start_with?("--context=") }
125
+ c_limit = context_arg ? context_arg.split("=", 2).last.to_i : 32_768
126
+
127
+ res = ctx.config.upsert_provider_model(p_name, m_name, context_limit: c_limit, persist: scope)
128
+ ctx.refresh_llm_registry!
129
+ if res[:ok]
130
+ ctx.out.puts "Added model '#{m_name}' (#{c_limit} tokens) to provider '#{p_name}' [#{Base.scope_label(scope)}]."
131
+ elsif res[:reason] == :unknown_provider
132
+ ctx.out.puts "Provider '#{p_name}' not found. Add it first via !provider add."
133
+ else
134
+ Base.report_persist_failure(ctx, res, scope)
135
+ end
136
+ end
137
+
138
+ private_class_method def self.handle_remove_provider_model(clean_args, ctx, scope)
139
+ p_name = clean_args[1]&.downcase
140
+ m_name = clean_args[2]
141
+ if p_name.nil? || p_name.empty? || m_name.nil? || m_name.empty?
142
+ ctx.out.puts "Usage: !provider remove-model <provider_name> <model_name> [--project|--global]"
143
+ return
144
+ end
145
+
146
+ res = ctx.config.remove_provider_model(p_name, m_name, persist: scope)
147
+ ctx.refresh_llm_registry!
148
+ if res[:ok]
149
+ ctx.out.puts "Removed model '#{m_name}' from provider '#{p_name}' [#{Base.scope_label(scope)}]."
150
+ elsif res[:reason] == :unknown_provider
151
+ ctx.out.puts "Provider '#{p_name}' not found."
152
+ else
153
+ Base.report_persist_failure(ctx, res, scope)
154
+ end
155
+ end
156
+
157
+ private_class_method def self.handle_set_provider_field(clean_args, ctx, scope)
158
+ p_name = clean_args[1]&.downcase
159
+ field = clean_args[2]&.downcase
160
+ val = clean_args[3]
161
+ if p_name.nil? || p_name.empty? || field.nil? || val.nil?
162
+ ctx.out.puts "Usage: !provider set <name> <endpoint|key|key-env|type|stream|stream_timeout|open_timeout|tool_native|num_ctx> <value> [--project|--global]"
163
+ return
164
+ end
165
+
166
+ p_spec = ctx.config.provider_spec(p_name)
167
+ if p_spec.nil?
168
+ ctx.out.puts "Provider '#{p_name}' not found."
169
+ return
170
+ end
171
+
172
+ case field
173
+ when "key", "api_key"
174
+ p_spec["api_key"] = val
175
+ when "key-env", "key_env", "api_key_env"
176
+ p_spec["api_key_env"] = val
177
+ when "endpoint", "url", "host"
178
+ unless val.start_with?("http://") || val.start_with?("https://")
179
+ ctx.out.puts "Error: Endpoint must be a valid HTTP/HTTPS URL (starting with http:// or https://). Received: '#{val}'"
180
+ return
181
+ end
182
+ p_spec["endpoint"] = val
183
+ when "type"
184
+ p_spec["type"] = val
185
+ else
186
+ # Free-form fields (stream, tool_native, num_ctx, ...) are
187
+ # typed through parse_scalar_value so "false" becomes a
188
+ # real false — a raw "false" String is truthy in Ruby and
189
+ # would silently re-enable the flag it means to disable.
190
+ p_spec[field] = ctx.config.parse_scalar_value(val)
191
+ end
192
+
193
+ res = ctx.config.set("providers.#{p_name}", p_spec, persist: scope)
194
+ ctx.refresh_llm_registry!
195
+ return if Base.report_persist_failure(ctx, res, scope)
196
+
197
+ ctx.out.puts "Updated provider '#{p_name}' field '#{field}' to '#{val}' [#{Base.scope_label(scope)}]."
198
+ end
199
+
200
+ private_class_method def self.handle_remove_provider(clean_args, ctx, scope)
201
+ p_name = clean_args[1]&.downcase
202
+ if p_name.nil? || p_name.empty?
203
+ ctx.out.puts "Usage: !provider remove <name> [--project|--global]"
204
+ return
205
+ end
206
+
207
+ res = ctx.config.remove_provider(p_name, persist: scope)
208
+ ctx.refresh_llm_registry!
209
+ if res[:ok]
210
+ ctx.out.puts "Removed provider '#{p_name}' [#{Base.scope_label(scope)}]."
211
+ elsif res[:reason] == :unknown_provider
212
+ ctx.out.puts "Provider '#{p_name}' not found."
213
+ else
214
+ Base.report_persist_failure(ctx, res, scope)
215
+ end
216
+ end
217
+
218
+ private_class_method def self.handle_set_provider_context(clean_args, ctx, scope)
219
+ p_name = clean_args[1]&.downcase
220
+ m_name = clean_args[2]
221
+ limit_val = clean_args[3]&.to_i
222
+ if p_name.nil? || p_name.empty? || m_name.nil? || m_name.empty? || limit_val.nil? || limit_val <= 0
223
+ ctx.out.puts "Usage: !provider set-context <provider> <model> <limit_in_tokens> [--project|--global]"
224
+ return
225
+ end
226
+
227
+ res = ctx.config.upsert_provider_model(p_name, m_name, context_limit: limit_val, persist: scope)
228
+ ctx.refresh_llm_registry!
229
+ if res[:ok]
230
+ ctx.out.puts "Updated context limit for model '#{m_name}' on provider '#{p_name}' to #{limit_val} tokens [#{Base.scope_label(scope)}]."
231
+ elsif res[:reason] == :unknown_provider
232
+ ctx.out.puts "Provider '#{p_name}' not found."
233
+ else
234
+ Base.report_persist_failure(ctx, res, scope)
235
+ end
236
+ end
237
+
204
238
  private_class_method def self.register_providers_command(shell)
205
239
  shell.register_command("providers", description: "List all configured LLM providers, endpoints, and models", audience: :both) do |_args, ctx|
206
240
  render_provider_catalog(ctx)