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.
Files changed (129) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +157 -0
  4. data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +3 -0
  5. data/lib/generators/ruby_llm/install/templates/create_chats_legacy_migration.rb.tt +8 -0
  6. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +8 -0
  7. data/lib/generators/ruby_llm/install/templates/create_messages_legacy_migration.rb.tt +16 -0
  8. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +16 -0
  9. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +43 -0
  10. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +15 -0
  11. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +9 -0
  12. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +4 -0
  13. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +3 -0
  14. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +3 -0
  15. data/lib/generators/ruby_llm/install_generator.rb +184 -0
  16. data/lib/generators/ruby_llm/migrate_model_fields/templates/migration.rb.tt +142 -0
  17. data/lib/generators/ruby_llm/migrate_model_fields_generator.rb +84 -0
  18. data/lib/ruby_llm/active_record/acts_as.rb +137 -0
  19. data/lib/ruby_llm/active_record/acts_as_legacy.rb +398 -0
  20. data/lib/ruby_llm/active_record/chat_methods.rb +315 -0
  21. data/lib/ruby_llm/active_record/message_methods.rb +72 -0
  22. data/lib/ruby_llm/active_record/model_methods.rb +84 -0
  23. data/lib/ruby_llm/aliases.json +274 -0
  24. data/lib/ruby_llm/aliases.rb +38 -0
  25. data/lib/ruby_llm/attachment.rb +191 -0
  26. data/lib/ruby_llm/chat.rb +212 -0
  27. data/lib/ruby_llm/chunk.rb +6 -0
  28. data/lib/ruby_llm/configuration.rb +69 -0
  29. data/lib/ruby_llm/connection.rb +137 -0
  30. data/lib/ruby_llm/content.rb +50 -0
  31. data/lib/ruby_llm/context.rb +29 -0
  32. data/lib/ruby_llm/embedding.rb +29 -0
  33. data/lib/ruby_llm/error.rb +76 -0
  34. data/lib/ruby_llm/image.rb +49 -0
  35. data/lib/ruby_llm/message.rb +76 -0
  36. data/lib/ruby_llm/mime_type.rb +67 -0
  37. data/lib/ruby_llm/model/info.rb +103 -0
  38. data/lib/ruby_llm/model/modalities.rb +22 -0
  39. data/lib/ruby_llm/model/pricing.rb +48 -0
  40. data/lib/ruby_llm/model/pricing_category.rb +46 -0
  41. data/lib/ruby_llm/model/pricing_tier.rb +33 -0
  42. data/lib/ruby_llm/model.rb +7 -0
  43. data/lib/ruby_llm/models.json +31418 -0
  44. data/lib/ruby_llm/models.rb +235 -0
  45. data/lib/ruby_llm/models_schema.json +168 -0
  46. data/lib/ruby_llm/provider.rb +215 -0
  47. data/lib/ruby_llm/providers/anthropic/capabilities.rb +134 -0
  48. data/lib/ruby_llm/providers/anthropic/chat.rb +106 -0
  49. data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
  50. data/lib/ruby_llm/providers/anthropic/media.rb +91 -0
  51. data/lib/ruby_llm/providers/anthropic/models.rb +48 -0
  52. data/lib/ruby_llm/providers/anthropic/streaming.rb +43 -0
  53. data/lib/ruby_llm/providers/anthropic/tools.rb +107 -0
  54. data/lib/ruby_llm/providers/anthropic.rb +36 -0
  55. data/lib/ruby_llm/providers/bedrock/capabilities.rb +167 -0
  56. data/lib/ruby_llm/providers/bedrock/chat.rb +63 -0
  57. data/lib/ruby_llm/providers/bedrock/media.rb +60 -0
  58. data/lib/ruby_llm/providers/bedrock/models.rb +98 -0
  59. data/lib/ruby_llm/providers/bedrock/signing.rb +831 -0
  60. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +51 -0
  61. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +56 -0
  62. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +67 -0
  63. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +78 -0
  64. data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +78 -0
  65. data/lib/ruby_llm/providers/bedrock/streaming.rb +18 -0
  66. data/lib/ruby_llm/providers/bedrock.rb +82 -0
  67. data/lib/ruby_llm/providers/deepseek/capabilities.rb +130 -0
  68. data/lib/ruby_llm/providers/deepseek/chat.rb +16 -0
  69. data/lib/ruby_llm/providers/deepseek.rb +30 -0
  70. data/lib/ruby_llm/providers/dify/capabilities.rb +16 -0
  71. data/lib/ruby_llm/providers/dify/chat.rb +59 -0
  72. data/lib/ruby_llm/providers/dify/media.rb +37 -0
  73. data/lib/ruby_llm/providers/dify/streaming.rb +28 -0
  74. data/lib/ruby_llm/providers/dify.rb +48 -0
  75. data/lib/ruby_llm/providers/gemini/capabilities.rb +276 -0
  76. data/lib/ruby_llm/providers/gemini/chat.rb +171 -0
  77. data/lib/ruby_llm/providers/gemini/embeddings.rb +37 -0
  78. data/lib/ruby_llm/providers/gemini/images.rb +47 -0
  79. data/lib/ruby_llm/providers/gemini/media.rb +54 -0
  80. data/lib/ruby_llm/providers/gemini/models.rb +40 -0
  81. data/lib/ruby_llm/providers/gemini/streaming.rb +61 -0
  82. data/lib/ruby_llm/providers/gemini/tools.rb +77 -0
  83. data/lib/ruby_llm/providers/gemini.rb +36 -0
  84. data/lib/ruby_llm/providers/gpustack/chat.rb +27 -0
  85. data/lib/ruby_llm/providers/gpustack/media.rb +45 -0
  86. data/lib/ruby_llm/providers/gpustack/models.rb +90 -0
  87. data/lib/ruby_llm/providers/gpustack.rb +34 -0
  88. data/lib/ruby_llm/providers/mistral/capabilities.rb +155 -0
  89. data/lib/ruby_llm/providers/mistral/chat.rb +24 -0
  90. data/lib/ruby_llm/providers/mistral/embeddings.rb +33 -0
  91. data/lib/ruby_llm/providers/mistral/models.rb +48 -0
  92. data/lib/ruby_llm/providers/mistral.rb +32 -0
  93. data/lib/ruby_llm/providers/ollama/chat.rb +27 -0
  94. data/lib/ruby_llm/providers/ollama/media.rb +45 -0
  95. data/lib/ruby_llm/providers/ollama/models.rb +36 -0
  96. data/lib/ruby_llm/providers/ollama.rb +30 -0
  97. data/lib/ruby_llm/providers/openai/capabilities.rb +291 -0
  98. data/lib/ruby_llm/providers/openai/chat.rb +83 -0
  99. data/lib/ruby_llm/providers/openai/embeddings.rb +33 -0
  100. data/lib/ruby_llm/providers/openai/images.rb +38 -0
  101. data/lib/ruby_llm/providers/openai/media.rb +80 -0
  102. data/lib/ruby_llm/providers/openai/models.rb +39 -0
  103. data/lib/ruby_llm/providers/openai/streaming.rb +41 -0
  104. data/lib/ruby_llm/providers/openai/tools.rb +78 -0
  105. data/lib/ruby_llm/providers/openai.rb +42 -0
  106. data/lib/ruby_llm/providers/openrouter/models.rb +73 -0
  107. data/lib/ruby_llm/providers/openrouter.rb +26 -0
  108. data/lib/ruby_llm/providers/perplexity/capabilities.rb +137 -0
  109. data/lib/ruby_llm/providers/perplexity/chat.rb +16 -0
  110. data/lib/ruby_llm/providers/perplexity/models.rb +42 -0
  111. data/lib/ruby_llm/providers/perplexity.rb +48 -0
  112. data/lib/ruby_llm/providers/vertexai/chat.rb +14 -0
  113. data/lib/ruby_llm/providers/vertexai/embeddings.rb +32 -0
  114. data/lib/ruby_llm/providers/vertexai/models.rb +130 -0
  115. data/lib/ruby_llm/providers/vertexai/streaming.rb +14 -0
  116. data/lib/ruby_llm/providers/vertexai.rb +55 -0
  117. data/lib/ruby_llm/railtie.rb +41 -0
  118. data/lib/ruby_llm/stream_accumulator.rb +97 -0
  119. data/lib/ruby_llm/streaming.rb +153 -0
  120. data/lib/ruby_llm/tool.rb +83 -0
  121. data/lib/ruby_llm/tool_call.rb +22 -0
  122. data/lib/ruby_llm/utils.rb +45 -0
  123. data/lib/ruby_llm/version.rb +5 -0
  124. data/lib/ruby_llm.rb +97 -0
  125. data/lib/tasks/models.rake +525 -0
  126. data/lib/tasks/release.rake +67 -0
  127. data/lib/tasks/ruby_llm.rake +15 -0
  128. data/lib/tasks/vcr.rake +92 -0
  129. metadata +291 -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,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