ruby_llm 1.5.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/ruby_llm/active_record/acts_as.rb +46 -6
  4. data/lib/ruby_llm/aliases.json +27 -3
  5. data/lib/ruby_llm/chat.rb +27 -6
  6. data/lib/ruby_llm/configuration.rb +7 -18
  7. data/lib/ruby_llm/connection.rb +11 -6
  8. data/lib/ruby_llm/context.rb +2 -3
  9. data/lib/ruby_llm/embedding.rb +3 -4
  10. data/lib/ruby_llm/error.rb +2 -2
  11. data/lib/ruby_llm/image.rb +3 -4
  12. data/lib/ruby_llm/message.rb +4 -0
  13. data/lib/ruby_llm/models.json +7306 -6676
  14. data/lib/ruby_llm/models.rb +22 -31
  15. data/lib/ruby_llm/provider.rb +150 -89
  16. data/lib/ruby_llm/providers/anthropic/capabilities.rb +1 -2
  17. data/lib/ruby_llm/providers/anthropic/chat.rb +1 -1
  18. data/lib/ruby_llm/providers/anthropic/embeddings.rb +1 -1
  19. data/lib/ruby_llm/providers/anthropic/media.rb +1 -1
  20. data/lib/ruby_llm/providers/anthropic/models.rb +1 -1
  21. data/lib/ruby_llm/providers/anthropic/streaming.rb +1 -1
  22. data/lib/ruby_llm/providers/anthropic/tools.rb +1 -1
  23. data/lib/ruby_llm/providers/anthropic.rb +17 -22
  24. data/lib/ruby_llm/providers/bedrock/capabilities.rb +3 -63
  25. data/lib/ruby_llm/providers/bedrock/chat.rb +5 -4
  26. data/lib/ruby_llm/providers/bedrock/media.rb +1 -1
  27. data/lib/ruby_llm/providers/bedrock/models.rb +5 -6
  28. data/lib/ruby_llm/providers/bedrock/signing.rb +1 -1
  29. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +5 -4
  30. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +1 -1
  31. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +1 -1
  32. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +1 -1
  33. data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +1 -1
  34. data/lib/ruby_llm/providers/bedrock/streaming.rb +1 -1
  35. data/lib/ruby_llm/providers/bedrock.rb +26 -31
  36. data/lib/ruby_llm/providers/deepseek/capabilities.rb +16 -57
  37. data/lib/ruby_llm/providers/deepseek/chat.rb +1 -1
  38. data/lib/ruby_llm/providers/deepseek.rb +12 -17
  39. data/lib/ruby_llm/providers/gemini/capabilities.rb +1 -1
  40. data/lib/ruby_llm/providers/gemini/chat.rb +1 -1
  41. data/lib/ruby_llm/providers/gemini/embeddings.rb +1 -1
  42. data/lib/ruby_llm/providers/gemini/images.rb +1 -1
  43. data/lib/ruby_llm/providers/gemini/media.rb +1 -1
  44. data/lib/ruby_llm/providers/gemini/models.rb +1 -1
  45. data/lib/ruby_llm/providers/gemini/streaming.rb +1 -1
  46. data/lib/ruby_llm/providers/gemini/tools.rb +1 -7
  47. data/lib/ruby_llm/providers/gemini.rb +18 -23
  48. data/lib/ruby_llm/providers/gpustack/chat.rb +1 -1
  49. data/lib/ruby_llm/providers/gpustack/models.rb +1 -1
  50. data/lib/ruby_llm/providers/gpustack.rb +16 -19
  51. data/lib/ruby_llm/providers/mistral/capabilities.rb +1 -1
  52. data/lib/ruby_llm/providers/mistral/chat.rb +1 -1
  53. data/lib/ruby_llm/providers/mistral/embeddings.rb +1 -1
  54. data/lib/ruby_llm/providers/mistral/models.rb +1 -1
  55. data/lib/ruby_llm/providers/mistral.rb +14 -19
  56. data/lib/ruby_llm/providers/ollama/chat.rb +1 -1
  57. data/lib/ruby_llm/providers/ollama/media.rb +1 -1
  58. data/lib/ruby_llm/providers/ollama.rb +13 -18
  59. data/lib/ruby_llm/providers/openai/capabilities.rb +2 -2
  60. data/lib/ruby_llm/providers/openai/chat.rb +2 -2
  61. data/lib/ruby_llm/providers/openai/embeddings.rb +1 -1
  62. data/lib/ruby_llm/providers/openai/images.rb +1 -1
  63. data/lib/ruby_llm/providers/openai/media.rb +1 -1
  64. data/lib/ruby_llm/providers/openai/models.rb +1 -1
  65. data/lib/ruby_llm/providers/openai/streaming.rb +1 -1
  66. data/lib/ruby_llm/providers/openai/tools.rb +1 -1
  67. data/lib/ruby_llm/providers/openai.rb +24 -36
  68. data/lib/ruby_llm/providers/openrouter/models.rb +1 -1
  69. data/lib/ruby_llm/providers/openrouter.rb +9 -14
  70. data/lib/ruby_llm/providers/perplexity/capabilities.rb +1 -30
  71. data/lib/ruby_llm/providers/perplexity/chat.rb +1 -1
  72. data/lib/ruby_llm/providers/perplexity/models.rb +1 -1
  73. data/lib/ruby_llm/providers/perplexity.rb +13 -18
  74. data/lib/ruby_llm/stream_accumulator.rb +3 -3
  75. data/lib/ruby_llm/streaming.rb +16 -3
  76. data/lib/ruby_llm/tool.rb +19 -0
  77. data/lib/ruby_llm/version.rb +1 -1
  78. data/lib/tasks/models_docs.rake +18 -11
  79. data/lib/tasks/models_update.rake +5 -4
  80. metadata +1 -1
