ruby_llm 1.2.0 → 1.3.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +80 -133
  3. data/lib/ruby_llm/active_record/acts_as.rb +144 -47
  4. data/lib/ruby_llm/aliases.json +187 -17
  5. data/lib/ruby_llm/attachment.rb +164 -0
  6. data/lib/ruby_llm/chat.rb +31 -20
  7. data/lib/ruby_llm/configuration.rb +34 -1
  8. data/lib/ruby_llm/connection.rb +121 -0
  9. data/lib/ruby_llm/content.rb +27 -79
  10. data/lib/ruby_llm/context.rb +30 -0
  11. data/lib/ruby_llm/embedding.rb +13 -5
  12. data/lib/ruby_llm/error.rb +2 -1
  13. data/lib/ruby_llm/image.rb +15 -8
  14. data/lib/ruby_llm/message.rb +14 -6
  15. data/lib/ruby_llm/mime_type.rb +67 -0
  16. data/lib/ruby_llm/model/info.rb +101 -0
  17. data/lib/ruby_llm/model/modalities.rb +22 -0
  18. data/lib/ruby_llm/model/pricing.rb +51 -0
  19. data/lib/ruby_llm/model/pricing_category.rb +48 -0
  20. data/lib/ruby_llm/model/pricing_tier.rb +34 -0
  21. data/lib/ruby_llm/model.rb +7 -0
  22. data/lib/ruby_llm/models.json +26279 -2362
  23. data/lib/ruby_llm/models.rb +95 -14
  24. data/lib/ruby_llm/provider.rb +48 -90
  25. data/lib/ruby_llm/providers/anthropic/capabilities.rb +76 -13
  26. data/lib/ruby_llm/providers/anthropic/chat.rb +7 -14
  27. data/lib/ruby_llm/providers/anthropic/media.rb +49 -28
  28. data/lib/ruby_llm/providers/anthropic/models.rb +16 -16
  29. data/lib/ruby_llm/providers/anthropic/tools.rb +2 -2
  30. data/lib/ruby_llm/providers/anthropic.rb +3 -3
  31. data/lib/ruby_llm/providers/bedrock/capabilities.rb +61 -2
  32. data/lib/ruby_llm/providers/bedrock/chat.rb +30 -73
  33. data/lib/ruby_llm/providers/bedrock/media.rb +59 -0
  34. data/lib/ruby_llm/providers/bedrock/models.rb +50 -58
  35. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +16 -0
  36. data/lib/ruby_llm/providers/bedrock.rb +14 -25
  37. data/lib/ruby_llm/providers/deepseek/capabilities.rb +35 -2
  38. data/lib/ruby_llm/providers/deepseek.rb +3 -3
  39. data/lib/ruby_llm/providers/gemini/capabilities.rb +84 -3
  40. data/lib/ruby_llm/providers/gemini/chat.rb +8 -37
  41. data/lib/ruby_llm/providers/gemini/embeddings.rb +18 -34
  42. data/lib/ruby_llm/providers/gemini/images.rb +4 -3
  43. data/lib/ruby_llm/providers/gemini/media.rb +28 -111
  44. data/lib/ruby_llm/providers/gemini/models.rb +17 -23
  45. data/lib/ruby_llm/providers/gemini/tools.rb +1 -1
  46. data/lib/ruby_llm/providers/gemini.rb +3 -3
  47. data/lib/ruby_llm/providers/ollama/chat.rb +28 -0
  48. data/lib/ruby_llm/providers/ollama/media.rb +48 -0
  49. data/lib/ruby_llm/providers/ollama.rb +34 -0
  50. data/lib/ruby_llm/providers/openai/capabilities.rb +78 -3
  51. data/lib/ruby_llm/providers/openai/chat.rb +6 -4
  52. data/lib/ruby_llm/providers/openai/embeddings.rb +8 -12
  53. data/lib/ruby_llm/providers/openai/images.rb +3 -2
  54. data/lib/ruby_llm/providers/openai/media.rb +48 -21
  55. data/lib/ruby_llm/providers/openai/models.rb +17 -18
  56. data/lib/ruby_llm/providers/openai/tools.rb +9 -5
  57. data/lib/ruby_llm/providers/openai.rb +7 -5
  58. data/lib/ruby_llm/providers/openrouter/models.rb +88 -0
  59. data/lib/ruby_llm/providers/openrouter.rb +31 -0
  60. data/lib/ruby_llm/stream_accumulator.rb +4 -4
  61. data/lib/ruby_llm/streaming.rb +48 -13
  62. data/lib/ruby_llm/utils.rb +27 -0
  63. data/lib/ruby_llm/version.rb +1 -1
  64. data/lib/ruby_llm.rb +15 -5
  65. data/lib/tasks/aliases.rake +235 -0
  66. data/lib/tasks/models_docs.rake +164 -121
  67. data/lib/tasks/models_update.rake +79 -0
  68. data/lib/tasks/release.rake +32 -0
  69. data/lib/tasks/vcr.rake +4 -2
  70. metadata +56 -32
  71. data/lib/ruby_llm/model_info.rb +0 -56
  72. data/lib/tasks/browser_helper.rb +0 -97
  73. data/lib/tasks/capability_generator.rb +0 -123
  74. data/lib/tasks/capability_scraper.rb +0 -224
  75. data/lib/tasks/cli_helper.rb +0 -22
  76. data/lib/tasks/code_validator.rb +0 -29
  77. data/lib/tasks/model_updater.rb +0 -66
  78. data/lib/tasks/models.rake +0 -43
