ruby_llm 0.1.0.pre29 → 0.1.0.pre31
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 +27 -0
- 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 +3 -1
- data/lib/ruby_llm/content.rb +79 -0
- data/lib/ruby_llm/embedding.rb +9 -3
- data/lib/ruby_llm/message.rb +9 -1
- data/lib/ruby_llm/models.json +22 -22
- data/lib/ruby_llm/provider.rb +39 -14
- 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/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 +15 -197
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +4 -2
- data/ruby_llm.gemspec +2 -0
- metadata +48 -8
- data/.github/workflows/test.yml +0 -35
- data/lib/ruby_llm/model_capabilities/anthropic.rb +0 -75
- 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: a3c91f92537c2e154f458422c8447345cf1d230da68fb982df3b45b102fe985d
|
4
|
+
data.tar.gz: fd24e6187a456c0ec7ade007cf39b0766e41da58bb8184a0567939ea1c048e75
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6cba0bb735838fb5700d504e8665842815001bbab5c8a1a829131eaefef6e10b7633fde20be63bf4174eaac045298590489a777572a5bbea9141a32392bc7a36
|
7
|
+
data.tar.gz: a2707917f85c6fdd31c8f61f6ae9d3414ac385d17bf017335572cf61be03bd6dcd344fd277e207fcbc0954e3cc445cbdeb40b17d432de0800191a31b0dab7383
|
@@ -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,27 @@
|
|
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/embeddings_spec.rb[1:1:1:1] | passed | 0.2892 seconds |
|
10
|
+
./spec/integration/embeddings_spec.rb[1:1:1:2] | passed | 0.31644 seconds |
|
11
|
+
./spec/integration/embeddings_spec.rb[1:1:2:1] | passed | 0.89277 seconds |
|
12
|
+
./spec/integration/embeddings_spec.rb[1:1:2:2] | passed | 1.55 seconds |
|
13
|
+
./spec/integration/error_handling_spec.rb[1:1] | passed | 0.21297 seconds |
|
14
|
+
./spec/integration/rails_spec.rb[1:1] | passed | 4.05 seconds |
|
15
|
+
./spec/integration/rails_spec.rb[1:2] | passed | 1.82 seconds |
|
16
|
+
./spec/integration/streaming_spec.rb[1:1:1:1] | passed | 0.58445 seconds |
|
17
|
+
./spec/integration/streaming_spec.rb[1:1:1:2] | passed | 6.04 seconds |
|
18
|
+
./spec/integration/streaming_spec.rb[1:1:2:1] | passed | 0.47171 seconds |
|
19
|
+
./spec/integration/streaming_spec.rb[1:1:2:2] | passed | 2.39 seconds |
|
20
|
+
./spec/integration/streaming_spec.rb[1:1:3:1] | passed | 0.72016 seconds |
|
21
|
+
./spec/integration/streaming_spec.rb[1:1:3:2] | passed | 3.59 seconds |
|
22
|
+
./spec/integration/tools_spec.rb[1:1:1:1] | passed | 3.1 seconds |
|
23
|
+
./spec/integration/tools_spec.rb[1:1:1:2] | passed | 7.04 seconds |
|
24
|
+
./spec/integration/tools_spec.rb[1:1:2:1] | passed | 1.42 seconds |
|
25
|
+
./spec/integration/tools_spec.rb[1:1:2:2] | passed | 2.24 seconds |
|
26
|
+
./spec/integration/tools_spec.rb[1:1:3:1] | passed | 2.16 seconds |
|
27
|
+
./spec/integration/tools_spec.rb[1:1:3:2] | passed | 5.26 seconds |
|
@@ -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,10 +16,12 @@ module RubyLLM
|
|
16
16
|
:deepseek_api_key,
|
17
17
|
:default_model,
|
18
18
|
:default_embedding_model,
|
19
|
-
:request_timeout
|
19
|
+
:request_timeout,
|
20
|
+
:max_retries
|
20
21
|
|
21
22
|
def initialize
|
22
23
|
@request_timeout = 120
|
24
|
+
@max_retries = 3
|
23
25
|
@default_model = 'gpt-4o-mini'
|
24
26
|
@default_embedding_model = 'text-embedding-3-small'
|
25
27
|
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
# Represents the content received from the LLM
|
5
|
+
class Content
|
6
|
+
def initialize(text = nil, attachments = {})
|
7
|
+
@parts = []
|
8
|
+
@parts << { type: 'text', text: text } unless text.nil? || text.empty?
|
9
|
+
|
10
|
+
Array(attachments[:image]).each do |source|
|
11
|
+
@parts << attach_image(source)
|
12
|
+
end
|
13
|
+
|
14
|
+
Array(attachments[:audio]).each do |source|
|
15
|
+
@parts << attach_audio(source)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_a
|
20
|
+
return if @parts.empty?
|
21
|
+
|
22
|
+
@parts
|
23
|
+
end
|
24
|
+
|
25
|
+
def format
|
26
|
+
return @parts.first[:text] if @parts.size == 1 && @parts.first[:type] == 'text'
|
27
|
+
|
28
|
+
to_a
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def attach_image(source) # rubocop:disable Metrics/MethodLength
|
34
|
+
source = File.expand_path(source) unless source.start_with?('http')
|
35
|
+
|
36
|
+
return { type: 'image_url', image_url: { url: source } } if source.start_with?('http')
|
37
|
+
|
38
|
+
data = Base64.strict_encode64(File.read(source))
|
39
|
+
mime_type = mime_type_for(source)
|
40
|
+
|
41
|
+
{
|
42
|
+
type: 'image',
|
43
|
+
source: {
|
44
|
+
type: 'base64',
|
45
|
+
media_type: mime_type,
|
46
|
+
data: data
|
47
|
+
}
|
48
|
+
}
|
49
|
+
end
|
50
|
+
|
51
|
+
def attach_audio(source)
|
52
|
+
source = File.expand_path(source) unless source.start_with?('http')
|
53
|
+
data = encode_file(source)
|
54
|
+
format = File.extname(source).delete('.') || 'wav'
|
55
|
+
|
56
|
+
{
|
57
|
+
type: 'input_audio',
|
58
|
+
input_audio: {
|
59
|
+
data: data,
|
60
|
+
format: format
|
61
|
+
}
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
def encode_file(source)
|
66
|
+
if source.start_with?('http')
|
67
|
+
response = Faraday.get(source)
|
68
|
+
Base64.strict_encode64(response.body)
|
69
|
+
else
|
70
|
+
Base64.strict_encode64(File.read(source))
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def mime_type_for(path)
|
75
|
+
ext = File.extname(path).delete('.')
|
76
|
+
"image/#{ext}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
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
|
|
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
@@ -82,7 +82,7 @@
|
|
82
82
|
"provider": "anthropic",
|
83
83
|
"context_window": 100000,
|
84
84
|
"max_tokens": 4096,
|
85
|
-
"type": "
|
85
|
+
"type": "chat",
|
86
86
|
"family": "claude2",
|
87
87
|
"supports_vision": false,
|
88
88
|
"supports_functions": false,
|
@@ -98,7 +98,7 @@
|
|
98
98
|
"provider": "anthropic",
|
99
99
|
"context_window": 100000,
|
100
100
|
"max_tokens": 4096,
|
101
|
-
"type": "
|
101
|
+
"type": "chat",
|
102
102
|
"family": "claude2",
|
103
103
|
"supports_vision": false,
|
104
104
|
"supports_functions": false,
|
@@ -114,7 +114,7 @@
|
|
114
114
|
"provider": "anthropic",
|
115
115
|
"context_window": 200000,
|
116
116
|
"max_tokens": 8192,
|
117
|
-
"type": "
|
117
|
+
"type": "chat",
|
118
118
|
"family": "claude35_haiku",
|
119
119
|
"supports_vision": false,
|
120
120
|
"supports_functions": true,
|
@@ -130,7 +130,7 @@
|
|
130
130
|
"provider": "anthropic",
|
131
131
|
"context_window": 200000,
|
132
132
|
"max_tokens": 8192,
|
133
|
-
"type": "
|
133
|
+
"type": "chat",
|
134
134
|
"family": "claude35_sonnet",
|
135
135
|
"supports_vision": true,
|
136
136
|
"supports_functions": true,
|
@@ -146,7 +146,7 @@
|
|
146
146
|
"provider": "anthropic",
|
147
147
|
"context_window": 200000,
|
148
148
|
"max_tokens": 8192,
|
149
|
-
"type": "
|
149
|
+
"type": "chat",
|
150
150
|
"family": "claude35_sonnet",
|
151
151
|
"supports_vision": true,
|
152
152
|
"supports_functions": true,
|
@@ -162,7 +162,7 @@
|
|
162
162
|
"provider": "anthropic",
|
163
163
|
"context_window": 200000,
|
164
164
|
"max_tokens": 4096,
|
165
|
-
"type": "
|
165
|
+
"type": "chat",
|
166
166
|
"family": "claude3_haiku",
|
167
167
|
"supports_vision": true,
|
168
168
|
"supports_functions": true,
|
@@ -178,7 +178,7 @@
|
|
178
178
|
"provider": "anthropic",
|
179
179
|
"context_window": 200000,
|
180
180
|
"max_tokens": 4096,
|
181
|
-
"type": "
|
181
|
+
"type": "chat",
|
182
182
|
"family": "claude3_opus",
|
183
183
|
"supports_vision": true,
|
184
184
|
"supports_functions": true,
|
@@ -194,7 +194,7 @@
|
|
194
194
|
"provider": "anthropic",
|
195
195
|
"context_window": 200000,
|
196
196
|
"max_tokens": 4096,
|
197
|
-
"type": "
|
197
|
+
"type": "chat",
|
198
198
|
"family": "claude3_sonnet",
|
199
199
|
"supports_vision": true,
|
200
200
|
"supports_functions": true,
|
@@ -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
|
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,7 +27,7 @@ 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
|
@@ -63,9 +59,29 @@ module RubyLLM
|
|
63
59
|
end
|
64
60
|
end
|
65
61
|
|
66
|
-
def connection
|
62
|
+
def connection # rubocop:disable Metrics/MethodLength
|
67
63
|
@connection ||= Faraday.new(api_base) do |f|
|
68
64
|
f.options.timeout = RubyLLM.config.request_timeout
|
65
|
+
|
66
|
+
# Add retry middleware before request/response handling
|
67
|
+
f.request :retry, {
|
68
|
+
max: RubyLLM.config.max_retries,
|
69
|
+
interval: 0.05,
|
70
|
+
interval_randomness: 0.5,
|
71
|
+
backoff_factor: 2,
|
72
|
+
exceptions: [
|
73
|
+
Errno::ETIMEDOUT,
|
74
|
+
Timeout::Error,
|
75
|
+
Faraday::TimeoutError,
|
76
|
+
Faraday::ConnectionFailed,
|
77
|
+
Faraday::RetriableResponse,
|
78
|
+
RubyLLM::RateLimitError,
|
79
|
+
RubyLLM::ServerError,
|
80
|
+
RubyLLM::ServiceUnavailableError
|
81
|
+
],
|
82
|
+
retry_statuses: [429, 500, 502, 503, 504]
|
83
|
+
}
|
84
|
+
|
69
85
|
f.request :json
|
70
86
|
f.response :json
|
71
87
|
f.adapter Faraday.default_adapter
|
@@ -111,9 +127,16 @@ module RubyLLM
|
|
111
127
|
maybe_json
|
112
128
|
end
|
113
129
|
|
130
|
+
def parse_error(response)
|
131
|
+
return if response.body.empty?
|
132
|
+
|
133
|
+
body = try_parse_json(response.body)
|
134
|
+
body.is_a?(Hash) ? body.dig('error', 'message') : body
|
135
|
+
end
|
136
|
+
|
114
137
|
def capabilities
|
115
138
|
provider_name = self.class.name.split('::').last
|
116
|
-
|
139
|
+
provider_name::Capabilities
|
117
140
|
end
|
118
141
|
|
119
142
|
def slug
|
@@ -121,15 +144,17 @@ module RubyLLM
|
|
121
144
|
end
|
122
145
|
|
123
146
|
class << self
|
124
|
-
def
|
125
|
-
|
147
|
+
def extended(base)
|
148
|
+
base.extend(Methods)
|
149
|
+
end
|
150
|
+
|
151
|
+
def register(name, provider_module)
|
152
|
+
providers[name.to_sym] = provider_module
|
126
153
|
end
|
127
154
|
|
128
155
|
def for(model)
|
129
156
|
model_info = Models.find(model)
|
130
|
-
|
131
|
-
|
132
|
-
provider_class.new
|
157
|
+
providers[model_info.provider.to_sym]
|
133
158
|
end
|
134
159
|
|
135
160
|
def providers
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
module Anthropic
|
6
|
+
# Determines capabilities and pricing for Anthropic models
|
7
|
+
module Capabilities
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def determine_context_window(model_id)
|
11
|
+
case model_id
|
12
|
+
when /claude-3/ then 200_000
|
13
|
+
else 100_000
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def determine_max_tokens(model_id)
|
18
|
+
case model_id
|
19
|
+
when /claude-3-5/ then 8_192
|
20
|
+
else 4_096
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def get_input_price(model_id)
|
25
|
+
PRICES.dig(model_family(model_id), :input) || default_input_price
|
26
|
+
end
|
27
|
+
|
28
|
+
def get_output_price(model_id)
|
29
|
+
PRICES.dig(model_family(model_id), :output) || default_output_price
|
30
|
+
end
|
31
|
+
|
32
|
+
def supports_vision?(model_id)
|
33
|
+
return false if model_id.match?(/claude-3-5-haiku/)
|
34
|
+
return false if model_id.match?(/claude-[12]/)
|
35
|
+
|
36
|
+
true
|
37
|
+
end
|
38
|
+
|
39
|
+
def supports_functions?(model_id)
|
40
|
+
model_id.include?('claude-3')
|
41
|
+
end
|
42
|
+
|
43
|
+
def supports_json_mode?(model_id)
|
44
|
+
model_id.include?('claude-3')
|
45
|
+
end
|
46
|
+
|
47
|
+
def model_family(model_id)
|
48
|
+
case model_id
|
49
|
+
when /claude-3-5-sonnet/ then :claude35_sonnet
|
50
|
+
when /claude-3-5-haiku/ then :claude35_haiku
|
51
|
+
when /claude-3-opus/ then :claude3_opus
|
52
|
+
when /claude-3-sonnet/ then :claude3_sonnet
|
53
|
+
when /claude-3-haiku/ then :claude3_haiku
|
54
|
+
else :claude2
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def model_type(_)
|
59
|
+
'chat'
|
60
|
+
end
|
61
|
+
|
62
|
+
PRICES = {
|
63
|
+
claude35_sonnet: { input: 3.0, output: 15.0 }, # $3.00/$15.00 per million tokens
|
64
|
+
claude35_haiku: { input: 0.80, output: 4.0 }, # $0.80/$4.00 per million tokens
|
65
|
+
claude3_opus: { input: 15.0, output: 75.0 }, # $15.00/$75.00 per million tokens
|
66
|
+
claude3_sonnet: { input: 3.0, output: 15.0 }, # $3.00/$15.00 per million tokens
|
67
|
+
claude3_haiku: { input: 0.25, output: 1.25 }, # $0.25/$1.25 per million tokens
|
68
|
+
claude2: { input: 3.0, output: 15.0 } # Default pricing for Claude 2.x models
|
69
|
+
}.freeze
|
70
|
+
|
71
|
+
def default_input_price
|
72
|
+
3.0
|
73
|
+
end
|
74
|
+
|
75
|
+
def default_output_price
|
76
|
+
15.0
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|