ruby_llm 1.1.2 → 1.3.0rc1

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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +80 -133
  3. data/lib/ruby_llm/active_record/acts_as.rb +212 -33
  4. data/lib/ruby_llm/aliases.json +48 -6
  5. data/lib/ruby_llm/attachments/audio.rb +12 -0
  6. data/lib/ruby_llm/attachments/image.rb +9 -0
  7. data/lib/ruby_llm/attachments/pdf.rb +9 -0
  8. data/lib/ruby_llm/attachments.rb +78 -0
  9. data/lib/ruby_llm/chat.rb +26 -10
  10. data/lib/ruby_llm/configuration.rb +32 -2
  11. data/lib/ruby_llm/connection.rb +95 -0
  12. data/lib/ruby_llm/content.rb +51 -72
  13. data/lib/ruby_llm/context.rb +30 -0
  14. data/lib/ruby_llm/embedding.rb +13 -5
  15. data/lib/ruby_llm/error.rb +1 -1
  16. data/lib/ruby_llm/image.rb +13 -5
  17. data/lib/ruby_llm/message.rb +12 -4
  18. data/lib/ruby_llm/mime_types.rb +713 -0
  19. data/lib/ruby_llm/model_info.rb +208 -27
  20. data/lib/ruby_llm/models.json +25782 -2102
  21. data/lib/ruby_llm/models.rb +95 -14
  22. data/lib/ruby_llm/provider.rb +48 -90
  23. data/lib/ruby_llm/providers/anthropic/capabilities.rb +76 -13
  24. data/lib/ruby_llm/providers/anthropic/chat.rb +7 -14
  25. data/lib/ruby_llm/providers/anthropic/media.rb +44 -34
  26. data/lib/ruby_llm/providers/anthropic/models.rb +15 -15
  27. data/lib/ruby_llm/providers/anthropic/tools.rb +2 -2
  28. data/lib/ruby_llm/providers/anthropic.rb +3 -3
  29. data/lib/ruby_llm/providers/bedrock/capabilities.rb +61 -2
  30. data/lib/ruby_llm/providers/bedrock/chat.rb +30 -73
  31. data/lib/ruby_llm/providers/bedrock/media.rb +56 -0
  32. data/lib/ruby_llm/providers/bedrock/models.rb +51 -52
  33. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +16 -0
  34. data/lib/ruby_llm/providers/bedrock.rb +14 -25
  35. data/lib/ruby_llm/providers/deepseek/capabilities.rb +35 -2
  36. data/lib/ruby_llm/providers/deepseek.rb +3 -3
  37. data/lib/ruby_llm/providers/gemini/capabilities.rb +84 -3
  38. data/lib/ruby_llm/providers/gemini/chat.rb +8 -37
  39. data/lib/ruby_llm/providers/gemini/embeddings.rb +18 -34
  40. data/lib/ruby_llm/providers/gemini/images.rb +2 -2
  41. data/lib/ruby_llm/providers/gemini/media.rb +39 -110
  42. data/lib/ruby_llm/providers/gemini/models.rb +16 -22
  43. data/lib/ruby_llm/providers/gemini/tools.rb +1 -1
  44. data/lib/ruby_llm/providers/gemini.rb +3 -3
  45. data/lib/ruby_llm/providers/ollama/chat.rb +28 -0
  46. data/lib/ruby_llm/providers/ollama/media.rb +44 -0
  47. data/lib/ruby_llm/providers/ollama.rb +34 -0
  48. data/lib/ruby_llm/providers/openai/capabilities.rb +79 -4
  49. data/lib/ruby_llm/providers/openai/chat.rb +6 -4
  50. data/lib/ruby_llm/providers/openai/embeddings.rb +8 -12
  51. data/lib/ruby_llm/providers/openai/media.rb +38 -21
  52. data/lib/ruby_llm/providers/openai/models.rb +16 -17
  53. data/lib/ruby_llm/providers/openai/tools.rb +9 -5
  54. data/lib/ruby_llm/providers/openai.rb +7 -5
  55. data/lib/ruby_llm/providers/openrouter/models.rb +88 -0
  56. data/lib/ruby_llm/providers/openrouter.rb +31 -0
  57. data/lib/ruby_llm/stream_accumulator.rb +4 -4
  58. data/lib/ruby_llm/streaming.rb +3 -3
  59. data/lib/ruby_llm/utils.rb +22 -0
  60. data/lib/ruby_llm/version.rb +1 -1
  61. data/lib/ruby_llm.rb +15 -5
  62. data/lib/tasks/models.rake +69 -33
  63. data/lib/tasks/models_docs.rake +167 -112
  64. data/lib/tasks/vcr.rake +4 -2
  65. metadata +23 -14
  66. data/lib/tasks/browser_helper.rb +0 -97
  67. data/lib/tasks/capability_generator.rb +0 -123
  68. data/lib/tasks/capability_scraper.rb +0 -224
  69. data/lib/tasks/cli_helper.rb +0 -22
  70. data/lib/tasks/code_validator.rb +0 -29
  71. data/lib/tasks/model_updater.rb +0 -66
