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
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ module Anthropic
6
+ # Models methods of the Anthropic API integration
7
+ module Models
8
+ private
9
+
10
+ def models_url
11
+ '/v1/models'
12
+ end
13
+
14
+ def parse_list_models_response(response) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
15
+ (response.body['data'] || []).map do |model|
16
+ ModelInfo.new(
17
+ id: model['id'],
18
+ created_at: Time.parse(model['created_at']),
19
+ display_name: model['display_name'],
20
+ provider: slug,
21
+ type: capabilities.model_type(model['id']),
22
+ family: capabilities.model_family(model['id']),
23
+ context_window: capabilities.determine_context_window(model['id']),
24
+ max_tokens: capabilities.determine_max_tokens(model['id']),
25
+ supports_vision: capabilities.supports_vision?(model['id']),
26
+ supports_functions: capabilities.supports_functions?(model['id']),
27
+ supports_json_mode: capabilities.supports_json_mode?(model['id']),
28
+ input_price_per_million: capabilities.get_input_price(model['id']),
29
+ output_price_per_million: capabilities.get_output_price(model['id'])
30
+ )
31
+ end
32
+ end
33
+
34
+ def extract_model_id(data)
35
+ data.dig('message', 'model')
36
+ end
37
+
38
+ def extract_input_tokens(data)
39
+ data.dig('message', 'usage', 'input_tokens')
40
+ end
41
+
42
+ def extract_output_tokens(data)
43
+ data.dig('message', 'usage', 'output_tokens') || data.dig('usage', 'output_tokens')
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ module Anthropic
6
+ # Streaming methods of the Anthropic API integration
7
+ module Streaming
8
+ private
9
+
10
+ def stream_url
11
+ completion_url
12
+ end
13
+
14
+ def handle_stream(&block)
15
+ to_json_stream do |data|
16
+ block.call(build_chunk(data))
17
+ end
18
+ end
19
+
20
+ def build_chunk(data)
21
+ Chunk.new(
22
+ role: :assistant,
23
+ model_id: extract_model_id(data),
24
+ content: data.dig('delta', 'text'),
25
+ input_tokens: extract_input_tokens(data),
26
+ output_tokens: extract_output_tokens(data),
27
+ tool_calls: extract_tool_calls(data)
28
+ )
29
+ end
30
+
31
+ def json_delta?(data)
32
+ data['type'] == 'content_block_delta' && data.dig('delta', 'type') == 'input_json_delta'
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ module Anthropic
6
+ # Tools methods of the Anthropic API integration
7
+ module Tools
8
+ private
9
+
10
+ def find_tool_use(blocks)
11
+ blocks.find { |c| c['type'] == 'tool_use' }
12
+ end
13
+
14
+ def format_tool_call(msg)
15
+ tool_call = msg.tool_calls.values.first
16
+
17
+ {
18
+ role: 'assistant',
19
+ content: [
20
+ format_text_block(msg.content),
21
+ format_tool_use_block(tool_call)
22
+ ]
23
+ }
24
+ end
25
+
26
+ def format_tool_result(msg)
27
+ {
28
+ role: 'user',
29
+ content: [format_tool_result_block(msg)]
30
+ }
31
+ end
32
+
33
+ def format_tool_use_block(tool_call)
34
+ {
35
+ type: 'tool_use',
36
+ id: tool_call.id,
37
+ name: tool_call.name,
38
+ input: tool_call.arguments
39
+ }
40
+ end
41
+
42
+ def format_tool_result_block(msg)
43
+ {
44
+ type: 'tool_result',
45
+ tool_use_id: msg.tool_call_id,
46
+ content: msg.content
47
+ }
48
+ end
49
+
50
+ def function_for(tool)
51
+ {
52
+ name: tool.name,
53
+ description: tool.description,
54
+ input_schema: {
55
+ type: 'object',
56
+ properties: clean_parameters(tool.parameters),
57
+ required: required_parameters(tool.parameters)
58
+ }
59
+ }
60
+ end
61
+
62
+ def extract_tool_calls(data)
63
+ if json_delta?(data)
64
+ { nil => ToolCall.new(id: nil, name: nil, arguments: data.dig('delta', 'partial_json')) }
65
+ else
66
+ parse_tool_calls(data['content_block'])
67
+ end
68
+ end
69
+
70
+ def parse_tool_calls(content_block)
71
+ return nil unless content_block && content_block['type'] == 'tool_use'
72
+
73
+ {
74
+ content_block['id'] => ToolCall.new(
75
+ id: content_block['id'],
76
+ name: content_block['name'],
77
+ arguments: content_block['input']
78
+ )
79
+ }
80
+ end
81
+
82
+ def clean_parameters(parameters)
83
+ parameters.transform_values do |param|
84
+ {
85
+ type: param.type,
86
+ description: param.description
87
+ }.compact
88
+ end
89
+ end
90
+
91
+ def required_parameters(parameters)
92
+ parameters.select { |_, param| param.required }.keys
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -4,17 +4,15 @@ module RubyLLM
4
4
  module Providers