@@ -2,103 +2,51 @@
2
2
 
3
3
  module RubyLLM
4
4
  # Represents the content sent to or received from an LLM.
5
- # Stores data in a standard internal format, letting providers
6
- # handle their own formatting needs.
5
+ # Selects the appropriate attachment class based on the content type.
7
6
  class Content
8
- def initialize(text = nil, attachments = {}) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
9
- @parts = []
10
- @parts << { type: 'text', text: text } unless text.nil? || text.empty?
7
+ attr_reader :text, :attachments
11
8
 
12
- Array(attachments[:image]).each do |source|
13
- @parts << attach_image(source)
14
- end
15
-
16
- Array(attachments[:audio]).each do |source|
17
- @parts << attach_audio(source)
18
- end
9
+ def initialize(text = nil, attachments = nil)
10
+ @text = text
11
+ @attachments = []
19
12
 
20
- Array(attachments[:pdf]).each do |source|
21
- @parts << attach_pdf(source)
22
- end
13
+ process_attachments(attachments)
14
+ raise ArgumentError, 'Text and attachments cannot be both nil' if @text.nil? && @attachments.empty?
23
15
  end
24
16
 
25
- def to_a
26
- return if @parts.empty?
27
-
28
- @parts
17
+ def add_attachment(source, filename: nil)
18
+ @attachments << Attachment.new(source, filename:)
19
+ self
29
20
  end
30
21
 
31
22
  def format
32
- return @parts.first[:text] if @parts.size == 1 && @parts.first[:type] == 'text'
33
-
34
- to_a
35
- end
36
-
37
- private
38
-
39
- def attach_image(source) # rubocop:disable Metrics/MethodLength
40
- source = File.expand_path(source) unless source.start_with?('http')
41
-
42
- return { type: 'image', source: { url: source } } if source.start_with?('http')
43
-
44
- data = Base64.strict_encode64(File.read(source))
45
- mime_type = mime_type_for(source)
46
-
47
- {
48
- type: 'image',
49
- source: {
50
- type: 'base64',
51
- media_type: mime_type,
52
- data: data
53
- }
54
- }
23
+ if @text && @attachments.empty?
24
+ @text
25
+ else
26
+ self
27
+ end
55
28
  end
56
29
 
57
- def attach_audio(source)
58
- source = File.expand_path(source) unless source.start_with?('http')
59
- data = encode_file(source)
60
- format = File.extname(source).delete('.') || 'wav'
61
-
62
- {
63
- type: 'input_audio',
64
- input_audio: {
65
- data: data,
66
- format: format
67
- }
68
- }
30
+ # For Rails serialization
31
+ def to_h
32
+ { text: @text, attachments: @attachments.map(&:to_h) }
69
33
  end
70
34
 
71
- def attach_pdf(source)
72
- source = File.expand_path(source) unless source.start_with?('http')
73
-
74
- pdf_data = {
75
- type: 'pdf',
76
- source: source
77
- }
78
-
79
- # For local files, validate they exist
80
- unless source.start_with?('http')
81
- raise Error, "PDF file not found: #{source}" unless File.exist?(source)
35
+ private
82
36
 
