ruby_llm 0.1.0.pre30 → 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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/{gem-push.yml → cicd.yml} +32 -4
  3. data/.rspec_status +27 -0
  4. data/lib/ruby_llm/active_record/acts_as.rb +5 -5
  5. data/lib/ruby_llm/chat.rb +2 -2
  6. data/lib/ruby_llm/configuration.rb +3 -1
  7. data/lib/ruby_llm/content.rb +79 -0
  8. data/lib/ruby_llm/embedding.rb +9 -3
  9. data/lib/ruby_llm/message.rb +9 -1
  10. data/lib/ruby_llm/models.json +14 -14
  11. data/lib/ruby_llm/provider.rb +39 -14
  12. data/lib/ruby_llm/providers/anthropic/capabilities.rb +81 -0
  13. data/lib/ruby_llm/providers/anthropic/chat.rb +86 -0
  14. data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
  15. data/lib/ruby_llm/providers/anthropic/models.rb +48 -0
  16. data/lib/ruby_llm/providers/anthropic/streaming.rb +37 -0
  17. data/lib/ruby_llm/providers/anthropic/tools.rb +97 -0
  18. data/lib/ruby_llm/providers/anthropic.rb +8 -234
  19. data/lib/ruby_llm/providers/deepseek/capabilites.rb +101 -0
  20. data/lib/ruby_llm/providers/deepseek.rb +4 -2
  21. data/lib/ruby_llm/providers/gemini/capabilities.rb +191 -0
  22. data/lib/ruby_llm/providers/gemini/models.rb +20 -0
  23. data/lib/ruby_llm/providers/gemini.rb +5 -10
  24. data/lib/ruby_llm/providers/openai/capabilities.rb +191 -0
  25. data/lib/ruby_llm/providers/openai/chat.rb +68 -0
  26. data/lib/ruby_llm/providers/openai/embeddings.rb +39 -0
  27. data/lib/ruby_llm/providers/openai/models.rb +40 -0
  28. data/lib/ruby_llm/providers/openai/streaming.rb +31 -0
  29. data/lib/ruby_llm/providers/openai/tools.rb +69 -0
  30. data/lib/ruby_llm/providers/openai.rb +15 -197
  31. data/lib/ruby_llm/version.rb +1 -1
  32. data/lib/ruby_llm.rb +4 -2
  33. data/ruby_llm.gemspec +2 -0
  34. metadata +48 -8
  35. data/.github/workflows/test.yml +0 -35
  36. data/lib/ruby_llm/model_capabilities/anthropic.rb +0 -79
  37. data/lib/ruby_llm/model_capabilities/deepseek.rb +0 -132
  38. data/lib/ruby_llm/model_capabilities/gemini.rb +0 -190
  39. 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: a3c91f92537c2e154f458422c8447345cf1d230da68fb982df3b45b102fe985d
4
+ data.tar.gz: fd24e6187a456c0ec7ade007cf39b0766e41da58bb8184a0567939ea1c048e75
5
5
  SHA512:
6
- metadata.gz: fa21a4d89b3704f384c257a7bcca71e42b0442b2f660ddd2c93d108f77bcf547b5d1c05101d85269abc5aab8fbe37e3bec3da8a4fd09d77f8184ed923fea0986
7
- data.tar.gz: 5295d0c1ec5660e4ce9e7388836bbc37d82eae0ccce77a4f601f59ed91933cb2dc31b84f79f5aab3271d5b8b318aa190ca0224926a28c69a0e409ed9035db4c8
6
+ metadata.gz: 6cba0bb735838fb5700d504e8665842815001bbab5c8a1a829131eaefef6e10b7633fde20be63bf4174eaac045298590489a777572a5bbea9141a32392bc7a36
7
+ data.tar.gz: a2707917f85c6fdd31c8f61f6ae9d3414ac385d17bf017335572cf61be03bd6dcd344fd277e207fcbc0954e3cc445cbdeb40b17d432de0800191a31b0dab7383
@@ -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,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 = 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,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
@@ -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
 
@@ -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
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,7 +27,7 @@ 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
@@ -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
- RubyLLM.const_get "ModelCapabilities::#{provider_name}"
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 register(name, provider_class)
125
- providers[name.to_sym] = provider_class
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
- provider_class = providers[model_info.provider.to_sym]
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
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ module Anthropic
6
+ # Chat methods of the OpenAI API integration
7
+ module Chat
8
+ private
9
+
10
+ def completion_url
11
+ '/v1/messages'
12
+ end
13
+
14
+ def render_payload(messages, tools:, temperature:, model:, stream: false)
15
+ {
16
+ model: model,
17
+ messages: messages.map { |msg| format_message(msg) },
18
+ temperature: temperature,
19
+ stream: stream,
20
+ max_tokens: RubyLLM.models.find(model).max_tokens
21
+ }.tap do |payload|
22
+ payload[:tools] = tools.values.map { |t| function_for(t) } if tools.any?
23
+ end
24
+ end
25
+
26
+ def parse_completion_response(response)
27
+ data = response.body
28
+ content_blocks = data['content'] || []
29
+
30
+ text_content = extract_text_content(content_blocks)
31
+ tool_use = find_tool_use(content_blocks)
32
+
33
+ build_message(data, text_content, tool_use)
34
+ end
35
+
36
+ def extract_text_content(blocks)
37
+ text_blocks = blocks.select { |c| c['type'] == 'text' }
38
+ text_blocks.map { |c| c['text'] }.join('')
39
+ end
40
+
41
+ def build_message(data, content, tool_use)
42
+ Message.new(
43
+ role: :assistant,
44
+ content: content,
45
+ tool_calls: parse_tool_calls(tool_use),
46
+ input_tokens: data.dig('usage', 'input_tokens'),
47
+ output_tokens: data.dig('usage', 'output_tokens'),
48
+ model_id: data['model']
49
+ )
50
+ end
51
+
52
+ def format_message(msg)
53
+ if msg.tool_call?
54
+ format_tool_call(msg)
55
+ elsif msg.tool_result?
56
+ format_tool_result(msg)
57
+ else
58
+ format_basic_message(msg)
59
+ end
60
+ end
61
+
62
+ def format_basic_message(msg)
63
+ {
64
+ role: convert_role(msg.role),
65
+ content: msg.content
66
+ }
67
+ end
68
+
69
+ def convert_role(role)
70
+ case role
71
+ when :tool then 'user'
72
+ when :user then 'user'
73
+ else 'assistant'
74
+ end
75
+ end
76
+
77
+ def format_text_block(content)
78
+ {
79
+ type: 'text',
80
+ text: content
81
+ }
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ module Anthropic
6
+ # Embeddings methods of the Anthropic API integration
7
+ module Embeddings
8
+ private
9
+
10
+ def embed
11
+ raise Error "Anthropic doesn't support embeddings"
12
+ end
13
+
14
+ alias render_embedding_payload embed
15
+ alias embedding_url embed
16
+ alias parse_embedding_response embed
17
+ end
18
+ end
19
+ end
20
+ end