ruby_llm 1.0.1 → 1.1.0rc1

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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +28 -12
  3. data/lib/ruby_llm/active_record/acts_as.rb +46 -7
  4. data/lib/ruby_llm/aliases.json +65 -0
  5. data/lib/ruby_llm/aliases.rb +56 -0
  6. data/lib/ruby_llm/chat.rb +10 -9
  7. data/lib/ruby_llm/configuration.rb +4 -0
  8. data/lib/ruby_llm/error.rb +15 -4
  9. data/lib/ruby_llm/models.json +1163 -303
  10. data/lib/ruby_llm/models.rb +40 -11
  11. data/lib/ruby_llm/provider.rb +32 -39
  12. data/lib/ruby_llm/providers/anthropic/capabilities.rb +8 -9
  13. data/lib/ruby_llm/providers/anthropic/chat.rb +31 -4
  14. data/lib/ruby_llm/providers/anthropic/streaming.rb +12 -6
  15. data/lib/ruby_llm/providers/anthropic.rb +4 -0
  16. data/lib/ruby_llm/providers/bedrock/capabilities.rb +168 -0
  17. data/lib/ruby_llm/providers/bedrock/chat.rb +108 -0
  18. data/lib/ruby_llm/providers/bedrock/models.rb +84 -0
  19. data/lib/ruby_llm/providers/bedrock/signing.rb +831 -0
  20. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +46 -0
  21. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +63 -0
  22. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +79 -0
  23. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +90 -0
  24. data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +91 -0
  25. data/lib/ruby_llm/providers/bedrock/streaming.rb +36 -0
  26. data/lib/ruby_llm/providers/bedrock.rb +83 -0
  27. data/lib/ruby_llm/providers/deepseek/chat.rb +17 -0
  28. data/lib/ruby_llm/providers/deepseek.rb +5 -0
  29. data/lib/ruby_llm/providers/gemini/capabilities.rb +50 -34
  30. data/lib/ruby_llm/providers/gemini/chat.rb +8 -15
  31. data/lib/ruby_llm/providers/gemini/images.rb +5 -10
  32. data/lib/ruby_llm/providers/gemini/streaming.rb +35 -76
  33. data/lib/ruby_llm/providers/gemini/tools.rb +12 -12
  34. data/lib/ruby_llm/providers/gemini.rb +4 -0
  35. data/lib/ruby_llm/providers/openai/capabilities.rb +146 -206
  36. data/lib/ruby_llm/providers/openai/streaming.rb +9 -13
  37. data/lib/ruby_llm/providers/openai.rb +4 -0
  38. data/lib/ruby_llm/streaming.rb +96 -0
  39. data/lib/ruby_llm/version.rb +1 -1
  40. data/lib/ruby_llm.rb +6 -3
  41. data/lib/tasks/browser_helper.rb +97 -0
  42. data/lib/tasks/capability_generator.rb +123 -0
  43. data/lib/tasks/capability_scraper.rb +224 -0
  44. data/lib/tasks/cli_helper.rb +22 -0
  45. data/lib/tasks/code_validator.rb +29 -0
  46. data/lib/tasks/model_updater.rb +66 -0
  47. data/lib/tasks/models.rake +28 -193
  48. data/lib/tasks/vcr.rake +13 -30
  49. metadata +27 -19
  50. data/.github/workflows/cicd.yml +0 -158
  51. data/.github/workflows/docs.yml +0 -53
  52. data/.gitignore +0 -59
  53. data/.overcommit.yml +0 -26
  54. data/.rspec +0 -3
  55. data/.rubocop.yml +0 -10
  56. data/.yardopts +0 -12
  57. data/CONTRIBUTING.md +0 -207
  58. data/Gemfile +0 -33
  59. data/Rakefile +0 -9
  60. data/bin/console +0 -17
  61. data/bin/setup +0 -6
  62. data/ruby_llm.gemspec +0 -44
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fd4d6fd8e0894176fff57bd2a3132b965afbb912108d45d94cba3b4b3106ff7c
4
- data.tar.gz: 071a8ff4dcffcba66d042d3e822c2297b8a2c4485c4ee1fb48d0a03ec49c0547
3
+ metadata.gz: 7f1ca42c438f99a40f6ada6ab87e23377403d9d6c8db6049d54960308b6dd42f
4
+ data.tar.gz: 0467734c40a0a64505b8f4de2eea4b929e2b0c615fedb3fe88d75f8cf20040de
5
5
  SHA512:
6
- metadata.gz: 5d995c552810743910286207c47d2342de7e2ee14b833b627267d4fcec3c02931650b499de048319de293914fd0c19689124f818e994e74de04e8c819b934e91
7
- data.tar.gz: 85598c0a2b633092eb5d5fa5b13b59f03b2c3c5effd826f483d64b87f58b8caaf3637b28308d027eaeed3a03d708a0267b420556ca54206de9a9ac69ab97455c
6
+ metadata.gz: ed886fb15081ce27309c0e583223e62adf171907f0f047344763e9eae5ede4dbb96657bb1b69dde9e68bc1ed56acedb93717273067236e40104dddcc09886f33
7
+ data.tar.gz: 7f2aa9c50cfd1a525273256ff014712944c976229f6b90fe338decaf2525bcb82f19dec187d14795ce0891a7fceadb5c048905c8afa8066b0ddbe37d54c48edd
data/README.md CHANGED
@@ -2,17 +2,20 @@
2
2
 
3
3
  A delightful Ruby way to work with AI. No configuration madness, no complex callbacks, no handler hell – just beautiful, expressive Ruby code.
4
4
 
5
- <div style="display: flex; align-items: center; flex-wrap: wrap; gap: 4px;">
5
+ <div style="display: flex; align-items: center; flex-wrap: wrap; margin-bottom: 1em">
6
6
  <img src="https://upload.wikimedia.org/wikipedia/commons/4/4d/OpenAI_Logo.svg" alt="OpenAI" height="40" width="120">
7
- &nbsp;&nbsp;&nbsp;&nbsp;
7
+ &nbsp;&nbsp;
8
8
  <img src="https://upload.wikimedia.org/wikipedia/commons/7/78/Anthropic_logo.svg" alt="Anthropic" height="40" width="120">
9
- &nbsp;&nbsp;&nbsp;&nbsp;
9
+ &nbsp;&nbsp;
10
10
  <img src="https://upload.wikimedia.org/wikipedia/commons/8/8a/Google_Gemini_logo.svg" alt="Google" height="40" width="120">
11
- &nbsp;&nbsp;&nbsp;&nbsp;
11
+ &nbsp;&nbsp;
12
+ <img src="https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/bedrock-color.svg" alt="Bedrock" height="40">
13
+ <img src="https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/bedrock-text.svg" alt="Bedrock" height="40" width="120">
14
+ &nbsp;&nbsp;
12
15
  <img src="https://upload.wikimedia.org/wikipedia/commons/e/ec/DeepSeek_logo.svg" alt="DeepSeek" height="40" width="120">
13
16
  </div>
14
17
 
15
- <a href="https://badge.fury.io/rb/ruby_llm"><img src="https://badge.fury.io/rb/ruby_llm.svg?dummy=unused" alt="Gem Version" /></a>
18
+ <a href="https://badge.fury.io/rb/ruby_llm"><img src="https://badge.fury.io/rb/ruby_llm.svg" alt="Gem Version" /></a>
16
19
  <a href="https://github.com/testdouble/standard"><img src="https://img.shields.io/badge/code_style-standard-brightgreen.svg" alt="Ruby Style Guide" /></a>
17
20
  <a href="https://rubygems.org/gems/ruby_llm"><img alt="Gem Downloads" src="https://img.shields.io/gem/dt/ruby_llm"></a>
18
21
  <a href="https://codecov.io/gh/crmne/ruby_llm"><img src="https://codecov.io/gh/crmne/ruby_llm/branch/main/graph/badge.svg" alt="codecov" /></a>
@@ -27,7 +30,7 @@ RubyLLM fixes all that. One beautiful API for everything. One consistent format.
27
30
 
28
31
  ## Features
29
32
 
30
- - 💬 **Chat** with OpenAI, Anthropic, Gemini, and DeepSeek models
33
+ - 💬 **Chat** with OpenAI, Anthropic, Gemini, AWS Bedrock Anthropic, and DeepSeek models
31
34
  - 👁️ **Vision and Audio** understanding
32
35
  - 📄 **PDF Analysis** for analyzing documents
33
36
  - 🖼️ **Image generation** with DALL-E and other providers
@@ -99,10 +102,16 @@ Configure with your API keys:
99
102
 
