ruby_llm_community 0.0.1 → 0.0.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 (112) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +22 -0
  3. data/README.md +172 -0
  4. data/lib/generators/ruby_llm/install/templates/INSTALL_INFO.md.tt +108 -0
  5. data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +3 -0
  6. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +8 -0
  7. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +15 -0
  8. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +14 -0
  9. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +6 -0
  10. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +3 -0
  11. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +3 -0
  12. data/lib/generators/ruby_llm/install_generator.rb +121 -0
  13. data/lib/ruby_llm/active_record/acts_as.rb +382 -0
  14. data/lib/ruby_llm/aliases.json +217 -0
  15. data/lib/ruby_llm/aliases.rb +56 -0
  16. data/lib/ruby_llm/attachment.rb +164 -0
  17. data/lib/ruby_llm/chat.rb +226 -0
  18. data/lib/ruby_llm/chunk.rb +6 -0
  19. data/lib/ruby_llm/configuration.rb +73 -0
  20. data/lib/ruby_llm/connection.rb +126 -0
  21. data/lib/ruby_llm/content.rb +52 -0
  22. data/lib/ruby_llm/context.rb +29 -0
  23. data/lib/ruby_llm/embedding.rb +30 -0
  24. data/lib/ruby_llm/error.rb +84 -0
  25. data/lib/ruby_llm/image.rb +53 -0
  26. data/lib/ruby_llm/message.rb +81 -0
  27. data/lib/ruby_llm/mime_type.rb +67 -0
  28. data/lib/ruby_llm/model/info.rb +101 -0
  29. data/lib/ruby_llm/model/modalities.rb +22 -0
  30. data/lib/ruby_llm/model/pricing.rb +51 -0
  31. data/lib/ruby_llm/model/pricing_category.rb +48 -0
  32. data/lib/ruby_llm/model/pricing_tier.rb +34 -0
  33. data/lib/ruby_llm/model.rb +7 -0
  34. data/lib/ruby_llm/models.json +29924 -0
  35. data/lib/ruby_llm/models.rb +214 -0
  36. data/lib/ruby_llm/models_schema.json +168 -0
  37. data/lib/ruby_llm/provider.rb +221 -0
  38. data/lib/ruby_llm/providers/anthropic/capabilities.rb +179 -0
  39. data/lib/ruby_llm/providers/anthropic/chat.rb +120 -0
  40. data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
  41. data/lib/ruby_llm/providers/anthropic/media.rb +116 -0
  42. data/lib/ruby_llm/providers/anthropic/models.rb +56 -0
  43. data/lib/ruby_llm/providers/anthropic/streaming.rb +45 -0
  44. data/lib/ruby_llm/providers/anthropic/tools.rb +108 -0
  45. data/lib/ruby_llm/providers/anthropic.rb +37 -0
  46. data/lib/ruby_llm/providers/bedrock/capabilities.rb +167 -0
  47. data/lib/ruby_llm/providers/bedrock/chat.rb +76 -0
  48. data/lib/ruby_llm/providers/bedrock/media.rb +73 -0
  49. data/lib/ruby_llm/providers/bedrock/models.rb +82 -0
  50. data/lib/ruby_llm/providers/bedrock/signing.rb +831 -0
  51. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +63 -0
  52. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +71 -0
  53. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +79 -0
  54. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +92 -0
  55. data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +91 -0
  56. data/lib/ruby_llm/providers/bedrock/streaming.rb +36 -0
  57. data/lib/ruby_llm/providers/bedrock.rb +83 -0
  58. data/lib/ruby_llm/providers/deepseek/capabilities.rb +131 -0
  59. data/lib/ruby_llm/providers/deepseek/chat.rb +17 -0
  60. data/lib/ruby_llm/providers/deepseek.rb +30 -0
  61. data/lib/ruby_llm/providers/gemini/capabilities.rb +351 -0
  62. data/lib/ruby_llm/providers/gemini/chat.rb +146 -0
  63. data/lib/ruby_llm/providers/gemini/embeddings.rb +39 -0
  64. data/lib/ruby_llm/providers/gemini/images.rb +48 -0
  65. data/lib/ruby_llm/providers/gemini/media.rb +55 -0
  66. data/lib/ruby_llm/providers/gemini/models.rb +41 -0
  67. data/lib/ruby_llm/providers/gemini/streaming.rb +66 -0
  68. data/lib/ruby_llm/providers/gemini/tools.rb +82 -0
  69. data/lib/ruby_llm/providers/gemini.rb +36 -0
  70. data/lib/ruby_llm/providers/gpustack/chat.rb +17 -0
  71. data/lib/ruby_llm/providers/gpustack/models.rb +55 -0
  72. data/lib/ruby_llm/providers/gpustack.rb +33 -0
  73. data/lib/ruby_llm/providers/mistral/capabilities.rb +163 -0
  74. data/lib/ruby_llm/providers/mistral/chat.rb +26 -0
  75. data/lib/ruby_llm/providers/mistral/embeddings.rb +36 -0
  76. data/lib/ruby_llm/providers/mistral/models.rb +49 -0
  77. data/lib/ruby_llm/providers/mistral.rb +32 -0
  78. data/lib/ruby_llm/providers/ollama/chat.rb +28 -0
  79. data/lib/ruby_llm/providers/ollama/media.rb +50 -0
  80. data/lib/ruby_llm/providers/ollama.rb +29 -0
  81. data/lib/ruby_llm/providers/openai/capabilities.rb +306 -0
  82. data/lib/ruby_llm/providers/openai/chat.rb +87 -0
  83. data/lib/ruby_llm/providers/openai/embeddings.rb +36 -0
  84. data/lib/ruby_llm/providers/openai/images.rb +38 -0
  85. data/lib/ruby_llm/providers/openai/media.rb +81 -0
  86. data/lib/ruby_llm/providers/openai/models.rb +39 -0
  87. data/lib/ruby_llm/providers/openai/response.rb +116 -0
  88. data/lib/ruby_llm/providers/openai/response_media.rb +76 -0
  89. data/lib/ruby_llm/providers/openai/streaming.rb +191 -0
  90. data/lib/ruby_llm/providers/openai/tools.rb +100 -0
  91. data/lib/ruby_llm/providers/openai.rb +44 -0
  92. data/lib/ruby_llm/providers/openai_base.rb +44 -0
  93. data/lib/ruby_llm/providers/openrouter/models.rb +88 -0
  94. data/lib/ruby_llm/providers/openrouter.rb +26 -0
  95. data/lib/ruby_llm/providers/perplexity/capabilities.rb +138 -0
  96. data/lib/ruby_llm/providers/perplexity/chat.rb +17 -0
  97. data/lib/ruby_llm/providers/perplexity/models.rb +42 -0
  98. data/lib/ruby_llm/providers/perplexity.rb +52 -0
  99. data/lib/ruby_llm/railtie.rb +17 -0
  100. data/lib/ruby_llm/stream_accumulator.rb +103 -0
  101. data/lib/ruby_llm/streaming.rb +162 -0
  102. data/lib/ruby_llm/tool.rb +100 -0
  103. data/lib/ruby_llm/tool_call.rb +31 -0
  104. data/lib/ruby_llm/utils.rb +49 -0
  105. data/lib/ruby_llm/version.rb +5 -0
  106. data/lib/ruby_llm.rb +98 -0
  107. data/lib/tasks/aliases.rake +235 -0
  108. data/lib/tasks/models_docs.rake +224 -0
  109. data/lib/tasks/models_update.rake +108 -0
  110. data/lib/tasks/release.rake +32 -0
  111. data/lib/tasks/vcr.rake +99 -0
  112. metadata +128 -7
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # Custom error class that wraps API errors from different providers
5
+ # into a consistent format with helpful error messages.
6
+ #
7
+ # Example:
8
+ # begin
9
+ # chat.ask "What's 2+2?"
10
+ # rescue RubyLLM::Error => e
11
+ # puts "Couldn't chat with AI: #{e.message}"
12
+ # end
13
+ class Error < StandardError
14
+ attr_reader :response
15
+
16
+ def initialize(response = nil, message = nil)
17
+ @response = response
18
+ super(message || response&.body)
19
+ end
20
+ end
21
+
22
+ # Error classes for non-HTTP errors
23
+ class ConfigurationError < StandardError; end
24
+ class InvalidRoleError < StandardError; end
25
+ class ModelNotFoundError < StandardError; end
26
+ class UnsupportedAttachmentError < StandardError; end
27
+
28
+ # Error classes for different HTTP status codes
29
+ class BadRequestError < Error; end
30
+ class ForbiddenError < Error; end
31
+ class OverloadedError < Error; end
32
+ class PaymentRequiredError < Error; end
33
+ class RateLimitError < Error; end
34
+ class ServerError < Error; end
35
+ class ServiceUnavailableError < Error; end
36
+ class UnauthorizedError < Error; end
37
+
38
+ # Faraday middleware that maps provider-specific API errors to RubyLLM errors.
39
+ # Uses provider's parse_error method to extract meaningful error messages.
40
+ class ErrorMiddleware < Faraday::Middleware
41
+ def initialize(app, options = {})
42
+ super(app)
43
+ @provider = options[:provider]
44
+ end
45
+
46
+ def call(env)
47
+ @app.call(env).on_complete do |response|
48
+ self.class.parse_error(provider: @provider, response: response)
49
+ end
50
+ end
51
+
52
+ class << self
53
+ def parse_error(provider:, response:) # rubocop:disable Metrics/PerceivedComplexity
54
+ message = provider&.parse_error(response)
55
+
56
+ case response.status
57
+ when 200..399
58
+ message
59
+ when 400
60
+ raise BadRequestError.new(response, message || 'Invalid request - please check your input')
61
+ when 401
62
+ raise UnauthorizedError.new(response, message || 'Invalid API key - check your credentials')
63
+ when 402
64
+ raise PaymentRequiredError.new(response, message || 'Payment required - please top up your account')
65
+ when 403
66
+ raise ForbiddenError.new(response,
67
+ message || 'Forbidden - you do not have permission to access this resource')
68
+ when 429
69
+ raise RateLimitError.new(response, message || 'Rate limit exceeded - please wait a moment')
70
+ when 500
71
+ raise ServerError.new(response, message || 'API server error - please try again')
72
+ when 502..503
73
+ raise ServiceUnavailableError.new(response, message || 'API server unavailable - please try again later')
74
+ when 529
75
+ raise OverloadedError.new(response, message || 'Service overloaded - please try again later')
76
+ else
77
+ raise Error.new(response, message || 'An unknown error occurred')
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ Faraday::Middleware.register_middleware(llm_errors: RubyLLM::ErrorMiddleware)
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # Represents a generated image from an AI model.
5
+ # Provides an interface to image generation capabilities
6
+ # from providers like DALL-E and Gemini's Imagen.
7
+ class Image
8
+ attr_reader :url, :data, :mime_type, :revised_prompt, :model_id
9
+
10
+ def initialize(url: nil, data: nil, mime_type: nil, revised_prompt: nil, model_id: nil)
11
+ @url = url
12
+ @data = data
13
+ @mime_type = mime_type
14
+ @revised_prompt = revised_prompt
15
+ @model_id = model_id
16
+ end
17
+
18
+ def base64?
19
+ !@data.nil?
20
+ end
21
+
22
+ # Returns the raw binary image data regardless of source
23
+ def to_blob
24
+ if base64?
25
+ Base64.decode64 @data
26
+ else
27
+ response = Connection.basic.get @url
28
+ response.body
29
+ end
30
+ end
31
+
32
+ # Saves the image to a file path
33
+ def save(path)
34
+ File.binwrite(File.expand_path(path), to_blob)
35
+ path
36
+ end
37
+
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_instance = Models.resolve(model, provider: provider, assume_exists: assume_model_exists,
47
+ config: config)
48
+ model_id = model.id
49
+
50
+ provider_instance.paint(prompt, model: model_id, size:)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # A single message in a chat conversation. Can represent user input,
5
+ # AI responses, or tool interactions. Tracks token usage and handles
6
+ # the complexities of tool calls and responses.
7
+ class Message
8
+ ROLES = %i[system user assistant tool].freeze
9
+
10
+ attr_reader :role, :tool_calls, :tool_call_id, :input_tokens, :output_tokens, :model_id, :raw,
11
+ :cached_tokens, :cache_creation_tokens
12
+ attr_writer :content
13
+
14
+ def initialize(options = {})
15
+ @role = options.fetch(:role).to_sym
16
+ @content = normalize_content(options.fetch(:content))
17
+ @tool_calls = options[:tool_calls]
18
+ @input_tokens = options[:input_tokens]
19
+ @output_tokens = options[:output_tokens]
20
+ @model_id = options[:model_id]
21
+ @tool_call_id = options[:tool_call_id]
22
+ @cached_tokens = options[:cached_tokens]
23
+ @cache_creation_tokens = options[:cache_creation_tokens]
24
+ @raw = options[:raw]
25
+
26
+ ensure_valid_role
27
+ end
28
+
29
+ def content
30
+ if @content.is_a?(Content) && @content.text && @content.attachments.empty?
31
+ @content.text
32
+ else
33
+ @content
34
+ end
35
+ end
36
+
37
+ def tool_call?
38
+ !tool_calls.nil? && !tool_calls.empty?
39
+ end
40
+
41
+ def tool_result?
42
+ !tool_call_id.nil? && !tool_call_id.empty?
43
+ end
44
+
45
+ def tool_results
46
+ content if tool_result?
47
+ end
48
+
49
+ def to_h
50
+ {
51
+ role: role,
52
+ content: content,
53
+ tool_calls: tool_calls,
54
+ tool_call_id: tool_call_id,
55
+ input_tokens: input_tokens,
56
+ output_tokens: output_tokens,
57
+ model_id: model_id,
58
+ cache_creation_tokens: cache_creation_tokens,
59
+ cached_tokens: cached_tokens
60
+ }.compact
61
+ end
62
+
63
+ def instance_variables
64
+ super - [:@raw]
65
+ end
66
+
67
+ private
68
+
69
+ def normalize_content(content)
70
+ case content
71
+ when String then Content.new(content)
72
+ when Hash then Content.new(content[:text], content)
73
+ else content # Pass through nil, Content, or other types
74
+ end
75
+ end
76
+
77
+ def ensure_valid_role
78
+ raise InvalidRoleError, "Expected role to be one of: #{ROLES.join(', ')}" unless ROLES.include?(role)
79
+ end
80
+ end
81
+ end
@@ -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 = Utils.to_time(data[:created_at])
24
+ @context_window = data[:context_window]
25
+ @max_output_tokens = data[:max_output_tokens]
26
+ @knowledge_cutoff = Utils.to_date(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