@@ -12,7 +12,6 @@ module RubyLLM
12
12
  class Models
13
13
  include Enumerable
14
14
 
15
- # Delegate class methods to the singleton instance
16
15
  class << self
17
16
  def instance
18
17
  @instance ||= new
@@ -26,21 +25,48 @@ module RubyLLM
26
25
  File.expand_path('models.json', __dir__)
27
26
  end
28
27
 
29
- def refresh! # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
30
- configured = Provider.configured_providers
28
+ def refresh!
29
+ # Collect models from both sources
30
+ provider_models = fetch_from_providers
31
+ parsera_models = fetch_from_parsera
31
32
 
32
- # Log provider status
33
- skipped = Provider.providers.values - configured
34
- RubyLLM.logger.info "Refreshing models from #{configured.map(&:slug).join(', ')}" if configured.any?
35
- RubyLLM.logger.info "Skipping #{skipped.map(&:slug).join(', ')} - providers not configured" if skipped.any?
33
+ # Merge with parsera data taking precedence
34
+ merged_models = merge_models(provider_models, parsera_models)
36
35
 
37
- # Store current models except from configured providers
38
- current = instance.load_models
39
- preserved = current.reject { |m| configured.map(&:slug).include?(m.provider) }
36
+ @instance = new(merged_models)
37
+ end
38
+
39
+ def fetch_from_providers
40
+ configured = Provider.configured_providers(RubyLLM.config).filter(&:remote?)
41
+
42
+ RubyLLM.logger.info "Fetching models from providers: #{configured.map(&:slug).join(', ')}"
40
43
 
41
- all = (preserved + configured.flat_map(&:list_models)).sort_by(&:id)
42
- @instance = new(all)
43
- @instance
44
+ configured.flat_map do |provider|
45
+ provider.list_models(connection: provider.connection(RubyLLM.config))
46
+ end
47
+ end
48
+
49
+ def resolve(model_id, provider: nil, assume_exists: false)
50
+ assume_exists = true if provider && Provider.providers[provider.to_sym].local?
51
+
52
+ if assume_exists
53
+ raise ArgumentError, 'Provider must be specified if assume_exists is true' unless provider
54
+
55
+ provider = Provider.providers[provider.to_sym] || raise(Error, "Unknown provider: #{provider.to_sym}")
56
+ model = Struct.new(:id, :provider, :capabilities, :modalities, :supports_vision?, :supports_functions?)
57
+ .new(model_id,
58
+ provider,
59
+ %w[function_calling streaming],
60
+ RubyLLM::Modalities.new({ input: %w[text image], output: %w[text] }),
61
+ true,
62
+ true)
63
+ RubyLLM.logger.warn "Assuming model '#{model_id}' exists for provider '#{provider}'. " \
64
+ 'Capabilities may not be accurately reflected.'
65
+ else
66
+ model = Models.find model_id, provider
67
+ provider = Provider.providers[model.provider.to_sym] || raise(Error, "Unknown provider: #{model.provider}")
68
+ end
69
+ [model, provider]
44
70
  end
45
71
 
46
72
  def method_missing(method, ...)
@@ -54,6 +80,61 @@ module RubyLLM
54
80
  def respond_to_missing?(method, include_private = false)
55
81
  instance.respond_to?(method, include_private) || super
56
82
  end
