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.
Files changed (154) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +175 -0
  4. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +187 -0
  5. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +39 -0
  6. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +24 -0
  7. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +14 -0
  8. data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +12 -0
  9. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +16 -0
  10. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +29 -0
  11. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +16 -0
  12. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/new.html.erb.tt +11 -0
  13. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +23 -0
  14. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_content.html.erb.tt +1 -0
  15. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +21 -0
  16. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +13 -0
  17. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +7 -0
  18. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +9 -0
  19. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +16 -0
  20. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +28 -0
  21. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +18 -0
  22. data/lib/generators/ruby_llm/generator_helpers.rb +194 -0
  23. data/lib/generators/ruby_llm/install/install_generator.rb +106 -0
  24. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +9 -0
  25. data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +3 -0
  26. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +7 -0
  27. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +16 -0
  28. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +45 -0
  29. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +20 -0
  30. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +12 -0
  31. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +4 -0
  32. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +3 -0
  33. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +3 -0
  34. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +145 -0
  35. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +124 -0
  36. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +15 -0
  37. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +49 -0
  38. data/lib/ruby_llm/active_record/acts_as.rb +174 -0
  39. data/lib/ruby_llm/active_record/acts_as_legacy.rb +384 -0
  40. data/lib/ruby_llm/active_record/chat_methods.rb +350 -0
  41. data/lib/ruby_llm/active_record/message_methods.rb +81 -0
  42. data/lib/ruby_llm/active_record/model_methods.rb +84 -0
  43. data/lib/ruby_llm/aliases.json +295 -0
  44. data/lib/ruby_llm/aliases.rb +38 -0
  45. data/lib/ruby_llm/attachment.rb +220 -0
  46. data/lib/ruby_llm/chat.rb +816 -0
  47. data/lib/ruby_llm/chunk.rb +6 -0
  48. data/lib/ruby_llm/configuration.rb +78 -0
  49. data/lib/ruby_llm/connection.rb +126 -0
  50. data/lib/ruby_llm/content.rb +73 -0
  51. data/lib/ruby_llm/context.rb +29 -0
  52. data/lib/ruby_llm/embedding.rb +29 -0
  53. data/lib/ruby_llm/error.rb +84 -0
  54. data/lib/ruby_llm/image.rb +49 -0
  55. data/lib/ruby_llm/message.rb +86 -0
  56. data/lib/ruby_llm/mime_type.rb +71 -0
  57. data/lib/ruby_llm/model/info.rb +111 -0
  58. data/lib/ruby_llm/model/modalities.rb +22 -0
  59. data/lib/ruby_llm/model/pricing.rb +48 -0
  60. data/lib/ruby_llm/model/pricing_category.rb +46 -0
  61. data/lib/ruby_llm/model/pricing_tier.rb +33 -0
  62. data/lib/ruby_llm/model.rb +7 -0
  63. data/lib/ruby_llm/models.json +33198 -0
  64. data/lib/ruby_llm/models.rb +231 -0
  65. data/lib/ruby_llm/models_schema.json +168 -0
  66. data/lib/ruby_llm/moderation.rb +56 -0
  67. data/lib/ruby_llm/provider.rb +243 -0
  68. data/lib/ruby_llm/providers/anthropic/capabilities.rb +134 -0
  69. data/lib/ruby_llm/providers/anthropic/chat.rb +125 -0
  70. data/lib/ruby_llm/providers/anthropic/content.rb +44 -0
  71. data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
  72. data/lib/ruby_llm/providers/anthropic/media.rb +92 -0
  73. data/lib/ruby_llm/providers/anthropic/models.rb +63 -0
  74. data/lib/ruby_llm/providers/anthropic/streaming.rb +45 -0
  75. data/lib/ruby_llm/providers/anthropic/tools.rb +109 -0
  76. data/lib/ruby_llm/providers/anthropic.rb +36 -0
  77. data/lib/ruby_llm/providers/bedrock/capabilities.rb +167 -0
  78. data/lib/ruby_llm/providers/bedrock/chat.rb +63 -0
  79. data/lib/ruby_llm/providers/bedrock/media.rb +61 -0
  80. data/lib/ruby_llm/providers/bedrock/models.rb +98 -0
  81. data/lib/ruby_llm/providers/bedrock/signing.rb +831 -0
  82. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +51 -0
  83. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +71 -0
  84. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +67 -0
  85. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +80 -0
  86. data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +78 -0
  87. data/lib/ruby_llm/providers/bedrock/streaming.rb +18 -0
  88. data/lib/ruby_llm/providers/bedrock.rb +82 -0
  89. data/lib/ruby_llm/providers/deepseek/capabilities.rb +130 -0
  90. data/lib/ruby_llm/providers/deepseek/chat.rb +16 -0
  91. data/lib/ruby_llm/providers/deepseek.rb +30 -0
  92. data/lib/ruby_llm/providers/gemini/capabilities.rb +281 -0
  93. data/lib/ruby_llm/providers/gemini/chat.rb +454 -0
  94. data/lib/ruby_llm/providers/gemini/embeddings.rb +37 -0
  95. data/lib/ruby_llm/providers/gemini/images.rb +47 -0
  96. data/lib/ruby_llm/providers/gemini/media.rb +112 -0
  97. data/lib/ruby_llm/providers/gemini/models.rb +40 -0
  98. data/lib/ruby_llm/providers/gemini/streaming.rb +61 -0
  99. data/lib/ruby_llm/providers/gemini/tools.rb +198 -0
  100. data/lib/ruby_llm/providers/gemini/transcription.rb +116 -0
  101. data/lib/ruby_llm/providers/gemini.rb +37 -0
  102. data/lib/ruby_llm/providers/gpustack/chat.rb +27 -0
  103. data/lib/ruby_llm/providers/gpustack/media.rb +46 -0
  104. data/lib/ruby_llm/providers/gpustack/models.rb +90 -0
  105. data/lib/ruby_llm/providers/gpustack.rb +34 -0
  106. data/lib/ruby_llm/providers/mistral/capabilities.rb +155 -0
  107. data/lib/ruby_llm/providers/mistral/chat.rb +24 -0
  108. data/lib/ruby_llm/providers/mistral/embeddings.rb +33 -0
  109. data/lib/ruby_llm/providers/mistral/models.rb +48 -0
  110. data/lib/ruby_llm/providers/mistral.rb +32 -0
  111. data/lib/ruby_llm/providers/ollama/chat.rb +27 -0
  112. data/lib/ruby_llm/providers/ollama/media.rb +46 -0
  113. data/lib/ruby_llm/providers/ollama/models.rb +36 -0
  114. data/lib/ruby_llm/providers/ollama.rb +30 -0
  115. data/lib/ruby_llm/providers/openai/capabilities.rb +299 -0
  116. data/lib/ruby_llm/providers/openai/chat.rb +88 -0
  117. data/lib/ruby_llm/providers/openai/embeddings.rb +33 -0
  118. data/lib/ruby_llm/providers/openai/images.rb +38 -0
  119. data/lib/ruby_llm/providers/openai/media.rb +81 -0
  120. data/lib/ruby_llm/providers/openai/models.rb +39 -0
  121. data/lib/ruby_llm/providers/openai/moderation.rb +34 -0
  122. data/lib/ruby_llm/providers/openai/streaming.rb +46 -0
  123. data/lib/ruby_llm/providers/openai/tools.rb +98 -0
  124. data/lib/ruby_llm/providers/openai/transcription.rb +70 -0
  125. data/lib/ruby_llm/providers/openai.rb +44 -0
  126. data/lib/ruby_llm/providers/openai_responses.rb +395 -0
  127. data/lib/ruby_llm/providers/openrouter/models.rb +73 -0
  128. data/lib/ruby_llm/providers/openrouter.rb +26 -0
  129. data/lib/ruby_llm/providers/perplexity/capabilities.rb +137 -0
  130. data/lib/ruby_llm/providers/perplexity/chat.rb +16 -0
  131. data/lib/ruby_llm/providers/perplexity/models.rb +42 -0
  132. data/lib/ruby_llm/providers/perplexity.rb +48 -0
  133. data/lib/ruby_llm/providers/vertexai/chat.rb +14 -0
  134. data/lib/ruby_llm/providers/vertexai/embeddings.rb +32 -0
  135. data/lib/ruby_llm/providers/vertexai/models.rb +130 -0
  136. data/lib/ruby_llm/providers/vertexai/streaming.rb +14 -0
  137. data/lib/ruby_llm/providers/vertexai/transcription.rb +16 -0
  138. data/lib/ruby_llm/providers/vertexai.rb +55 -0
  139. data/lib/ruby_llm/railtie.rb +35 -0
  140. data/lib/ruby_llm/responses_session.rb +77 -0
  141. data/lib/ruby_llm/stream_accumulator.rb +101 -0
  142. data/lib/ruby_llm/streaming.rb +153 -0
  143. data/lib/ruby_llm/tool.rb +209 -0
  144. data/lib/ruby_llm/tool_call.rb +22 -0
  145. data/lib/ruby_llm/tool_executors.rb +125 -0
  146. data/lib/ruby_llm/transcription.rb +35 -0
  147. data/lib/ruby_llm/utils.rb +91 -0
  148. data/lib/ruby_llm/version.rb +5 -0
  149. data/lib/ruby_llm.rb +140 -0
  150. data/lib/tasks/models.rake +525 -0
  151. data/lib/tasks/release.rake +67 -0
  152. data/lib/tasks/ruby_llm.rake +15 -0
  153. data/lib/tasks/vcr.rake +92 -0
  154. metadata +346 -0
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ class Chunk < Message
5
+ end
6
+ end
@@ -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