ruby_llm 0.1.0.pre30 → 0.1.0.pre33

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/{gem-push.yml → cicd.yml} +32 -4
  3. data/.rspec_status +38 -0
  4. data/README.md +52 -3
  5. data/lib/ruby_llm/active_record/acts_as.rb +5 -5
  6. data/lib/ruby_llm/chat.rb +2 -2
  7. data/lib/ruby_llm/configuration.rb +5 -1
  8. data/lib/ruby_llm/content.rb +81 -0
  9. data/lib/ruby_llm/embedding.rb +9 -3
  10. data/lib/ruby_llm/image.rb +24 -0
  11. data/lib/ruby_llm/message.rb +9 -1
  12. data/lib/ruby_llm/models.json +14 -14
  13. data/lib/ruby_llm/provider.rb +57 -16
  14. data/lib/ruby_llm/providers/anthropic/capabilities.rb +81 -0
  15. data/lib/ruby_llm/providers/anthropic/chat.rb +86 -0
  16. data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
  17. data/lib/ruby_llm/providers/anthropic/models.rb +48 -0
  18. data/lib/ruby_llm/providers/anthropic/streaming.rb +37 -0
  19. data/lib/ruby_llm/providers/anthropic/tools.rb +97 -0
  20. data/lib/ruby_llm/providers/anthropic.rb +8 -234
  21. data/lib/ruby_llm/providers/deepseek/capabilites.rb +101 -0
  22. data/lib/ruby_llm/providers/deepseek.rb +4 -2
  23. data/lib/ruby_llm/providers/gemini/capabilities.rb +191 -0
  24. data/lib/ruby_llm/providers/gemini/models.rb +20 -0
  25. data/lib/ruby_llm/providers/gemini.rb +5 -10
  26. data/lib/ruby_llm/providers/openai/capabilities.rb +191 -0
  27. data/lib/ruby_llm/providers/openai/chat.rb +68 -0
  28. data/lib/ruby_llm/providers/openai/embeddings.rb +39 -0
  29. data/lib/ruby_llm/providers/openai/images.rb +38 -0
  30. data/lib/ruby_llm/providers/openai/media.rb +52 -0
  31. data/lib/ruby_llm/providers/openai/models.rb +40 -0
  32. data/lib/ruby_llm/providers/openai/streaming.rb +31 -0
  33. data/lib/ruby_llm/providers/openai/tools.rb +69 -0
  34. data/lib/ruby_llm/providers/openai.rb +22 -200
  35. data/lib/ruby_llm/version.rb +1 -1
  36. data/lib/ruby_llm.rb +8 -2
  37. data/ruby_llm.gemspec +7 -5
  38. metadata +57 -13
  39. data/.github/workflows/test.yml +0 -35
  40. data/lib/ruby_llm/model_capabilities/anthropic.rb +0 -79
  41. data/lib/ruby_llm/model_capabilities/deepseek.rb +0 -132
  42. data/lib/ruby_llm/model_capabilities/gemini.rb +0 -190
  43. data/lib/ruby_llm/model_capabilities/openai.rb +0 -189
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 33b602ae544211e79207cf51bbfad5f1ed9c78cd1d9710d4f57916c95377bb60
4
- data.tar.gz: 3fac0d6aed252218d7f7bd8e460f7b281c0306ca066e0194193e11a44ac8400d
3
+ metadata.gz: e36a864dcd3852d03d467684aafcc811273f0f44dbd1925484440808c6c8895b
4
+ data.tar.gz: 5ed1b436a1b91f7fac06ab76d43fd0c7dfee925cc1af280a10bb40e448dabf0b
5
5
  SHA512:
6
- metadata.gz: fa21a4d89b3704f384c257a7bcca71e42b0442b2f660ddd2c93d108f77bcf547b5d1c05101d85269abc5aab8fbe37e3bec3da8a4fd09d77f8184ed923fea0986
7
- data.tar.gz: 5295d0c1ec5660e4ce9e7388836bbc37d82eae0ccce77a4f601f59ed91933cb2dc31b84f79f5aab3271d5b8b318aa190ca0224926a28c69a0e409ed9035db4c8
6
+ metadata.gz: 6dd5a24c4a374a5fd291c7f47363dd282abcc850361464e67859f82ad88f51f49f0d4fda89299cf3871df838896acb111170a3615963c215a2aedb6b4154d0bd
7
+ data.tar.gz: 07f0931ca91abe8c42b7ca52910f4ac31f34ec4e049a321a7a4d59808cd190813aab1534312bb279d609a94f2ee6fbb2164faa6330a82a80ff520697741a7088
@@ -1,18 +1,46 @@
1
- name: Ruby Gem
1
+ name: CI
2
2
 
3
3
  on:
4
4
  push:
5
5
  branches: [ "main" ]
6
6
  pull_request:
7
7
  branches: [ "main" ]
8
+ workflow_call:
8
9
 
9
10
  jobs:
10
11
  test:
11
- uses: ./.github/workflows/test.yml
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ matrix:
15
+ ruby-version: ['3.1']
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - name: Set up Ruby
21
+ uses: ruby/setup-ruby@v1
22
+ with:
23
+ ruby-version: ${{ matrix.ruby-version }}
24
+ bundler-cache: true
25
+
26
+ - name: Install dependencies
27
+ run: bundle install
28
+
29
+ - name: Check code format
30
+ run: bundle exec rubocop
31
+
32
+ - name: Run tests
33
+ env:
34
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
35
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
36
+ GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
37
+ DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
38
+ run: bundle exec rspec
12
39
 
13
- build:
14
- needs: test # This ensures tests must pass before building/publishing
40
+ publish:
15
41
  name: Build + Publish
42
+ # needs: test
43
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
16
44
  runs-on: ubuntu-latest
17
45
  permissions:
18
46
  contents: read
data/.rspec_status ADDED
@@ -0,0 +1,38 @@
1
+ example_id | status | run_time |
2
+ -------------------------------------------------- | ------ | --------------- |
3
+ ./spec/integration/chat_spec.rb[1:1:1:1] | passed | 0.5826 seconds |
4
+ ./spec/integration/chat_spec.rb[1:1:1:2] | passed | 3.43 seconds |
5
+ ./spec/integration/chat_spec.rb[1:1:2:1] | passed | 0.49383 seconds |
6
+ ./spec/integration/chat_spec.rb[1:1:2:2] | passed | 1.36 seconds |
7
+ ./spec/integration/chat_spec.rb[1:1:3:1] | passed | 0.74049 seconds |
8
+ ./spec/integration/chat_spec.rb[1:1:3:2] | passed | 4.11 seconds |
9
+ ./spec/integration/content_spec.rb[1:1:1] | passed | 3.87 seconds |
10
+ ./spec/integration/content_spec.rb[1:1:2] | passed | 1.23 seconds |
11
+ ./spec/integration/content_spec.rb[1:1:3] | passed | 2.07 seconds |
12
+ ./spec/integration/content_spec.rb[1:2:1] | passed | 2.05 seconds |
13
+ ./spec/integration/content_spec.rb[1:2:2] | passed | 2.88 seconds |
14
+ ./spec/integration/embeddings_spec.rb[1:1:1:1] | passed | 0.30185 seconds |
15
+ ./spec/integration/embeddings_spec.rb[1:1:1:2] | passed | 0.30812 seconds |
16
+ ./spec/integration/embeddings_spec.rb[1:1:2:1] | passed | 13.05 seconds |
17
+ ./spec/integration/embeddings_spec.rb[1:1:2:2] | passed | 0.78135 seconds |
18
+ ./spec/integration/error_handling_spec.rb[1:1] | passed | 0.21297 seconds |
19
+ ./spec/integration/image_generation_spec.rb[1:1:1] | passed | 12.44 seconds |
20
+ ./spec/integration/image_generation_spec.rb[1:1:2] | passed | 17.66 seconds |
21
+ ./spec/integration/image_generation_spec.rb[1:1:3] | passed | 0.00324 seconds |
22
+ ./spec/integration/image_generation_spec.rb[1:1:4] | failed | 0.15682 seconds |
23
+ ./spec/integration/image_generation_spec.rb[1:1:5] | passed | 18.69 seconds |
24
+ ./spec/integration/image_generation_spec.rb[1:1:6] | passed | 0.00032 seconds |
25
+ ./spec/integration/rails_spec.rb[1:1] | passed | 4.05 seconds |
26
+ ./spec/integration/rails_spec.rb[1:2] | passed | 1.82 seconds |
27
+ ./spec/integration/streaming_spec.rb[1:1:1:1] | passed | 0.58445 seconds |
28
+ ./spec/integration/streaming_spec.rb[1:1:1:2] | passed | 6.04 seconds |
29
+ ./spec/integration/streaming_spec.rb[1:1:2:1] | passed | 0.47171 seconds |
30
+ ./spec/integration/streaming_spec.rb[1:1:2:2] | passed | 2.39 seconds |
31
+ ./spec/integration/streaming_spec.rb[1:1:3:1] | passed | 0.72016 seconds |
32
+ ./spec/integration/streaming_spec.rb[1:1:3:2] | passed | 3.59 seconds |
33
+ ./spec/integration/tools_spec.rb[1:1:1:1] | passed | 3.1 seconds |
34
+ ./spec/integration/tools_spec.rb[1:1:1:2] | passed | 7.04 seconds |
35
+ ./spec/integration/tools_spec.rb[1:1:2:1] | passed | 1.42 seconds |
36
+ ./spec/integration/tools_spec.rb[1:1:2:2] | passed | 2.24 seconds |
37
+ ./spec/integration/tools_spec.rb[1:1:3:1] | passed | 2.16 seconds |
38
+ ./spec/integration/tools_spec.rb[1:1:3:2] | passed | 5.26 seconds |
data/README.md CHANGED
@@ -1,20 +1,33 @@
1
1
  # RubyLLM