83
+
84
+ def fetch_from_parsera
85
+ RubyLLM.logger.info 'Fetching models from Parsera API...'
86
+
87
+ connection = Faraday.new('https://api.parsera.org') do |f|
88
+ f.request :json
89
+ f.response :json
90
+ f.response :raise_error
91
+ f.adapter Faraday.default_adapter
92
+ end
93
+
94
+ response = connection.get('/v1/llm-specs')
95
+ response.body.map { |data| ModelInfo.new(Utils.deep_symbolize_keys(data)) }
96
+ end
97
+
98
+ def merge_models(provider_models, parsera_models)
99
+ # Create lookups for both sets of models
100
+ parsera_by_key = index_by_key(parsera_models)
101
+ provider_by_key = index_by_key(provider_models)
102
+
103
+ # All keys from both sources
104
+ all_keys = parsera_by_key.keys | provider_by_key.keys
105
+
106
+ # Merge data, with parsera taking precedence
107
+ models = all_keys.map do |key|
108
+ if (parsera_model = parsera_by_key[key])
109
+ # Parsera has this model - use it as the base
110
+ if (provider_model = provider_by_key[key])
111
+ # Both sources have this model, add provider metadata
112
+ add_provider_metadata(parsera_model, provider_model)
113
+ else
114
+ # Only parsera has this model
115
+ parsera_model
116
+ end
117
+ else
118
+ # Only provider has this model
119
+ provider_by_key[key]
120
+ end
121
+ end
122
+
123
+ models.sort_by { |m| [m.provider, m.id] }
124
+ end
125
+
126
+ def index_by_key(models)
127
+ models.each_with_object({}) do |model, hash|
128
+ hash["#{model.provider}:#{model.id}"] = model
129
+ end
130
+ end
131
+
132
+ def add_provider_metadata(parsera_model, provider_model)
133
+ # Create a new ModelInfo with parsera data but include provider metadata
134
+ data = parsera_model.to_h
135
+ data[:metadata] = provider_model.metadata.merge(data[:metadata] || {})
136
+ ModelInfo.new(data)
137
+ end
57
138
  end
58
139
 
59
140
  # Initialize with optional pre-filtered models
@@ -64,7 +145,7 @@ module RubyLLM
64
145
  # Load models from the JSON file
65
146
  def load_models
66
147
  data = File.exist?(self.class.models_file) ? File.read(self.class.models_file) : '[]'
67
- JSON.parse(data).map { |model| ModelInfo.new(model.transform_keys(&:to_sym)) }
148
+ JSON.parse(data).map { |model| ModelInfo.new(Utils.deep_symbolize_keys(model)) }
68
149
  rescue JSON::ParserError
69
150
  []
70
151
  end
@@ -7,15 +7,11 @@ module RubyLLM
7
7
  module Provider
8
8
  # Common functionality for all LLM providers. Implements the core provider
9
9
  # interface so specific providers only need to implement a few key methods.
10
- module Methods # rubocop:disable Metrics/ModuleLength
10
+ module Methods
11
11
  extend Streaming
12
12
 
13
- def complete(messages, tools:, temperature:, model:, &block) # rubocop:disable Metrics/MethodLength
14
- normalized_temperature = if capabilities.respond_to?(:normalize_temperature)
15
- capabilities.normalize_temperature(temperature, model)
16
- else
17
- temperature
18
- end
13
+ def complete(messages, tools:, temperature:, model:, connection:, &)
14
+ normalized_temperature = maybe_normalize_temperature(temperature, model)
19
15
 
20
16
  payload = render_payload(messages,
21
17
  tools: tools,
@@ -24,112 +20,62 @@ module RubyLLM
24
20
  stream: block_given?)
25
21
 
26
22
  if block_given?
27
- stream_response payload, &block
23
+ stream_response connection, payload, &
28
24
  else
29
- sync_response payload
25
+ sync_response connection, payload
30
26
  end
31
27
  end
32
28
 
33
- def list_models
34
- response = connection.get(models_url) do |req|
35
- req.headers.merge! headers
36
- end
37
-
29
+ def list_models(connection:)
30
+ response = connection.get models_url
38
31
  parse_list_models_response response, slug, capabilities
39
32
  end
40
33
 
41
- def embed(text, model:)
42
- payload = render_embedding_payload text, model: model
43
- response = post embedding_url, payload
44
- parse_embedding_response response
34
+ def embed(text, model:, connection:, dimensions:)
35
+ payload = render_embedding_payload(text, model:, dimensions:)
36
+ response = connection.post(embedding_url(model:), payload)
37
+ parse_embedding_response(response, model:)
45
38
  end
46
39
 
47
- def paint(prompt, model:, size:)
40
+ def paint(prompt, model:, size:, connection:)
48
41
  payload = render_image_payload(prompt, model:, size:)
