Skip to content

Commit ede0f1d

Browse files
committed
Bump version to 0.1.0.pre26, add Gemini and DeepSeek providers, and enhance error handling
1 parent ff70044 commit ede0f1d

21 files changed

+2479
-742
lines changed

README.md

+19-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
# RubyLLM
22

3-
A delightful Ruby interface to the latest large language models. Stop wrestling with multiple APIs and inconsistent interfaces. RubyLLM gives you a clean, unified way to work with models from OpenAI, Anthropic, and more.
3+
A delightful Ruby interface to the latest large language models. Stop wrestling with multiple APIs and inconsistent interfaces. RubyLLM gives you a clean, unified way to work with models from OpenAI, Anthropic, Google, and DeepSeek.
4+
5+
<p align="center">
6+
<img src="https://upload.wikimedia.org/wikipedia/commons/4/4d/OpenAI_Logo.svg" alt="OpenAI" height="40" width="120">
7+
&nbsp;&nbsp;&nbsp;&nbsp;
8+
<img src="https://upload.wikimedia.org/wikipedia/commons/7/78/Anthropic_logo.svg" alt="Anthropic" height="40" width="120">
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">
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"]>
13+
</p>
414

515
[![Gem Version](https://badge.fury.io/rb/ruby_llm.svg)](https://badge.fury.io/rb/ruby_llm)
616
[![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
@@ -28,6 +38,8 @@ require 'ruby_llm'
2838
RubyLLM.configure do |config|
2939
config.openai_api_key = ENV['OPENAI_API_KEY']
3040
config.anthropic_api_key = ENV['ANTHROPIC_API_KEY']
41+
config.gemini_api_key = ENV['GEMINI_API_KEY']
42+
config.deepseek_api_key = ENV['DEEPSEEK_API_KEY']
3143
end
3244
```
3345

@@ -61,7 +73,7 @@ image_models = RubyLLM.models.image_models
6173
Conversations are simple and natural:
6274

6375
```ruby
64-
chat = RubyLLM.chat model: 'claude-3-5-sonnet-20241022'
76+
chat = RubyLLM.chat model: 'claude-3-opus-20240229'
6577

6678
# Ask questions
6779
response = chat.ask "What's your favorite Ruby feature?"
@@ -153,7 +165,7 @@ search = Search.new repo: Document
153165
chat.with_tools search, Calculator
154166

155167
# Configure as needed
156-
chat.with_model('claude-3-5-sonnet-20241022')
168+
chat.with_model('claude-3-opus-20240229')
157169
.with_temperature(0.9)
158170

159171
chat.ask "What's 2+2?"
@@ -185,6 +197,10 @@ rescue RubyLLM::UnauthorizedError
185197
puts "Check your API credentials"
186198
rescue RubyLLM::BadRequestError => e
187199
puts "Something went wrong: #{e.message}"
200+
rescue RubyLLM::PaymentRequiredError
201+
puts "Time to top up your API credits"
202+
rescue RubyLLM::ServiceUnavailableError
203+
puts "API service is temporarily down"
188204
end
189205
```
190206

Rakefile

+3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# frozen_string_literal: true
22

3+
require 'bundler/setup'
34
require 'bundler/gem_tasks'
45
require 'rake/clean'
56

7+
Dir.glob('lib/tasks/**/*.rake').each { |r| load r }
8+
69
task default: %w[build]

bin/console

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ require 'irb'
1010
RubyLLM.configure do |config|
1111
config.openai_api_key = ENV['OPENAI_API_KEY']
1212
config.anthropic_api_key = ENV['ANTHROPIC_API_KEY']
13+
config.gemini_api_key = ENV['GEMINI_API_KEY']
14+
config.deepseek_api_key = ENV['DEEPSEEK_API_KEY']
1315
end
1416

1517
IRB.start(__FILE__)

lib/ruby_llm.rb

+8-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
'ruby_llm' => 'RubyLLM',
1313
'llm' => 'LLM',
1414
'openai' => 'OpenAI',
15-
'api' => 'API'
15+
'api' => 'API',
16+
'deepseek' => 'DeepSeek'
1617
)
1718
loader.setup
1819

@@ -35,6 +36,10 @@ def models
3536
Models
3637
end
3738

39+
def providers
40+
Provider.providers.values
41+
end
42+
3843
def configure
3944
yield config
4045
end
@@ -55,6 +60,8 @@ def logger
5560

5661
RubyLLM::Provider.register :openai, RubyLLM::Providers::OpenAI
5762
RubyLLM::Provider.register :anthropic, RubyLLM::Providers::Anthropic
63+
RubyLLM::Provider.register :gemini, RubyLLM::Providers::Gemini
64+
RubyLLM::Provider.register :deepseek, RubyLLM::Providers::DeepSeek
5865

5966
if defined?(Rails::Railtie)
6067
require 'ruby_llm/railtie'

lib/ruby_llm/configuration.rb

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ module RubyLLM
1212
class Configuration
1313
attr_accessor :openai_api_key,
1414
:anthropic_api_key,
15+
:gemini_api_key,
16+
:deepseek_api_key,
1517
:default_model,
1618
:default_embedding_model,
1719
:request_timeout

lib/ruby_llm/error.rb

+12-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ class ModelNotFoundError < StandardError; end
2323
class InvalidRoleError < StandardError; end
2424
class UnsupportedFunctionsError < StandardError; end
2525
class UnauthorizedError < Error; end
26+
class PaymentRequiredError < Error; end
27+
class ServiceUnavailableError < Error; end
2628
class BadRequestError < Error; end
2729
class RateLimitError < Error; end
2830
class ServerError < Error; end
@@ -42,18 +44,26 @@ def call(env)
4244
end
4345

4446
class << self
45-
def parse_error(provider:, response:) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength
47+
def parse_error(provider:, response:) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/AbcSize,Metrics/PerceivedComplexity
4648
message = provider&.parse_error(response)
4749

4850
case response.status
51+
when 200..399
52+
message
4953
when 400
5054
raise BadRequestError.new(response, message || 'Invalid request - please check your input')
5155
when 401
5256
raise UnauthorizedError.new(response, message || 'Invalid API key - check your credentials')
57+
when 402
58+
raise PaymentRequiredError.new(response, message || 'Payment required - please top up your account')
5359
when 429
5460
raise RateLimitError.new(response, message || 'Rate limit exceeded - please wait a moment')
55-
when 500..599
61+
when 500
5662
raise ServerError.new(response, message || 'API server error - please try again')
63+
when 503
64+
raise ServiceUnavailableError.new(response, message || 'API server unavailable - please try again later')
65+
else
66+
raise Error.new(response, message || 'An unknown error occurred')
5767
end
5868
end
5969
end

lib/ruby_llm/model_capabilities/anthropic.rb

+1-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module RubyLLM
44
module ModelCapabilities
55
# Determines capabilities and pricing for Anthropic models
66
module Anthropic
7-
extend self
7+
module_function
88

99
def determine_context_window(model_id)
1010
case model_id
@@ -43,8 +43,6 @@ def supports_json_mode?(model_id)
4343
model_id.include?('claude-3')
4444
end
4545

46-
private
47-
4846
def model_family(model_id)
4947
case model_id
5048
when /claude-3-5-sonnet/ then :claude35_sonnet
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# frozen_string_literal: true
2+
3+
module RubyLLM
4+
module ModelCapabilities
5+
# Determines capabilities and pricing for DeepSeek models
6+
module DeepSeek
7+
module_function
8+
9+
def context_window_for(model_id)
10+
case model_id
11+
when /deepseek-(?:chat|reasoner)/ then 64_000
12+
else 32_768 # Sensible default
13+
end
14+
end
15+
16+
def max_tokens_for(_model_id)
17+
8_192
18+
end
19+
20+
def input_price_for(model_id)
21+
PRICES.dig(model_family(model_id), :input_miss) || default_input_price
22+
end
23+
24+
def output_price_for(model_id)
25+
PRICES.dig(model_family(model_id), :output) || default_output_price
26+
end
27+
28+
def cache_hit_price_for(model_id)
29+
PRICES.dig(model_family(model_id), :input_hit) || default_cache_hit_price
30+
end
31+
32+
def supports_vision?(_model_id)
33+
true # Both deepseek-chat and deepseek-reasoner support vision
34+
end
35+
36+
def supports_functions?(_model_id)
37+
true # Both models support function calling
38+
end
39+
40+
def supports_json_mode?(_model_id)
41+
true # Both models support JSON mode
42+
end
43+
44+
def format_display_name(model_id)
45+
case model_id
46+
when 'deepseek-chat' then 'DeepSeek V3'
47+
when 'deepseek-reasoner' then 'DeepSeek R1'
48+
else
49+
model_id.split('-')
50+
.map(&:capitalize)
51+
.join(' ')
52+
end
53+
end
54+
55+
def model_type(_model_id)
56+
'chat' # Both models are chat models
57+
end
58+
59+
def model_family(model_id)
60+
case model_id
61+
when /deepseek-chat/ then 'deepseek'
62+
when /deepseek-reasoner/ then 'deepseek_reasoner'
63+
else 'deepseek' # Default to base deepseek family
64+
end
65+
end
66+
67+
PRICES = {
68+
chat: {
69+
input_hit: 0.07, # $0.07 per million tokens on cache hit
70+
input_miss: 0.27, # $0.27 per million tokens on cache miss
71+
output: 1.10 # $1.10 per million tokens output
72+
},
73+
reasoner: {
74+
input_hit: 0.14, # $0.14 per million tokens on cache hit
75+
input_miss: 0.55, # $0.55 per million tokens on cache miss
76+
output: 2.19 # $2.19 per million tokens output
77+
}
78+
}.freeze
79+
80+
def default_input_price
81+
0.27 # Default to chat cache miss price
82+
end
83+
84+
def default_output_price
85+
1.10 # Default to chat output price
86+
end
87+
88+
def default_cache_hit_price
89+
0.07 # Default to chat cache hit price
90+
end
91+
end
92+
end
93+
end
+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# frozen_string_literal: true
2+
3+
module RubyLLM
4+
module ModelCapabilities
5+
# Determines capabilities and pricing for Google Gemini models
6+
module Gemini # rubocop:disable Metrics/ModuleLength
7+
module_function
8+
9+
def context_window_for(model_id)
10+
case model_id
11+
when /gemini-2\.0-flash/ then 1_048_576
12+
when /gemini-1\.5-pro/ then 2_097_152
13+
when /gemini-1\.5/ then 1_048_576
14+
when /text-embedding/, /embedding-001/ then 2_048
15+
else 32_768 # Sensible default for unknown models
16+
end
17+
end
18+
19+
def max_tokens_for(model_id)
20+
case model_id
21+
when /gemini-2\.0-flash/, /gemini-1\.5/ then 8_192
22+
when /text-embedding/, /embedding-001/ then 768 # Output dimension size for embeddings
23+
when /aqa/ then 1_024
24+
else 4_096 # Sensible default
25+
end
26+
end
27+
28+
def input_price_for(model_id)
29+
PRICES.dig(pricing_family(model_id), :input) || default_input_price
30+
end
31+
32+
def output_price_for(model_id)
33+
PRICES.dig(pricing_family(model_id), :output) || default_output_price
34+
end
35+
36+
def supports_vision?(model_id)
37+
return false if model_id.match?(/text-embedding|embedding-001|aqa/)
38+
return false if model_id.match?(/flash-lite/)
39+
return false if model_id.match?(/imagen/)
40+
41+
# Only pro and regular flash models support vision
42+
model_id.match?(/gemini-[12]\.(?:5|0)-(?:pro|flash)(?!-lite)/)
43+
end
44+
45+
def supports_functions?(model_id)
46+
return false if model_id.match?(/text-embedding|embedding-001|aqa/)
47+
return false if model_id.match?(/imagen/)
48+
return false if model_id.match?(/flash-lite/)
49+
return false if model_id.match?(/bison|gecko|evergreen/)
50+
51+
# Currently only full models support function calling
52+
model_id.match?(/gemini-[12]\.(?:5|0)-(?:pro|flash)(?!-lite)/)
53+
end
54+
55+
def supports_json_mode?(model_id)
56+
return false if model_id.match?(/text-embedding|embedding-001|aqa/)
57+
return false if model_id.match?(/imagen/)
58+
return false if model_id.match?(/flash-lite/)
59+
return false if model_id.match?(/bison|gecko|evergreen/)
60+
61+
# Gemini 1.5+ models support JSON mode
62+
model_id.match?(/gemini-[12]\.(?:5|0)-(?:pro|flash)(?!-lite)/)
63+
end
64+
65+
def format_display_name(model_id)
66+
return model_id unless model_id.start_with?('models/')
67+
68+
model_id
69+
.delete_prefix('models/')
70+
.split('-')
71+
.map(&:capitalize)
72+
.join(' ')
73+
.gsub(/(\d+\.\d+)/, ' \1') # Add space before version numbers
74+
.gsub(/\s+/, ' ') # Clean up multiple spaces
75+
.strip
76+
end
77+
78+
def model_type(model_id)
79+
case model_id
80+
when /text-embedding|embedding/ then 'embedding'
81+
when /imagen/ then 'image'
82+
when /bison|text-bison/ then 'legacy'
83+
else 'chat'
84+
end
85+
end
86+
87+
def model_family(model_id) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength
88+
case model_id
89+
when /gemini-2\.0-flash-lite/ then 'gemini20_flash_lite'
90+
when /gemini-2\.0-flash/ then 'gemini20_flash'
91+
when /gemini-1\.5-flash-8b/ then 'gemini15_flash_8b'
92+
when /gemini-1\.5-flash/ then 'gemini15_flash'
93+
when /gemini-1\.5-pro/ then 'gemini15_pro'
94+
when /text-embedding-004/ then 'embedding4'
95+
when /embedding-001/ then 'embedding1'
96+
when /bison|text-bison/ then 'bison'
97+
when /imagen/ then 'imagen3'
98+
else 'other'
99+
end
100+
end
101+
102+
def pricing_family(model_id)
103+
case model_id
104+
when /gemini-2\.0-flash-lite/ then :flash_lite_2 # rubocop:disable Naming/VariableNumber
105+
when /gemini-2\.0-flash/ then :flash_2 # rubocop:disable Naming/VariableNumber
106+
when /gemini-1\.5-flash-8b/ then :flash_8b
107+
when /gemini-1\.5-flash/ then :flash
108+
when /gemini-1\.5-pro/ then :pro
109+
when /text-embedding|embedding/ then :embedding
110+
else :base
111+
end
112+
end
113+
114+
PRICES = {
115+
flash_2: { input: 0.10, output: 0.40 }, # Gemini 2.0 Flash # rubocop:disable Naming/VariableNumber
116+
flash_lite_2: { input: 0.075, output: 0.30 }, # Gemini 2.0 Flash Lite # rubocop:disable Naming/VariableNumber
117+
flash: { input: 0.075, output: 0.30 }, # Gemini 1.5 Flash basic pricing
118+
flash_8b: { input: 0.0375, output: 0.15 }, # Gemini 1.5 Flash 8B
119+
pro: { input: 1.25, output: 5.0 }, # Gemini 1.5 Pro
120+
embedding: { input: 0.00, output: 0.00 } # Text Embedding models are free
121+
}.freeze
122+
123+
def default_input_price
124+
0.075 # Default to Flash pricing
125+
end
126+
127+
def default_output_price
128+
0.30 # Default to Flash pricing
129+
end
130+
end
131+
end
132+
end

0 commit comments

Comments
 (0)