dify_llm 1.6.4
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +157 -0
- data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +3 -0
- data/lib/generators/ruby_llm/install/templates/create_chats_legacy_migration.rb.tt +8 -0
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +8 -0
- data/lib/generators/ruby_llm/install/templates/create_messages_legacy_migration.rb.tt +16 -0
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +16 -0
- data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +43 -0
- data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +15 -0
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +9 -0
- data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +4 -0
- data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +3 -0
- data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +3 -0
- data/lib/generators/ruby_llm/install_generator.rb +184 -0
- data/lib/generators/ruby_llm/migrate_model_fields/templates/migration.rb.tt +142 -0
- data/lib/generators/ruby_llm/migrate_model_fields_generator.rb +84 -0
- data/lib/ruby_llm/active_record/acts_as.rb +137 -0
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +398 -0
- data/lib/ruby_llm/active_record/chat_methods.rb +315 -0
- data/lib/ruby_llm/active_record/message_methods.rb +72 -0
- data/lib/ruby_llm/active_record/model_methods.rb +84 -0
- data/lib/ruby_llm/aliases.json +274 -0
- data/lib/ruby_llm/aliases.rb +38 -0
- data/lib/ruby_llm/attachment.rb +191 -0
- data/lib/ruby_llm/chat.rb +212 -0
- data/lib/ruby_llm/chunk.rb +6 -0
- data/lib/ruby_llm/configuration.rb +69 -0
- data/lib/ruby_llm/connection.rb +137 -0
- data/lib/ruby_llm/content.rb +50 -0
- data/lib/ruby_llm/context.rb +29 -0
- data/lib/ruby_llm/embedding.rb +29 -0
- data/lib/ruby_llm/error.rb +76 -0
- data/lib/ruby_llm/image.rb +49 -0
- data/lib/ruby_llm/message.rb +76 -0
- data/lib/ruby_llm/mime_type.rb +67 -0
- data/lib/ruby_llm/model/info.rb +103 -0
- data/lib/ruby_llm/model/modalities.rb +22 -0
- data/lib/ruby_llm/model/pricing.rb +48 -0
- data/lib/ruby_llm/model/pricing_category.rb +46 -0
- data/lib/ruby_llm/model/pricing_tier.rb +33 -0
- data/lib/ruby_llm/model.rb +7 -0
- data/lib/ruby_llm/models.json +31418 -0
- data/lib/ruby_llm/models.rb +235 -0
- data/lib/ruby_llm/models_schema.json +168 -0
- data/lib/ruby_llm/provider.rb +215 -0
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +134 -0
- data/lib/ruby_llm/providers/anthropic/chat.rb +106 -0
- data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
- data/lib/ruby_llm/providers/anthropic/media.rb +91 -0
- data/lib/ruby_llm/providers/anthropic/models.rb +48 -0
- data/lib/ruby_llm/providers/anthropic/streaming.rb +43 -0
- data/lib/ruby_llm/providers/anthropic/tools.rb +107 -0
- data/lib/ruby_llm/providers/anthropic.rb +36 -0
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +167 -0
- data/lib/ruby_llm/providers/bedrock/chat.rb +63 -0
- data/lib/ruby_llm/providers/bedrock/media.rb +60 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +98 -0
- data/lib/ruby_llm/providers/bedrock/signing.rb +831 -0
- data/lib/ruby_llm/providers/bedrock/streaming/base.rb +51 -0
- data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +56 -0
- data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +67 -0
- data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +78 -0
- data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +78 -0
- data/lib/ruby_llm/providers/bedrock/streaming.rb +18 -0
- data/lib/ruby_llm/providers/bedrock.rb +82 -0
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +130 -0
- data/lib/ruby_llm/providers/deepseek/chat.rb +16 -0
- data/lib/ruby_llm/providers/deepseek.rb +30 -0
- data/lib/ruby_llm/providers/dify/capabilities.rb +16 -0
- data/lib/ruby_llm/providers/dify/chat.rb +59 -0
- data/lib/ruby_llm/providers/dify/media.rb +37 -0
- data/lib/ruby_llm/providers/dify/streaming.rb +28 -0
- data/lib/ruby_llm/providers/dify.rb +48 -0
- data/lib/ruby_llm/providers/gemini/capabilities.rb +276 -0
- data/lib/ruby_llm/providers/gemini/chat.rb +171 -0
- data/lib/ruby_llm/providers/gemini/embeddings.rb +37 -0
- data/lib/ruby_llm/providers/gemini/images.rb +47 -0
- data/lib/ruby_llm/providers/gemini/media.rb +54 -0
- data/lib/ruby_llm/providers/gemini/models.rb +40 -0
- data/lib/ruby_llm/providers/gemini/streaming.rb +61 -0
- data/lib/ruby_llm/providers/gemini/tools.rb +77 -0
- data/lib/ruby_llm/providers/gemini.rb +36 -0
- data/lib/ruby_llm/providers/gpustack/chat.rb +27 -0
- data/lib/ruby_llm/providers/gpustack/media.rb +45 -0
- data/lib/ruby_llm/providers/gpustack/models.rb +90 -0
- data/lib/ruby_llm/providers/gpustack.rb +34 -0
- data/lib/ruby_llm/providers/mistral/capabilities.rb +155 -0
- data/lib/ruby_llm/providers/mistral/chat.rb +24 -0
- data/lib/ruby_llm/providers/mistral/embeddings.rb +33 -0
- data/lib/ruby_llm/providers/mistral/models.rb +48 -0
- data/lib/ruby_llm/providers/mistral.rb +32 -0
- data/lib/ruby_llm/providers/ollama/chat.rb +27 -0
- data/lib/ruby_llm/providers/ollama/media.rb +45 -0
- data/lib/ruby_llm/providers/ollama/models.rb +36 -0
- data/lib/ruby_llm/providers/ollama.rb +30 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +291 -0
- data/lib/ruby_llm/providers/openai/chat.rb +83 -0
- data/lib/ruby_llm/providers/openai/embeddings.rb +33 -0
- data/lib/ruby_llm/providers/openai/images.rb +38 -0
- data/lib/ruby_llm/providers/openai/media.rb +80 -0
- data/lib/ruby_llm/providers/openai/models.rb +39 -0
- data/lib/ruby_llm/providers/openai/streaming.rb +41 -0
- data/lib/ruby_llm/providers/openai/tools.rb +78 -0
- data/lib/ruby_llm/providers/openai.rb +42 -0
- data/lib/ruby_llm/providers/openrouter/models.rb +73 -0
- data/lib/ruby_llm/providers/openrouter.rb +26 -0
- data/lib/ruby_llm/providers/perplexity/capabilities.rb +137 -0
- data/lib/ruby_llm/providers/perplexity/chat.rb +16 -0
- data/lib/ruby_llm/providers/perplexity/models.rb +42 -0
- data/lib/ruby_llm/providers/perplexity.rb +48 -0
- data/lib/ruby_llm/providers/vertexai/chat.rb +14 -0
- data/lib/ruby_llm/providers/vertexai/embeddings.rb +32 -0
- data/lib/ruby_llm/providers/vertexai/models.rb +130 -0
- data/lib/ruby_llm/providers/vertexai/streaming.rb +14 -0
- data/lib/ruby_llm/providers/vertexai.rb +55 -0
- data/lib/ruby_llm/railtie.rb +41 -0
- data/lib/ruby_llm/stream_accumulator.rb +97 -0
- data/lib/ruby_llm/streaming.rb +153 -0
- data/lib/ruby_llm/tool.rb +83 -0
- data/lib/ruby_llm/tool_call.rb +22 -0
- data/lib/ruby_llm/utils.rb +45 -0
- data/lib/ruby_llm/version.rb +5 -0
- data/lib/ruby_llm.rb +97 -0
- data/lib/tasks/models.rake +525 -0
- data/lib/tasks/release.rake +67 -0
- data/lib/tasks/ruby_llm.rake +15 -0
- data/lib/tasks/vcr.rake +92 -0
- metadata +291 -0
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
# Global configuration for RubyLLM
|
5
|
+
class Configuration
|
6
|
+
attr_accessor :openai_api_key,
|
7
|
+
:openai_api_base,
|
8
|
+
:openai_organization_id,
|
9
|
+
:openai_project_id,
|
10
|
+
:openai_use_system_role,
|
11
|
+
:anthropic_api_key,
|
12
|
+
:gemini_api_key,
|
13
|
+
:vertexai_project_id,
|
14
|
+
:vertexai_location,
|
15
|
+
:deepseek_api_key,
|
16
|
+
:dify_api_base,
|
17
|
+
:dify_api_key,
|
18
|
+
:dify_user,
|
19
|
+
:perplexity_api_key,
|
20
|
+
:bedrock_api_key,
|
21
|
+
:bedrock_secret_key,
|
22
|
+
:bedrock_region,
|
23
|
+
:bedrock_session_token,
|
24
|
+
:openrouter_api_key,
|
25
|
+
:ollama_api_base,
|
26
|
+
:gpustack_api_base,
|
27
|
+
:gpustack_api_key,
|
28
|
+
:mistral_api_key,
|
29
|
+
# Default models
|
30
|
+
:default_model,
|
31
|
+
:default_embedding_model,
|
32
|
+
:default_image_model,
|
33
|
+
# Model registry
|
34
|
+
:model_registry_class,
|
35
|
+
# Connection configuration
|
36
|
+
:request_timeout,
|
37
|
+
:max_retries,
|
38
|
+
:retry_interval,
|
39
|
+
:retry_backoff_factor,
|
40
|
+
:retry_interval_randomness,
|
41
|
+
:http_proxy,
|
42
|
+
# Logging configuration
|
43
|
+
:logger,
|
44
|
+
:log_file,
|
45
|
+
:log_level,
|
46
|
+
:log_stream_debug
|
47
|
+
|
48
|
+
def initialize
|
49
|
+
@request_timeout = 120
|
50
|
+
@max_retries = 3
|
51
|
+
@retry_interval = 0.1
|
52
|
+
@retry_backoff_factor = 2
|
53
|
+
@retry_interval_randomness = 0.5
|
54
|
+
@http_proxy = nil
|
55
|
+
|
56
|
+
@default_model = 'gpt-4.1-nano'
|
57
|
+
@default_embedding_model = 'text-embedding-3-small'
|
58
|
+
@default_image_model = 'gpt-image-1'
|
59
|
+
|
60
|
+
@log_file = $stdout
|
61
|
+
@log_level = ENV['RUBYLLM_DEBUG'] ? Logger::DEBUG : Logger::INFO
|
62
|
+
@log_stream_debug = ENV['RUBYLLM_STREAM_DEBUG'] == 'true'
|
63
|
+
end
|
64
|
+
|
65
|
+
def instance_variables
|
66
|
+
super.reject { |ivar| ivar.to_s.match?(/_id|_key|_secret|_token$/) }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
# Connection class for managing API connections to various providers.
|
5
|
+
class Connection
|
6
|
+
attr_reader :provider, :connection, :config
|
7
|
+
|
8
|
+
def self.basic(&)
|
9
|
+
Faraday.new do |f|
|
10
|
+
f.response :logger,
|
11
|
+
RubyLLM.logger,
|
12
|
+
bodies: false,
|
13
|
+
response: false,
|
14
|
+
errors: true,
|
15
|
+
headers: false,
|
16
|
+
log_level: :debug
|
17
|
+
f.response :raise_error
|
18
|
+
yield f if block_given?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(provider, config)
|
23
|
+
@provider = provider
|
24
|
+
@config = config
|
25
|
+
|
26
|
+
ensure_configured!
|
27
|
+
@connection ||= Faraday.new(provider.api_base) do |faraday|
|
28
|
+
setup_timeout(faraday)
|
29
|
+
setup_logging(faraday)
|
30
|
+
setup_retry(faraday)
|
31
|
+
setup_middleware(faraday)
|
32
|
+
setup_http_proxy(faraday)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def post(url, payload, &)
|
37
|
+
body = payload.is_a?(Hash) ? JSON.generate(payload, ascii_only: false) : payload
|
38
|
+
@connection.post url, body do |req|
|
39
|
+
req.headers.merge! @provider.headers if @provider.respond_to?(:headers)
|
40
|
+
yield req if block_given?
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def upload(url, payload)
|
45
|
+
connection = Faraday.new(@provider.api_base) do |faraday|
|
46
|
+
faraday.request :multipart, content_type: 'multipart/form-data'
|
47
|
+
faraday.response :json, parser_options: { symbolize_names: true }
|
48
|
+
setup_http_proxy(faraday)
|
49
|
+
end
|
50
|
+
connection.post(url, payload) do |req|
|
51
|
+
req.headers.merge! @provider.headers if @provider.respond_to?(:headers)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def get(url, &)
|
56
|
+
@connection.get url do |req|
|
57
|
+
req.headers.merge! @provider.headers if @provider.respond_to?(:headers)
|
58
|
+
yield req if block_given?
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def instance_variables
|
63
|
+
super - %i[@config @connection]
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def setup_timeout(faraday)
|
69
|
+
faraday.options.timeout = @config.request_timeout
|
70
|
+
end
|
71
|
+
|
72
|
+
def setup_logging(faraday)
|
73
|
+
faraday.response :logger,
|
74
|
+
RubyLLM.logger,
|
75
|
+
bodies: true,
|
76
|
+
response: true,
|
77
|
+
errors: true,
|
78
|
+
headers: false,
|
79
|
+
log_level: :debug do |logger|
|
80
|
+
logger.filter(%r{[A-Za-z0-9+/=]{100,}}, 'data":"[BASE64 DATA]"')
|
81
|
+
logger.filter(/[-\d.e,\s]{100,}/, '[EMBEDDINGS ARRAY]')
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def setup_retry(faraday)
|
86
|
+
faraday.request :retry, {
|
87
|
+
max: @config.max_retries,
|
88
|
+
interval: @config.retry_interval,
|
89
|
+
interval_randomness: @config.retry_interval_randomness,
|
90
|
+
backoff_factor: @config.retry_backoff_factor,
|
91
|
+
exceptions: retry_exceptions,
|
92
|
+
retry_statuses: [429, 500, 502, 503, 504, 529]
|
93
|
+
}
|
94
|
+
end
|
95
|
+
|
96
|
+
def setup_middleware(faraday)
|
97
|
+
faraday.request :json
|
98
|
+
faraday.response :json
|
99
|
+
faraday.adapter Faraday.default_adapter
|
100
|
+
faraday.use :llm_errors, provider: @provider
|
101
|
+
end
|
102
|
+
|
103
|
+
def setup_http_proxy(faraday)
|
104
|
+
return unless @config.http_proxy
|
105
|
+
|
106
|
+
faraday.proxy = @config.http_proxy
|
107
|
+
end
|
108
|
+
|
109
|
+
def retry_exceptions
|
110
|
+
[
|
111
|
+
Errno::ETIMEDOUT,
|
112
|
+
Timeout::Error,
|
113
|
+
Faraday::TimeoutError,
|
114
|
+
Faraday::ConnectionFailed,
|
115
|
+
Faraday::RetriableResponse,
|
116
|
+
RubyLLM::RateLimitError,
|
117
|
+
RubyLLM::ServerError,
|
118
|
+
RubyLLM::ServiceUnavailableError,
|
119
|
+
RubyLLM::OverloadedError
|
120
|
+
]
|
121
|
+
end
|
122
|
+
|
123
|
+
def ensure_configured!
|
124
|
+
return if @provider.configured?
|
125
|
+
|
126
|
+
missing = @provider.configuration_requirements.reject { |req| @config.send(req) }
|
127
|
+
config_block = <<~RUBY
|
128
|
+
RubyLLM.configure do |config|
|
129
|
+
#{missing.map { |key| "config.#{key} = ENV['#{key.to_s.upcase}']" }.join("\n ")}
|
130
|
+
end
|
131
|
+
RUBY
|
132
|
+
|
133
|
+
raise ConfigurationError,
|
134
|
+
"#{@provider.name} provider is not configured. Add this to your initialization:\n\n#{config_block}"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
# Represents the content sent to or received from an LLM.
|
5
|
+
class Content
|
6
|
+
attr_reader :text, :attachments
|
7
|
+
|
8
|
+
def initialize(text = nil, attachments = nil)
|
9
|
+
@text = text
|
10
|
+
@attachments = []
|
11
|
+
|
12
|
+
process_attachments(attachments)
|
13
|
+
raise ArgumentError, 'Text and attachments cannot be both nil' if @text.nil? && @attachments.empty?
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_attachment(source, filename: nil)
|
17
|
+
@attachments << Attachment.new(source, filename:)
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def format
|
22
|
+
if @text && @attachments.empty?
|
23
|
+
@text
|
24
|
+
else
|
25
|
+
self
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# For Rails serialization
|
30
|
+
def to_h
|
31
|
+
{ text: @text, attachments: @attachments.map(&:to_h) }
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def process_attachments_array_or_string(attachments)
|
37
|
+
Utils.to_safe_array(attachments).each do |file|
|
38
|
+
add_attachment(file)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def process_attachments(attachments)
|
43
|
+
if attachments.is_a?(Hash)
|
44
|
+
attachments.each_value { |attachment| process_attachments_array_or_string(attachment) }
|
45
|
+
else
|
46
|
+
process_attachments_array_or_string attachments
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,29 @@
|
|
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_instance)
|
26
|
+
provider_instance.connection
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
# Core embedding interface.
|
5
|
+
class Embedding
|
6
|
+
attr_reader :vectors, :model, :input_tokens
|
7
|
+
|
8
|
+
def initialize(vectors:, model:, input_tokens: 0)
|
9
|
+
@vectors = vectors
|
10
|
+
@model = model
|
11
|
+
@input_tokens = input_tokens
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.embed(text, # rubocop:disable Metrics/ParameterLists
|
15
|
+
model: nil,
|
16
|
+
provider: nil,
|
17
|
+
assume_model_exists: false,
|
18
|
+
context: nil,
|
19
|
+
dimensions: nil)
|
20
|
+
config = context&.config || RubyLLM.config
|
21
|
+
model ||= config.default_embedding_model
|
22
|
+
model, provider_instance = Models.resolve(model, provider: provider, assume_exists: assume_model_exists,
|
23
|
+
config: config)
|
24
|
+
model_id = model.id
|
25
|
+
|
26
|
+
provider_instance.embed(text, model: model_id, dimensions:)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,76 @@
|
|
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
|
+
class Error < StandardError
|
7
|
+
attr_reader :response
|
8
|
+
|
9
|
+
def initialize(response = nil, message = nil)
|
10
|
+
@response = response
|
11
|
+
super(message || response&.body)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Error classes for non-HTTP errors
|
16
|
+
class ConfigurationError < StandardError; end
|
17
|
+
class InvalidRoleError < StandardError; end
|
18
|
+
class ModelNotFoundError < StandardError; end
|
19
|
+
class UnsupportedAttachmentError < StandardError; end
|
20
|
+
|
21
|
+
# Error classes for different HTTP status codes
|
22
|
+
class BadRequestError < Error; end
|
23
|
+
class ForbiddenError < Error; end
|
24
|
+
class OverloadedError < Error; end
|
25
|
+
class PaymentRequiredError < Error; end
|
26
|
+
class RateLimitError < Error; end
|
27
|
+
class ServerError < Error; end
|
28
|
+
class ServiceUnavailableError < Error; end
|
29
|
+
class UnauthorizedError < Error; end
|
30
|
+
|
31
|
+
# Faraday middleware that maps provider-specific API errors to RubyLLM errors.
|
32
|
+
class ErrorMiddleware < Faraday::Middleware
|
33
|
+
def initialize(app, options = {})
|
34
|
+
super(app)
|
35
|
+
@provider = options[:provider]
|
36
|
+
end
|
37
|
+
|
38
|
+
def call(env)
|
39
|
+
@app.call(env).on_complete do |response|
|
40
|
+
self.class.parse_error(provider: @provider, response: response)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class << self
|
45
|
+
def parse_error(provider:, response:) # rubocop:disable Metrics/PerceivedComplexity
|
46
|
+
message = provider&.parse_error(response)
|
47
|
+
|
48
|
+
case response.status
|
49
|
+
when 200..399
|
50
|
+
message
|
51
|
+
when 400
|
52
|
+
raise BadRequestError.new(response, message || 'Invalid request - please check your input')
|
53
|
+
when 401
|
54
|
+
raise UnauthorizedError.new(response, message || 'Invalid API key - check your credentials')
|
55
|
+
when 402
|
56
|
+
raise PaymentRequiredError.new(response, message || 'Payment required - please top up your account')
|
57
|
+
when 403
|
58
|
+
raise ForbiddenError.new(response,
|
59
|
+
message || 'Forbidden - you do not have permission to access this resource')
|
60
|
+
when 429
|
61
|
+
raise RateLimitError.new(response, message || 'Rate limit exceeded - please wait a moment')
|
62
|
+
when 500
|
63
|
+
raise ServerError.new(response, message || 'API server error - please try again')
|
64
|
+
when 502..503
|
65
|
+
raise ServiceUnavailableError.new(response, message || 'API server unavailable - please try again later')
|
66
|
+
when 529
|
67
|
+
raise OverloadedError.new(response, message || 'Service overloaded - please try again later')
|
68
|
+
else
|
69
|
+
raise Error.new(response, message || 'An unknown error occurred')
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
Faraday::Middleware.register_middleware(llm_errors: RubyLLM::ErrorMiddleware)
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
# Represents a generated image from an AI model.
|
5
|
+
class Image
|
6
|
+
attr_reader :url, :data, :mime_type, :revised_prompt, :model_id
|
7
|
+
|
8
|
+
def initialize(url: nil, data: nil, mime_type: nil, revised_prompt: nil, model_id: nil)
|
9
|
+
@url = url
|
10
|
+
@data = data
|
11
|
+
@mime_type = mime_type
|
12
|
+
@revised_prompt = revised_prompt
|
13
|
+
@model_id = model_id
|
14
|
+
end
|
15
|
+
|
16
|
+
def base64?
|
17
|
+
!@data.nil?
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_blob
|
21
|
+
if base64?
|
22
|
+
Base64.decode64 @data
|
23
|
+
else
|
24
|
+
response = Connection.basic.get @url
|
25
|
+
response.body
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def save(path)
|
30
|
+
File.binwrite(File.expand_path(path), to_blob)
|
31
|
+
path
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.paint(prompt, # rubocop:disable Metrics/ParameterLists
|
35
|
+
model: nil,
|
36
|
+
provider: nil,
|
37
|
+
assume_model_exists: false,
|
38
|
+
size: '1024x1024',
|
39
|
+
context: nil)
|
40
|
+
config = context&.config || RubyLLM.config
|
41
|
+
model ||= config.default_image_model
|
42
|
+
model, provider_instance = Models.resolve(model, provider: provider, assume_exists: assume_model_exists,
|
43
|
+
config: config)
|
44
|
+
model_id = model.id
|
45
|
+
|
46
|
+
provider_instance.paint(prompt, model: model_id, size:)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
# A single message in a chat conversation.
|
5
|
+
class Message
|
6
|
+
ROLES = %i[system user assistant tool].freeze
|
7
|
+
|
8
|
+
attr_reader :role, :tool_calls, :tool_call_id, :input_tokens, :output_tokens, :model_id, :raw, :conversation_id
|
9
|
+
attr_writer :content
|
10
|
+
|
11
|
+
def initialize(options = {})
|
12
|
+
@role = options.fetch(:role).to_sym
|
13
|
+
@content = normalize_content(options.fetch(:content))
|
14
|
+
@tool_calls = options[:tool_calls]
|
15
|
+
@input_tokens = options[:input_tokens]
|
16
|
+
@output_tokens = options[:output_tokens]
|
17
|
+
@model_id = options[:model_id]
|
18
|
+
@conversation_id = options[:conversation_id]
|
19
|
+
@tool_call_id = options[:tool_call_id]
|
20
|
+
@raw = options[:raw]
|
21
|
+
|
22
|
+
ensure_valid_role
|
23
|
+
end
|
24
|
+
|
25
|
+
def content
|
26
|
+
if @content.is_a?(Content) && @content.text && @content.attachments.empty?
|
27
|
+
@content.text
|
28
|
+
else
|
29
|
+
@content
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def tool_call?
|
34
|
+
!tool_calls.nil? && !tool_calls.empty?
|
35
|
+
end
|
36
|
+
|
37
|
+
def tool_result?
|
38
|
+
!tool_call_id.nil? && !tool_call_id.empty?
|
39
|
+
end
|
40
|
+
|
41
|
+
def tool_results
|
42
|
+
content if tool_result?
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_h
|
46
|
+
{
|
47
|
+
role: role,
|
48
|
+
content: content,
|
49
|
+
tool_calls: tool_calls,
|
50
|
+
tool_call_id: tool_call_id,
|
51
|
+
input_tokens: input_tokens,
|
52
|
+
output_tokens: output_tokens,
|
53
|
+
conversation_id: conversation_id,
|
54
|
+
model_id: model_id
|
55
|
+
}.compact
|
56
|
+
end
|
57
|
+
|
58
|
+
def instance_variables
|
59
|
+
super - [:@raw]
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def normalize_content(content)
|
65
|
+
case content
|
66
|
+
when String then Content.new(content)
|
67
|
+
when Hash then Content.new(content[:text], content)
|
68
|
+
else content
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def ensure_valid_role
|
73
|
+
raise InvalidRoleError, "Expected role to be one of: #{ROLES.join(', ')}" unless ROLES.include?(role)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
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,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Model
|
5
|
+
# Information about an AI model's capabilities, pricing, and metadata.
|
6
|
+
class Info
|
7
|
+
attr_reader :id, :name, :provider, :family, :created_at, :context_window, :max_output_tokens, :knowledge_cutoff,
|
8
|
+
:modalities, :capabilities, :pricing, :metadata
|
9
|
+
|
10
|
+
# Create a default model with assumed capabilities
|
11
|
+
def self.default(model_id, provider)
|
12
|
+
new(
|
13
|
+
id: model_id,
|
14
|
+
name: model_id.tr('-', ' ').capitalize,
|
15
|
+
provider: provider,
|
16
|
+
capabilities: %w[function_calling streaming vision structured_output],
|
17
|
+
modalities: { input: %w[text image], output: %w[text] },
|
18
|
+
metadata: { warning: 'Assuming model exists, capabilities may not be accurate' }
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(data)
|
23
|
+
@id = data[:id]
|
24
|
+
@name = data[:name]
|
25
|
+
@provider = data[:provider]
|
26
|
+
@family = data[:family]
|
27
|
+
@created_at = Utils.to_time(data[:created_at])
|
28
|
+
@context_window = data[:context_window]
|
29
|
+
@max_output_tokens = data[:max_output_tokens]
|
30
|
+
@knowledge_cutoff = Utils.to_date(data[:knowledge_cutoff])
|
31
|
+
@modalities = Modalities.new(data[:modalities] || {})
|
32
|
+
@capabilities = data[:capabilities] || []
|
33
|
+
@pricing = Pricing.new(data[:pricing] || {})
|
34
|
+
@metadata = data[:metadata] || {}
|
35
|
+
end
|
36
|
+
|
37
|
+
def supports?(capability)
|
38
|
+
capabilities.include?(capability.to_s)
|
39
|
+
end
|
40
|
+
|
41
|
+
%w[function_calling structured_output batch reasoning citations streaming].each do |cap|
|
42
|
+
define_method "#{cap}?" do
|
43
|
+
supports?(cap)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def display_name
|
48
|
+
name
|
49
|
+
end
|
50
|
+
|
51
|
+
def max_tokens
|
52
|
+
max_output_tokens
|
53
|
+
end
|
54
|
+
|
55
|
+
def supports_vision?
|
56
|
+
modalities.input.include?('image')
|
57
|
+
end
|
58
|
+
|
59
|
+
def supports_functions?
|
60
|
+
function_calling?
|
61
|
+
end
|
62
|
+
|
63
|
+
def input_price_per_million
|
64
|
+
pricing.text_tokens.input
|
65
|
+
end
|
66
|
+
|
67
|
+
def output_price_per_million
|
68
|
+
pricing.text_tokens.output
|
69
|
+
end
|
70
|
+
|
71
|
+
def type # rubocop:disable Metrics/PerceivedComplexity
|
72
|
+
if modalities.output.include?('embeddings') && !modalities.output.include?('text')
|
73
|
+
'embedding'
|
74
|
+
elsif modalities.output.include?('image') && !modalities.output.include?('text')
|
75
|
+
'image'
|
76
|
+
elsif modalities.output.include?('audio') && !modalities.output.include?('text')
|
77
|
+
'audio'
|
78
|
+
elsif modalities.output.include?('moderation')
|
79
|
+
'moderation'
|
80
|
+
else
|
81
|
+
'chat'
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def to_h
|
86
|
+
{
|
87
|
+
id: id,
|
88
|
+
name: name,
|
89
|
+
provider: provider,
|
90
|
+
family: family,
|
91
|
+
created_at: created_at,
|
92
|
+
context_window: context_window,
|
93
|
+
max_output_tokens: max_output_tokens,
|
94
|
+
knowledge_cutoff: knowledge_cutoff,
|
95
|
+
modalities: modalities.to_h,
|
96
|
+
capabilities: capabilities,
|
97
|
+
pricing: pricing.to_h,
|
98
|
+
metadata: metadata
|
99
|
+
}
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|