49
-
50
- response = post(images_url, payload)
51
- parse_image_response(response)
42
+ response = connection.post images_url, payload
43
+ parse_image_response response
52
44
  end
53
45
 
54
- def configured?
55
- missing_configs.empty?
46
+ def configured?(config = nil)
47
+ config ||= RubyLLM.config
48
+ missing_configs(config).empty?
56
49
  end
57
50
 
58
- private
59
-
60
- def missing_configs
51
+ def missing_configs(config)
61
52
  configuration_requirements.select do |key|
62
- value = RubyLLM.config.send(key)
53
+ value = config.send(key)
63
54
  value.nil? || value.empty?
64
55
  end
65
56
  end
66
57
 
67
- def ensure_configured!
68
- return if configured?
69
-
70
- config_block = <<~RUBY
71
- RubyLLM.configure do |config|
72
- #{missing_configs.map { |key| "config.#{key} = ENV['#{key.to_s.upcase}']" }.join("\n ")}
73
- end
74
- RUBY
75
-
76
- raise ConfigurationError,
77
- "#{slug} provider is not configured. Add this to your initialization:\n\n#{config_block}"
58
+ def local?
59
+ false
78
60
  end
79
61
 
80
- def sync_response(payload)
81
- response = post completion_url, payload
82
- parse_completion_response response
62
+ def remote?
63
+ !local?
83
64
  end
84
65
 
85
- def post(url, payload)
86
- connection.post url, payload do |req|
87
- req.headers.merge! headers
88
- yield req if block_given?
66
+ private
67
+
68
+ def maybe_normalize_temperature(temperature, model)
69
+ if capabilities.respond_to?(:normalize_temperature)
70
+ capabilities.normalize_temperature(temperature, model)
71
+ else
72
+ temperature
89
73
  end
90
74
  end
91
75
 
92
- def connection # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
93
- ensure_configured!
94
-
95
- @connection ||= Faraday.new(api_base) do |f| # rubocop:disable Metrics/BlockLength
96
- f.options.timeout = RubyLLM.config.request_timeout
97
-
98
- f.response :logger,
99
- RubyLLM.logger,
100
- bodies: true,
101
- response: true,
102
- errors: true,
103
- headers: false,
104
- log_level: :debug do |logger|
105
- logger.filter(%r{"[A-Za-z0-9+/=]{100,}"}, 'data":"[BASE64 DATA]"')
106
- logger.filter(/[-\d.e,\s]{100,}/, '[EMBEDDINGS ARRAY]')
107
- end
108
-
109
- f.request :retry, {
110
- max: RubyLLM.config.max_retries,
111
- interval: RubyLLM.config.retry_interval,
112
- interval_randomness: RubyLLM.config.retry_interval_randomness,
113
- backoff_factor: RubyLLM.config.retry_backoff_factor,
114
- exceptions: [
115
- Errno::ETIMEDOUT,
116
- Timeout::Error,
117
- Faraday::TimeoutError,
118
- Faraday::ConnectionFailed,
119
- Faraday::RetriableResponse,
120
- RubyLLM::RateLimitError,
121
- RubyLLM::ServerError,
122
- RubyLLM::ServiceUnavailableError,
123
- RubyLLM::OverloadedError
124
- ],
125
- retry_statuses: [429, 500, 502, 503, 504, 529]
126
- }
127
-
128
- f.request :json
129
- f.response :json
130
- f.adapter Faraday.default_adapter
131
- f.use :llm_errors, provider: self
132
- end
76
+ def sync_response(connection, payload)
77
+ response = connection.post completion_url, payload
78
+ parse_completion_response response
133
79
  end
134
80
  end
135
81
 
@@ -141,7 +87,7 @@ module RubyLLM
141
87
  maybe_json
142
88
  end
143
89
 
144
- def parse_error(response) # rubocop:disable Metrics/MethodLength
90
+ def parse_error(response)
145
91
  return if response.body.empty?
146
92
 
147
93
  body = try_parse_json(response.body)
@@ -167,6 +113,10 @@ module RubyLLM
167
113
  nil
168
114
  end
169
115
 
116
+ def connection(config)
117
+ @connection ||= Connection.new(self, config)
118
+ end
119
+
170
120
  class << self
171
121
  def extended(base)
172
122
  base.extend(Methods)
@@ -186,8 +136,16 @@ module RubyLLM
186
136
  @providers ||= {}