5
5
  # Anthropic Claude API integration. Handles the complexities of
6
6
  # Claude's unique message format and tool calling conventions.
7
- class Anthropic # rubocop:disable Metrics/ClassLength
8
- include Provider
7
+ module Anthropic
8
+ extend Provider
9
+ extend Anthropic::Chat
10
+ extend Anthropic::Embeddings
11
+ extend Anthropic::Models
12
+ extend Anthropic::Streaming
13
+ extend Anthropic::Tools
9
14
 
10
- def parse_error(response)
11
- return if response.body.empty?
12
-
13
- body = try_parse_json(response.body)
14
- body.is_a?(Hash) ? body.dig('error', 'message') : body
15
- end
16
-
17
- private
15
+ module_function
18
16
 
19
17
  def api_base
20
18
  'https://api.anthropic.com'
@@ -26,230 +24,6 @@ module RubyLLM
26
24
  'anthropic-version' => '2023-06-01'
27
25
  }
28
26
  end
29
-
30
- def completion_url
31
- '/v1/messages'
32
- end
33
-
34
- def stream_url
35
- completion_url
36
- end
37
-
38
- def models_url
39
- '/v1/models'
40
- end
41
-
42
- def build_payload(messages, tools:, temperature:, model:, stream: false)
43
- {
44
- model: model,
45
- messages: format_messages(messages),
46
- temperature: temperature,
47
- stream: stream,
48
- max_tokens: RubyLLM.models.find(model).max_tokens
49
- }.tap do |payload|
50
- payload[:tools] = tools.values.map { |t| function_for(t) } if tools.any?
51
- end
52
- end
53
-
54
- def parse_completion_response(response)
55
- data = response.body
56
- content_blocks = data['content'] || []
57
-
58
- text_content = extract_text_content(content_blocks)
59
- tool_use = find_tool_use(content_blocks)
60
-
61
- build_message(data, text_content, tool_use)
62
- end
63
-
64
- def extract_text_content(blocks)
65
- text_blocks = blocks.select { |c| c['type'] == 'text' }
66
- text_blocks.map { |c| c['text'] }.join('')
67
- end
68
-
69
- def find_tool_use(blocks)
70
- blocks.find { |c| c['type'] == 'tool_use' }
71
- end
72
-
73
- def build_message(data, content, tool_use)
74
- Message.new(
75
- role: :assistant,
76
- content: content,
77
- tool_calls: parse_tool_calls(tool_use),
78
- input_tokens: data.dig('usage', 'input_tokens'),
79
- output_tokens: data.dig('usage', 'output_tokens'),
80
- model_id: data['model']
81
- )
82
- end
83
-
84
- def parse_list_models_response(response) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
85
- (response.body['data'] || []).map do |model|
86
- ModelInfo.new(
87
- id: model['id'],
88
- created_at: Time.parse(model['created_at']),
89
- display_name: model['display_name'],
90
- provider: slug,
91
- type: capabilities.model_type(model['id']),
92
- family: capabilities.model_family(model['id']),
93
- context_window: capabilities.determine_context_window(model['id']),
94
- max_tokens: capabilities.determine_max_tokens(model['id']),
95
- supports_vision: capabilities.supports_vision?(model['id']),
96
- supports_functions: capabilities.supports_functions?(model['id']),
97
- supports_json_mode: capabilities.supports_json_mode?(model['id']),
98
- input_price_per_million: capabilities.get_input_price(model['id']),
99
- output_price_per_million: capabilities.get_output_price(model['id'])
100
- )
101
- end
102
- end
103
-
104
- def handle_stream(&block)
105
- to_json_stream do |data|
106
- block.call(build_chunk(data))
107
- end
108
- end
109
-
110
- def build_chunk(data)
111
- Chunk.new(
112
- role: :assistant,
113
- model_id: extract_model_id(data),
114
- content: data.dig('delta', 'text'),
115
- input_tokens: extract_input_tokens(data),
116
- output_tokens: extract_output_tokens(data),
117
- tool_calls: extract_tool_calls(data)
118
- )
119
- end
120
-
121
- def extract_model_id(data)
122
- data.dig('message', 'model')
123
- end
124
-
125
- def extract_input_tokens(data)
126
- data.dig('message', 'usage', 'input_tokens')
127
- end
128
-
129
- def extract_output_tokens(data)
130
- data.dig('message', 'usage', 'output_tokens') || data.dig('usage', 'output_tokens')
131
- end
132
-
133
- def extract_tool_calls(data)
134
- if json_delta?(data)
135
- { nil => ToolCall.new(id: nil, name: nil, arguments: data.dig('delta', 'partial_json')) }
136
- else
137
- parse_tool_calls(data['content_block'])
138
- end
139
- end
140
-
141
- def json_delta?(data)
142
- data['type'] == 'content_block_delta' && data.dig('delta', 'type') == 'input_json_delta'
143
- end
144
-
145
- def parse_tool_calls(content_block)
146
- return nil unless content_block && content_block['type'] == 'tool_use'
147
-
148
- {
149
- content_block['id'] => ToolCall.new(
150
- id: content_block['id'],
151
- name: content_block['name'],
152
- arguments: content_block['input']
153
- )
154
- }
155
- end
156
-
157
- def function_for(tool)
158
- {
159
- name: tool.name,
160
- description: tool.description,
161
- input_schema: {
162
- type: 'object',
163
- properties: clean_parameters(tool.parameters),
164
- required: required_parameters(tool.parameters)
165
- }
166
- }
167
- end
168
-
169
- def format_messages(messages)
170
- messages.map { |msg| format_message(msg) }
171
- end
172
-
173
- def format_message(msg)
174
- if msg.tool_call?
175
- format_tool_call(msg)
176
- elsif msg.tool_result?
177
- format_tool_result(msg)
178
- else
179
- format_basic_message(msg)
180
- end
181
- end
182
-
183
- def format_tool_call(msg)
184
- tool_call = msg.tool_calls.values.first
185
-
186
- {
187
- role: 'assistant',
188
- content: [
189
- format_text_block(msg.content),
190
- format_tool_use_block(tool_call)
191
- ]
192
- }
193
- end
194
-
195
- def format_tool_result(msg)
196
- {
197
- role: 'user',
198
- content: [format_tool_result_block(msg)]
199
- }
200
- end
201
-
202
- def format_basic_message(msg)
203
- {
204
- role: convert_role(msg.role),
205
- content: msg.content
206
- }
207
- end
208
-
209
- def format_text_block(content)
210
- {
211
- type: 'text',
212
- text: content
213
- }
214
- end
215
-
216
- def format_tool_use_block(tool_call)
217
- {
218
- type: 'tool_use',
219
- id: tool_call.id,
220
- name: tool_call.name,
221
- input: tool_call.arguments
222
- }
223
- end
224
-
225
- def format_tool_result_block(msg)
226
- {
227
- type: 'tool_result',
228
- tool_use_id: msg.tool_call_id,
229
- content: msg.content
230
- }
231
- end
232
-
233
- def convert_role(role)
234
- case role
235
- when :tool then 'user'
236
- when :user then 'user'
237
- else 'assistant'
238
- end
239
- end
240
-
241
- def clean_parameters(parameters)
242
- parameters.transform_values do |param|
243
- {
244
- type: param.type,
245
- description: param.description
246
- }.compact
247
- end
248
- end
249
-
250
- def required_parameters(parameters)
251
- parameters.select { |_, param| param.required }.keys
252
- end
253
27
  end