83
- # Preload file content for providers that need it
84
- pdf_data[:content] = File.read(source)
37
+ def process_attachments_array_or_string(attachments)
38
+ Utils.to_safe_array(attachments).each do |file|
39
+ add_attachment(file)
85
40
  end
86
-
87
- pdf_data
88
41
  end
89
42
 
90
- def encode_file(source)
91
- if source.start_with?('http')
92
- response = Faraday.get(source)
93
- Base64.strict_encode64(response.body)
43
+ def process_attachments(attachments)
44
+ if attachments.is_a?(Hash)
45
+ # Ignores types (like :image, :audio, :text, :pdf) since we have robust MIME type detection
46
+ attachments.each_value(&method(:process_attachments_array_or_string))
94
47
  else
95
- Base64.strict_encode64(File.read(source))
48
+ process_attachments_array_or_string attachments
96
49
  end
97
50
  end
98
-
99
- def mime_type_for(path)
100
- ext = File.extname(path).delete('.')
101
- "image/#{ext}"
102
- end
103
51
  end
104
52
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # Holds per-call configs
5
+ class Context
6
+ attr_reader :config
7
+
8
+ def initialize(config)
9
+ @config = config
10
+ @connections = {}
11
+ end
12
+
13
+ def chat(*args, **kwargs, &)
14
+ Chat.new(*args, **kwargs, context: self, &)
15
+ end
16
+
17
+ def embed(*args, **kwargs, &)
18
+ Embedding.embed(*args, **kwargs, context: self, &)
19
+ end
20
+
21
+ def paint(*args, **kwargs, &)
22
+ Image.paint(*args, **kwargs, context: self, &)
23
+ end
24
+
25
+ def connection_for(provider_module)
26
+ slug = provider_module.slug.to_sym
27
+ @connections[slug] ||= Connection.new(provider_module, @config)
28
+ end
29
+ end
30
+ end
@@ -12,12 +12,20 @@ module RubyLLM
12
12
  @input_tokens = input_tokens
13
13
  end
14
14
 
15
- def self.embed(text, model: nil)
16
- model_id = model || RubyLLM.config.default_embedding_model
17
- Models.find(model_id)
15
+ def self.embed(text, # rubocop:disable Metrics/ParameterLists
16
+ model: nil,
17
+ provider: nil,
18
+ assume_model_exists: false,
19
+ context: nil,
20
+ dimensions: nil)
21
+ config = context&.config || RubyLLM.config
22
+ model ||= config.default_embedding_model
23
+ model, provider = Models.resolve(model, provider: provider, assume_exists: assume_model_exists)
24
+ model_id = model.id
18
25
 
19
- provider = Provider.for(model_id)
20
- provider.embed(text, model: model_id)
26
+ provider = Provider.for(model_id) if provider.nil?
27
+ connection = context ? context.connection_for(provider) : provider.connection(config)
28
+ provider.embed(text, model: model_id, connection:, dimensions:)
21
29
  end
22
30
  end
23
31
  end
@@ -24,6 +24,7 @@ module RubyLLM
24
24
  class InvalidRoleError < StandardError; end
25
25
  class ModelNotFoundError < StandardError; end
26
26
  class UnsupportedFunctionsError < StandardError; end
27
+ class UnsupportedAttachmentError < StandardError; end
27
28
 
28
29
  # Error classes for different HTTP status codes
29
30
  class BadRequestError < Error; end
@@ -50,7 +51,7 @@ module RubyLLM
50
51
  end
51
52
 
52
53
  class << self
53
- def parse_error(provider:, response:) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/AbcSize,Metrics/PerceivedComplexity
54
+ def parse_error(provider:, response:) # rubocop:disable Metrics/PerceivedComplexity
54
55
  message = provider&.parse_error(response)
55
56
 
56
57
  case response.status
@@ -22,10 +22,9 @@ module RubyLLM
22
22
  # Returns the raw binary image data regardless of source
23
23
  def to_blob
24
24
  if base64?
25
- Base64.decode64(@data)
25
+ Base64.decode64 @data
26
26
  else