187
137
  end
188
138
 
189
- def configured_providers
190
- providers.select { |_name, provider| provider.configured? }.values
139
+ def local_providers
140
+ providers.select { |_slug, provider| provider.local? }
141
+ end
142
+
143
+ def remote_providers
144
+ providers.select { |_slug, provider| provider.remote? }
145
+ end
146
+
147
+ def configured_providers(config = nil)
148
+ providers.select { |_slug, provider| provider.configured?(config) }.values
191
149
  end
192
150
  end
193
151
  end
@@ -73,13 +73,13 @@ module RubyLLM
73
73
  # @return [Symbol] the model family identifier
74
74
  def model_family(model_id)
75
75
  case model_id
76
- when /claude-3-7-sonnet/ then :claude37_sonnet
77
- when /claude-3-5-sonnet/ then :claude35_sonnet
78
- when /claude-3-5-haiku/ then :claude35_haiku
79
- when /claude-3-opus/ then :claude3_opus
80
- when /claude-3-sonnet/ then :claude3_sonnet
81
- when /claude-3-haiku/ then :claude3_haiku
82
- else :claude2
76
+ when /claude-3-7-sonnet/ then 'claude-3-7-sonnet'
77
+ when /claude-3-5-sonnet/ then 'claude-3-5-sonnet'
78
+ when /claude-3-5-haiku/ then 'claude-3-5-haiku'
79
+ when /claude-3-opus/ then 'claude-3-opus'
80
+ when /claude-3-sonnet/ then 'claude-3-sonnet'
81
+ when /claude-3-haiku/ then 'claude-3-haiku'
82
+ else 'claude-2'
83
83
  end
84
84
  end
85
85
 
@@ -92,12 +92,12 @@ module RubyLLM
92
92
 
93
93
  # Pricing information for Anthropic models (per million tokens)
94
94
  PRICES = {
95
- claude37_sonnet: { input: 3.0, output: 15.0 },
96
- claude35_sonnet: { input: 3.0, output: 15.0 },
97
- claude35_haiku: { input: 0.80, output: 4.0 },
98
- claude3_opus: { input: 15.0, output: 75.0 },
99
- claude3_haiku: { input: 0.25, output: 1.25 },
100
- claude2: { input: 3.0, output: 15.0 }
95
+ 'claude-3-7-sonnet': { input: 3.0, output: 15.0 },
96
+ 'claude-3-5-sonnet': { input: 3.0, output: 15.0 },
97
+ 'claude-3-5-haiku': { input: 0.80, output: 4.0 },
98
+ 'claude-3-opus': { input: 15.0, output: 75.0 },
99
+ 'claude-3-haiku': { input: 0.25, output: 1.25 },
100
+ 'claude-2': { input: 3.0, output: 15.0 }
101
101
  }.freeze
102
102
 
103
103
  # Default input price if model not found in PRICES
@@ -111,6 +111,69 @@ module RubyLLM
111
111
  def default_output_price
112
112
  15.0
113
113
  end
114
+
115
+ def modalities_for(model_id)
116
+ modalities = {
117
+ input: ['text'],
118
+ output: ['text']
119
+ }
120
+
121
+ # All Claude 3+ models support vision
122
+ unless model_id.match?(/claude-[12]/)
123
+ modalities[:input] << 'image'
124
+ modalities[:input] << 'pdf'
125
+ end
126
+
127
+ modalities
128
+ end
129
+
130
+ def capabilities_for(model_id)
131
+ capabilities = ['streaming']
132
+
133
+ # Function calling for Claude 3+
134
+ if model_id.match?(/claude-3/)
135
+ capabilities << 'function_calling'
136
+ capabilities << 'structured_output'
137
+ capabilities << 'batch'
138
+ end
139
+
140
+ # Extended thinking (reasoning) for Claude 3.7
141
+ capabilities << 'reasoning' if model_id.match?(/claude-3-7/)
142
+
143
+ # Citations
144
+ capabilities << 'citations' if model_id.match?(/claude-3\.5|claude-3-7/)
145
+
146
+ capabilities
147
+ end
148
+
149
+ def pricing_for(model_id)
150
+ family = model_family(model_id)
151
+ prices = PRICES.fetch(family.to_sym, { input: default_input_price, output: default_output_price })
152
+
153
+ standard_pricing = {
154
+ input_per_million: prices[:input],
155
+ output_per_million: prices[:output]
156
+ }
157
+
158
+ # Batch is typically half the price
159
+ batch_pricing = {
160
+ input_per_million: prices[:input] * 0.5,
161
+ output_per_million: prices[:output] * 0.5
162
+ }
163
+
164
+ # Add reasoning output pricing for 3.7 models
165
+ if model_id.match?(/claude-3-7/)
166
+ standard_pricing[:reasoning_output_per_million] = prices[:output] * 2.5
167
+ batch_pricing[:reasoning_output_per_million] = prices[:output] * 1.25
168
+ end
169
+
170
+ {
171
+ text_tokens: {
172
+ standard: standard_pricing,
173
+ batch: batch_pricing
174
+ }
175
+ }
176
+ end
114
177
  end