2
2
 
3
- A delightful Ruby way to work with AI language models. Provides a unified interface to OpenAI, Anthropic, Google, and DeepSeek models with automatic token counting, proper streaming support, and a focus on developer happiness. No wrapping your head around multiple APIs - just clean Ruby code that works.
3
+ A delightful Ruby way to work with AI. Chat in text, analyze and generate images, understand audio, and use tools through a unified interface to OpenAI, Anthropic, Google, and DeepSeek. Built for developer happiness with automatic token counting, proper streaming, and Rails integration. No wrapping your head around multiple APIs - just clean Ruby code that works.
4
4
 
5
5
  <p align="center">
6
6
  <img src="https://upload.wikimedia.org/wikipedia/commons/4/4d/OpenAI_Logo.svg" alt="OpenAI" height="40" width="120">
7
7
  &nbsp;&nbsp;&nbsp;&nbsp;
8
8
  <img src="https://upload.wikimedia.org/wikipedia/commons/7/78/Anthropic_logo.svg" alt="Anthropic" height="40" width="120">
9
9
  &nbsp;&nbsp;&nbsp;&nbsp;
10
- <img src="https://upload.wikimedia.org/wikipedia/commons/8/8a/Google_Gemini_logo.svg" alt="Google" height="40" width="120">
10
+ <img src="https://upload.wikimedia.org/wikipedia/commons/8/8a/Google_Gemini_logo.svg" alt="Google" height="40" width="120">
11
11
  &nbsp;&nbsp;&nbsp;&nbsp;
12
- <img src="https://upload.wikimedia.org/wikipedia/commons/e/ec/DeepSeek_logo.svg" alt="DeepSeek" height="40" width="120"]>
12
+ <img src="https://upload.wikimedia.org/wikipedia/commons/e/ec/DeepSeek_logo.svg" alt="DeepSeek" height="40" width="120">
13
13
  </p>
14
14
 
