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.
- checksums.yaml +4 -4
- data/.github/workflows/{gem-push.yml → cicd.yml} +32 -4
- data/.rspec_status +38 -0
- data/README.md +52 -3
- data/lib/ruby_llm/active_record/acts_as.rb +5 -5
- data/lib/ruby_llm/chat.rb +2 -2
- data/lib/ruby_llm/configuration.rb +5 -1
- data/lib/ruby_llm/content.rb +81 -0
- data/lib/ruby_llm/embedding.rb +9 -3
- data/lib/ruby_llm/image.rb +24 -0
- data/lib/ruby_llm/message.rb +9 -1
- data/lib/ruby_llm/models.json +14 -14
- data/lib/ruby_llm/provider.rb +57 -16
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +81 -0
- data/lib/ruby_llm/providers/anthropic/chat.rb +86 -0
- data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
- data/lib/ruby_llm/providers/anthropic/models.rb +48 -0
- data/lib/ruby_llm/providers/anthropic/streaming.rb +37 -0
- data/lib/ruby_llm/providers/anthropic/tools.rb +97 -0
- data/lib/ruby_llm/providers/anthropic.rb +8 -234
- data/lib/ruby_llm/providers/deepseek/capabilites.rb +101 -0
- data/lib/ruby_llm/providers/deepseek.rb +4 -2
- data/lib/ruby_llm/providers/gemini/capabilities.rb +191 -0
- data/lib/ruby_llm/providers/gemini/models.rb +20 -0
- data/lib/ruby_llm/providers/gemini.rb +5 -10
- data/lib/ruby_llm/providers/openai/capabilities.rb +191 -0
- data/lib/ruby_llm/providers/openai/chat.rb +68 -0
- data/lib/ruby_llm/providers/openai/embeddings.rb +39 -0
- data/lib/ruby_llm/providers/openai/images.rb +38 -0
- data/lib/ruby_llm/providers/openai/media.rb +52 -0
- data/lib/ruby_llm/providers/openai/models.rb +40 -0
- data/lib/ruby_llm/providers/openai/streaming.rb +31 -0
- data/lib/ruby_llm/providers/openai/tools.rb +69 -0
- data/lib/ruby_llm/providers/openai.rb +22 -200
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +8 -2
- data/ruby_llm.gemspec +7 -5
- metadata +57 -13
- data/.github/workflows/test.yml +0 -35
- data/lib/ruby_llm/model_capabilities/anthropic.rb +0 -79
- data/lib/ruby_llm/model_capabilities/deepseek.rb +0 -132
- data/lib/ruby_llm/model_capabilities/gemini.rb +0 -190
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e36a864dcd3852d03d467684aafcc811273f0f44dbd1925484440808c6c8895b
|
4
|
+
data.tar.gz: 5ed1b436a1b91f7fac06ab76d43fd0c7dfee925cc1af280a10bb40e448dabf0b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6dd5a24c4a374a5fd291c7f47363dd282abcc850361464e67859f82ad88f51f49f0d4fda89299cf3871df838896acb111170a3615963c215a2aedb6b4154d0bd
|
7
|
+
data.tar.gz: 07f0931ca91abe8c42b7ca52910f4ac31f34ec4e049a321a7a4d59808cd190813aab1534312bb279d609a94f2ee6fbb2164faa6330a82a80ff520697741a7088
|
@@ -1,18 +1,46 @@
|
|
1
|
-
name:
|
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
|
-
|
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
|
-
|
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
|
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
|
|
8
8
|
<img src="https://upload.wikimedia.org/wikipedia/commons/7/78/Anthropic_logo.svg" alt="Anthropic" height="40" width="120">
|
9
9
|
|
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
|
|
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
|
[](https://badge.fury.io/rb/ruby_llm)
|
16
16
|
[](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
|
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
|
-
|
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
|
-
|
91
|
+
to_llm.complete(&block)
|
92
92
|
end
|
93
93
|
|
94
94
|
alias say ask
|
data/lib/ruby_llm/chat.rb
CHANGED
@@ -16,12 +16,16 @@ module RubyLLM
|
|
16
16
|
:deepseek_api_key,
|
17
17
|
:default_model,
|
18
18
|
:default_embedding_model,
|
19
|
-
:
|
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
|
data/lib/ruby_llm/embedding.rb
CHANGED
@@ -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
|
-
|
7
|
-
|
6
|
+
class Embedding
|
7
|
+
attr_reader :vectors, :model, :input_tokens
|
8
8
|
|
9
|
-
def
|
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
|
data/lib/ruby_llm/message.rb
CHANGED
@@ -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
|
data/lib/ruby_llm/models.json
CHANGED
@@ -727,7 +727,7 @@
|
|
727
727
|
"family": "gemini20_flash",
|
728
728
|
"supports_vision": true,
|
729
729
|
"supports_functions": true,
|
730
|
-
"supports_json_mode":
|
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":
|
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":
|
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":
|
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":
|
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":
|
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":
|
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":
|
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":
|
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":
|
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":
|
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":
|
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":
|
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":
|
977
|
+
"supports_json_mode": true,
|
978
978
|
"input_price_per_million": 0.075,
|
979
979
|
"output_price_per_million": 0.3,
|
980
980
|
"metadata": {
|
data/lib/ruby_llm/provider.rb
CHANGED
@@ -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
|
10
|
+
module Methods # rubocop:disable Metrics/ModuleLength
|
15
11
|
def complete(messages, tools:, temperature:, model:, &block)
|
16
|
-
payload =
|
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 =
|
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
|
-
|
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
|
125
|
-
|
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
|
-
|
131
|
-
|
132
|
-
provider_class.new
|
173
|
+
providers[model_info.provider.to_sym]
|
133
174
|
end
|
134
175
|
|
135
176
|
def providers
|