115
178
  end
116
179
  end
@@ -5,7 +5,7 @@ module RubyLLM
5
5
  module Anthropic
6
6
  # Chat methods of the OpenAI API integration
7
7
  module Chat
8
- private
8
+ module_function
9
9
 
10
10
  def completion_url
11
11
  '/v1/messages'
@@ -41,12 +41,12 @@ module RubyLLM
41
41
  messages: chat_messages.map { |msg| format_message(msg) },
42
42
  temperature: temperature,
43
43
  stream: stream,
44
- max_tokens: RubyLLM.models.find(model).max_tokens
44
+ max_tokens: RubyLLM.models.find(model)&.max_tokens || 4096
45
45
  }
46
46
  end
47
47
 
48
48
  def add_optional_fields(payload, system_content:, tools:)
49
- payload[:tools] = tools.values.map { |t| function_for(t) } if tools.any?
49
+ payload[:tools] = tools.values.map { |t| Tools.function_for(t) } if tools.any?
50
50
  payload[:system] = system_content unless system_content.empty?
51
51
  end
52
52
 
@@ -55,7 +55,7 @@ module RubyLLM
55
55
  content_blocks = data['content'] || []
56
56
 
57
57
  text_content = extract_text_content(content_blocks)
58
- tool_use = find_tool_use(content_blocks)
58
+ tool_use = Tools.find_tool_use(content_blocks)
59
59
 
60
60
  build_message(data, text_content, tool_use)
61
61
  end