15
15
  [![Gem Version](https://badge.fury.io/rb/ruby_llm.svg)](https://badge.fury.io/rb/ruby_llm)
16
16
  [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
17
17
 
18
+ ## Features
19
+
20
+ - 💬 **Beautiful Chat Interface** - Converse with AI models as easily as `RubyLLM.chat.ask "teach me Ruby"`
21
+ - 🎵 **Audio Analysis** - Get audio transcription and understanding with `chat.ask "what's said here?", with: { audio: "clip.wav" }`
22
+ - 👁️ **Vision Understanding** - Let AIs analyze images with a simple `chat.ask "what's in this?", with: { image: "photo.jpg" }`
23
+ - 🌊 **Streaming** - Real-time responses with proper Ruby streaming with `chat.ask "hello" do |chunk| puts chunk.content end`
24
+ - 🚂 **Rails Integration** - Persist chats and messages with ActiveRecord with `acts_as_{chat|message|tool_call}`
25
+ - 🛠️ **Tool Support** - Give AIs access to your Ruby code with `chat.with_tool(Calculator).ask "what's 2+2?"`
26
+ - 🎨 **Paint with AI** - Create images as easily as `RubyLLM.paint "a sunset over mountains"`
27
+ - 📊 **Embeddings** - Generate vector embeddings for your text with `RubyLLM.embed "hello"`
28
+ - 🔄 **Multi-Provider Support** - Works with OpenAI, Anthropic, Google, and DeepSeek
29
+ - 🎯 **Token Tracking** - Automatic usage tracking across providers
30
+
18
31
  ## Installation
19
32
 
20
33
  Add it to your Gemfile:
@@ -87,11 +100,47 @@ chat.ask "Tell me a story about a Ruby programmer" do |chunk|
87
100
  print chunk.content
88
101
  end
89
102
 
103
+ # Ask about images
104
+ chat.ask "What do you see in this image?", with: { image: "ruby_logo.png" }
105
+
106
+ # Get analysis of audio content
107
+ chat.ask "What's being said in this recording?", with: { audio: "meeting.wav" }
108
+
109
+ # Combine multiple pieces of content
110
+ chat.ask "Compare these diagrams", with: { image: ["diagram1.png", "diagram2.png"] }
111
+
90
112
  # Check token usage
91
113
  last_message = chat.messages.last
92
114
  puts "Conversation used #{last_message.input_tokens} input tokens and #{last_message.output_tokens} output tokens"
93
115
  ```
94
116
 
117
+ You can provide content as local files or URLs - RubyLLM handles the rest. Vision and audio capabilities are available with compatible models (Claude 3, GPT-4V, Gemini Pro Vision). The API stays clean and consistent whether you're working with text, images, or audio.
118
+
119
+ ## Image Generation
120
+
121
+ Want to create AI-generated images? RubyLLM makes it super simple:
122
+
123
+ ```ruby
124
+ # Paint a picture!
125
+ image = RubyLLM.paint "a starry night over San Francisco in Van Gogh's style"
126
+ image.url # => "https://..."
127
+ image.revised_prompt # Shows how DALL-E interpreted your prompt
128
+
129
+ # Choose size and model
130
+ image = RubyLLM.paint(
131
+ "a cyberpunk cityscape at sunset",
132
+ model: "dall-e-3",
133
+ size: "1792x1024"
134
+ )
135
+
136
+ # Set your default model
137
+ RubyLLM.configure do |config|
138
+ config.default_image_model = "dall-e-3"
139
+ end
140
+ ```
141
+
142
+ RubyLLM automatically handles all the complexities of the DALL-E API, token/credit management, and error handling, so you can focus on being creative.
143
+
95
144
  ## Text Embeddings
96
145
 
97
146
  Need vector embeddings for your text? RubyLLM makes it simple:
@@ -73,22 +73,22 @@ module RubyLLM
73
73
  end
74
74
 
75
75
  def to_llm
76
- chat = RubyLLM.chat(model: model_id)
76
+ @chat ||= RubyLLM.chat(model: model_id)
77
77
 
78
78
  # Load existing messages into chat
79
79
  messages.each do |msg|
80
- chat.add_message(msg.to_llm)
80
+ @chat.add_message(msg.to_llm)
81
81
  end
82
82
 
83
83
  # Set up message persistence
84
- chat.on_new_message { persist_new_message }
85
- .on_end_message { |msg| persist_message_completion(msg) }
84
+ @chat.on_new_message { persist_new_message }
85
+ .on_end_message { |msg| persist_message_completion(msg) }
86
86
  end
87
87
 
88
88
  def ask(message, &block)
89
89
  message = { role: :user, content: message }
90
90
  messages.create!(**message)
91
- chat.complete(&block)
91
+ to_llm.complete(&block)
92
92
  end
93
93
 
94
94
  alias say ask
data/lib/ruby_llm/chat.rb CHANGED
@@ -25,8 +25,8 @@ module RubyLLM
25
25
  }
26
26
  end
27
27
 
28
- def ask(message, &block)
29
- add_message role: :user, content: message
28
+ def ask(message = nil, with: {}, &block)
29
+ add_message role: :user, content: Content.new(message, with)
30
30
  complete(&block)
31
31
  end
32
32
 
@@ -16,12 +16,16 @@ module RubyLLM
16
16
  :deepseek_api_key,
17
17
  :default_model,
18
18
  :default_embedding_model,
19
- :request_timeout
19
+ :default_image_model,
20
+ :request_timeout,
21
+ :max_retries
20
22
 
21
23
  def initialize
22
24
  @request_timeout = 120
25
+ @max_retries = 3
23
26
  @default_model = 'gpt-4o-mini'
24
27
  @default_embedding_model = 'text-embedding-3-small'
28
+ @default_image_model = 'dall-e-3'
25
29
  end
26
30
  end
27
31
  end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # Represents the content sent to or received from an LLM.
5
+ # Stores data in a standard internal format, letting providers
6
+ # handle their own formatting needs.
7
+ class Content
8
+ def initialize(text = nil, attachments = {})
9
+ @parts = []
10
+ @parts << { type: 'text', text: text } unless text.nil? || text.empty?
11
+
12
+ Array(attachments[:image]).each do |source|
13
+ @parts << attach_image(source)
14
+ end
15
+
16
+ Array(attachments[:audio]).each do |source|
17
+ @parts << attach_audio(source)
18
+ end
19
+ end
20
+
21
+ def to_a
22
+ return if @parts.empty?
23
+
24
+ @parts
25
+ end
26
+
27
+ def format
28
+ return @parts.first[:text] if @parts.size == 1 && @parts.first[:type] == 'text'
29
+
30
+ to_a
31
+ end
32
+
33
+ private
34
+
35
+ def attach_image(source) # rubocop:disable Metrics/MethodLength
36
+ source = File.expand_path(source) unless source.start_with?('http')
37
+
38
+ return { type: 'image', source: { url: source } } if source.start_with?('http')
39
+
40
+ data = Base64.strict_encode64(File.read(source))
41
+ mime_type = mime_type_for(source)
42
+
43
+ {
44
+ type: 'image',
45
+ source: {
46
+ type: 'base64',
47
+ media_type: mime_type,
48
+ data: data
49
+ }
50
+ }
51
+ end
52
+
53
+ def attach_audio(source)
54
+ source = File.expand_path(source) unless source.start_with?('http')
55
+ data = encode_file(source)
56
+ format = File.extname(source).delete('.') || 'wav'
57
+
58
+ {
59
+ type: 'input_audio',
60
+ input_audio: {
61
+ data: data,
62
+ format: format
63
+ }
64
+ }
65
+ end
66
+
67
+ def encode_file(source)
68
+ if source.start_with?('http')
69
+ response = Faraday.get(source)
70
+ Base64.strict_encode64(response.body)
71
+ else
72
+ Base64.strict_encode64(File.read(source))
73
+ end
74
+ end
75
+
76
+ def mime_type_for(path)
77
+ ext = File.extname(path).delete('.')
78
+ "image/#{ext}"
79
+ end
80
+ end
81
+ end
@@ -3,10 +3,16 @@
3
3
  module RubyLLM
4
4
  # Core embedding interface. Provides a clean way to generate embeddings
5
5
  # from text using various provider models.
6
- module Embedding
7
- module_function
6
+ class Embedding
7
+ attr_reader :vectors, :model, :input_tokens
8
8
 
9
- def embed(text, model: nil)
9
+ def initialize(vectors:, model:, input_tokens: 0)
10
+ @vectors = vectors
11
+ @model = model
12
+ @input_tokens = input_tokens
13
+ end
14
+
15
+ def self.embed(text, model: nil)
10
16
  model_id = model || RubyLLM.config.default_embedding_model
11
17
  Models.find(model_id)
12
18
 
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # Represents a generated image from an AI model.
5
+ # Provides an interface to image generation capabilities
6
+ # from providers like DALL-E.
7
+ class Image
8
+ attr_reader :url, :revised_prompt, :model_id
9
+
10
+ def initialize(url:, revised_prompt: nil, model_id: nil)
11
+ @url = url
12
+ @revised_prompt = revised_prompt
13
+ @model_id = model_id
14
+ end
15
+
16
+ def self.paint(prompt, model: nil, size: '1024x1024')
17
+ model_id = model || RubyLLM.config.default_image_model
18
+ Models.find(model_id) # Validate model exists
19
+
20
+ provider = Provider.for(model_id)
21
+ provider.paint(prompt, model: model_id, size: size)
22
+ end
23
+ end
24
+ end
@@ -11,7 +11,7 @@ module RubyLLM
11
11
 
12
12
  def initialize(options = {})
13
13
  @role = options[:role].to_sym
14
- @content = options[:content]
14
+ @content = normalize_content(options[:content])
15
15
  @tool_calls = options[:tool_calls]
16
16
  @input_tokens = options[:input_tokens]
17
17
  @output_tokens = options[:output_tokens]
@@ -47,6 +47,14 @@ module RubyLLM
47
47
 
48
48
  private
49
49
 
50
+ def normalize_content(content)
51
+ case content
52
+ when Content then content.format
53
+ when String then Content.new(content).format
54
+ else content
55
+ end
56
+ end
57
+
50
58
  def ensure_valid_role
51
59
  raise InvalidRoleError, "Expected role to be one of: #{ROLES.join(', ')}" unless ROLES.include?(role)
52
60
  end
@@ -727,7 +727,7 @@
727
727
  "family": "gemini20_flash",
728
728
  "supports_vision": true,
729
729
  "supports_functions": true,
730
- "supports_json_mode": false,
730
+ "supports_json_mode": true,
731
731
  "input_price_per_million": 0.1,
732
732
  "output_price_per_million": 0.4,
733
733
  "metadata": {
@@ -746,7 +746,7 @@
746
746
  "family": "gemini20_flash",
747
747
  "supports_vision": true,
748
748
  "supports_functions": true,
749
- "supports_json_mode": false,
749
+ "supports_json_mode": true,
750
750
  "input_price_per_million": 0.1,
751
751
  "output_price_per_million": 0.4,
752
752
  "metadata": {
@@ -765,7 +765,7 @@
765
765
  "family": "gemini20_flash",
766
766
  "supports_vision": true,
767
767
  "supports_functions": true,
768
- "supports_json_mode": false,
768
+ "supports_json_mode": true,
769
769
  "input_price_per_million": 0.1,
770
770
  "output_price_per_million": 0.4,
771
771
  "metadata": {
@@ -784,7 +784,7 @@
784
784
  "family": "gemini20_flash",
785
785
  "supports_vision": true,
786
786
  "supports_functions": true,
787
- "supports_json_mode": false,
787
+ "supports_json_mode": true,
788
788
  "input_price_per_million": 0.1,
789
789
  "output_price_per_million": 0.4,
790
790
  "metadata": {
@@ -803,7 +803,7 @@
803
803
  "family": "gemini20_flash",
804
804
  "supports_vision": true,
805
805
  "supports_functions": true,
806
- "supports_json_mode": false,
806
+ "supports_json_mode": true,
807
807
  "input_price_per_million": 0.1,
808
808
  "output_price_per_million": 0.4,
809
809
  "metadata": {
@@ -822,7 +822,7 @@
822
822
  "family": "gemini20_flash",
823
823
  "supports_vision": true,
824
824
  "supports_functions": true,
825
- "supports_json_mode": false,
825
+ "supports_json_mode": true,
826
826
  "input_price_per_million": 0.1,
827
827
  "output_price_per_million": 0.4,
828
828
  "metadata": {
@@ -841,7 +841,7 @@
841
841
  "family": "gemini20_flash_lite",
842
842
  "supports_vision": true,
843
843
  "supports_functions": false,
844
- "supports_json_mode": false,
844
+ "supports_json_mode": true,
845
845
  "input_price_per_million": 0.075,
846
846
  "output_price_per_million": 0.3,
847
847
  "metadata": {
@@ -860,7 +860,7 @@
860
860
  "family": "gemini20_flash_lite",
861
861
  "supports_vision": true,
862
862
  "supports_functions": false,
863
- "supports_json_mode": false,
863
+ "supports_json_mode": true,
864
864
  "input_price_per_million": 0.075,
865
865
  "output_price_per_million": 0.3,
866
866
  "metadata": {
@@ -879,7 +879,7 @@
879
879
  "family": "gemini20_flash",
880
880
  "supports_vision": true,
881
881
  "supports_functions": true,
882
- "supports_json_mode": false,
882
+ "supports_json_mode": true,
883
883
  "input_price_per_million": 0.1,
884
884
  "output_price_per_million": 0.4,
885
885
  "metadata": {
@@ -898,7 +898,7 @@
898
898
  "family": "gemini20_flash",
899
899
  "supports_vision": true,
900
900
  "supports_functions": true,
901
- "supports_json_mode": false,
901
+ "supports_json_mode": true,
902
902
  "input_price_per_million": 0.1,
903
903
  "output_price_per_million": 0.4,
904
904
  "metadata": {
@@ -917,7 +917,7 @@
917
917
  "family": "gemini20_flash",
918
918
  "supports_vision": true,
919
919
  "supports_functions": true,
920
- "supports_json_mode": false,
920
+ "supports_json_mode": true,
921
921
  "input_price_per_million": 0.1,
922
922
  "output_price_per_million": 0.4,
923
923
  "metadata": {
@@ -936,7 +936,7 @@
936
936
  "family": "gemini20_flash",
937
937
  "supports_vision": true,
938
938
  "supports_functions": true,
939
- "supports_json_mode": false,
939
+ "supports_json_mode": true,
940
940
  "input_price_per_million": 0.1,
941
941
  "output_price_per_million": 0.4,
942
942
  "metadata": {
@@ -955,7 +955,7 @@
955
955
  "family": "other",
956
956
  "supports_vision": true,
957
957
  "supports_functions": true,
958
- "supports_json_mode": false,
958
+ "supports_json_mode": true,
959
959
  "input_price_per_million": 0.075,
960
960
  "output_price_per_million": 0.3,
961
961
  "metadata": {
@@ -974,7 +974,7 @@
974
974
  "family": "other",
975
975
  "supports_vision": true,
976
976
  "supports_functions": true,
977
- "supports_json_mode": false,
977
+ "supports_json_mode": true,
978
978
  "input_price_per_million": 0.075,
979
979
  "output_price_per_million": 0.3,
980
980
  "metadata": {
@@ -5,15 +5,11 @@ module RubyLLM
5
5
  # Handles the complexities of API communication, streaming responses,
6
6
  # and error handling so individual providers can focus on their unique features.
7
7
  module Provider
8
- def self.included(base)
9
- base.include(InstanceMethods)
10
- end
11
-
12
8
  # Common functionality for all LLM providers. Implements the core provider
13
9
  # interface so specific providers only need to implement a few key methods.
14
- module InstanceMethods
10
+ module Methods # rubocop:disable Metrics/ModuleLength
15
11
  def complete(messages, tools:, temperature:, model:, &block)
16
- payload = build_payload messages, tools: tools, temperature: temperature, model: model, stream: block_given?
12
+ payload = render_payload messages, tools: tools, temperature: temperature, model: model, stream: block_given?
17
13
 
18
14
  if block_given?
19
15
  stream_response payload, &block
@@ -31,11 +27,18 @@ module RubyLLM
31
27
  end
32
28
 
33
29
  def embed(text, model:)
34
- payload = build_embedding_payload text, model: model
30
+ payload = render_embedding_payload text, model: model
35
31
  response = post embedding_url, payload
36
32
  parse_embedding_response response
37
33
  end
38
34
 
35
+ def paint(prompt, model:, size:)
36
+ payload = render_image_payload(prompt, model:, size:)
37
+
38
+ response = post(images_url, payload)
39
+ parse_image_response(response)
40
+ end
41
+
39
42
  private
40
43
 
41
44
  def sync_response(payload)
@@ -63,14 +66,43 @@ module RubyLLM
63
66
  end
64
67
  end
65
68
 
66
- def connection
67
- @connection ||= Faraday.new(api_base) do |f|
69
+ def connection # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
70
+ @connection ||= Faraday.new(api_base) do |f| # rubocop:disable Metrics/BlockLength
68
71
  f.options.timeout = RubyLLM.config.request_timeout
72
+
73
+ f.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
+
84
+ f.request :retry, {
85
+ max: RubyLLM.config.max_retries,
86
+ interval: 0.05,
87
+ interval_randomness: 0.5,
88
+ backoff_factor: 2,
89
+ exceptions: [
90
+ Errno::ETIMEDOUT,
91
+ Timeout::Error,
92
+ Faraday::TimeoutError,
93
+ Faraday::ConnectionFailed,
94
+ Faraday::RetriableResponse,
95
+ RubyLLM::RateLimitError,
96
+ RubyLLM::ServerError,
97
+ RubyLLM::ServiceUnavailableError
98
+ ],
99
+ retry_statuses: [429, 500, 502, 503, 504]
100
+ }
101
+
69
102
  f.request :json
70
103
  f.response :json
71
104
  f.adapter Faraday.default_adapter
72
105
  f.use :llm_errors, provider: self
73
- f.response :logger, RubyLLM.logger, { headers: false, bodies: true, errors: true, log_level: :debug }
74
106
  end
75
107
  end
76
108
 
@@ -111,9 +143,16 @@ module RubyLLM
111
143
  maybe_json
112
144
  end
113
145
 
146
+ def parse_error(response)
147
+ return if response.body.empty?
148
+
149
+ body = try_parse_json(response.body)
150
+ body.is_a?(Hash) ? body.dig('error', 'message') : body
151
+ end
152
+
114
153
  def capabilities
115
154
  provider_name = self.class.name.split('::').last
116
- RubyLLM.const_get "ModelCapabilities::#{provider_name}"
155
+ provider_name::Capabilities
117
156
  end
118
157
 
119
158
  def slug
@@ -121,15 +160,17 @@ module RubyLLM
121
160
  end
122
161
 
123
162
  class << self
124
- def register(name, provider_class)
125
- providers[name.to_sym] = provider_class
163
+ def extended(base)
164
+ base.extend(Methods)
165
+ end
166
+
167
+ def register(name, provider_module)
168
+ providers[name.to_sym] = provider_module
126
169
  end
127
170
 
128
171
  def for(model)
129
172
  model_info = Models.find(model)
130
- provider_class = providers[model_info.provider.to_sym]
131
-
132
- provider_class.new
173
+ providers[model_info.provider.to_sym]
133
174
  end
134
175
 
135
176
  def providers