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.
- checksums.yaml +4 -4
- data/README.md +80 -133
- data/lib/ruby_llm/active_record/acts_as.rb +144 -47
- data/lib/ruby_llm/aliases.json +187 -17
- data/lib/ruby_llm/attachment.rb +164 -0
- data/lib/ruby_llm/chat.rb +31 -20
- data/lib/ruby_llm/configuration.rb +34 -1
- data/lib/ruby_llm/connection.rb +121 -0
- data/lib/ruby_llm/content.rb +27 -79
- data/lib/ruby_llm/context.rb +30 -0
- data/lib/ruby_llm/embedding.rb +13 -5
- data/lib/ruby_llm/error.rb +2 -1
- data/lib/ruby_llm/image.rb +15 -8
- data/lib/ruby_llm/message.rb +14 -6
- data/lib/ruby_llm/mime_type.rb +67 -0
- data/lib/ruby_llm/model/info.rb +101 -0
- data/lib/ruby_llm/model/modalities.rb +22 -0
- data/lib/ruby_llm/model/pricing.rb +51 -0
- data/lib/ruby_llm/model/pricing_category.rb +48 -0
- data/lib/ruby_llm/model/pricing_tier.rb +34 -0
- data/lib/ruby_llm/model.rb +7 -0
- data/lib/ruby_llm/models.json +26279 -2362
- data/lib/ruby_llm/models.rb +95 -14
- data/lib/ruby_llm/provider.rb +48 -90
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +76 -13
- data/lib/ruby_llm/providers/anthropic/chat.rb +7 -14
- data/lib/ruby_llm/providers/anthropic/media.rb +49 -28
- data/lib/ruby_llm/providers/anthropic/models.rb +16 -16
- data/lib/ruby_llm/providers/anthropic/tools.rb +2 -2
- data/lib/ruby_llm/providers/anthropic.rb +3 -3
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +61 -2
- data/lib/ruby_llm/providers/bedrock/chat.rb +30 -73
- data/lib/ruby_llm/providers/bedrock/media.rb +59 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +50 -58
- data/lib/ruby_llm/providers/bedrock/streaming/base.rb +16 -0
- data/lib/ruby_llm/providers/bedrock.rb +14 -25
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +35 -2
- data/lib/ruby_llm/providers/deepseek.rb +3 -3
- data/lib/ruby_llm/providers/gemini/capabilities.rb +84 -3
- data/lib/ruby_llm/providers/gemini/chat.rb +8 -37
- data/lib/ruby_llm/providers/gemini/embeddings.rb +18 -34
- data/lib/ruby_llm/providers/gemini/images.rb +4 -3
- data/lib/ruby_llm/providers/gemini/media.rb +28 -111
- data/lib/ruby_llm/providers/gemini/models.rb +17 -23
- data/lib/ruby_llm/providers/gemini/tools.rb +1 -1
- data/lib/ruby_llm/providers/gemini.rb +3 -3
- data/lib/ruby_llm/providers/ollama/chat.rb +28 -0
- data/lib/ruby_llm/providers/ollama/media.rb +48 -0
- data/lib/ruby_llm/providers/ollama.rb +34 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +78 -3
- data/lib/ruby_llm/providers/openai/chat.rb +6 -4
- data/lib/ruby_llm/providers/openai/embeddings.rb +8 -12
- data/lib/ruby_llm/providers/openai/images.rb +3 -2
- data/lib/ruby_llm/providers/openai/media.rb +48 -21
- data/lib/ruby_llm/providers/openai/models.rb +17 -18
- data/lib/ruby_llm/providers/openai/tools.rb +9 -5
- data/lib/ruby_llm/providers/openai.rb +7 -5
- data/lib/ruby_llm/providers/openrouter/models.rb +88 -0
- data/lib/ruby_llm/providers/openrouter.rb +31 -0
- data/lib/ruby_llm/stream_accumulator.rb +4 -4
- data/lib/ruby_llm/streaming.rb +48 -13
- data/lib/ruby_llm/utils.rb +27 -0
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +15 -5
- data/lib/tasks/aliases.rake +235 -0
- data/lib/tasks/models_docs.rake +164 -121
- data/lib/tasks/models_update.rake +79 -0
- data/lib/tasks/release.rake +32 -0
- data/lib/tasks/vcr.rake +4 -2
- metadata +56 -32
- data/lib/ruby_llm/model_info.rb +0 -56
- data/lib/tasks/browser_helper.rb +0 -97
- data/lib/tasks/capability_generator.rb +0 -123
- data/lib/tasks/capability_scraper.rb +0 -224
- data/lib/tasks/cli_helper.rb +0 -22
- data/lib/tasks/code_validator.rb +0 -29
- data/lib/tasks/model_updater.rb +0 -66
- data/lib/tasks/models.rake +0 -43
data/lib/ruby_llm/content.rb
CHANGED
@@ -2,103 +2,51 @@
|
|
2
2
|
|
3
3
|
module RubyLLM
|
4
4
|
# Represents the content sent to or received from an LLM.
|
5
|
-
#
|
6
|
-
# handle their own formatting needs.
|
5
|
+
# Selects the appropriate attachment class based on the content type.
|
7
6
|
class Content
|
8
|
-
|
9
|
-
@parts = []
|
10
|
-
@parts << { type: 'text', text: text } unless text.nil? || text.empty?
|
7
|
+
attr_reader :text, :attachments
|
11
8
|
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
21
|
-
|
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
|
26
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
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
|
-
|
84
|
-
|
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
|
91
|
-
if
|
92
|
-
|
93
|
-
|
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
|
-
|
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
|
data/lib/ruby_llm/embedding.rb
CHANGED
@@ -12,12 +12,20 @@ module RubyLLM
|
|
12
12
|
@input_tokens = input_tokens
|
13
13
|
end
|
14
14
|
|
15
|
-
def self.embed(text,
|
16
|
-
|
17
|
-
|
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
|
-
|
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
|
data/lib/ruby_llm/error.rb
CHANGED
@@ -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/
|
54
|
+
def parse_error(provider:, response:) # rubocop:disable Metrics/PerceivedComplexity
|
54
55
|
message = provider&.parse_error(response)
|
55
56
|
|
56
57
|
case response.status
|
data/lib/ruby_llm/image.rb
CHANGED
@@ -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
|
25
|
+
Base64.decode64 @data
|
26
26
|
else
|
27
|
-
|
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,
|
40
|
-
|
41
|
-
|
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
|
-
|
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
|
data/lib/ruby_llm/message.rb
CHANGED
@@ -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, :
|
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
|
14
|
-
@content = normalize_content(options
|
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
|
53
|
-
when
|
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
|