ruby_llm 1.15.0 → 1.16.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 +5 -4
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +1 -1
- data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +3 -3
- data/lib/ruby_llm/active_record/acts_as.rb +1 -26
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +71 -4
- data/lib/ruby_llm/active_record/chat_methods.rb +2 -2
- data/lib/ruby_llm/active_record/message_methods.rb +70 -3
- data/lib/ruby_llm/agent.rb +1 -0
- data/lib/ruby_llm/aliases.json +78 -75
- data/lib/ruby_llm/aliases.rb +3 -0
- data/lib/ruby_llm/attachment.rb +34 -17
- data/lib/ruby_llm/chat.rb +176 -47
- data/lib/ruby_llm/configuration.rb +14 -1
- data/lib/ruby_llm/connection.rb +36 -7
- data/lib/ruby_llm/content.rb +15 -1
- data/lib/ruby_llm/deprecator.rb +24 -0
- data/lib/ruby_llm/embedding.rb +31 -1
- data/lib/ruby_llm/error.rb +11 -75
- data/lib/ruby_llm/error_middleware.rb +81 -0
- data/lib/ruby_llm/image.rb +2 -0
- data/lib/ruby_llm/instrumentation.rb +36 -0
- data/lib/ruby_llm/mime_type.rb +25 -0
- data/lib/ruby_llm/model/info.rb +36 -2
- data/lib/ruby_llm/model/pricing.rb +19 -9
- data/lib/ruby_llm/model/pricing_tier.rb +20 -9
- data/lib/ruby_llm/model_registry.rb +39 -0
- data/lib/ruby_llm/models.json +18225 -19144
- data/lib/ruby_llm/models.rb +95 -30
- data/lib/ruby_llm/provider.rb +11 -2
- data/lib/ruby_llm/providers/anthropic/chat.rb +49 -15
- data/lib/ruby_llm/providers/anthropic/models.rb +2 -0
- data/lib/ruby_llm/providers/anthropic/streaming.rb +2 -0
- data/lib/ruby_llm/providers/anthropic/tools.rb +28 -2
- data/lib/ruby_llm/providers/azure/media.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/auth.rb +1 -0
- data/lib/ruby_llm/providers/bedrock/chat.rb +2 -0
- data/lib/ruby_llm/providers/bedrock/media.rb +21 -3
- data/lib/ruby_llm/providers/bedrock/models.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/streaming.rb +6 -0
- data/lib/ruby_llm/providers/bedrock.rb +2 -2
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +43 -0
- data/lib/ruby_llm/providers/deepseek/chat.rb +9 -0
- data/lib/ruby_llm/providers/gemini/chat.rb +2 -3
- data/lib/ruby_llm/providers/gemini/media.rb +16 -9
- data/lib/ruby_llm/providers/gemini/streaming.rb +2 -0
- data/lib/ruby_llm/providers/gemini/tools.rb +2 -0
- data/lib/ruby_llm/providers/gpustack/chat.rb +8 -1
- data/lib/ruby_llm/providers/gpustack/models.rb +2 -0
- data/lib/ruby_llm/providers/mistral/capabilities.rb +1 -1
- data/lib/ruby_llm/providers/mistral/chat.rb +1 -1
- data/lib/ruby_llm/providers/mistral/media.rb +55 -0
- data/lib/ruby_llm/providers/mistral/models.rb +2 -0
- data/lib/ruby_llm/providers/mistral.rb +2 -2
- data/lib/ruby_llm/providers/ollama/chat.rb +8 -1
- data/lib/ruby_llm/providers/openai/chat.rb +16 -1
- data/lib/ruby_llm/providers/openai/images.rb +9 -9
- data/lib/ruby_llm/providers/openai/media.rb +40 -16
- data/lib/ruby_llm/providers/openai/streaming.rb +2 -0
- data/lib/ruby_llm/providers/openai/tools.rb +2 -0
- data/lib/ruby_llm/providers/openai/transcription.rb +1 -0
- data/lib/ruby_llm/providers/openrouter/chat.rb +6 -2
- data/lib/ruby_llm/providers/perplexity/chat.rb +11 -0
- data/lib/ruby_llm/providers/perplexity/media.rb +62 -0
- data/lib/ruby_llm/providers/perplexity.rb +2 -2
- data/lib/ruby_llm/providers/vertexai.rb +5 -1
- data/lib/ruby_llm/providers/xai/chat.rb +9 -0
- data/lib/ruby_llm/providers/xai/models.rb +15 -27
- data/lib/ruby_llm/providers/xai.rb +2 -2
- data/lib/ruby_llm/railtie.rb +5 -1
- data/lib/ruby_llm/stream_accumulator.rb +45 -30
- data/lib/ruby_llm/streaming.rb +4 -0
- data/lib/ruby_llm/tool_concurrency.rb +105 -0
- data/lib/ruby_llm/transcription.rb +2 -1
- data/lib/ruby_llm/utils.rb +39 -0
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +9 -2
- data/lib/tasks/models.rake +32 -4
- data/lib/tasks/release.rake +50 -23
- metadata +17 -10
data/lib/ruby_llm/error.rb
CHANGED
|
@@ -23,7 +23,17 @@ module RubyLLM
|
|
|
23
23
|
class InvalidRoleError < StandardError; end
|
|
24
24
|
class InvalidToolChoiceError < StandardError; end
|
|
25
25
|
class ModelNotFoundError < StandardError; end
|
|
26
|
-
|
|
26
|
+
|
|
27
|
+
# Raised when RubyLLM cannot format an attachment for the selected provider.
|
|
28
|
+
class UnsupportedAttachmentError < StandardError
|
|
29
|
+
GUIDANCE = 'Consider using a model that supports this attachment type.'
|
|
30
|
+
|
|
31
|
+
def initialize(type = nil)
|
|
32
|
+
message = 'Unsupported attachment type'
|
|
33
|
+
message = "#{message}: #{type}" if type
|
|
34
|
+
super("#{message}. #{GUIDANCE}")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
27
37
|
|
|
28
38
|
# Error classes for different HTTP status codes
|
|
29
39
|
class BadRequestError < Error; end
|
|
@@ -35,78 +45,4 @@ module RubyLLM
|
|
|
35
45
|
class ServerError < Error; end
|
|
36
46
|
class ServiceUnavailableError < Error; end
|
|
37
47
|
class UnauthorizedError < Error; end
|
|
38
|
-
|
|
39
|
-
# Faraday middleware that maps provider-specific API errors to RubyLLM errors.
|
|
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
|
-
CONTEXT_LENGTH_PATTERNS = [
|
|
54
|
-
/context length/i,
|
|
55
|
-
/context window/i,
|
|
56
|
-
/maximum context/i,
|
|
57
|
-
/request too large/i,
|
|
58
|
-
/too many tokens/i,
|
|
59
|
-
/token count exceeds/i,
|
|
60
|
-
/input[_\s-]?token/i,
|
|
61
|
-
/input or output tokens? must be reduced/i,
|
|
62
|
-
/reduce the length of messages/i
|
|
63
|
-
].freeze
|
|
64
|
-
|
|
65
|
-
def parse_error(provider:, response:) # rubocop:disable Metrics/PerceivedComplexity
|
|
66
|
-
message = provider&.parse_error(response)
|
|
67
|
-
|
|
68
|
-
case response.status
|
|
69
|
-
when 200..399
|
|
70
|
-
message
|
|
71
|
-
when 400
|
|
72
|
-
if context_length_exceeded?(message)
|
|
73
|
-
raise ContextLengthExceededError.new(response, message || 'Context length exceeded')
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
raise BadRequestError.new(response, message || 'Invalid request - please check your input')
|
|
77
|
-
when 401
|
|
78
|
-
raise UnauthorizedError.new(response, message || 'Invalid API key - check your credentials')
|
|
79
|
-
when 402
|
|
80
|
-
raise PaymentRequiredError.new(response, message || 'Payment required - please top up your account')
|
|
81
|
-
when 403
|
|
82
|
-
raise ForbiddenError.new(response,
|
|
83
|
-
message || 'Forbidden - you do not have permission to access this resource')
|
|
84
|
-
when 429
|
|
85
|
-
if context_length_exceeded?(message)
|
|
86
|
-
raise ContextLengthExceededError.new(response, message || 'Context length exceeded')
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
raise RateLimitError.new(response, message || 'Rate limit exceeded - please wait a moment')
|
|
90
|
-
when 500
|
|
91
|
-
raise ServerError.new(response, message || 'API server error - please try again')
|
|
92
|
-
when 502..504
|
|
93
|
-
raise ServiceUnavailableError.new(response, message || 'API server unavailable - please try again later')
|
|
94
|
-
when 529
|
|
95
|
-
raise OverloadedError.new(response, message || 'Service overloaded - please try again later')
|
|
96
|
-
else
|
|
97
|
-
raise Error.new(response, message || 'An unknown error occurred')
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
private
|
|
102
|
-
|
|
103
|
-
def context_length_exceeded?(message)
|
|
104
|
-
return false if message.to_s.empty?
|
|
105
|
-
|
|
106
|
-
CONTEXT_LENGTH_PATTERNS.any? { |pattern| message.match?(pattern) }
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
48
|
end
|
|
111
|
-
|
|
112
|
-
Faraday::Middleware.register_middleware(llm_errors: RubyLLM::ErrorMiddleware)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
require 'ruby_llm/error'
|
|
5
|
+
|
|
6
|
+
module RubyLLM
|
|
7
|
+
# Faraday middleware that maps provider-specific API errors to RubyLLM errors.
|
|
8
|
+
class ErrorMiddleware < Faraday::Middleware
|
|
9
|
+
def initialize(app, options = {})
|
|
10
|
+
super(app)
|
|
11
|
+
@provider = options[:provider]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(env)
|
|
15
|
+
@app.call(env).on_complete do |response|
|
|
16
|
+
self.class.parse_error(provider: @provider, response: response)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
CONTEXT_LENGTH_PATTERNS = [
|
|
22
|
+
/context length/i,
|
|
23
|
+
/context window/i,
|
|
24
|
+
/maximum context/i,
|
|
25
|
+
/request too large/i,
|
|
26
|
+
/too many tokens/i,
|
|
27
|
+
/token count exceeds/i,
|
|
28
|
+
/input[_\s-]?token/i,
|
|
29
|
+
/input or output tokens? must be reduced/i,
|
|
30
|
+
/reduce the length of messages/i,
|
|
31
|
+
/prompt is too long/i
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
def parse_error(provider:, response:) # rubocop:disable Metrics/PerceivedComplexity
|
|
35
|
+
message = provider&.parse_error(response)
|
|
36
|
+
|
|
37
|
+
case response.status
|
|
38
|
+
when 200..399
|
|
39
|
+
message
|
|
40
|
+
when 400
|
|
41
|
+
if context_length_exceeded?(message)
|
|
42
|
+
raise ContextLengthExceededError.new(response, message || 'Context length exceeded')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
raise BadRequestError.new(response, message || 'Invalid request - please check your input')
|
|
46
|
+
when 401
|
|
47
|
+
raise UnauthorizedError.new(response, message || 'Invalid API key - check your credentials')
|
|
48
|
+
when 402
|
|
49
|
+
raise PaymentRequiredError.new(response, message || 'Payment required - please top up your account')
|
|
50
|
+
when 403
|
|
51
|
+
raise ForbiddenError.new(response,
|
|
52
|
+
message || 'Forbidden - you do not have permission to access this resource')
|
|
53
|
+
when 429
|
|
54
|
+
if context_length_exceeded?(message)
|
|
55
|
+
raise ContextLengthExceededError.new(response, message || 'Context length exceeded')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
raise RateLimitError.new(response, message || 'Rate limit exceeded - please wait a moment')
|
|
59
|
+
when 500
|
|
60
|
+
raise ServerError.new(response, message || 'API server error - please try again')
|
|
61
|
+
when 502..504
|
|
62
|
+
raise ServiceUnavailableError.new(response, message || 'API server unavailable - please try again later')
|
|
63
|
+
when 529
|
|
64
|
+
raise OverloadedError.new(response, message || 'Service overloaded - please try again later')
|
|
65
|
+
else
|
|
66
|
+
raise Error.new(response, message || 'An unknown error occurred')
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def context_length_exceeded?(message)
|
|
73
|
+
return false if message.to_s.empty?
|
|
74
|
+
|
|
75
|
+
CONTEXT_LENGTH_PATTERNS.any? { |pattern| message.match?(pattern) }
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
Faraday::Middleware.register_middleware(llm_errors: RubyLLM::ErrorMiddleware)
|
data/lib/ruby_llm/image.rb
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# Emits structured RubyLLM events without requiring a specific observability
|
|
5
|
+
# backend. Rails apps can use ActiveSupport::Notifications as the instrumenter.
|
|
6
|
+
module Instrumentation
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def instrument(name, payload = nil, config: nil, **attributes)
|
|
10
|
+
payload = build_payload(payload, attributes)
|
|
11
|
+
instrumenter = instrumenter_for(config)
|
|
12
|
+
return yield(payload) if block_given? && !instrumenter
|
|
13
|
+
|
|
14
|
+
unless instrumenter.respond_to?(:instrument)
|
|
15
|
+
return yield(payload) if block_given?
|
|
16
|
+
|
|
17
|
+
return
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
if block_given?
|
|
21
|
+
instrumenter.instrument(name, payload) { yield(payload) }
|
|
22
|
+
else
|
|
23
|
+
instrumenter.instrument(name, payload)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def build_payload(payload, attributes)
|
|
28
|
+
payload ||= {}
|
|
29
|
+
attributes.empty? ? payload : payload.merge(attributes)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def instrumenter_for(config)
|
|
33
|
+
(config || RubyLLM.config).instrumenter
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/ruby_llm/mime_type.rb
CHANGED
|
@@ -27,6 +27,13 @@ module RubyLLM
|
|
|
27
27
|
type == 'application/pdf'
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
def document?(type)
|
|
31
|
+
return false if pdf?(type) || text?(type)
|
|
32
|
+
|
|
33
|
+
DOCUMENT_MIME_TYPES.include?(type) ||
|
|
34
|
+
DOCUMENT_MIME_PREFIXES.any? { |prefix| type.start_with?(prefix) }
|
|
35
|
+
end
|
|
36
|
+
|
|
30
37
|
def text?(type)
|
|
31
38
|
type.start_with?('text/') ||
|
|
32
39
|
TEXT_SUFFIXES.any? { |suffix| type.end_with?(suffix) } ||
|
|
@@ -67,5 +74,23 @@ module RubyLLM
|
|
|
67
74
|
'application/yaml', # Standard for YAML
|
|
68
75
|
'application/toml' # TOML configuration files
|
|
69
76
|
].freeze
|
|
77
|
+
|
|
78
|
+
DOCUMENT_MIME_TYPES = [
|
|
79
|
+
'application/msword',
|
|
80
|
+
'application/rtf',
|
|
81
|
+
'application/vnd.apple.keynote',
|
|
82
|
+
'application/vnd.apple.numbers',
|
|
83
|
+
'application/vnd.apple.pages',
|
|
84
|
+
'application/vnd.google-apps.document',
|
|
85
|
+
'application/vnd.google-apps.presentation',
|
|
86
|
+
'application/vnd.google-apps.spreadsheet',
|
|
87
|
+
'application/vnd.ms-excel',
|
|
88
|
+
'application/vnd.ms-powerpoint'
|
|
89
|
+
].freeze
|
|
90
|
+
|
|
91
|
+
DOCUMENT_MIME_PREFIXES = [
|
|
92
|
+
'application/vnd.openxmlformats-officedocument.',
|
|
93
|
+
'application/vnd.oasis.opendocument.'
|
|
94
|
+
].freeze
|
|
70
95
|
end
|
|
71
96
|
end
|
data/lib/ruby_llm/model/info.rb
CHANGED
|
@@ -5,7 +5,7 @@ module RubyLLM
|
|
|
5
5
|
# Information about an AI model's capabilities, pricing, and metadata.
|
|
6
6
|
class Info
|
|
7
7
|
attr_reader :id, :name, :provider, :family, :created_at, :context_window, :max_output_tokens, :knowledge_cutoff,
|
|
8
|
-
:modalities, :capabilities, :pricing, :metadata
|
|
8
|
+
:modalities, :capabilities, :pricing, :metadata, :reasoning_options
|
|
9
9
|
|
|
10
10
|
# Create a default model with assumed capabilities
|
|
11
11
|
def self.default(model_id, provider)
|
|
@@ -31,7 +31,9 @@ module RubyLLM
|
|
|
31
31
|
@modalities = Modalities.new(data[:modalities] || {})
|
|
32
32
|
@capabilities = data[:capabilities] || []
|
|
33
33
|
@pricing = Pricing.new(data[:pricing] || {})
|
|
34
|
-
@metadata = data[:metadata] || {}
|
|
34
|
+
@metadata = data[:metadata]&.dup || {}
|
|
35
|
+
@reasoning_options = normalize_reasoning_options(reasoning_options_from(data))
|
|
36
|
+
store_reasoning_options_metadata
|
|
35
37
|
end
|
|
36
38
|
|
|
37
39
|
def supports?(capability)
|
|
@@ -61,6 +63,14 @@ module RubyLLM
|
|
|
61
63
|
modalities.input.include?('image')
|
|
62
64
|
end
|
|
63
65
|
|
|
66
|
+
def reasoning_option(type)
|
|
67
|
+
reasoning_options.find { |option| option[:type] == type.to_s }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def reasoning_option_values(type)
|
|
71
|
+
Array(reasoning_option(type)&.fetch(:values, nil))
|
|
72
|
+
end
|
|
73
|
+
|
|
64
74
|
def supports_video?
|
|
65
75
|
modalities.input.include?('video')
|
|
66
76
|
end
|
|
@@ -125,6 +135,30 @@ module RubyLLM
|
|
|
125
135
|
metadata: metadata
|
|
126
136
|
}
|
|
127
137
|
end
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
def reasoning_options_from(data)
|
|
142
|
+
data[:reasoning_options] || metadata[:reasoning_options] || metadata['reasoning_options']
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def store_reasoning_options_metadata
|
|
146
|
+
return unless reasoning_options.any?
|
|
147
|
+
|
|
148
|
+
metadata.delete('reasoning_options')
|
|
149
|
+
metadata[:reasoning_options] = reasoning_options
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def normalize_reasoning_options(options)
|
|
153
|
+
Array(options).filter_map do |option|
|
|
154
|
+
next unless option.is_a?(Hash)
|
|
155
|
+
|
|
156
|
+
normalized = option.to_h.transform_keys(&:to_sym)
|
|
157
|
+
normalized[:type] = normalized[:type].to_s if normalized[:type]
|
|
158
|
+
normalized[:values] = Array(normalized[:values]).map(&:to_s) if normalized.key?(:values)
|
|
159
|
+
normalized
|
|
160
|
+
end
|
|
161
|
+
end
|
|
128
162
|
end
|
|
129
163
|
end
|
|
130
164
|
end
|
|
@@ -4,24 +4,30 @@ module RubyLLM
|
|
|
4
4
|
module Model
|
|
5
5
|
# A collection that manages and provides access to different categories of pricing information
|
|
6
6
|
class Pricing
|
|
7
|
+
CATEGORIES = %i[text_tokens images audio_tokens embeddings].freeze
|
|
8
|
+
|
|
7
9
|
def initialize(data)
|
|
8
10
|
@data = {}
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
CATEGORIES.each do |category|
|
|
11
13
|
@data[category] = PricingCategory.new(data[category]) if data[category] && !empty_pricing?(data[category])
|
|
12
14
|
end
|
|
13
15
|
end
|
|
14
16
|
|
|
15
|
-
def
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
def text_tokens
|
|
18
|
+
category(:text_tokens)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def images
|
|
22
|
+
category(:images)
|
|
21
23
|
end
|
|
22
24
|
|
|
23
|
-
def
|
|
24
|
-
|
|
25
|
+
def audio_tokens
|
|
26
|
+
category(:audio_tokens)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def embeddings
|
|
30
|
+
category(:embeddings)
|
|
25
31
|
end
|
|
26
32
|
|
|
27
33
|
def to_h
|
|
@@ -30,6 +36,10 @@ module RubyLLM
|
|
|
30
36
|
|
|
31
37
|
private
|
|
32
38
|
|
|
39
|
+
def category(name)
|
|
40
|
+
@data[name] || PricingCategory.new
|
|
41
|
+
end
|
|
42
|
+
|
|
33
43
|
def empty_pricing?(data)
|
|
34
44
|
return true unless data
|
|
35
45
|
|
|
@@ -2,8 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module RubyLLM
|
|
4
4
|
module Model
|
|
5
|
-
#
|
|
5
|
+
# Stores non-zero pricing values for a single pricing tier.
|
|
6
6
|
class PricingTier
|
|
7
|
+
ATTRIBUTES = %i[
|
|
8
|
+
input_per_million
|
|
9
|
+
output_per_million
|
|
10
|
+
cache_read_input_per_million
|
|
11
|
+
cache_write_input_per_million
|
|
12
|
+
cached_input_per_million
|
|
13
|
+
cache_creation_input_per_million
|
|
14
|
+
reasoning_output_per_million
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
7
17
|
def initialize(data = {})
|
|
8
18
|
@values = {}
|
|
9
19
|
|
|
@@ -12,17 +22,18 @@ module RubyLLM
|
|
|
12
22
|
end
|
|
13
23
|
end
|
|
14
24
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
ATTRIBUTES.each do |attribute|
|
|
26
|
+
define_method(attribute) do
|
|
27
|
+
@values[attribute]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
define_method("#{attribute}=") do |value|
|
|
31
|
+
@values[attribute] = value if value && value != 0.0
|
|
21
32
|
end
|
|
22
33
|
end
|
|
23
34
|
|
|
24
|
-
def
|
|
25
|
-
|
|
35
|
+
def [](key)
|
|
36
|
+
@values[key.to_sym]
|
|
26
37
|
end
|
|
27
38
|
|
|
28
39
|
def to_h
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# Sources for model registry data.
|
|
5
|
+
module ModelRegistry
|
|
6
|
+
# Reads model registry data from the configured JSON file.
|
|
7
|
+
class JsonSource
|
|
8
|
+
def initialize(file = nil)
|
|
9
|
+
@file = file
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def read
|
|
13
|
+
Models.read_from_json(@file || RubyLLM.config.model_registry_file)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Reads model registry data from the configured Active Record model class.
|
|
18
|
+
class ActiveRecordSource
|
|
19
|
+
def read
|
|
20
|
+
model_class = resolve_model_class
|
|
21
|
+
return [] unless model_class.respond_to?(:table_exists?) && model_class.table_exists?
|
|
22
|
+
|
|
23
|
+
model_class.all.map(&:to_llm)
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
RubyLLM.logger.debug { "Failed to load models from database: #{e.message}, falling back to JSON" }
|
|
26
|
+
[]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def resolve_model_class
|
|
32
|
+
model_class = RubyLLM.config.model_registry_class
|
|
33
|
+
return model_class unless model_class.is_a?(String)
|
|
34
|
+
|
|
35
|
+
model_class.split('::').inject(Object) { |scope, name| scope.const_get(name) }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|