27
- # Use Faraday instead of URI.open for better security
28
- response = Faraday.get(@url)
27
+ response = Connection.basic.get @url
29
28
  response.body
30
29
  end
31
30
  end
@@ -36,12 +35,20 @@ module RubyLLM
36
35
  path
37
36
  end
38
37
 
39
- def self.paint(prompt, model: nil, size: '1024x1024')
40
- model_id = model || RubyLLM.config.default_image_model
41
- Models.find(model_id) # Validate model exists
38
+ def self.paint(prompt, # rubocop:disable Metrics/ParameterLists
39
+ model: nil,
40
+ provider: nil,
41
+ assume_model_exists: false,
42
+ size: '1024x1024',
43
+ context: nil)
44
+ config = context&.config || RubyLLM.config
45
+ model ||= config.default_image_model
46
+ model, provider = Models.resolve(model, provider: provider, assume_exists: assume_model_exists)
47
+ model_id = model.id
42
48
 
43
- provider = Provider.for(model_id)
44
- provider.paint(prompt, model: model_id, size: size)
49
+ provider = Provider.for(model_id) if provider.nil?
50
+ connection = context ? context.connection_for(provider) : provider.connection(config)
51
+ provider.paint(prompt, model: model_id, size:, connection:)
45
52
  end
46
53
  end
47
54
  end
@@ -7,11 +7,11 @@ module RubyLLM
7
7
  class Message
8
8
  ROLES = %i[system user assistant tool].freeze
9
9
 
10
- attr_reader :role, :content, :tool_calls, :tool_call_id, :input_tokens, :output_tokens, :model_id
10
+ attr_reader :role, :tool_calls, :tool_call_id, :input_tokens, :output_tokens, :model_id
11
11
 
12
12
  def initialize(options = {})
13
- @role = options[:role].to_sym
14
- @content = normalize_content(options[:content])
13
+ @role = options.fetch(:role).to_sym
14
+ @content = normalize_content(options.fetch(:content))
15
15
  @tool_calls = options[:tool_calls]
16
16
  @input_tokens = options[:input_tokens]
17
17
  @output_tokens = options[:output_tokens]
@@ -21,6 +21,14 @@ module RubyLLM
21
21
  ensure_valid_role
22
22
  end
23
23
 
24
+ def content
25
+ if @content.is_a?(Content) && @content.text && @content.attachments.empty?
26
+ @content.text
27
+ else
28
+ @content
29
+ end
30
+ end
31
+
24
32
  def tool_call?
25
33
  !tool_calls.nil? && !tool_calls.empty?
26
34
  end
@@ -49,9 +57,9 @@ module RubyLLM
49
57
 
50
58
  def normalize_content(content)
51
59
  case content
52
- when Content then content.format
53
- when String then Content.new(content).format
54
- else content
60
+ when String then Content.new(content)
61
+ when Hash then Content.new(content[:text], content)
62
+ else content # Pass through nil, Content, or other types
55
63
  end
56
64
  end