@@ -37,26 +37,35 @@ module RubyLLM
37
37
  end
38
38
 
39
39
  def fetch_from_providers
40
- configured = Provider.configured_providers(RubyLLM.config).filter(&:remote?)
40
+ config = RubyLLM.config
41
+ configured_classes = Provider.configured_remote_providers(config)
42
+ configured = configured_classes.map { |klass| klass.new(config) }
41
43
 
42
- RubyLLM.logger.info "Fetching models from providers: #{configured.map(&:slug).join(', ')}"
44
+ RubyLLM.logger.info "Fetching models from providers: #{configured.map(&:name).join(', ')}"
43
45
 
44
- configured.flat_map do |provider|
45
- provider.list_models(connection: provider.connection(RubyLLM.config))
46
- end
46
+ configured.flat_map(&:list_models)
47
47
  end
48
48
 
49
- def resolve(model_id, provider: nil, assume_exists: false) # rubocop:disable Metrics/PerceivedComplexity
50
- assume_exists = true if provider && Provider.providers[provider.to_sym].local?
49
+ def resolve(model_id, provider: nil, assume_exists: false, config: nil) # rubocop:disable Metrics/PerceivedComplexity
50
+ config ||= RubyLLM.config
51
+ provider_class = provider ? Provider.providers[provider.to_sym] : nil
52
+
53
+ # Check if provider is local
54
+ if provider_class
55
+ temp_instance = provider_class.new(config)
56
+ assume_exists = true if temp_instance.local?
57
+ end
51
58
 
52
59
  if assume_exists
53
60
  raise ArgumentError, 'Provider must be specified if assume_exists is true' unless provider
54
61
 
