ruby_llm 1.6.2 → 1.6.3

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 +73 -91
  3. data/lib/ruby_llm/active_record/acts_as.rb +2 -10
  4. data/lib/ruby_llm/aliases.json +4 -0
  5. data/lib/ruby_llm/aliases.rb +7 -25
  6. data/lib/ruby_llm/chat.rb +2 -10
  7. data/lib/ruby_llm/configuration.rb +1 -12
  8. data/lib/ruby_llm/content.rb +0 -2
  9. data/lib/ruby_llm/embedding.rb +1 -2
  10. data/lib/ruby_llm/error.rb +0 -8
  11. data/lib/ruby_llm/image.rb +0 -4
  12. data/lib/ruby_llm/message.rb +2 -4
  13. data/lib/ruby_llm/model/info.rb +0 -10
  14. data/lib/ruby_llm/model/pricing.rb +0 -3
  15. data/lib/ruby_llm/model/pricing_category.rb +0 -2
  16. data/lib/ruby_llm/model/pricing_tier.rb +0 -1
  17. data/lib/ruby_llm/models.json +623 -452
  18. data/lib/ruby_llm/models.rb +5 -13
  19. data/lib/ruby_llm/provider.rb +1 -5
  20. data/lib/ruby_llm/providers/anthropic/capabilities.rb +1 -46
  21. data/lib/ruby_llm/providers/anthropic/media.rb +0 -1
  22. data/lib/ruby_llm/providers/anthropic/tools.rb +0 -1
  23. data/lib/ruby_llm/providers/anthropic.rb +1 -2
  24. data/lib/ruby_llm/providers/bedrock/chat.rb +0 -2
  25. data/lib/ruby_llm/providers/bedrock/media.rb +0 -1
  26. data/lib/ruby_llm/providers/bedrock/models.rb +0 -2
  27. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +0 -12
  28. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +0 -7
  29. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +0 -12
  30. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +0 -12
  31. data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +0 -13
  32. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -18
  33. data/lib/ruby_llm/providers/bedrock.rb +1 -2
  34. data/lib/ruby_llm/providers/deepseek/capabilities.rb +1 -2
  35. data/lib/ruby_llm/providers/deepseek/chat.rb +0 -1
  36. data/lib/ruby_llm/providers/gemini/capabilities.rb +26 -101
  37. data/lib/ruby_llm/providers/gemini/chat.rb +5 -7
  38. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -2
  39. data/lib/ruby_llm/providers/gemini/images.rb +0 -1
  40. data/lib/ruby_llm/providers/gemini/media.rb +0 -1
  41. data/lib/ruby_llm/providers/gemini/models.rb +1 -2
  42. data/lib/ruby_llm/providers/gemini/tools.rb +0 -5
  43. data/lib/ruby_llm/providers/gpustack/chat.rb +0 -1
  44. data/lib/ruby_llm/providers/gpustack/models.rb +3 -4
  45. data/lib/ruby_llm/providers/mistral/capabilities.rb +2 -10
  46. data/lib/ruby_llm/providers/mistral/chat.rb +0 -2
  47. data/lib/ruby_llm/providers/mistral/embeddings.rb +0 -3
  48. data/lib/ruby_llm/providers/mistral/models.rb +0 -1
  49. data/lib/ruby_llm/providers/ollama/chat.rb +0 -1
  50. data/lib/ruby_llm/providers/ollama/media.rb +0 -1
  51. data/lib/ruby_llm/providers/openai/capabilities.rb +0 -15
  52. data/lib/ruby_llm/providers/openai/chat.rb +0 -3
  53. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -3
  54. data/lib/ruby_llm/providers/openai/media.rb +0 -1
  55. data/lib/ruby_llm/providers/openai.rb +1 -3
  56. data/lib/ruby_llm/providers/openrouter/models.rb +1 -16
  57. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -1
  58. data/lib/ruby_llm/providers/perplexity/chat.rb +0 -1
  59. data/lib/ruby_llm/providers/perplexity.rb +1 -5
  60. data/lib/ruby_llm/railtie.rb +0 -1
  61. data/lib/ruby_llm/stream_accumulator.rb +1 -3
  62. data/lib/ruby_llm/streaming.rb +15 -24
  63. data/lib/ruby_llm/tool.rb +2 -19
  64. data/lib/ruby_llm/tool_call.rb +0 -9
  65. data/lib/ruby_llm/version.rb +1 -1
  66. data/lib/ruby_llm.rb +0 -2
  67. data/lib/tasks/aliases.rake +5 -35
  68. data/lib/tasks/models_docs.rake +1 -11
  69. data/lib/tasks/models_update.rake +1 -1
  70. data/lib/tasks/vcr.rake +0 -7
  71. metadata +1 -1