100
103
  ```ruby
101
104
  RubyLLM.configure do |config|
102
- config.openai_api_key = ENV['OPENAI_API_KEY']
103
- config.anthropic_api_key = ENV['ANTHROPIC_API_KEY']
104
- config.gemini_api_key = ENV['GEMINI_API_KEY']
105
- config.deepseek_api_key = ENV['DEEPSEEK_API_KEY']
105
+ config.openai_api_key = ENV.fetch('OPENAI_API_KEY', nil)
106
+ config.anthropic_api_key = ENV.fetch('ANTHROPIC_API_KEY', nil)
107
+ config.gemini_api_key = ENV.fetch('GEMINI_API_KEY', nil)
108
+ config.deepseek_api_key = ENV.fetch('DEEPSEEK_API_KEY', nil)
109
+
110
+ # Bedrock
111
+ config.bedrock_api_key = ENV.fetch('AWS_ACCESS_KEY_ID', nil)
112
+ config.bedrock_secret_key = ENV.fetch('AWS_SECRET_ACCESS_KEY', nil)
113
+ config.bedrock_region = ENV.fetch('AWS_REGION', nil)
114
+ config.bedrock_session_token = ENV.fetch('AWS_SESSION_TOKEN', nil)
106
115
  end
107
116
  ```
108
117
 
@@ -126,6 +135,9 @@ chat.ask "Tell me a story about a Ruby programmer" do |chunk|
126
135
  print chunk.content
127
136
  end
128
137
 
138
+ # Set personality or behavior with instructions (aka system prompts) - available from 1.1.0
139
+ chat.with_instructions "You are a friendly Ruby expert who loves to help beginners"
140
+
129
141
  # Understand content in multiple forms
130
142
  chat.ask "Compare these diagrams", with: { image: ["diagram1.png", "diagram2.png"] }
131
143
  chat.ask "Summarize this document", with: { pdf: "contract.pdf" }
@@ -156,8 +168,12 @@ class ToolCall < ApplicationRecord
156
168
  acts_as_tool_call
157
169
  end
158
170
 
159
- # In your controller
160
- chat = Chat.create!(model_id: "gpt-4o-mini")
171
+ # In a background job
172
+ chat = Chat.create! model_id: "gpt-4o-mini"
173
+
174
+ # Set personality or behavior with instructions (aka system prompts) - they're persisted too! - available from 1.1.0
175
+ chat.with_instructions "You are a friendly Ruby expert who loves to help beginners"
176
+
161
177
  chat.ask("What's your favorite Ruby gem?") do |chunk|
