ruby_llm_swarm 1.9.1
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 +175 -0
- data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +187 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +39 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +24 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +12 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +16 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +29 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +16 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/new.html.erb.tt +11 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +23 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_content.html.erb.tt +1 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +21 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +7 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +9 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +16 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +28 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +18 -0
- data/lib/generators/ruby_llm/generator_helpers.rb +194 -0
- data/lib/generators/ruby_llm/install/install_generator.rb +106 -0
- data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +9 -0
- data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +3 -0
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +7 -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 +45 -0
- data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +20 -0
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +12 -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/upgrade_to_v1_7/templates/migration.rb.tt +145 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +124 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +15 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +49 -0
- data/lib/ruby_llm/active_record/acts_as.rb +174 -0
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +384 -0
- data/lib/ruby_llm/active_record/chat_methods.rb +350 -0
- data/lib/ruby_llm/active_record/message_methods.rb +81 -0
- data/lib/ruby_llm/active_record/model_methods.rb +84 -0
- data/lib/ruby_llm/aliases.json +295 -0
- data/lib/ruby_llm/aliases.rb +38 -0
- data/lib/ruby_llm/attachment.rb +220 -0
- data/lib/ruby_llm/chat.rb +816 -0
- data/lib/ruby_llm/chunk.rb +6 -0
- data/lib/ruby_llm/configuration.rb +78 -0
- data/lib/ruby_llm/connection.rb +126 -0
- data/lib/ruby_llm/content.rb +73 -0
- data/lib/ruby_llm/context.rb +29 -0
- data/lib/ruby_llm/embedding.rb +29 -0
- data/lib/ruby_llm/error.rb +84 -0
- data/lib/ruby_llm/image.rb +49 -0
- data/lib/ruby_llm/message.rb +86 -0
- data/lib/ruby_llm/mime_type.rb +71 -0
- data/lib/ruby_llm/model/info.rb +111 -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 +33198 -0
- data/lib/ruby_llm/models.rb +231 -0
- data/lib/ruby_llm/models_schema.json +168 -0
- data/lib/ruby_llm/moderation.rb +56 -0
- data/lib/ruby_llm/provider.rb +243 -0
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +134 -0
- data/lib/ruby_llm/providers/anthropic/chat.rb +125 -0
- data/lib/ruby_llm/providers/anthropic/content.rb +44 -0
- data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
- data/lib/ruby_llm/providers/anthropic/media.rb +92 -0
- data/lib/ruby_llm/providers/anthropic/models.rb +63 -0
- data/lib/ruby_llm/providers/anthropic/streaming.rb +45 -0
- data/lib/ruby_llm/providers/anthropic/tools.rb +109 -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 +61 -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 +71 -0
- data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +67 -0
- data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +80 -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/gemini/capabilities.rb +281 -0
- data/lib/ruby_llm/providers/gemini/chat.rb +454 -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 +112 -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 +198 -0
- data/lib/ruby_llm/providers/gemini/transcription.rb +116 -0
- data/lib/ruby_llm/providers/gemini.rb +37 -0
- data/lib/ruby_llm/providers/gpustack/chat.rb +27 -0
- data/lib/ruby_llm/providers/gpustack/media.rb +46 -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 +46 -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 +299 -0
- data/lib/ruby_llm/providers/openai/chat.rb +88 -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 +81 -0
- data/lib/ruby_llm/providers/openai/models.rb +39 -0
- data/lib/ruby_llm/providers/openai/moderation.rb +34 -0
- data/lib/ruby_llm/providers/openai/streaming.rb +46 -0
- data/lib/ruby_llm/providers/openai/tools.rb +98 -0
- data/lib/ruby_llm/providers/openai/transcription.rb +70 -0
- data/lib/ruby_llm/providers/openai.rb +44 -0
- data/lib/ruby_llm/providers/openai_responses.rb +395 -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/transcription.rb +16 -0
- data/lib/ruby_llm/providers/vertexai.rb +55 -0
- data/lib/ruby_llm/railtie.rb +35 -0
- data/lib/ruby_llm/responses_session.rb +77 -0
- data/lib/ruby_llm/stream_accumulator.rb +101 -0
- data/lib/ruby_llm/streaming.rb +153 -0
- data/lib/ruby_llm/tool.rb +209 -0
- data/lib/ruby_llm/tool_call.rb +22 -0
- data/lib/ruby_llm/tool_executors.rb +125 -0
- data/lib/ruby_llm/transcription.rb +35 -0
- data/lib/ruby_llm/utils.rb +91 -0
- data/lib/ruby_llm/version.rb +5 -0
- data/lib/ruby_llm.rb +140 -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 +346 -0
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
:gemini_api_base,
|
|
14
|
+
:vertexai_project_id,
|
|
15
|
+
:vertexai_location,
|
|
16
|
+
:deepseek_api_key,
|
|
17
|
+
:perplexity_api_key,
|
|
18
|
+
:bedrock_api_key,
|
|
19
|
+
:bedrock_secret_key,
|
|
20
|
+
:bedrock_region,
|
|
21
|
+
:bedrock_session_token,
|
|
22
|
+
:openrouter_api_key,
|
|
23
|
+
:ollama_api_base,
|
|
24
|
+
:gpustack_api_base,
|
|
25
|
+
:gpustack_api_key,
|
|
26
|
+
:mistral_api_key,
|
|
27
|
+
# Default models
|
|
28
|
+
:default_model,
|
|
29
|
+
:default_embedding_model,
|
|
30
|
+
:default_moderation_model,
|
|
31
|
+
:default_image_model,
|
|
32
|
+
:default_transcription_model,
|
|
33
|
+
# Model registry
|
|
34
|
+
:model_registry_file,
|
|
35
|
+
:model_registry_class,
|
|
36
|
+
# Rails integration
|
|
37
|
+
:use_new_acts_as,
|
|
38
|
+
# Connection configuration
|
|
39
|
+
:request_timeout,
|
|
40
|
+
:max_retries,
|
|
41
|
+
:retry_interval,
|
|
42
|
+
:retry_backoff_factor,
|
|
43
|
+
:retry_interval_randomness,
|
|
44
|
+
:http_proxy,
|
|
45
|
+
# Logging configuration
|
|
46
|
+
:logger,
|
|
47
|
+
:log_file,
|
|
48
|
+
:log_level,
|
|
49
|
+
:log_stream_debug
|
|
50
|
+
|
|
51
|
+
def initialize
|
|
52
|
+
@request_timeout = 300
|
|
53
|
+
@max_retries = 3
|
|
54
|
+
@retry_interval = 0.1
|
|
55
|
+
@retry_backoff_factor = 2
|
|
56
|
+
@retry_interval_randomness = 0.5
|
|
57
|
+
@http_proxy = nil
|
|
58
|
+
|
|
59
|
+
@default_model = 'gpt-4.1-nano'
|
|
60
|
+
@default_embedding_model = 'text-embedding-3-small'
|
|
61
|
+
@default_moderation_model = 'omni-moderation-latest'
|
|
62
|
+
@default_image_model = 'gpt-image-1'
|
|
63
|
+
@default_transcription_model = 'whisper-1'
|
|
64
|
+
|
|
65
|
+
@model_registry_file = File.expand_path('models.json', __dir__)
|
|
66
|
+
@model_registry_class = 'Model'
|
|
67
|
+
@use_new_acts_as = false
|
|
68
|
+
|
|
69
|
+
@log_file = $stdout
|
|
70
|
+
@log_level = ENV['RUBYLLM_DEBUG'] ? Logger::DEBUG : Logger::INFO
|
|
71
|
+
@log_stream_debug = ENV['RUBYLLM_STREAM_DEBUG'] == 'true'
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def instance_variables
|
|
75
|
+
super.reject { |ivar| ivar.to_s.match?(/_id|_key|_secret|_token$/) }
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
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
|
+
@connection.post url, payload do |req|
|
|
38
|
+
req.headers.merge! @provider.headers if @provider.respond_to?(:headers)
|
|
39
|
+
yield req if block_given?
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def get(url, &)
|
|
44
|
+
@connection.get url do |req|
|
|
45
|
+
req.headers.merge! @provider.headers if @provider.respond_to?(:headers)
|
|
46
|
+
yield req if block_given?
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def instance_variables
|
|
51
|
+
super - %i[@config @connection]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def setup_timeout(faraday)
|
|
57
|
+
faraday.options.timeout = @config.request_timeout
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def setup_logging(faraday)
|
|
61
|
+
faraday.response :logger,
|
|
62
|
+
RubyLLM.logger,
|
|
63
|
+
bodies: true,
|
|
64
|
+
response: true,
|
|
65
|
+
errors: true,
|
|
66
|
+
headers: false,
|
|
67
|
+
log_level: :debug do |logger|
|
|
68
|
+
logger.filter(%r{[A-Za-z0-9+/=]{100,}}, '[BASE64 DATA]')
|
|
69
|
+
logger.filter(/[-\d.e,\s]{100,}/, '[EMBEDDINGS ARRAY]')
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def setup_retry(faraday)
|
|
74
|
+
faraday.request :retry, {
|
|
75
|
+
max: @config.max_retries,
|
|
76
|
+
interval: @config.retry_interval,
|
|
77
|
+
interval_randomness: @config.retry_interval_randomness,
|
|
78
|
+
backoff_factor: @config.retry_backoff_factor,
|
|
79
|
+
exceptions: retry_exceptions,
|
|
80
|
+
retry_statuses: [429, 500, 502, 503, 504, 529]
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def setup_middleware(faraday)
|
|
85
|
+
faraday.request :multipart
|
|
86
|
+
faraday.request :json
|
|
87
|
+
faraday.response :json
|
|
88
|
+
faraday.adapter :net_http
|
|
89
|
+
faraday.use :llm_errors, provider: @provider
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def setup_http_proxy(faraday)
|
|
93
|
+
return unless @config.http_proxy
|
|
94
|
+
|
|
95
|
+
faraday.proxy = @config.http_proxy
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def retry_exceptions
|
|
99
|
+
[
|
|
100
|
+
Errno::ETIMEDOUT,
|
|
101
|
+
Timeout::Error,
|
|
102
|
+
Faraday::TimeoutError,
|
|
103
|
+
Faraday::ConnectionFailed,
|
|
104
|
+
Faraday::RetriableResponse,
|
|
105
|
+
RubyLLM::RateLimitError,
|
|
106
|
+
RubyLLM::ServerError,
|
|
107
|
+
RubyLLM::ServiceUnavailableError,
|
|
108
|
+
RubyLLM::OverloadedError
|
|
109
|
+
]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def ensure_configured!
|
|
113
|
+
return if @provider.configured?
|
|
114
|
+
|
|
115
|
+
missing = @provider.configuration_requirements.reject { |req| @config.send(req) }
|
|
116
|
+
config_block = <<~RUBY
|
|
117
|
+
RubyLLM.configure do |config|
|
|
118
|
+
#{missing.map { |key| "config.#{key} = ENV['#{key.to_s.upcase}']" }.join("\n ")}
|
|
119
|
+
end
|
|
120
|
+
RUBY
|
|
121
|
+
|
|
122
|
+
raise ConfigurationError,
|
|
123
|
+
"#{@provider.name} provider is not configured. Add this to your initialization:\n\n#{config_block}"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
|
51
|
+
|
|
52
|
+
module RubyLLM
|
|
53
|
+
class Content
|
|
54
|
+
# Represents provider-specific payloads that should bypass RubyLLM formatting.
|
|
55
|
+
class Raw
|
|
56
|
+
attr_reader :value
|
|
57
|
+
|
|
58
|
+
def initialize(value)
|
|
59
|
+
raise ArgumentError, 'Raw content payload cannot be nil' if value.nil?
|
|
60
|
+
|
|
61
|
+
@value = value
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def format
|
|
65
|
+
@value
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def to_h
|
|
69
|
+
@value
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
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,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
|
+
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
|
+
# Responses API specific errors
|
|
32
|
+
class ResponsesApiError < Error; end
|
|
33
|
+
class ResponseIdNotFoundError < ResponsesApiError; end
|
|
34
|
+
class ResponseFailedError < ResponsesApiError; end
|
|
35
|
+
class ResponseInProgressError < ResponsesApiError; end
|
|
36
|
+
class ResponseCancelledError < ResponsesApiError; end
|
|
37
|
+
class ResponseIncompleteError < ResponsesApiError; 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
|
+
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,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,86 @@
|
|
|
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, :model_id, :tool_calls, :tool_call_id, :input_tokens, :output_tokens,
|
|
9
|
+
:cached_tokens, :cache_creation_tokens, :raw,
|
|
10
|
+
:response_id, :reasoning_summary, :reasoning_tokens
|
|
11
|
+
attr_writer :content
|
|
12
|
+
|
|
13
|
+
def initialize(options = {})
|
|
14
|
+
@role = options.fetch(:role).to_sym
|
|
15
|
+
@content = normalize_content(options.fetch(:content))
|
|
16
|
+
@model_id = options[:model_id]
|
|
17
|
+
@tool_calls = options[:tool_calls]
|
|
18
|
+
@tool_call_id = options[:tool_call_id]
|
|
19
|
+
@input_tokens = options[:input_tokens]
|
|
20
|
+
@output_tokens = options[:output_tokens]
|
|
21
|
+
@cached_tokens = options[:cached_tokens]
|
|
22
|
+
@cache_creation_tokens = options[:cache_creation_tokens]
|
|
23
|
+
@raw = options[:raw]
|
|
24
|
+
@response_id = options[:response_id]
|
|
25
|
+
@reasoning_summary = options[:reasoning_summary]
|
|
26
|
+
@reasoning_tokens = options[:reasoning_tokens]
|
|
27
|
+
|
|
28
|
+
ensure_valid_role
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def content
|
|
32
|
+
if @content.is_a?(Content) && @content.text && @content.attachments.empty?
|
|
33
|
+
@content.text
|
|
34
|
+
else
|
|
35
|
+
@content
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def tool_call?
|
|
40
|
+
!tool_calls.nil? && !tool_calls.empty?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def tool_result?
|
|
44
|
+
!tool_call_id.nil? && !tool_call_id.empty?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def tool_results
|
|
48
|
+
content if tool_result?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def to_h
|
|
52
|
+
{
|
|
53
|
+
role: role,
|
|
54
|
+
content: content,
|
|
55
|
+
model_id: model_id,
|
|
56
|
+
tool_calls: tool_calls,
|
|
57
|
+
tool_call_id: tool_call_id,
|
|
58
|
+
input_tokens: input_tokens,
|
|
59
|
+
output_tokens: output_tokens,
|
|
60
|
+
cached_tokens: cached_tokens,
|
|
61
|
+
cache_creation_tokens: cache_creation_tokens,
|
|
62
|
+
response_id: response_id,
|
|
63
|
+
reasoning_summary: reasoning_summary,
|
|
64
|
+
reasoning_tokens: reasoning_tokens
|
|
65
|
+
}.compact
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def instance_variables
|
|
69
|
+
super - [:@raw]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def normalize_content(content)
|
|
75
|
+
case content
|
|
76
|
+
when String then Content.new(content)
|
|
77
|
+
when Hash then Content.new(content[:text], content)
|
|
78
|
+
else content
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def ensure_valid_role
|
|
83
|
+
raise InvalidRoleError, "Expected role to be one of: #{ROLES.join(', ')}" unless ROLES.include?(role)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
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 video?(type)
|
|
19
|
+
type.start_with?('video/')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def audio?(type)
|
|
23
|
+
type.start_with?('audio/')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def pdf?(type)
|
|
27
|
+
type == 'application/pdf'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def text?(type)
|
|
31
|
+
type.start_with?('text/') ||
|
|
32
|
+
TEXT_SUFFIXES.any? { |suffix| type.end_with?(suffix) } ||
|
|
33
|
+
NON_TEXT_PREFIX_TEXT_MIME_TYPES.include?(type)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# MIME types that have a text/ prefix but need to be handled differently
|
|
37
|
+
TEXT_SUFFIXES = ['+json', '+xml', '+html', '+yaml', '+csv', '+plain', '+javascript', '+svg'].freeze
|
|
38
|
+
|
|
39
|
+
# MIME types that don't have a text/ prefix but should be treated as text
|
|
40
|
+
NON_TEXT_PREFIX_TEXT_MIME_TYPES = [
|
|
41
|
+
'application/json', # Base type, even if specific ones end with +json
|
|
42
|
+
'application/xml', # Base type, even if specific ones end with +xml
|
|
43
|
+
'application/javascript',
|
|
44
|
+
'application/ecmascript',
|
|
45
|
+
'application/rtf',
|
|
46
|
+
'application/sql',
|
|
47
|
+
'application/x-sh',
|
|
48
|
+
'application/x-csh',
|
|
49
|
+
'application/x-httpd-php',
|
|
50
|
+
'application/sdp',
|
|
51
|
+
'application/sparql-query',
|
|
52
|
+
'application/graphql',
|
|
53
|
+
'application/yang', # Data modeling language, often serialized as XML/JSON but the type itself is distinct
|
|
54
|
+
'application/mbox', # Mailbox format
|
|
55
|
+
'application/x-tex',
|
|
56
|
+
'application/x-latex',
|
|
57
|
+
'application/x-perl',
|
|
58
|
+
'application/x-python',
|
|
59
|
+
'application/x-tcl',
|
|
60
|
+
'application/pgp-signature', # Often ASCII armored
|
|
61
|
+
'application/pgp-keys', # Often ASCII armored
|
|
62
|
+
'application/vnd.coffeescript',
|
|
63
|
+
'application/vnd.dart',
|
|
64
|
+
'application/vnd.oai.openapi', # Base for OpenAPI, often with +json or +yaml suffix
|
|
65
|
+
'application/vnd.zul', # ZK User Interface Language (can be XML-like)
|
|
66
|
+
'application/x-yaml', # Common non-standard for YAML
|
|
67
|
+
'application/yaml', # Standard for YAML
|
|
68
|
+
'application/toml' # TOML configuration files
|
|
69
|
+
].freeze
|
|
70
|
+
end
|
|
71
|
+
end
|