@@ -23,9 +23,6 @@ module RubyLLM
23
23
  data = response.body
24
24
  input_tokens = data.dig('usage', 'prompt_tokens') || 0
25
25
  vectors = data['data'].map { |d| d['embedding'] }
26
-
27
- # If we only got one embedding AND the input was a single string (not an array),
28
- # return it as a single vector
29
26
  vectors = vectors.first if vectors.length == 1 && !text.is_a?(Array)
30
27
 
31
28
  Embedding.new(vectors:, model:, input_tokens:)
@@ -8,7 +8,6 @@ module RubyLLM
8
8
  module_function
9
9
 
10
10
  def format_content(content)
11
- # Convert Hash/Array back to JSON string for API
12
11
  return content.to_json if content.is_a?(Hash) || content.is_a?(Array)
13
12
  return content unless content.is_a?(Content)
14
13
 
@@ -2,9 +2,7 @@
2
2
 
3
3
  module RubyLLM
4
4
  module Providers
5
- # OpenAI API integration. Handles chat completion, function calling,
6
- # and OpenAI's unique streaming format. Supports GPT-4, GPT-3.5,
7
- # and other OpenAI models.
5
+ # OpenAI API integration.
8
6
  class OpenAI < Provider
9
7
  include OpenAI::Chat
10
8
  include OpenAI::Embeddings
@@ -13,13 +13,11 @@ module RubyLLM
13
13
 
14
14
  def parse_list_models_response(response, slug, _capabilities)
15
15
  Array(response.body['data']).map do |model_data| # rubocop:disable Metrics/BlockLength
16
- # Extract modalities directly from architecture
17
16
  modalities = {
18
17
  input: Array(model_data.dig('architecture', 'input_modalities')),
19
18
  output: Array(model_data.dig('architecture', 'output_modalities'))
20
19
  }
21
20
 
22
- # Construct pricing from API data, only adding non-zero values
23
21
  pricing = { text_tokens: { standard: {} } }
24
22
 