254
28
  end
255
29
  end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ module DeepSeek
6
+ # Determines capabilities and pricing for DeepSeek models
7
+ module Capabilities
8
+ module_function
9
+
10
+ def context_window_for(model_id)
11
+ case model_id
12
+ when /deepseek-(?:chat|reasoner)/ then 64_000
13
+ else 32_768 # Sensible default
14
+ end
15
+ end
16
+
17
+ def max_tokens_for(model_id)
18
+ case model_id
19
+ when /deepseek-(?:chat|reasoner)/ then 8_192
20
+ else 4_096 # Default if max_tokens not specified
21
+ end
22
+ end
23
+
24
+ def input_price_for(model_id)
25
+ PRICES.dig(model_family(model_id), :input_miss) || default_input_price
26
+ end
27
+
28
+ def output_price_for(model_id)
29
+ PRICES.dig(model_family(model_id), :output) || default_output_price
30
+ end
31
+
32
+ def cache_hit_price_for(model_id)
33
+ PRICES.dig(model_family(model_id), :input_hit) || default_cache_hit_price
34
+ end
35
+
36
+ def supports_vision?(_model_id)
37
+ false # DeepSeek models don't currently support vision
38
+ end
39
+
40
+ def supports_functions?(model_id)
41
+ model_id.match?(/deepseek-chat/) # Only deepseek-chat supports function calling
42
+ end
43
+
44
+ def supports_json_mode?(model_id)
45
+ model_id.match?(/deepseek-chat/) # Only deepseek-chat supports JSON mode
46
+ end
47
+
48
+ def format_display_name(model_id)
49
+ case model_id
50
+ when 'deepseek-chat' then 'DeepSeek V3'
51
+ when 'deepseek-reasoner' then 'DeepSeek R1'
52
+ else
53
+ model_id.split('-')
54
+ .map(&:capitalize)
55
+ .join(' ')
56
+ end
57
+ end
58
+
59
+ def model_type(_model_id)
60
+ 'chat' # All DeepSeek models are chat models
61
+ end
62
+
63
+ def model_family(model_id)
64
+ case model_id
65
+ when /deepseek-chat/ then :chat
66
+ when /deepseek-reasoner/ then :reasoner
67
+ else :chat # Default to chat family
68
+ end
69
+ end
70
+
71
+ # Pricing information for DeepSeek models (USD per 1M tokens)
72
+ PRICES = {
73
+ chat: {
74
+ input_hit: 0.07, # $0.07 per million tokens on cache hit
75
+ input_miss: 0.27, # $0.27 per million tokens on cache miss
76
+ output: 1.10 # $1.10 per million tokens output
77
+ },
78
+ reasoner: {
79
+ input_hit: 0.14, # $0.14 per million tokens on cache hit
80
+ input_miss: 0.55, # $0.55 per million tokens on cache miss
81
+ output: 2.19 # $2.19 per million tokens output
82
+ }
83
+ }.freeze
84
+
85
+ private
86
+
87
+ def default_input_price
88
+ 0.27 # Default to chat cache miss price
89
+ end
90
+
91
+ def default_output_price
92
+ 1.10 # Default to chat output price
93
+ end
94
+
95
+ def default_cache_hit_price
96
+ 0.07 # Default to chat cache hit price
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -3,8 +3,10 @@
3
3
  module RubyLLM
4
4
  module Providers
5
5
  # DeepSeek API integration.
6
- class DeepSeek < OpenAI
7
- private
6
+ module DeepSeek
7
+ extend OpenAI
8
+
9
+ module_function
8
10
 
9
11
  def api_base
10
12
  'https://api.deepseek.com'