@@ -69,7 +69,7 @@ module RubyLLM
69
69
  Message.new(
70
70
  role: :assistant,
71
71
  content: content,
72
- tool_calls: parse_tool_calls(tool_use),
72
+ tool_calls: Tools.parse_tool_calls(tool_use),
73
73
  input_tokens: data.dig('usage', 'input_tokens'),
74
74
  output_tokens: data.dig('usage', 'output_tokens'),
75
75
  model_id: data['model']
@@ -78,9 +78,9 @@ module RubyLLM
78
78
 
79
79
  def format_message(msg)
80
80
  if msg.tool_call?
81
- format_tool_call(msg)
81
+ Tools.format_tool_call(msg)
82
82
  elsif msg.tool_result?
83
- format_tool_result(msg)
83
+ Tools.format_tool_result(msg)
84
84
  else
85
85
  format_basic_message(msg)
86
86
  end
@@ -99,13 +99,6 @@ module RubyLLM
99
99
  else 'assistant'
100
100
  end
101
101
  end
102
-
103
- def format_text_block(content)
104
- {
105
- type: 'text',
106
- text: content
107
- }
108
- end
109
102
  end
110
103
  end
111
104
  end
@@ -7,62 +7,72 @@ module RubyLLM
7
7
  module Media
8
8
  module_function
9
9
 
10
- def format_content(content) # rubocop:disable Metrics/MethodLength
11
- return content unless content.is_a?(Array)
10
+ def format_content(content)
11
+ return [format_text(content)] unless content.is_a?(Content)
12
12
 
13
- content.map do |part|
14
- case part[:type]
15
- when 'image'
16
- format_image(part)
17
- when 'pdf'
18
- format_pdf(part)
19
- when 'text'
20
- format_text_block(part[:text])
21
- else
22
- part
13
+ parts = []
14
+ parts << format_text(content.text) if content.text
15
+
16
+ content.attachments.each do |attachment|
17
+ case attachment
18
+ when Attachments::Image
19
+ parts << format_image(attachment)
20
+ when Attachments::PDF
21
+ parts << format_pdf(attachment)
23
22
  end
24
23
  end
24
+
25
+ parts
25
26
  end
26
27
 
27
- def format_image(part)
28
- # Handle image formatting for Anthropic
29
- # This is just a placeholder - implement based on Anthropic's requirements
30
- part
28
+ def format_text(text)
29
+ {
30
+ type: 'text',
31
+ text: text
32
+ }
31
33
  end
32
34
 
33
- def format_pdf(part) # rubocop:disable Metrics/MethodLength
34
- source = part[:source]
35
+ def format_image(image)
36
+ if image.url?
37
+ {
38
+ type: 'image',
39
+ source: {
40
+ type: 'url',
41
+ url: image.source
42
+ }
43
+ }
44
+ else
45
+ {
46
+ type: 'image',
47
+ source: {
48
+ type: 'base64',
49
+ media_type: image.mime_type,
50
+ data: image.encoded
51
+ }
52
+ }
53
+ end
54
+ end
35
55
 
36
- if source.start_with?('http')
37
- # For URLs - add "type": "url" here
56
+ def format_pdf(pdf)
57
+ if pdf.url?
38
58
  {
39
59
  type: 'document',
40
60
  source: {
41
- type: 'url', # This line is missing in the current implementation
42
- url: source
61
+ type: 'url',
62
+ url: pdf.source
43
63
  }
44
64
  }
45
65
  else
46
- # For local files
47
- data = Base64.strict_encode64(part[:content])
48
-
49
66
  {
50
67
  type: 'document',
51
68
  source: {
52
69
  type: 'base64',
53
- media_type: 'application/pdf',
54
- data: data
70
+ media_type: pdf.mime_type,
71
+ data: pdf.encoded
55
72
  }
56
73
  }
57
74
  end
58
75
  end
59
-
60
- def format_text_block(text)
61
- {
62
- type: 'text',
63
- text: text
64
- }
65
- end
66
76
  end
67
77
  end
68
78
  end
@@ -5,28 +5,28 @@ module RubyLLM
5
5
  module Anthropic
6
6
  # Models methods of the Anthropic API integration
7
7
  module Models
8
- private
8
+ module_function
9
9
 
10
10
  def models_url
11
11
  '/v1/models'
12
12
  end
13
13
 
14
- def parse_list_models_response(response, slug, capabilities) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
15
- (response.body['data'] || []).map do |model|
14
+ def parse_list_models_response(response, slug, capabilities)
15
+ Array(response.body['data']).map do |model_data|
16
+ model_id = model_data['id']
17
+
16
18
  ModelInfo.new(
17
- id: model['id'],
18
- created_at: Time.parse(model['created_at']),
19
- display_name: model['display_name'],
19
+ id: model_id,
20
+ name: model_data['display_name'],
20
21
  provider: slug,
21
- type: capabilities.model_type(model['id']),
22
- family: capabilities.model_family(model['id']),
23
- context_window: capabilities.determine_context_window(model['id']),
24
- max_tokens: capabilities.determine_max_tokens(model['id']),
25
- supports_vision: capabilities.supports_vision?(model['id']),
26
- supports_functions: capabilities.supports_functions?(model['id']),
27
- supports_json_mode: capabilities.supports_json_mode?(model['id']),
28
- input_price_per_million: capabilities.get_input_price(model['id']),
29
- output_price_per_million: capabilities.get_output_price(model['id'])
22
+ family: capabilities.model_family(model_id),
23
+ created_at: Time.parse(model_data['created_at']),
24
+ context_window: capabilities.determine_context_window(model_id),
25
+ max_output_tokens: capabilities.determine_max_tokens(model_id),
26
+ modalities: capabilities.modalities_for(model_id),
27
+ capabilities: capabilities.capabilities_for(model_id),
28
+ pricing: capabilities.pricing_for(model_id),
29
+ metadata: {}
30
30
  )
31
31
  end
32
32
  end
@@ -5,7 +5,7 @@ module RubyLLM
5
5
  module Anthropic
6
6
  # Tools methods of the Anthropic API integration
7
7
  module Tools
8
- private
8
+ module_function
9
9
 
10
10
  def find_tool_use(blocks)
11
11
  blocks.find { |c| c['type'] == 'tool_use' }
@@ -17,7 +17,7 @@ module RubyLLM
17
17
  {
18
18
  role: 'assistant',
19
19
  content: [
20
- format_text_block(msg.content),
20
+ Media.format_text(msg.content),
21
21
  format_tool_use_block(tool_call)
22
22
  ]
23
23
  }