55
- provider = Provider.providers[provider.to_sym] || raise(Error, "Unknown provider: #{provider.to_sym}")
62
+ provider_class ||= raise(Error, "Unknown provider: #{provider.to_sym}")
63
+ provider_instance = provider_class.new(config)
64
+
56
65
  model = Model::Info.new(
57
66
  id: model_id,
58
67
  name: model_id.gsub('-', ' ').capitalize,
59
- provider: provider.slug,
68
+ provider: provider_instance.slug,
60
69
  capabilities: %w[function_calling streaming],
61
70
  modalities: { input: %w[text image], output: %w[text] },
62
71
  metadata: { warning: 'Assuming model exists, capabilities may not be accurate' }
@@ -67,9 +76,11 @@ module RubyLLM
67
76
  end
68
77
  else
69
78
  model = Models.find model_id, provider
70
- provider = Provider.providers[model.provider.to_sym] || raise(Error, "Unknown provider: #{model.provider}")
79
+ provider_class = Provider.providers[model.provider.to_sym] || raise(Error,
80
+ "Unknown provider: #{model.provider}")
81
+ provider_instance = provider_class.new(config)
71
82
  end
72
- [model, provider]
83
+ [model, provider_instance]
73
84
  end
74
85
 
75
86
  def method_missing(method, ...)
@@ -97,26 +108,19 @@ module RubyLLM
97
108
  end
98
109
 
99
110
  def merge_models(provider_models, parsera_models)
100
- # Create lookups for both sets of models
101
111
  parsera_by_key = index_by_key(parsera_models)
102
112
  provider_by_key = index_by_key(provider_models)
103
113
 
104
- # All keys from both sources
105
114
  all_keys = parsera_by_key.keys | provider_by_key.keys
106
115
 
107
- # Merge data, with parsera taking precedence
108
116
  models = all_keys.map do |key|
109
117
  if (parsera_model = parsera_by_key[key])
110
- # Parsera has this model - use it as the base
111
118
  if (provider_model = provider_by_key[key])
112
- # Both sources have this model, add provider metadata
113
119
  add_provider_metadata(parsera_model, provider_model)
114
120
  else
115
- # Only parsera has this model
116
121
  parsera_model
117
122
  end
118
123
  else
119
- # Only provider has this model
120
124
  provider_by_key[key]
121
125
  end
122
126
  end
@@ -131,19 +135,16 @@ module RubyLLM
131
135
  end
132
136
 
133
137
  def add_provider_metadata(parsera_model, provider_model)
134
- # Create a new Model::Info with parsera data but include provider metadata
135
138
  data = parsera_model.to_h
136
139
  data[:metadata] = provider_model.metadata.merge(data[:metadata] || {})
137
140
  Model::Info.new(data)
138
141
  end
139
142
  end
140
143
 
141
- # Initialize with optional pre-filtered models
142
144
  def initialize(models = nil)
143
145
  @models = models || load_models
144
146
  end
145
147
 
146
- # Load models from the JSON file
147
148
  def load_models
148
149
  data = File.exist?(self.class.models_file) ? File.read(self.class.models_file) : '[]'
149
150
  JSON.parse(data, symbolize_names: true).map { |model| Model::Info.new(model) }
@@ -155,17 +156,14 @@ module RubyLLM
155
156
  File.write(self.class.models_file, JSON.pretty_generate(all.map(&:to_h)))
156
157
  end
157
158
 
158
- # Return all models in the collection
159
159
  def all
160
160
  @models
161
161
  end
162
162
 
163
- # Allow enumeration over all models
164
163
  def each(&)
165
164
  all.each(&)
166
165
  end
167
166
 
168
- # Find a specific model by ID
169
167
  def find(model_id, provider = nil)
170
168
  if provider
171
169
  find_with_provider(model_id, provider)
@@ -174,37 +172,30 @@ module RubyLLM
174
172
  end
175
173
  end
176
174
 
177
- # Filter to only chat models
178
175
  def chat_models
179
176
  self.class.new(all.select { |m| m.type == 'chat' })
180
177
  end
181
178
 
182
- # Filter to only embedding models
183
179
  def embedding_models
184
180
  self.class.new(all.select { |m| m.type == 'embedding' })
185
181
  end
186
182
 
187
- # Filter to only audio models
188
183
  def audio_models
189
184
  self.class.new(all.select { |m| m.type == 'audio' })
190
185
  end
191
186
 
192
- # Filter to only image models
193
187
  def image_models
194
188
  self.class.new(all.select { |m| m.type == 'image' })
195
189
  end
196
190
 
197
- # Filter models by family
198
191
  def by_family(family)
199
192
  self.class.new(all.select { |m| m.family == family.to_s })
200
193
  end
201
194
 
202
- # Filter models by provider
203
195
  def by_provider(provider)
204
196
  self.class.new(all.select { |m| m.provider == provider.to_s })
205
197
  end
206
198
 
207
- # Instance method to refresh models
208
199
  def refresh!
209
200
  self.class.refresh!
210
201
  end
@@ -1,96 +1,94 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- # Base interface for LLM providers like OpenAI and Anthropic.
4
+ # Base class for LLM providers like OpenAI and Anthropic.
5
5
  # Handles the complexities of API communication, streaming responses,
6
6
  # and error handling so individual providers can focus on their unique features.
7
- module Provider
8
- # Common functionality for all LLM providers. Implements the core provider
9
- # interface so specific providers only need to implement a few key methods.
10
- module Methods
11
- extend Streaming
12
-
13
- def complete(messages, tools:, temperature:, model:, connection:, params: {}, schema: nil, &) # rubocop:disable Metrics/ParameterLists
14
- normalized_temperature = maybe_normalize_temperature(temperature, model)
15
-
16
- payload = Utils.deep_merge(
17
- params,
18
- render_payload(
19
- messages,
20
- tools: tools,
21
- temperature: normalized_temperature,
22
- model: model,
23
- stream: block_given?,
24
- schema: schema
25
- )
26
- )
7
+ # Encapsulates configuration and connection to eliminate parameter threading.
8
+ class Provider
9
+ include Streaming
27
10
 
28
- if block_given?
29
- stream_response connection, payload, &
30
- else
31
- sync_response connection, payload
32
- end
33
- end
11
+ attr_reader :config, :connection
34
12
 
35
- def list_models(connection:)
36
- response = connection.get models_url
37
- parse_list_models_response response, slug, capabilities
38
- end
13
+ def initialize(config)
14
+ @config = config
15
+ ensure_configured!
16
+ @connection = Connection.new(self, @config)
17
+ end
39
18
 
40
- def embed(text, model:, connection:, dimensions:)
41
- payload = render_embedding_payload(text, model:, dimensions:)
42
- response = connection.post(embedding_url(model:), payload)
43
- parse_embedding_response(response, model:, text:)
44
- end
19
+ def api_base
20
+ raise NotImplementedError
21
+ end
45
22
 
46
- def paint(prompt, model:, size:, connection:)
47
- payload = render_image_payload(prompt, model:, size:)
48
- response = connection.post images_url, payload
49
- parse_image_response(response, model:)
50
- end
23
+ def headers
24
+ {}
25
+ end
51
26
 
52
- def configured?(config = nil)
53
- config ||= RubyLLM.config
54
- missing_configs(config).empty?
55
- end
27
+ def slug
28
+ self.class.slug
29
+ end
56
30
 
57
- def missing_configs(config)
58
- configuration_requirements.select do |key|
59
- value = config.send(key)
60
- value.nil? || value.empty?
61
- end
62
- end
31
+ def name
32
+ self.class.name
33
+ end
63
34
 
64
- def local?
65
- false
66
- end
35
+ def capabilities
36
+ self.class.capabilities
37
+ end
67
38
 
68
- def remote?
69
- !local?
70
- end
39
+ def configuration_requirements
40
+ self.class.configuration_requirements
41
+ end
71
42
 
72
- private
43
+ def complete(messages, tools:, temperature:, model:, params: {}, headers: {}, schema: nil, &) # rubocop:disable Metrics/ParameterLists
44
+ normalized_temperature = maybe_normalize_temperature(temperature, model)
45
+
46
+ payload = Utils.deep_merge(
47
+ params,
48
+ render_payload(
49
+ messages,
50
+ tools: tools,
51
+ temperature: normalized_temperature,
52
+ model: model,
53
+ stream: block_given?,
54
+ schema: schema
55
+ )
56
+ )
73
57
 
74
- def maybe_normalize_temperature(temperature, model)
75
- if capabilities.respond_to?(:normalize_temperature)
76
- capabilities.normalize_temperature(temperature, model)
77
- else
78
- temperature
79
- end
58
+ if block_given?
59
+ stream_response @connection, payload, headers, &
60
+ else
61
+ sync_response @connection, payload, headers
80
62
  end
63
+ end
81
64
 
82
- def sync_response(connection, payload)
83
- response = connection.post completion_url, payload
84
- parse_completion_response response
85
- end
65
+ def list_models
66
+ response = @connection.get models_url
67
+ parse_list_models_response response, slug, capabilities
86
68
  end
87
69
 
88
- def try_parse_json(maybe_json)
89
- return maybe_json unless maybe_json.is_a?(String)
70
+ def embed(text, model:, dimensions:)
71
+ payload = render_embedding_payload(text, model:, dimensions:)
72
+ response = @connection.post(embedding_url(model:), payload)
73
+ parse_embedding_response(response, model:, text:)
74
+ end
90
75
 
91
- JSON.parse(maybe_json)
92
- rescue JSON::ParserError
93
- maybe_json
76
+ def paint(prompt, model:, size:)
77
+ payload = render_image_payload(prompt, model:, size:)
78
+ response = @connection.post images_url, payload
79
+ parse_image_response(response, model:)
80
+ end
81
+
82
+ def configured?
83
+ configuration_requirements.all? { |req| @config.send(req) }
84
+ end
85
+
86
+ def local?
87
+ self.class.local?
88
+ end
89
+
90
+ def remote?
91
+ self.class.remote?
94
92
  end
95
93
 
96
94
  def parse_error(response)
@@ -109,28 +107,54 @@ module RubyLLM
109
107
  end
110
108
  end
111
109
 
112
- def parse_data_uri(uri)
113
- if uri&.start_with?('data:')
114
- match = uri.match(/\Adata:([^;]+);base64,(.+)\z/)
115
- return { mime_type: match[1], data: match[2] } if match
110
+ def format_messages(messages)
111
+ messages.map do |msg|
112
+ {
113
+ role: msg.role.to_s,
114
+ content: msg.content
115
+ }
116
116
  end
117
+ end
117
118
 
118
- # If it's not a data URI, return nil
119
+ def format_tool_calls(_tool_calls)
119
120
  nil
120
121
  end
121
122
 
122
- def connection(config)
123
- @connection ||= Connection.new(self, config)
123
+ def parse_tool_calls(_tool_calls)
124
+ nil
124
125
  end
125
126
 
126
127
  class << self
127
- def extended(base)
128
- base.extend(Methods)
129
- base.extend(Streaming)
128
+ def name
129
+ to_s.split('::').last
130
+ end
131
+
132
+ def slug
133
+ name.downcase
134
+ end
135
+
136
+ def capabilities
137
+ raise NotImplementedError
138
+ end
139
+
140
+ def configuration_requirements
141
+ []
142
+ end
143
+
144
+ def local?
145
+ false
130
146
  end
131
147
 
132
- def register(name, provider_module)
133
- providers[name.to_sym] = provider_module
148
+ def remote?
149
+ !local?
150
+ end
151
+
152
+ def configured?(config)
153
+ configuration_requirements.all? { |req| config.send(req) }
154
+ end
155
+
156
+ def register(name, provider_class)
157
+ providers[name.to_sym] = provider_class
134
158
  end
135
159
 
136
160
  def for(model)
@@ -143,16 +167,53 @@ module RubyLLM
143
167
  end
144
168
 
145
169
  def local_providers
146
- providers.select { |_slug, provider| provider.local? }
170
+ providers.select { |_slug, provider_class| provider_class.local? }
147
171
  end
148
172
 
149
173
  def remote_providers
150
- providers.select { |_slug, provider| provider.remote? }
174
+ providers.select { |_slug, provider_class| provider_class.remote? }
175
+ end
176
+
177
+ def configured_providers(config)
178
+ providers.select do |_slug, provider_class|
179
+ provider_class.configured?(config)
180
+ end.values
181
+ end
182
+
183
+ def configured_remote_providers(config)
184
+ providers.select do |_slug, provider_class|
185
+ provider_class.remote? && provider_class.configured?(config)
186
+ end.values
151
187
  end
188
+ end
189
+
190
+ private
191
+
192
+ def try_parse_json(maybe_json)
193
+ return maybe_json unless maybe_json.is_a?(String)
194
+
195
+ JSON.parse(maybe_json)
196
+ rescue JSON::ParserError
197
+ maybe_json
198
+ end
199
+
200
+ def ensure_configured!
201
+ missing = configuration_requirements.reject { |req| @config.send(req) }
202
+ return if missing.empty?
203
+
204
+ raise ConfigurationError, "Missing configuration for #{name}: #{missing.join(', ')}"
205
+ end
206
+
207
+ def maybe_normalize_temperature(temperature, _model_id)
208
+ temperature
209
+ end
152
210
 
153
- def configured_providers(config = nil)
154
- providers.select { |_slug, provider| provider.configured?(config) }.values
211
+ def sync_response(connection, payload, additional_headers = {})
212
+ response = connection.post completion_url, payload do |req|
213
+ # Merge additional headers, with existing headers taking precedence
214
+ req.headers = additional_headers.merge(req.headers) unless additional_headers.empty?
155
215
  end
216
+ parse_completion_response response
156
217
  end
157
218
  end
158
219
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module RubyLLM
4
4
  module Providers
5
- module Anthropic
5
+ class Anthropic
6
6
  # Determines capabilities and pricing for Anthropic models
7
7
  module Capabilities
8
8
  module_function
@@ -133,7 +133,6 @@ module RubyLLM
133
133
  # Function calling for Claude 3+
134
134
  if model_id.match?(/claude-3/)
135
135
  capabilities << 'function_calling'
136
- capabilities << 'structured_output'
137
136
  capabilities << 'batch'
138
137
  end
139
138
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  module RubyLLM
4
4
  module Providers
5
- module Anthropic
5
+ class Anthropic
6
6
  # Chat methods of the OpenAI API integration
7
7
  module Chat
8
8
  module_function
@@ -2,7 +2,7 @@
2
2
 
3
3
  module RubyLLM
4
4
  module Providers
5
- module Anthropic
5
+ class Anthropic
6
6
  # Embeddings methods of the Anthropic API integration
7
7
  module Embeddings
8
8
  private
@@ -2,7 +2,7 @@
2
2
 
3
3
  module RubyLLM
4
4
  module Providers
5
- module Anthropic
5
+ class Anthropic
6
6
  # Handles formatting of media content (images, PDFs, audio) for Anthropic
7
7
  module Media
8
8
  module_function
@@ -2,7 +2,7 @@
2
2
 
3
3
  module RubyLLM
4
4
  module Providers
5
- module Anthropic
5
+ class Anthropic
6
6
  # Models methods of the Anthropic API integration
7
7
  module Models
8
8
  module_function
@@ -2,7 +2,7 @@
2
2
 
3
3
  module RubyLLM
4
4
  module Providers
5
- module Anthropic
5
+ class Anthropic
6
6
  # Streaming methods of the Anthropic API integration
7
7
  module Streaming
8
8
  private
@@ -2,7 +2,7 @@
2
2
 
3
3
  module RubyLLM
4
4
  module Providers
5
- module Anthropic
5
+ class Anthropic
6
6
  # Tools methods of the Anthropic API integration
7
7
  module Tools
8
8
  module_function
@@ -4,38 +4,33 @@ module RubyLLM
4
4
  module Providers
5
5
  # Anthropic Claude API integration. Handles the complexities of
6
6
  # Claude's unique message format and tool calling conventions.
7
- module Anthropic
8
- extend Provider
9
- extend Anthropic::Chat
10
- extend Anthropic::Embeddings
11
- extend Anthropic::Media
12
- extend Anthropic::Models
13
- extend Anthropic::Streaming
14
- extend Anthropic::Tools
7
+ class Anthropic < Provider
8
+ include Anthropic::Chat
9
+ include Anthropic::Embeddings
10
+ include Anthropic::Media
11
+ include Anthropic::Models
12
+ include Anthropic::Streaming
13
+ include Anthropic::Tools
15
14
 
16
- module_function
17
-
18
- def api_base(_config)
15
+ def api_base
19
16
  'https://api.anthropic.com'
20
17
  end
21
18
 
22
- def headers(config)
19
+ def headers
23
20
  {
24
- 'x-api-key' => config.anthropic_api_key,
21
+ 'x-api-key' => @config.anthropic_api_key,
25
22
  'anthropic-version' => '2023-06-01'
26
23
  }
27
24
  end
28
25
 
29
- def capabilities
30
- Anthropic::Capabilities
31
- end
32
-
33
- def slug
34
- 'anthropic'
35
- end
26
+ class << self
27
+ def capabilities
28
+ Anthropic::Capabilities
29
+ end
36
30
 
37
- def configuration_requirements
38
- %i[anthropic_api_key]
31
+ def configuration_requirements
32
+ %i[anthropic_api_key]
33
+ end
39
34
  end
40
35
  end
41
36
  end