162
178
  Turbo::StreamsChannel.broadcast_append_to(
163
179
  chat,
@@ -9,7 +9,7 @@ module RubyLLM
9
9
  extend ActiveSupport::Concern
10
10
 
11
11
  class_methods do # rubocop:disable Metrics/BlockLength
12
- def acts_as_chat(message_class: 'Message', tool_call_class: 'ToolCall') # rubocop:disable Metrics/MethodLength
12
+ def acts_as_chat(message_class: 'Message', tool_call_class: 'ToolCall')
13
13
  include ChatMethods
14
14
 
15
15
  @message_class = message_class.to_s
@@ -21,12 +21,6 @@ module RubyLLM
21
21
  dependent: :destroy
22
22
 
23
23
  delegate :complete,
24
- :with_tool,
25
- :with_tools,
26
- :with_model,
27
- :with_temperature,
28
- :on_new_message,
29
- :on_end_message,
30
24
  :add_message,
31
25
  to: :to_llm
32
26
  end
@@ -85,6 +79,51 @@ module RubyLLM
85
79
  .on_end_message { |msg| persist_message_completion(msg) }
86
80
  end
87
81
 
82
+ def with_instructions(instructions, replace: false)
83
+ transaction do
84
+ # If replace is true, remove existing system messages
85
+ messages.where(role: :system).destroy_all if replace
86
+
87
+ # Create the new system message
88
+ messages.create!(
89
+ role: :system,
90
+ content: instructions
91
+ )
92
+ end
93
+ to_llm.with_instructions(instructions)
94
+ self
95
+ end
96
+
97
+ def with_tool(tool)
98
+ to_llm.with_tool(tool)
99
+ self
100
+ end
101
+
102
+ def with_tools(*tools)
103
+ to_llm.with_tools(*tools)
104
+ self
105
+ end
106
+
107
+ def with_model(model_id, provider: nil)
108
+ to_llm.with_model(model_id, provider: provider)
109
+ self
110
+ end
111
+
112
+ def with_temperature(temperature)
113
+ to_llm.with_temperature(temperature)
114
+ self
115
+ end
116
+
117
+ def on_new_message(&)
118
+ to_llm.on_new_message(&)
119
+ self
120
+ end
121
+
122
+ def on_end_message(&)
123
+ to_llm.on_end_message(&)
124
+ self
125
+ end
126
+
88
127
  def ask(message, &)
89
128
  message = { role: :user, content: message }
90
129
  messages.create!(**message)
@@ -0,0 +1,65 @@
1
+ {
2
+ "claude-3-5-sonnet": {
3
+ "anthropic": "claude-3-5-sonnet-20241022",
4
+ "bedrock": "anthropic.claude-3-5-sonnet-20241022-v2:0"
5
+ },
6
+ "claude-3-5-haiku": {
7
+ "anthropic": "claude-3-5-haiku-20241022",
8
+ "bedrock": "anthropic.claude-3-5-haiku-20241022-v1:0"
9
+ },
10
+ "claude-3-7-sonnet": {
11
+ "anthropic": "claude-3-7-sonnet-20250219",
12
+ "bedrock": "us.anthropic.claude-3-7-sonnet-20250219-v1:0"
13
+ },
14
+ "claude-3-opus": {
15
+ "anthropic": "claude-3-opus-20240229",
16
+ "bedrock": "anthropic.claude-3-opus-20240229-v1:0"
17
+ },
18
+ "claude-3-sonnet": {
19
+ "anthropic": "claude-3-sonnet-20240229",
20
+ "bedrock": "anthropic.claude-3-sonnet-20240229-v1:0"
21
+ },
22
+ "claude-3-haiku": {
23
+ "anthropic": "claude-3-haiku-20240307",
24
+ "bedrock": "anthropic.claude-3-haiku-20240307-v1:0"
25
+ },
26
+ "claude-3": {
27
+ "anthropic": "claude-3-sonnet-20240229",
28
+ "bedrock": "anthropic.claude-3-sonnet-20240229-v1:0"
29
+ },
30
+ "claude-2": {
31
+ "anthropic": "claude-2.0",
32
+ "bedrock": "anthropic.claude-2.0"
33
+ },
34
+ "claude-2-1": {
35
+ "anthropic": "claude-2.1",
36
+ "bedrock": "anthropic.claude-2.1"
37
+ },
38
+ "gpt-4o": {
39
+ "openai": "gpt-4o-2024-11-20"
40
+ },
41
+ "gpt-4o-mini": {
42
+ "openai": "gpt-4o-mini-2024-07-18"
43
+ },
44
+ "gpt-4-turbo": {
45
+ "openai": "gpt-4-turbo-2024-04-09"
46
+ },
47
+ "gemini-1.5-flash": {
48
+ "gemini": "gemini-1.5-flash-002"
49
+ },
50
+ "gemini-1.5-flash-8b": {
51
+ "gemini": "gemini-1.5-flash-8b-001"
52
+ },
53
+ "gemini-1.5-pro": {
54
+ "gemini": "gemini-1.5-pro-002"
55
+ },
56
+ "gemini-2.0-flash": {
57
+ "gemini": "gemini-2.0-flash-001"
58
+ },
59
+ "o1": {
60
+ "openai": "o1-2024-12-17"
61
+ },
62
+ "o3-mini": {
63
+ "openai": "o3-mini-2025-01-31"
64
+ }
65
+ }
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # Manages model aliases, allowing users to reference models by simpler names
5
+ # that map to specific model versions across different providers.
6
+ #
7
+ # Aliases are defined in aliases.json and follow the format:
8
+ # {
9
+ # "simple-name": {
10
+ # "provider1": "specific-version-for-provider1",
11
+ # "provider2": "specific-version-for-provider2"
12
+ # }
13
+ # }
14
+ class Aliases
15
+ class << self
16
+ # Resolves a model ID to its provider-specific version
17
+ #
18
+ # @param model_id [String] the model identifier or alias
19
+ # @param provider_slug [String, Symbol, nil] optional provider to resolve for
20
+ # @return [String] the resolved model ID or the original if no alias exists
21
+ def resolve(model_id, provider = nil)
22
+ return model_id unless aliases[model_id]
23
+
24
+ if provider
25
+ aliases[model_id][provider.to_s] || model_id
26
+ else
27
+ # Get native provider's version
28
+ aliases[model_id].values.first || model_id
29
+ end
30
+ end
31
+
32
+ # Returns the loaded aliases mapping
33
+ # @return [Hash] the aliases mapping
34
+ def aliases
35
+ @aliases ||= load_aliases
36
+ end
37
+
38
+ # Loads aliases from the JSON file
39
+ # @return [Hash] the loaded aliases
40
+ def load_aliases
41
+ file_path = File.expand_path('aliases.json', __dir__)
42
+ if File.exist?(file_path)
43
+ JSON.parse(File.read(file_path))
44
+ else
45
+ {}
46
+ end
47
+ end
48
+
49
+ # Reloads aliases from disk
50
+ # @return [Hash] the reloaded aliases
51
+ def reload!
52
+ @aliases = load_aliases
53
+ end
54
+ end
55
+ end
56
+ end
data/lib/ruby_llm/chat.rb CHANGED
@@ -13,9 +13,9 @@ module RubyLLM
13
13
 
14
14
  attr_reader :model, :messages, :tools
15
15
 
16
- def initialize(model: nil)
16
+ def initialize(model: nil, provider: nil)
17
17
  model_id = model || RubyLLM.config.default_model
18
- self.model = model_id
18
+ with_model(model_id, provider: provider)
19
19
  @temperature = 0.7
20
20
  @messages = []
21
21
  @tools = {}
@@ -32,6 +32,11 @@ module RubyLLM
32
32
 
33
33
  alias say ask
34
34
 
35
+ def with_instructions(instructions)
36
+ add_message role: :system, content: instructions
37
+ self
38
+ end
39
+
35
40
  def with_tool(tool)
36
41
  unless @model.supports_functions
37
42
  raise UnsupportedFunctionsError, "Model #{@model.id} doesn't support function calling"
@@ -47,13 +52,9 @@ module RubyLLM
47
52
  self
48
53
  end
49
54
 
50
- def model=(model_id)
51
- @model = Models.find model_id
52
- @provider = Models.provider_for model_id
53
- end
54
-
55
- def with_model(model_id)
56
- self.model = model_id
55
+ def with_model(model_id, provider: nil)
56
+ @model = Models.find model_id, provider
57
+ @provider = Provider.providers[@model.provider.to_sym] || raise(Error, "Unknown provider: #{@model.provider}")
57
58
  self
58
59
  end
59
60
 
@@ -14,6 +14,10 @@ module RubyLLM
14
14
  :anthropic_api_key,
15
15
  :gemini_api_key,
16
16
  :deepseek_api_key,
17
+ :bedrock_api_key,
18
+ :bedrock_secret_key,
19
+ :bedrock_region,
20
+ :bedrock_session_token,
17
21
  :default_model,
18
22
  :default_embedding_model,
19
23
  :default_image_model,
@@ -19,15 +19,21 @@ module RubyLLM
19
19
  end
20
20
  end
21
21
 
22
- class ModelNotFoundError < StandardError; end
22
+ # Error classes for non-HTTP errors
23
+ class ConfigurationError < StandardError; end
23
24
  class InvalidRoleError < StandardError; end
25
+ class ModelNotFoundError < StandardError; end
24
26
  class UnsupportedFunctionsError < StandardError; end
25
- class UnauthorizedError < Error; end
26
- class PaymentRequiredError < Error; end
27
- class ServiceUnavailableError < Error; end
27
+
28
+ # Error classes for different HTTP status codes
28
29
  class BadRequestError < Error; end
30
+ class ForbiddenError < Error; end
31
+ class OverloadedError < Error; end
32
+ class PaymentRequiredError < Error; end
29
33
  class RateLimitError < Error; end
30
34
  class ServerError < Error; end
35
+ class ServiceUnavailableError < Error; end
36
+ class UnauthorizedError < Error; end
31
37
 
32
38
  # Faraday middleware that maps provider-specific API errors to RubyLLM errors.
33
39
  # Uses provider's parse_error method to extract meaningful error messages.
@@ -56,12 +62,17 @@ module RubyLLM
56
62
  raise UnauthorizedError.new(response, message || 'Invalid API key - check your credentials')
57
63
  when 402
58
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')
59
68
  when 429
60
69
  raise RateLimitError.new(response, message || 'Rate limit exceeded - please wait a moment')
61
70
  when 500
62
71
  raise ServerError.new(response, message || 'API server error - please try again')
63
72
  when 502..503
64
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')
65
76
  else
66
77
  raise Error.new(response, message || 'An unknown error occurred')
67
78
  end