25
23
  pricing_types = {
@@ -34,7 +32,6 @@ module RubyLLM
34
32
  pricing[:text_tokens][:standard][target_key] = value * 1_000_000 if value.positive?
35
33
  end
36
34
 
37
- # Convert OpenRouter's supported parameters to our capability format
38
35
  capabilities = supported_parameters_to_capabilities(model_data['supported_parameters'])
39
36
 
40
37
  Model::Info.new(
@@ -63,23 +60,11 @@ module RubyLLM
63
60
  return [] unless params
64
61
 
65
62
  capabilities = []
66
-
67
- # Standard capabilities mapping
68
- capabilities << 'streaming' # Assume all OpenRouter models support streaming
69
-
70
- # Function calling capability
63
+ capabilities << 'streaming'
71
64
  capabilities << 'function_calling' if params.include?('tools') || params.include?('tool_choice')
72
-
73
- # Structured output capability
74
65
  capabilities << 'structured_output' if params.include?('response_format')
75
-
76
- # Batch capability
77
66
  capabilities << 'batch' if params.include?('batch')
78
-
79
- # Additional mappings based on params
80
- # Handles advanced model capabilities that might be inferred from supported params
81
67
  capabilities << 'predicted_outputs' if params.include?('logit_bias') && params.include?('top_k')
82
-
83
68
  capabilities
84
69
  end
85
70
  end
@@ -106,7 +106,6 @@ module RubyLLM
106
106
  }
107
107
  end
108
108
 
109
- # Pricing information for Perplexity models (USD per 1M tokens)
110
109
  PRICES = {
111
110
  sonar: {
112
111
  input: 1.0,
@@ -8,7 +8,6 @@ module RubyLLM
8
8
  module_function
9
9
 
10
10
  def format_role(role)
11
- # Perplexity doesn't use the new OpenAI convention for system prompts
12
11
  role.to_s
13
12
  end
14
13
  end
@@ -34,17 +34,13 @@ module RubyLLM
34
34
 
35
35
  # If response is HTML (Perplexity returns HTML for auth errors)
36
36
  if body.include?('<html>') && body.include?('<title>')
37
- # Extract title content
38
37
  title_match = body.match(%r{<title>(.+?)</title>})
39
38
  if title_match
40
- # Clean up the title - remove status code if present
41
39
  message = title_match[1]
42
- message = message.sub(/^\d+\s+/, '') # Remove leading digits and space
40
+ message = message.sub(/^\d+\s+/, '')
43
41
  return message
44
42
  end
45
43
  end
46
-
47
- # Fall back to parent's implementation
48
44
  super
49
45
  end
50
46
  end
@@ -9,7 +9,6 @@ module RubyLLM
9
9
  end
10
10
  end
11
11
 
12
- # Register generators
13
12
  generators do
14
13
  require 'generators/ruby_llm/install_generator'
15
14
  end
@@ -2,8 +2,6 @@
2
2
 
3
3
  module RubyLLM
4
4
  # Assembles streaming responses from LLMs into complete messages.
5
- # Handles the complexities of accumulating content and tool calls
6
- # from partial chunks while tracking token usage.
7
5
  class StreamAccumulator
8
6
  attr_reader :content, :model_id, :tool_calls
9
7
 
@@ -48,7 +46,7 @@ module RubyLLM
48
46
  arguments = if tc.arguments.is_a?(String) && !tc.arguments.empty?
49
47
  JSON.parse(tc.arguments)
50
48
  elsif tc.arguments.is_a?(String)
51
- {} # Return empty hash for empty string arguments
49
+ {}
52
50
  else
53
51
  tc.arguments
54
52
  end
@@ -1,10 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- # Handles streaming responses from AI providers. Provides a unified way to process
5
- # chunked responses, accumulate content, and handle provider-specific streaming formats.
6
- # Each provider implements provider-specific parsing while sharing common stream handling
7
- # patterns.
4
+ # Handles streaming responses from AI providers.
8
5
  module Streaming
9
6
  module_function
10
7
 
@@ -12,17 +9,14 @@ module RubyLLM
12
9
  accumulator = StreamAccumulator.new
13
10
 
14
11
  response = connection.post stream_url, payload do |req|
15
- # Merge additional headers, with existing headers taking precedence
16
12
  req.headers = additional_headers.merge(req.headers) unless additional_headers.empty?
17
- if req.options.respond_to?(:on_data)
18
- # Handle Faraday 2.x streaming with on_data method
19
- req.options.on_data = handle_stream do |chunk|
13
+ if faraday_1?
14
+ req.options[:on_data] = handle_stream do |chunk|
20
15
  accumulator.add chunk
21
16
  block.call chunk
22
17
  end
23
18
  else
24
- # Handle Faraday 1.x streaming with :on_data key
25
- req.options[:on_data] = handle_stream do |chunk|
19
+ req.options.on_data = handle_stream do |chunk|
26
20
  accumulator.add chunk
27
21
  block.call chunk
28
22
  end
@@ -42,6 +36,10 @@ module RubyLLM
42
36
 
43
37
  private
44
38
 
39
+ def faraday_1?
40
+ Faraday::VERSION.start_with?('1')
41
+ end
42
+
45
43
  def to_json_stream(&)
46
44
  buffer = +''
47
45
  parser = EventStreamParser::Parser.new
@@ -50,11 +48,9 @@ module RubyLLM
50
48
  end
51
49
 
52
50
  def create_stream_processor(parser, buffer, &)
53
- if Faraday::VERSION.start_with?('1')
54
- # Faraday 1.x: on_data receives (chunk, size)
51
+ if faraday_1?
55
52
  legacy_stream_processor(parser, &)
56
53
  else
57
- # Faraday 2.x: on_data receives (chunk, bytes, env)
58
54
  stream_processor(parser, buffer, &)
59
55
  end
60
56
  end
@@ -94,12 +90,10 @@ module RubyLLM
94
90
  status, _message = parse_streaming_error(error_data)
95
91
  parsed_data = JSON.parse(error_data)
96
92
 
97
- # Create a response-like object that works for both Faraday v1 and v2
98
- error_response = if env
99
- env.merge(body: parsed_data, status: status)
100
- else
101
- # For Faraday v1, create a simple object that responds to .status and .body
93
+ error_response = if faraday_1?
102
94
  Struct.new(:body, :status).new(parsed_data, status)
95
+ else
96
+ env.merge(body: parsed_data, status: status)
103
97
  end
104
98
 
105
99
  ErrorMiddleware.parse_error(provider: self, response: error_response)
@@ -137,12 +131,10 @@ module RubyLLM
137
131
  status, _message = parse_streaming_error(data)
138
132
  parsed_data = JSON.parse(data)
139
133
 
140
- # Create a response-like object that works for both Faraday v1 and v2
141
- error_response = if env
142
- env.merge(body: parsed_data, status: status)
143
- else
144
- # For Faraday v1, create a simple object that responds to .status and .body
134
+ error_response = if faraday_1?
145
135
  Struct.new(:body, :status).new(parsed_data, status)
136
+ else
137
+ env.merge(body: parsed_data, status: status)
146
138
  end
147
139
 
148
140
  ErrorMiddleware.parse_error(provider: self, response: error_response)
@@ -150,7 +142,6 @@ module RubyLLM
150
142
  RubyLLM.logger.debug "Failed to parse error event: #{e.message}"
151
143
  end
152
144
 
153
- # Default implementation - providers should override this method
154
145
  def parse_streaming_error(data)
155
146
  error_data = JSON.parse(data)
156
147
  [500, error_data['message'] || 'Unknown streaming error']
data/lib/ruby_llm/tool.rb CHANGED
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- # Parameter definition for Tool methods. Specifies type constraints,
5
- # descriptions, and whether parameters are required.
4
+ # Parameter definition for Tool methods.
6
5
  class Parameter
7
6
  attr_reader :name, :type, :description, :required
8
7
 
@@ -14,23 +13,7 @@ module RubyLLM
14
13
  end
15
14
  end
16
15
 
17
- # Base class for creating tools that AI models can use. Provides a simple
18
- # interface for defining parameters and implementing tool behavior.
19
- #
20
- # Example:
21
- # require 'tzinfo'
22
- #
23
- # class TimeInfo < RubyLLM::Tool
24
- # description 'Gets the current time in various timezones'
25
- # param :timezone, desc: "Timezone name (e.g., 'UTC', 'America/New_York')"
26
- #
27
- # def execute(timezone:)
28
- # time = TZInfo::Timezone.get(timezone).now.strftime('%Y-%m-%d %H:%M:%S')
29
- # "Current time in #{timezone}: #{time}"
30
- # rescue StandardError => e
31
- # { error: e.message }
32
- # end
33
- # end
16
+ # Base class for creating tools that AI models can use
34
17
  class Tool
35
18
  # Stops conversation continuation after tool execution
36
19
  class Halt
@@ -2,15 +2,6 @@
2
2
 
3
3
  module RubyLLM
4
4
  # Represents a function call from an AI model to a Tool.
5
- # Encapsulates the function name, arguments, and execution results
6
- # in a clean Ruby interface.
7
- #
8
- # Example:
9
- # tool_call = ToolCall.new(
10
- # id: "call_123",
11
- # name: "calculator",
12
- # arguments: { expression: "2 + 2" }
13
- # )
14
5
  class ToolCall
15
6
  attr_reader :id, :name, :arguments
16
7
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- VERSION = '1.6.2'
4
+ VERSION = '1.6.3'
5
5
  end
data/lib/ruby_llm.rb CHANGED
@@ -30,8 +30,6 @@ loader.ignore("#{__dir__}/generators")
30
30
  loader.setup
31
31
 
32
32
  # A delightful Ruby interface to modern AI language models.
33
- # Provides a unified way to interact with models from OpenAI, Anthropic and others
34
- # with a focus on developer happiness and convention over configuration.
35
33
  module RubyLLM
36
34
  class Error < StandardError; end
37
35
 
@@ -28,11 +28,9 @@ namespace :aliases do # rubocop:disable Metrics/BlockLength
28
28
  }
29
29
  end
30
30
 
31
- # Anthropic models - group by base name and find latest
32
31
  anthropic_latest = group_anthropic_models_by_base_name(models['anthropic'])
33
32
 
34
33
  anthropic_latest.each do |base_name, latest_model|
35
- # Check OpenRouter naming patterns for the BASE NAME (not the full dated model)
36
34
  openrouter_variants = [
37
35
  "anthropic/#{base_name}", # anthropic/claude-3-5-sonnet
38
36
  "anthropic/#{base_name.gsub(/-(\d)/, '.\1')}", # anthropic/claude-3.5-sonnet
@@ -42,10 +40,8 @@ namespace :aliases do # rubocop:disable Metrics/BlockLength
42
40
 
43
41
  openrouter_model = openrouter_variants.find { |v| models['openrouter'].include?(v) }
44
42
 
45
- # Find corresponding Bedrock model
46
43
  bedrock_model = find_best_bedrock_model(latest_model, models['bedrock'])
47
44
 
48
- # Create alias if we have any match (OpenRouter OR Bedrock) OR if it's Anthropic-only
49
45
  next unless openrouter_model || bedrock_model || models['anthropic'].include?(latest_model)
50
46
 
51
47
  aliases[base_name] = {
@@ -56,21 +52,16 @@ namespace :aliases do # rubocop:disable Metrics/BlockLength
56
52
  aliases[base_name]['bedrock'] = bedrock_model if bedrock_model
57
53
  end
58
54
 
59
- # Also check if Bedrock has models that Anthropic doesn't
60
55
  models['bedrock'].each do |bedrock_model|
61
56
  next unless bedrock_model.start_with?('anthropic.')
62
57
 
63
- # Extract the Claude model name
64
58
  next unless bedrock_model =~ /anthropic\.(claude-[\d\.]+-[a-z]+)/
65
59
 
66
60
  base_name = Regexp.last_match(1)
67
- # Normalize to Anthropic naming convention
68
61
  anthropic_name = base_name.tr('.', '-')
69
62
 
70
- # Skip if we already have an alias for this
71
63
  next if aliases[anthropic_name]
72
64
 
73
- # Check if this model exists in OpenRouter
74
65
  openrouter_variants = [
75
66
  "anthropic/#{anthropic_name}",
76
67
  "anthropic/#{base_name}" # Keep the dots
@@ -86,9 +77,7 @@ namespace :aliases do # rubocop:disable Metrics/BlockLength
86
77
  aliases[anthropic_name]['openrouter'] = openrouter_model if openrouter_model
87
78
  end
88
79
 
89
- # Gemini models
90
80
  models['gemini'].each do |model|
91
- # OpenRouter uses "google/" prefix and sometimes different naming
92
81
  openrouter_variants = [
93
82
  "google/#{model}",
94
83
  "google/#{model.gsub('gemini-', 'gemini-').tr('.', '-')}",
@@ -105,7 +94,6 @@ namespace :aliases do # rubocop:disable Metrics/BlockLength
105
94
  }
106
95
  end
107
96
 
108
- # DeepSeek models
109
97
  models['deepseek'].each do |model|
110
98
  openrouter_model = "deepseek/#{model}"
111
99
  next unless models['openrouter'].include?(openrouter_model)
@@ -117,9 +105,8 @@ namespace :aliases do # rubocop:disable Metrics/BlockLength
117
105
  }
118
106
  end
119
107
 
120
- # Write the result
121
108
  sorted_aliases = aliases.sort.to_h
122
- File.write('lib/ruby_llm/aliases.json', JSON.pretty_generate(sorted_aliases))
109
+ File.write(RubyLLM::Aliases.aliases_file, JSON.pretty_generate(sorted_aliases))
123
110
 
124
111
  puts "Generated #{sorted_aliases.size} aliases"
125
112
  end
@@ -132,13 +119,11 @@ namespace :aliases do # rubocop:disable Metrics/BlockLength
132
119
  grouped[base_name] << model
133
120
  end
134
121
 
135
- # Find the latest model for each base name
136
122
  latest_models = {}
137
123
  grouped.each do |base_name, model_list|
138
124
  if model_list.size == 1
139
125
  latest_models[base_name] = model_list.first
140
126
  else
141
- # Sort by date and take the latest
142
127
  latest_model = model_list.max_by { |model| extract_date_from_model(model) }
143
128
  latest_models[base_name] = latest_model
144
129
  end
@@ -148,26 +133,22 @@ namespace :aliases do # rubocop:disable Metrics/BlockLength
148
133
  end
149
134
 
150
135
  def extract_base_name(model) # rubocop:disable Rake/MethodDefinitionInTask
151
- # Remove date suffix (YYYYMMDD) from model name
152
136
  if model =~ /^(.+)-(\d{8})$/
153
137
  Regexp.last_match(1)
154
138
  else
155
- # Models without date suffix (like claude-2.0, claude-2.1)
156
139
  model
157
140
  end
158
141
  end
159
142
 
160
143
  def extract_date_from_model(model) # rubocop:disable Rake/MethodDefinitionInTask
161
- # Extract date for comparison, return '00000000' for models without dates
162
144
  if model =~ /-(\d{8})$/
163
145
  Regexp.last_match(1)
164
146
  else
165
- '00000000' # Ensures models without dates sort before dated ones
147
+ '00000000'
166
148
  end
167
149
  end
168
150
 
169
151
  def find_best_bedrock_model(anthropic_model, bedrock_models) # rubocop:disable Metrics/PerceivedComplexity,Rake/MethodDefinitionInTask
170
- # Special mapping for Claude 2.x models
171
152
  base_pattern = case anthropic_model
172
153
  when 'claude-2.0', 'claude-2'
173
154
  'claude-v2'
@@ -176,20 +157,16 @@ namespace :aliases do # rubocop:disable Metrics/BlockLength
176
157
  when 'claude-instant-v1', 'claude-instant'
177
158
  'claude-instant'
178
159
  else
179
- # For Claude 3+ models, extract base name
180
160
  extract_base_name(anthropic_model)
181
161
  end
182
162
 
183
- # Find all matching Bedrock models by stripping provider prefix and comparing base name
184
163
  matching_models = bedrock_models.select do |bedrock_model|
185
- # Strip any provider prefix (anthropic. or us.anthropic.)
186
164
  model_without_prefix = bedrock_model.sub(/^(?:us\.)?anthropic\./, '')
187
165
  model_without_prefix.start_with?(base_pattern)
188
166
  end
189
167
 
190
168
  return nil if matching_models.empty?
191
169
 
192
- # Get model info to check context window
193
170
  begin
194
171
  model_info = RubyLLM.models.find(anthropic_model)
195
172
  target_context = model_info.context_window
@@ -197,12 +174,9 @@ namespace :aliases do # rubocop:disable Metrics/BlockLength
197
174
  target_context = nil
198
175
  end
199
176
 
200
- # If we have context window info, try to match it
201
177
  if target_context
202
- # Convert to k format (200000 -> 200k)
203
178
  target_k = target_context / 1000
204
179
 
205
- # Find models with this specific context window
206
180
  with_context = matching_models.select do |m|
207
181
  m.include?(":#{target_k}k") || m.include?(":0:#{target_k}k")
208
182
  end
@@ -210,25 +184,21 @@ namespace :aliases do # rubocop:disable Metrics/BlockLength
210
184
  return with_context.first if with_context.any?
211
185
  end
212
186
 
213
- # Otherwise, pick the one with the highest context window or latest version
214
187
  matching_models.min_by do |model|
215
- # Extract context window if specified
216
188
  context_priority = if model =~ /:(?:\d+:)?(\d+)k/
217
- -Regexp.last_match(1).to_i # Negative for descending sort
189
+ -Regexp.last_match(1).to_i
218
190
  else
219
- 0 # No context specified
191
+ 0
220
192
  end
221
193
 
222
- # Extract version if present
223
194
  version_priority = if model =~ /-v(\d+):/
224
- -Regexp.last_match(1).to_i # Negative for descending sort (latest version first)
195
+ -Regexp.last_match(1).to_i
225
196
  else
226
197
  0
227
198
  end
228
199
 
229
200
  # Prefer models with explicit context windows
230
201
  has_context_priority = model.include?('k') ? -1 : 0
231
-
232
202
  [has_context_priority, context_priority, version_priority]
233
203
  end
234
204
  end
@@ -6,12 +6,8 @@ require 'fileutils'
6
6
  namespace :models do
7
7
  desc 'Generate available models documentation'
8
8
  task :docs do
9
- FileUtils.mkdir_p('docs') # ensure output directory exists
10
-
11
- # Generate markdown content
9
+ FileUtils.mkdir_p('docs')
12
10
  output = generate_models_markdown
13
-
14
- # Write the output
15
11
  File.write('docs/_reference/available-models.md', output)
16
12
  puts 'Generated docs/_reference/available-models.md'
17
13
  end
@@ -121,7 +117,6 @@ end
121
117
  def generate_modality_sections # rubocop:disable Metrics/PerceivedComplexity
122
118
  sections = []
123
119
 
124
- # Models that support vision/images
125
120
  vision_models = RubyLLM.models.select { |m| (m.modalities.input || []).include?('image') }
126
121
  if vision_models.any?
127
122
  sections << <<~SECTION
@@ -133,7 +128,6 @@ def generate_modality_sections # rubocop:disable Metrics/PerceivedComplexity
133
128
  SECTION
134
129
  end
135
130
 
136
- # Models that support audio
137
131
  audio_models = RubyLLM.models.select { |m| (m.modalities.input || []).include?('audio') }
138
132
  if audio_models.any?
139
133
  sections << <<~SECTION
@@ -145,7 +139,6 @@ def generate_modality_sections # rubocop:disable Metrics/PerceivedComplexity
145
139
  SECTION
146
140
  end
147
141
 
148
- # Models that support PDFs
149
142
  pdf_models = RubyLLM.models.select { |m| (m.modalities.input || []).include?('pdf') }
150
143
  if pdf_models.any?
151
144
  sections << <<~SECTION
@@ -157,7 +150,6 @@ def generate_modality_sections # rubocop:disable Metrics/PerceivedComplexity
157
150
  SECTION
158
151
  end
159
152
 
160
- # Models for embeddings
161
153
  embedding_models = RubyLLM.models.select { |m| (m.modalities.output || []).include?('embeddings') }
162
154
  if embedding_models.any?
163
155
  sections << <<~SECTION
@@ -179,7 +171,6 @@ def models_table(models)
179
171
  alignment = [':--', ':--', '--:', '--:', ':--']
180
172
 
181
173
  rows = models.sort_by { |m| [m.provider, m.name] }.map do |model|
182
- # Format pricing information
183
174
  pricing = standard_pricing_display(model)
184
175
 
185
176
  [
@@ -203,7 +194,6 @@ def models_table(models)
203
194
  end
204
195
 
205
196
  def standard_pricing_display(model)
206
- # Access pricing data using to_h to get the raw hash
207
197
  pricing_data = model.pricing.to_h[:text_tokens]&.dig(:standard) || {}
208
198
 
209
199
  if pricing_data.any?
@@ -61,7 +61,7 @@ def refresh_models
61
61
  end
62
62
 
63
63
  def validate_models!(models)
64
- schema_path = File.expand_path('../ruby_llm/models_schema.json', __dir__)
64
+ schema_path = RubyLLM::Models.schema_file
65
65
  models_data = models.all.map(&:to_h)
66
66
 
67
67
  validation_errors = JSON::Validator.fully_validate(schema_path, models_data)
data/lib/tasks/vcr.rake CHANGED
@@ -2,9 +2,7 @@
2
2
 
3
3
  require 'dotenv/load'
4
4
 
5
- # Helper functions at the top level
6
5
  def record_all_cassettes(cassette_dir)
7
- # Re-record all cassettes
8
6
  FileUtils.rm_rf(cassette_dir)
9
7
  FileUtils.mkdir_p(cassette_dir)
10
8
 
@@ -14,10 +12,8 @@ def record_all_cassettes(cassette_dir)
14
12
  end
15
13
 
16
14
  def record_for_providers(providers, cassette_dir)
17
- # Get the list of available providers from RubyLLM itself
18
15
  all_providers = RubyLLM::Provider.providers.keys.map(&:to_s)
19
16
 
20
- # Check for valid providers
21
17
  if providers.empty?
22
18
  puts "Please specify providers or 'all'. Example: rake vcr:record[openai,anthropic]"
23
19
  puts "Available providers: #{all_providers.join(', ')}"
@@ -31,7 +27,6 @@ def record_for_providers(providers, cassette_dir)
31
27
  return
32
28
  end
33
29
 
34
- # Find and delete matching cassettes
35
30
  cassettes_to_delete = find_matching_cassettes(cassette_dir, providers)
36
31
 
37
32
  if cassettes_to_delete.empty?
@@ -54,9 +49,7 @@ def find_matching_cassettes(dir, providers)
54
49
  Dir.glob("#{dir}/**/*.yml").each do |file|
55
50
  basename = File.basename(file)
56
51
 
57
- # Precise matching to avoid cross-provider confusion
58
52
  providers.each do |provider|
59
- # Match only exact provider prefixes
60
53
  next unless basename =~ /^[^_]*_#{provider}_/ || # For first section like "chat_openai_"
61
54
  basename =~ /_#{provider}_[^_]+_/ # For middle sections like "_openai_gpt4_"
62
55
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.2
4
+ version: 1.6.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carmine Paolino