57
65
 
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'marcel'
4
+
5
+ module RubyLLM
6
+ # MimeTypes module provides methods to handle MIME types using Marcel gem
7
+ module MimeType
8
+ module_function
9
+
10
+ def for(...)
11
+ Marcel::MimeType.for(...)
12
+ end
13
+
14
+ def image?(type)
15
+ type.start_with?('image/')
16
+ end
17
+
18
+ def audio?(type)
19
+ type.start_with?('audio/')
20
+ end
21
+
22
+ def pdf?(type)
23
+ type == 'application/pdf'
24
+ end
25
+
26
+ def text?(type)
27
+ type.start_with?('text/') ||
28
+ TEXT_SUFFIXES.any? { |suffix| type.end_with?(suffix) } ||
29
+ NON_TEXT_PREFIX_TEXT_MIME_TYPES.include?(type)
30
+ end
31
+
32
+ # MIME types that have a text/ prefix but need to be handled differently
33
+ TEXT_SUFFIXES = ['+json', '+xml', '+html', '+yaml', '+csv', '+plain', '+javascript', '+svg'].freeze
34
+
35
+ # MIME types that don't have a text/ prefix but should be treated as text
36
+ NON_TEXT_PREFIX_TEXT_MIME_TYPES = [
37
+ 'application/json', # Base type, even if specific ones end with +json
38
+ 'application/xml', # Base type, even if specific ones end with +xml
39
+ 'application/javascript',
40
+ 'application/ecmascript',
41
+ 'application/rtf',
42
+ 'application/sql',
43
+ 'application/x-sh',
44
+ 'application/x-csh',
45
+ 'application/x-httpd-php',
46
+ 'application/sdp',
47
+ 'application/sparql-query',
48
+ 'application/graphql',
49
+ 'application/yang', # Data modeling language, often serialized as XML/JSON but the type itself is distinct
50
+ 'application/mbox', # Mailbox format
51
+ 'application/x-tex',
52
+ 'application/x-latex',
53
+ 'application/x-perl',
54
+ 'application/x-python',
55
+ 'application/x-tcl',
56
+ 'application/pgp-signature', # Often ASCII armored
57
+ 'application/pgp-keys', # Often ASCII armored
58
+ 'application/vnd.coffeescript',
59
+ 'application/vnd.dart',
60
+ 'application/vnd.oai.openapi', # Base for OpenAPI, often with +json or +yaml suffix
61
+ 'application/vnd.zul', # ZK User Interface Language (can be XML-like)
62
+ 'application/x-yaml', # Common non-standard for YAML
63
+ 'application/yaml', # Standard for YAML
64
+ 'application/toml' # TOML configuration files
65
+ ].freeze
66
+ end
67
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Model
5
+ # Information about an AI model's capabilities, pricing, and metadata.
6
+ # Used by the Models registry to help developers choose the right model
7
+ # for their needs.
8
+ #
9
+ # Example:
10
+ # model = RubyLLM.models.find('gpt-4')
11
+ # model.supports_vision? # => true
12
+ # model.supports_functions? # => true
13
+ # model.input_price_per_million # => 30.0
14
+ class Info
15
+ attr_reader :id, :name, :provider, :family, :created_at, :context_window, :max_output_tokens, :knowledge_cutoff,
16
+ :modalities, :capabilities, :pricing, :metadata
17
+
18
+ def initialize(data)
19
+ @id = data[:id]
20
+ @name = data[:name]
21
+ @provider = data[:provider]
22
+ @family = data[:family]
23
+ @created_at = data[:created_at]
24
+ @context_window = data[:context_window]
25
+ @max_output_tokens = data[:max_output_tokens]
26
+ @knowledge_cutoff = data[:knowledge_cutoff]
27
+ @modalities = Modalities.new(data[:modalities] || {})
28
+ @capabilities = data[:capabilities] || []
29
+ @pricing = Pricing.new(data[:pricing] || {})
30
+ @metadata = data[:metadata] || {}
31
+ end
32
+
33
+ # Capability methods
34
+ def supports?(capability)
35
+ capabilities.include?(capability.to_s)
36
+ end
37
+
38
+ %w[function_calling structured_output batch reasoning citations streaming].each do |cap|
39
+ define_method "#{cap}?" do
40
+ supports?(cap)
41
+ end
42
+ end
43
+
44
+ # Backward compatibility methods
45
+ def display_name
46
+ name
47
+ end
48
+
49
+ def max_tokens
50
+ max_output_tokens
51
+ end
52
+
53
+ def supports_vision?
54
+ modalities.input.include?('image')
55
+ end
56
+
57
+ def supports_functions?
58
+ function_calling?
59
+ end
60
+
61
+ def input_price_per_million
62
+ pricing.text_tokens.input
63
+ end
64
+
65
+ def output_price_per_million
66
+ pricing.text_tokens.output
67
+ end
68
+
69
+ def type # rubocop:disable Metrics/PerceivedComplexity
70
+ if modalities.output.include?('embeddings') && !modalities.output.include?('text')
71
+ 'embedding'
72
+ elsif modalities.output.include?('image') && !modalities.output.include?('text')
73
+ 'image'
74
+ elsif modalities.output.include?('audio') && !modalities.output.include?('text')
75
+ 'audio'
76
+ elsif modalities.output.include?('moderation')
77
+ 'moderation'
78
+ else
79
+ 'chat'
80
+ end
81
+ end
82
+
83
+ def to_h
84
+ {
85
+ id: id,
86
+ name: name,
87
+ provider: provider,
88
+ family: family,
89
+ created_at: created_at,
90
+ context_window: context_window,
91
+ max_output_tokens: max_output_tokens,
92
+ knowledge_cutoff: knowledge_cutoff,
93
+ modalities: modalities.to_h,
94
+ capabilities: capabilities,
95
+ pricing: pricing.to_h,
96
+ metadata: metadata
97
+ }
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Model
5
+ # Holds and manages input and output modalities for a language model
6
+ class Modalities
7
+ attr_reader :input, :output
8
+
9
+ def initialize(data)
10
+ @input = Array(data[:input]).map(&:to_s)
11
+ @output = Array(data[:output]).map(&:to_s)
12
+ end
13
+
14
+ def to_h
15
+ {
16
+ input: input,
17
+ output: output
18
+ }
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Model
5
+ # A collection that manages and provides access to different categories of pricing information
6
+ # (text tokens, images, audio tokens, embeddings)
7
+ class Pricing
8
+ def initialize(data)
9
+ @data = {}
10
+
11
+ # Initialize pricing categories
12
+ %i[text_tokens images audio_tokens embeddings].each do |category|
13
+ @data[category] = PricingCategory.new(data[category]) if data[category] && !empty_pricing?(data[category])
14
+ end
15
+ end
16
+
17
+ def method_missing(method, *args)
18
+ if respond_to_missing?(method)
19
+ @data[method.to_sym] || PricingCategory.new
20
+ else
21
+ super
22
+ end
23
+ end
24
+
25
+ def respond_to_missing?(method, include_private = false)
26
+ %i[text_tokens images audio_tokens embeddings].include?(method.to_sym) || super
27
+ end
28
+
29
+ def to_h
30
+ @data.transform_values(&:to_h)
31
+ end
32
+
33
+ private
34
+
35
+ def empty_pricing?(data)
36
+ # Check if all pricing values in this category are zero or nil
37
+ return true unless data
38
+
39
+ %i[standard batch].each do |tier|
40
+ next unless data[tier]
41
+
42
+ data[tier].each_value do |value|
43
+ return false if value && value != 0.0
44
+ end
45
+ end
46
+
47
+ true
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Model
5
+ # Represents pricing tiers for different usage categories (standard and batch)
6
+ class PricingCategory
7
+ attr_reader :standard, :batch
8
+
9
+ def initialize(data = {})
10
+ @standard = PricingTier.new(data[:standard] || {}) unless empty_tier?(data[:standard])
11
+ @batch = PricingTier.new(data[:batch] || {}) unless empty_tier?(data[:batch])
12
+ end
13
+
14
+ # Shorthand methods that default to standard tier
15
+ def input
16
+ standard&.input_per_million
17
+ end
18
+
19
+ def output
20
+ standard&.output_per_million
21
+ end
22
+
23
+ def cached_input
24
+ standard&.cached_input_per_million
25
+ end
26
+
27
+ # Get value for a specific tier
28
+ def [](key)
29
+ key == :batch ? batch : standard
30
+ end
31
+
32
+ def to_h
33
+ result = {}
34
+ result[:standard] = standard.to_h if standard
35
+ result[:batch] = batch.to_h if batch
36
+ result
37
+ end
38
+
39
+ private
40
+
41
+ def empty_tier?(tier_data)
42
+ return true unless tier_data
43
+
44
+ tier_data.values.all? { |v| v.nil? || v == 0.0 }
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Model
5
+ # A dynamic class for storing non-zero pricing values with flexible attribute access
6
+ class PricingTier
7
+ def initialize(data = {})
8
+ @values = {}
9
+
10
+ # Only store non-zero values
11
+ data.each do |key, value|
12
+ @values[key.to_sym] = value if value && value != 0.0
13
+ end
14
+ end
15
+
16
+ def method_missing(method, *args)
17
+ if method.to_s.end_with?('=')
18
+ key = method.to_s.chomp('=').to_sym
19
+ @values[key] = args.first if args.first && args.first != 0.0
20
+ elsif @values.key?(method)
21
+ @values[method]
22
+ end
23
+ end
24
+
25
+ def respond_to_missing?(method, include_private = false)
26
+ method.to_s.end_with?('=') || @values.key?(method.to_sym) || super
27
+ end
28
+
29
+ def to_h
30
+ @values
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # Model-related classes for working with LLM models
5
+ module Model
6
+ end
7
+ end