durable-llm 0.1.3 → 0.1.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1e531cd0c24bb1c3d7a2c5490b3d268694827c08d831955219d5fa32884b1a51
4
- data.tar.gz: dd4bedf9b3a9a09addef7306fe51bcad5082a75163d0d383eff449df5c1ebf54
3
+ metadata.gz: 41b81b7592103ed34098a473f004036c0a9ed22a6f43cd764c32d057ac840cd9
4
+ data.tar.gz: 0caf0d378409b4d025261049c41deb769b111eb556a77843495202cc58cea430
5
5
  SHA512:
6
- metadata.gz: d5bf3a883a62807f86b578169eda22074a8d30e8b7005f9177dd16b04a2be26af5625e26b5b963f6ece166b81c8082d18da2fb9851a03edec3e2388ce2443dce
7
- data.tar.gz: 03d93aeb004df8d6bf0cfbf945734565d7e5b4d7fe4b98a5b3f4b2a622202b7f907d51730cbcbf37ec9dbed9c0098844c7272ff6a6a00be7c3aeea9e19531607
6
+ metadata.gz: 395a3c6e8b542107c01122c983beb0d43918a3d5567137c9652a716ddd01d89b7d8399e3e00f7059fde2bca82e80141063bf1dc15602bb9c665304aa4a393505
7
+ data.tar.gz: af5ea2f180fdbac3cd52a6f856f37b419b1568f93c03aad36df0906ab79c336b5091372c4b048dcc4307e0984d8ed66f72cc1b047fd295ca9174e1f2c784bdf2
data/CONFIGURE.md ADDED
@@ -0,0 +1,132 @@
1
+ # Configuring Durable-LLM
2
+
3
+ ## Introduction
4
+
5
+ Durable-LLM supports multiple LLM providers and can be configured using environment variables or a configuration block. This document outlines the various configuration options available.
6
+
7
+ ## General Configuration
8
+
9
+ You can configure Durable-LLM using a configuration block:
10
+
11
+ ```ruby
12
+ Durable::Llm.configure do |config|
13
+ # Configuration options go here
14
+ end
15
+ ```
16
+
17
+ ## Provider-specific Configuration
18
+
19
+ ### OpenAI
20
+
21
+ To configure the OpenAI provider, you can set the following environment variables:
22
+
23
+ - `OPENAI_API_KEY`: Your OpenAI API key
24
+ - `OPENAI_ORGANIZATION`: (Optional) Your OpenAI organization ID
25
+
26
+ Alternatively, you can configure it in the configuration block:
27
+
28
+ ```ruby
29
+ Durable::Llm.configure do |config|
30
+ config.openai.api_key = 'your-api-key'
31
+ config.openai.organization = 'your-organization-id' # Optional
32
+ end
33
+ ```
34
+
35
+ ### Anthropic
36
+
37
+ To configure the Anthropic provider, you can set the following environment variable:
38
+
39
+ - `ANTHROPIC_API_KEY`: Your Anthropic API key
40
+
41
+ Alternatively, you can configure it in the configuration block:
42
+
43
+ ```ruby
44
+ Durable::Llm.configure do |config|
45
+ config.anthropic.api_key = 'your-api-key'
46
+ end
47
+ ```
48
+
49
+ ### Hugging Face
50
+
51
+ To configure the Hugging Face provider, you can set the following environment variable:
52
+
53
+ - `HUGGINGFACE_API_KEY`: Your Hugging Face API key
54
+
55
+ Alternatively, you can configure it in the configuration block:
56
+
57
+ ```ruby
58
+ Durable::Llm.configure do |config|
59
+ config.huggingface.api_key = 'your-api-key'
60
+ end
61
+ ```
62
+
63
+ ### Groq
64
+
65
+ To configure the Groq provider, you can set the following environment variable:
66
+
67
+ - `GROQ_API_KEY`: Your Groq API key
68
+
69
+ Alternatively, you can configure it in the configuration block:
70
+
71
+ ```ruby
72
+ Durable::Llm.configure do |config|
73
+ config.groq.api_key = 'your-api-key'
74
+ end
75
+ ```
76
+
77
+ ## Using Environment Variables
78
+
79
+ You can also use environment variables configure any provider. The format is:
80
+
81
+ ```
82
+ DLLM__PROVIDER__SETTING
83
+ ```
84
+
85
+ For example:
86
+
87
+ ```
88
+ DLLM__OPENAI__API_KEY=your-openai-api-key
89
+ DLLM__ANTHROPIC__API_KEY=your-anthropic-api-key
90
+ ```
91
+
92
+ ## Loading Configuration from Datasette
93
+
94
+ Durable-LLM can load configuration from a io.datasette.llm configuration file located at `~/.config/io.datasette.llm/keys.json`. If this file exists, it will be parsed and used to set API keys for the supported providers.
95
+
96
+ ## Default Provider
97
+
98
+ You can set a default provider in the configuration:
99
+
100
+ ```ruby
101
+ Durable::Llm.configure do |config|
102
+ config.default_provider = 'openai'
103
+ end
104
+ ```
105
+
106
+ The default provider is set to 'openai' if not specified.
107
+
108
+ ## Supported Models
109
+
110
+ Each provider supports a set of models. You can get the list of supported models for a provider using the `models` method:
111
+
112
+ ```ruby
113
+ Durable::Llm::Providers::OpenAI.models
114
+ Durable::Llm::Providers::Anthropic.models
115
+ Durable::Llm::Providers::Huggingface.models
116
+ Durable::Llm::Providers::Groq.models
117
+ ```
118
+
119
+ Note that some services (Anthropic, for example) don't offer a models endpoint, so they are hardcoded; others (Huggingface) have a inordinately long list, so also have a hardcoded list, at least for now.
120
+
121
+ ## Streaming Support
122
+
123
+ Some providers support streaming responses. You can check if a provider supports streaming:
124
+
125
+ ```ruby
126
+ Durable::Llm::Providers::OpenAI.stream?
127
+ ```
128
+
129
+ ## Conclusion
130
+
131
+ By properly configuring Durable-LLM, you can easily switch between different LLM providers and models in your application. Remember to keep your API keys secure and never commit them to version control.
132
+
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- durable-llm (0.1.2)
4
+ durable-llm (0.1.3)
5
5
  event_stream_parser (~> 1.0)
6
6
  faraday (> 1.0)
7
7
  highline (~> 3.1)
@@ -10,29 +10,26 @@ module Durable
10
10
  true
11
11
  end
12
12
 
13
- desc "prompt PROMPT", "Run a prompt"
14
- option :model, aliases: "-m", desc: "Specify the model to use"
15
- option :system, aliases: "-s", desc: "Set a system prompt"
16
- option :continue, aliases: "-c", type: :boolean, desc: "Continue the previous conversation"
17
- option :conversation, aliases: "--cid", desc: "Continue a specific conversation by ID"
18
- option :no_stream, type: :boolean, desc: "Disable streaming of tokens"
19
- option :option, aliases: "-o", type: :hash, desc: "Set model-specific options"
20
-
21
- def prompt(prompt)
22
- config = Durable::Llm.configuration
23
- model = options[:model] || "gpt-3.5-turbo"
13
+ desc 'prompt PROMPT', 'Run a prompt'
14
+ option :model, aliases: '-m', desc: 'Specify the model to use'
15
+ option :system, aliases: '-s', desc: 'Set a system prompt'
16
+ option :continue, aliases: '-c', type: :boolean, desc: 'Continue the previous conversation'
17
+ option :conversation, aliases: '--cid', desc: 'Continue a specific conversation by ID'
18
+ option :no_stream, type: :boolean, desc: 'Disable streaming of tokens'
19
+ option :option, aliases: '-o', type: :hash, desc: 'Set model-specific options'
20
+
21
+ def prompt(*prompt)
22
+ model = options[:model] || 'gpt-3.5-turbo'
24
23
  provider_class = Durable::Llm::Providers.model_id_to_provider(model)
25
24
 
26
- if provider_class.nil?
27
- raise "no provider found for model '#{model}'"
28
- end
25
+ raise "no provider found for model '#{model}'" if provider_class.nil?
29
26
 
30
27
  provider_name = provider_class.name.split('::').last.downcase.to_sym
31
28
  client = Durable::Llm::Client.new(provider_name)
32
-
29
+
33
30
  messages = []
34
- messages << { role: "system", content: options[:system] } if options[:system]
35
- messages << { role: "user", content: prompt }
31
+ messages << { role: 'system', content: options[:system] } if options[:system]
32
+ messages << { role: 'user', content: prompt.join(' ') }
36
33
 
37
34
  params = {
38
35
  model: model,
@@ -40,37 +37,34 @@ module Durable
40
37
  }
41
38
  params.merge!(options[:option]) if options[:option]
42
39
 
43
- if options[:no_stream]
40
+ if options[:no_stream] || !client.stream?
44
41
  response = client.completion(params)
45
- puts response.choices.first.to_s
42
+ puts response.choices.first
46
43
  else
47
44
  client.stream(params) do |chunk|
48
- print chunk.to_s
45
+ print chunk
49
46
  $stdout.flush
50
47
  end
51
48
  end
52
49
  end
53
50
 
54
- desc "chat", "Start an interactive chat"
55
- option :model, aliases: "-m", desc: "Specify the model to use"
56
- option :system, aliases: "-s", desc: "Set a system prompt"
57
- option :continue, aliases: "-c", type: :boolean, desc: "Continue the previous conversation"
58
- option :conversation, aliases: "--cid", desc: "Continue a specific conversation by ID"
59
- option :option, aliases: "-o", type: :hash, desc: "Set model-specific options"
51
+ desc 'chat', 'Start an interactive chat'
52
+ option :model, aliases: '-m', desc: 'Specify the model to use'
53
+ option :system, aliases: '-s', desc: 'Set a system prompt'
54
+ option :continue, aliases: '-c', type: :boolean, desc: 'Continue the previous conversation'
55
+ option :conversation, aliases: '--cid', desc: 'Continue a specific conversation by ID'
56
+ option :option, aliases: '-o', type: :hash, desc: 'Set model-specific options'
60
57
  def chat
61
- config = Durable::Llm.configuration
62
- model = options[:model] || "gpt-3.5-turbo"
58
+ model = options[:model] || 'gpt-3.5-turbo'
63
59
  provider_class = Durable::Llm::Providers.model_id_to_provider(model)
64
60
 
65
- if provider_class.nil? || provider_class.name.nil?
66
- raise "no provider found for model '#{model}'"
67
- end
61
+ raise "no provider found for model '#{model}'" if provider_class.nil? || provider_class.name.nil?
68
62
 
69
63
  provider_name = provider_class.name.split('::').last.downcase.to_sym
70
64
  client = Durable::Llm::Client.new(provider_name)
71
-
65
+
72
66
  messages = []
73
- messages << { role: "system", content: options[:system] } if options[:system]
67
+ messages << { role: 'system', content: options[:system] } if options[:system]
74
68
 
75
69
  cli = HighLine.new
76
70
 
@@ -79,16 +73,16 @@ module Durable
79
73
  cli.say("Type '!multi' to enter multiple lines, then '!end' to finish")
80
74
 
81
75
  loop do
82
- input = cli.ask("> ")
83
- break if ['exit', 'quit'].include?(input.downcase)
76
+ input = cli.ask('> ')
77
+ break if %w[exit quit].include?(input.downcase)
84
78
 
85
- if input == "!multi"
79
+ if input == '!multi'
86
80
  input = cli.ask("Enter multiple lines. Type '!end' to finish:") do |q|
87
- q.gather = "!end"
81
+ q.gather = '!end'
88
82
  end
89
83
  end
90
84
 
91
- messages << { role: "user", content: input }
85
+ messages << { role: 'user', content: input }
92
86
  params = {
93
87
  model: model,
94
88
  messages: messages
@@ -97,20 +91,20 @@ module Durable
97
91
 
98
92
  response = client.completion(params)
99
93
  cli.say(response.choices.first.to_s)
100
- messages << { role: "assistant", content: response.choices.first.to_s }
94
+ messages << { role: 'assistant', content: response.choices.first.to_s }
101
95
  end
102
96
  end
103
97
 
104
- desc "models", "List available models"
105
- option :options, type: :boolean, desc: "Show model options"
98
+ desc 'models', 'List available models'
99
+ option :options, type: :boolean, desc: 'Show model options'
106
100
  def models
107
101
  cli = HighLine.new
108
- cli.say("Available models:")
109
-
102
+ cli.say('Available models:')
103
+
110
104
  Durable::Llm::Providers.providers.each do |provider_name|
111
105
  provider_class = Durable::Llm::Providers.const_get(provider_name.to_s.capitalize)
112
- provider_models = provider_class.new.models
113
-
106
+ provider_models = provider_class.models
107
+
114
108
  cli.say("#{provider_name.to_s.capitalize}:")
115
109
  provider_models.each do |model|
116
110
  cli.say(" #{model}")
@@ -8,10 +8,7 @@ module Durable
8
8
  attr_accessor :model
9
9
 
10
10
  def initialize(provider_name, options = {})
11
-
12
- if options['model'] || options[:model]
13
- @model = options.delete('model') || options.delete(:model)
14
- end
11
+ @model = options.delete('model') || options.delete(:model) if options['model'] || options[:model]
15
12
 
16
13
  provider_class = Durable::Llm::Providers.const_get(provider_name.to_s.capitalize)
17
14
 
@@ -21,12 +18,13 @@ module Durable
21
18
  def default_params
22
19
  { model: @model }
23
20
  end
24
- def quick_complete(text, opts = {})
25
21
 
26
- response = completion(process_params(messages:[{role: 'user', content: text}]))
22
+ def quick_complete(text, _opts = {})
23
+ response = completion(process_params(messages: [{ role: 'user', content: text }]))
27
24
 
28
25
  response.choices.first.message.content
29
26
  end
27
+
30
28
  def completion(params = {})
31
29
  @provider.completion(process_params(params))
32
30
  end
@@ -43,6 +41,10 @@ module Durable
43
41
  @provider.stream(process_params(params), &block)
44
42
  end
45
43
 
44
+ def stream?
45
+ @provider.stream?
46
+ end
47
+
46
48
  private
47
49
 
48
50
  def process_params(opts = {})
@@ -10,44 +10,42 @@ module Durable
10
10
  @providers = {}
11
11
  @default_provider = 'openai'
12
12
  load_from_env
13
+ end
13
14
 
15
+ def clear
16
+ @providers.clear
17
+ @default_provider = 'openai'
14
18
  end
15
19
 
16
20
  def load_from_datasette
17
-
18
21
  config_file = File.expand_path('~/.config/io.datasette.llm/keys.json')
19
22
 
20
23
  if File.exist?(config_file)
21
24
  config_data = JSON.parse(File.read(config_file))
22
25
 
23
26
  Durable::Llm::Providers.providers.each do |provider|
24
-
25
27
  @providers[provider.to_sym] ||= OpenStruct.new
26
28
 
27
- if config_data[provider.to_s]
28
- @providers[provider.to_sym][:api_key] = config_data[provider.to_s]
29
- end
30
-
29
+ @providers[provider.to_sym][:api_key] = config_data[provider.to_s] if config_data[provider.to_s]
31
30
  end
32
31
  end
33
-
34
32
  rescue JSON::ParserError => e
35
33
  puts "Error parsing JSON file: #{e.message}"
36
34
  end
37
35
 
38
36
  def load_from_env
39
37
  ENV.each do |key, value|
40
- if key.start_with?('DLLM__')
41
- parts = key.split('__')
42
- provider = parts[1].downcase.to_sym
43
- setting = parts[2].downcase.to_sym
44
- @providers[provider] ||= OpenStruct.new
45
- @providers[provider][setting] = value
46
- end
38
+ next unless key.start_with?('DLLM__')
39
+
40
+ parts = key.split('__')
41
+ provider = parts[1].downcase.to_sym
42
+ setting = parts[2].downcase.to_sym
43
+ @providers[provider] ||= OpenStruct.new
44
+ @providers[provider][setting] = value
47
45
  end
48
46
  end
49
47
 
50
- def method_missing(method_name, *args, &block)
48
+ def method_missing(method_name, *args)
51
49
  if method_name.to_s.end_with?('=')
52
50
  provider = method_name.to_s.chomp('=').to_sym
53
51
  @providers[provider] = args.first
@@ -1,4 +1,3 @@
1
-
2
1
  require 'faraday'
3
2
  require 'json'
4
3
  require 'durable/llm/errors'
@@ -27,7 +26,7 @@ module Durable
27
26
  end
28
27
 
29
28
  def completion(options)
30
- options['max_tokens'] ||=1024
29
+ options['max_tokens'] ||= 1024
31
30
  response = @conn.post('/v1/messages') do |req|
32
31
  req.headers['x-api-key'] = @api_key
33
32
  req.headers['anthropic-version'] = '2023-06-01'
@@ -40,6 +39,7 @@ module Durable
40
39
  def models
41
40
  self.class.models
42
41
  end
42
+
43
43
  def self.models
44
44
  ['claude-3-5-sonnet-20240620', 'claude-3-opus-20240229', 'claude-3-haiku-20240307']
45
45
  end
@@ -47,15 +47,17 @@ module Durable
47
47
  def self.stream?
48
48
  true
49
49
  end
50
- def stream(options, &block)
50
+
51
+ def stream(options)
51
52
  options[:stream] = true
52
53
  response = @conn.post('/v1/messages') do |req|
53
54
  req.headers['x-api-key'] = @api_key
54
55
  req.headers['anthropic-version'] = '2023-06-01'
55
56
  req.headers['Accept'] = 'text/event-stream'
56
57
  req.body = options
57
- req.options.on_data = Proc.new do |chunk, size, total|
58
+ req.options.on_data = proc do |chunk, _size, _total|
58
59
  next if chunk.strip.empty?
60
+
59
61
  yield AnthropicStreamResponse.new(chunk) if chunk.start_with?('data: ')
60
62
  end
61
63
  end
@@ -66,20 +68,20 @@ module Durable
66
68
  private
67
69
 
68
70
  def handle_response(response)
69
- case response.status
70
- when 200..299
71
- AnthropicResponse.new(response.body)
72
- when 401
73
- raise Durable::Llm::AuthenticationError, response.body.dig('error', 'message')
74
- when 429
75
- raise Durable::Llm::RateLimitError, response.body.dig('error', 'message')
76
- when 400..499
77
- raise Durable::Llm::InvalidRequestError, response.body.dig('error', 'message')
78
- when 500..599
79
- raise Durable::Llm::ServerError, response.body.dig('error', 'message')
80
- else
81
- raise Durable::Llm::APIError, "Unexpected response code: #{response.status}"
82
- end
71
+ case response.status
72
+ when 200..299
73
+ AnthropicResponse.new(response.body)
74
+ when 401
75
+ raise Durable::Llm::AuthenticationError, response.body.dig('error', 'message')
76
+ when 429
77
+ raise Durable::Llm::RateLimitError, response.body.dig('error', 'message')
78
+ when 400..499
79
+ raise Durable::Llm::InvalidRequestError, response.body.dig('error', 'message')
80
+ when 500..599
81
+ raise Durable::Llm::ServerError, response.body.dig('error', 'message')
82
+ else
83
+ raise Durable::Llm::APIError, "Unexpected response code: #{response.status}"
84
+ end
83
85
  end
84
86
 
85
87
  class AnthropicResponse
@@ -114,8 +116,8 @@ module Durable
114
116
  attr_reader :role, :content
115
117
 
116
118
  def initialize(content)
117
- @role = [content].flatten.map { |_| _['type']}.join(' ')
118
- @content = [content].flatten.map { |_| _['text']}.join(' ')
119
+ @role = [content].flatten.map { |_| _['type'] }.join(' ')
120
+ @content = [content].flatten.map { |_| _['text'] }.join(' ')
119
121
  end
120
122
 
121
123
  def to_s
@@ -127,7 +129,7 @@ module Durable
127
129
  attr_reader :choices
128
130
 
129
131
  def initialize(fragment)
130
- parsed = JSON.parse(fragment.split("data: ").last)
132
+ parsed = JSON.parse(fragment.split('data: ').last)
131
133
  @choices = [AnthropicStreamChoice.new(parsed['delta'])]
132
134
  end
133
135
 
@@ -3,7 +3,7 @@ module Durable
3
3
  module Providers
4
4
  class Base
5
5
  def default_api_key
6
- raise NotImplementedError, "Subclasses must implement default_api_key"
6
+ raise NotImplementedError, 'Subclasses must implement default_api_key'
7
7
  end
8
8
 
9
9
  attr_accessor :api_key
@@ -12,37 +12,52 @@ module Durable
12
12
  @api_key = api_key || default_api_key
13
13
  end
14
14
 
15
-
16
15
  def completion(options)
17
- raise NotImplementedError, "Subclasses must implement completion"
16
+ raise NotImplementedError, 'Subclasses must implement completion'
18
17
  end
19
18
 
20
- def self.models
21
- []
19
+ def self.models
20
+ cache_dir = File.expand_path("#{Dir.home}/.local/durable-llm/cache")
21
+
22
+ FileUtils.mkdir_p(cache_dir) unless File.directory?(cache_dir)
23
+ cache_file = File.join(cache_dir, "#{name.split('::').last}.json")
24
+
25
+ file_exists = File.exist?(cache_file)
26
+ file_new_enough = file_exists && File.mtime(cache_file) > Time.now - 3600
27
+
28
+ if file_exists && file_new_enough
29
+ JSON.parse(File.read(cache_file))
30
+ else
31
+ models = new.models
32
+ File.write(cache_file, JSON.generate(models)) if models.length > 0
33
+ models
34
+ end
22
35
  end
36
+
23
37
  def models
24
- raise NotImplementedError, "Subclasses must implement models"
38
+ raise NotImplementedError, 'Subclasses must implement models'
25
39
  end
26
40
 
27
41
  def self.stream?
28
42
  false
29
43
  end
44
+
30
45
  def stream?
31
46
  self.class.stream?
32
47
  end
33
48
 
34
49
  def stream(options, &block)
35
- raise NotImplementedError, "Subclasses must implement stream"
50
+ raise NotImplementedError, 'Subclasses must implement stream'
36
51
  end
37
52
 
38
53
  def embedding(model:, input:, **options)
39
- raise NotImplementedError, "Subclasses must implement embedding"
54
+ raise NotImplementedError, 'Subclasses must implement embedding'
40
55
  end
41
56
 
42
57
  private
43
58
 
44
59
  def handle_response(response)
45
- raise NotImplementedError, "Subclasses must implement handle_response"
60
+ raise NotImplementedError, 'Subclasses must implement handle_response'
46
61
  end
47
62
  end
48
63
  end
@@ -0,0 +1,100 @@
1
+ require 'faraday'
2
+ require 'json'
3
+ require 'durable/llm/errors'
4
+ require 'durable/llm/providers/base'
5
+
6
+ module Durable
7
+ module Llm
8
+ module Providers
9
+ class Cohere < Durable::Llm::Providers::Base
10
+ BASE_URL = 'https://api.cohere.ai/v2'
11
+
12
+ def default_api_key
13
+ Durable::Llm.configuration.cohere&.api_key || ENV['COHERE_API_KEY']
14
+ end
15
+
16
+ attr_accessor :api_key
17
+
18
+ def initialize(api_key: nil)
19
+ @api_key = api_key || default_api_key
20
+ @conn = Faraday.new(url: BASE_URL) do |faraday|
21
+ faraday.request :json
22
+ faraday.response :json
23
+ faraday.adapter Faraday.default_adapter
24
+ end
25
+ end
26
+
27
+ def completion(options)
28
+ response = @conn.post('chat') do |req|
29
+ req.headers['Authorization'] = "Bearer #{@api_key}"
30
+ req.headers['Content-Type'] = 'application/json'
31
+ req.body = options
32
+ end
33
+
34
+ handle_response(response)
35
+ end
36
+
37
+ def models
38
+ response = @conn.get('models') do |req|
39
+ req.headers['Authorization'] = "Bearer #{@api_key}"
40
+ req.headers['OpenAI-Organization'] = @organization if @organization
41
+ end
42
+
43
+ data = handle_response(response).raw_response
44
+ data['models']&.map { |model| model['name'] }
45
+ end
46
+
47
+ def self.stream?
48
+ false
49
+ end
50
+
51
+ private
52
+
53
+ def handle_response(response)
54
+ case response.status
55
+ when 200..299
56
+ CohereResponse.new(response.body)
57
+ when 401
58
+ raise Durable::Llm::AuthenticationError, response.body['message']
59
+ when 429
60
+ raise Durable::Llm::RateLimitError, response.body['message']
61
+ when 400..499
62
+ raise Durable::Llm::InvalidRequestError, response.body['message']
63
+ when 500..599
64
+ raise Durable::Llm::ServerError, response.body['message']
65
+ else
66
+ raise Durable::Llm::APIError, "Unexpected response code: #{response.status}"
67
+ end
68
+ end
69
+
70
+ class CohereResponse
71
+ attr_reader :raw_response
72
+
73
+ def initialize(response)
74
+ @raw_response = response
75
+ end
76
+
77
+ def choices
78
+ [@raw_response.dig('message', 'content')].flatten.map { |generation| CohereChoice.new(generation) }
79
+ end
80
+
81
+ def to_s
82
+ choices.map(&:to_s).join(' ')
83
+ end
84
+ end
85
+
86
+ class CohereChoice
87
+ attr_reader :text
88
+
89
+ def initialize(generation)
90
+ @text = generation['text']
91
+ end
92
+
93
+ def to_s
94
+ @text
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -22,6 +22,7 @@ module Durable
22
22
  faraday.adapter Faraday.default_adapter
23
23
  end
24
24
  end
25
+
25
26
  def conn
26
27
  self.class.conn
27
28
  end
@@ -49,8 +50,6 @@ module Durable
49
50
  end
50
51
 
51
52
  def models
52
-
53
-
54
53
  response = conn.get('models') do |req|
55
54
  req.headers['Authorization'] = "Bearer #{@api_key}"
56
55
  end
@@ -59,9 +58,6 @@ module Durable
59
58
 
60
59
  resp['data'].map { |model| model['id'] }
61
60
  end
62
- def self.models
63
- Groq.new.models
64
- end
65
61
 
66
62
  def self.stream?
67
63
  false
@@ -100,6 +96,7 @@ module Durable
100
96
  def to_s
101
97
  choices.map(&:to_s).join(' ')
102
98
  end
99
+
103
100
  def to_h
104
101
  @raw_response.dup
105
102
  end
@@ -135,7 +132,7 @@ module Durable
135
132
  attr_reader :choices
136
133
 
137
134
  def initialize(fragment)
138
- json_frag = fragment.split("data: ").last.strip
135
+ json_frag = fragment.split('data: ').last.strip
139
136
  puts json_frag
140
137
  parsed = JSON.parse(json_frag)
141
138
  @choices = parsed['choices'].map { |choice| GroqStreamChoice.new(choice) }
@@ -37,8 +37,9 @@ module Durable
37
37
  def models
38
38
  self.class.models
39
39
  end
40
+
40
41
  def self.models
41
- ['gpt2', 'bert-base-uncased', 'distilbert-base-uncased'] # could use expansion
42
+ %w[gpt2 bert-base-uncased distilbert-base-uncased] # could use expansion
42
43
  end
43
44
 
44
45
  private
@@ -55,39 +55,27 @@ module Durable
55
55
  handle_response(response).data.map { |model| model['id'] }
56
56
  end
57
57
 
58
- def self.models
59
- self.new.models
60
- end
61
-
62
58
  def self.stream?
63
59
  true
64
60
  end
65
61
 
66
- def stream(options, &block)
67
-
62
+ def stream(options)
68
63
  options[:stream] = true
69
64
 
70
65
  response = @conn.post('chat/completions') do |req|
71
-
72
66
  req.headers['Authorization'] = "Bearer #{@api_key}"
73
67
  req.headers['OpenAI-Organization'] = @organization if @organization
74
68
  req.headers['Accept'] = 'text/event-stream'
75
69
 
76
- if options['temperature']
77
- options['temperature'] = options['temperature'].to_f
78
- end
70
+ options['temperature'] = options['temperature'].to_f if options['temperature']
79
71
 
80
72
  req.body = options
81
73
 
82
- user_proc = Proc.new do |chunk, size, total|
83
-
84
-
74
+ user_proc = proc do |chunk, _size, _total|
85
75
  yield OpenAIStreamResponse.new(chunk)
86
-
87
76
  end
88
77
 
89
- req.options.on_data = to_json_stream( user_proc: user_proc )
90
-
78
+ req.options.on_data = to_json_stream(user_proc: user_proc)
91
79
  end
92
80
 
93
81
  handle_response(response)
@@ -113,7 +101,7 @@ module Durable
113
101
  end
114
102
 
115
103
  parser.feed(chunk) do |_type, data|
116
- user_proc.call(JSON.parse(data)) unless data == "[DONE]"
104
+ user_proc.call(JSON.parse(data)) unless data == '[DONE]'
117
105
  end
118
106
  end
119
107
  end
@@ -125,8 +113,8 @@ module Durable
125
113
  end
126
114
 
127
115
  # END-CODE-FROM
128
-
129
- def handle_response(response, responseClass=OpenAIResponse)
116
+
117
+ def handle_response(response, responseClass = OpenAIResponse)
130
118
  case response.status
131
119
  when 200..299
132
120
  responseClass.new(response.body)
@@ -144,7 +132,11 @@ module Durable
144
132
  end
145
133
 
146
134
  def parse_error_message(response)
147
- body = JSON.parse(response.body) rescue nil
135
+ body = begin
136
+ JSON.parse(response.body)
137
+ rescue StandardError
138
+ nil
139
+ end
148
140
  message = body&.dig('error', 'message') || response.body
149
141
  "#{response.status} Error: #{message}"
150
142
  end
@@ -203,8 +195,7 @@ module Durable
203
195
  attr_reader :choices
204
196
 
205
197
  def initialize(parsed)
206
-
207
- @choices = OpenAIStreamChoice.new(parsed['choices'])
198
+ @choices = OpenAIStreamChoice.new(parsed['choices'])
208
199
  end
209
200
 
210
201
  def to_s
@@ -9,15 +9,13 @@ module Durable
9
9
  end
10
10
 
11
11
  def self.providers
12
+ @provider_list ||= constants.select do |const_name|
13
+ const = const_get(const_name)
14
+ last_component = const.name.split('::').last
15
+ next if last_component == 'Base'
12
16
 
13
- @provider_list ||= begin
14
- constants.select do |const_name|
15
- const = const_get(const_name)
16
- last_component = const.name.split('::').last
17
- next if last_component == 'Base'
18
- const.is_a?(Class) && const.to_s.split('::').last.to_s == const_name.to_s
19
- end.map(&:to_s).map(&:downcase).map(&:to_sym)
20
- end
17
+ const.is_a?(Class) && const.to_s.split('::').last.to_s == const_name.to_s
18
+ end.map(&:to_s).map(&:downcase).map(&:to_sym)
21
19
  end
22
20
 
23
21
  def self.model_ids
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Durable
4
4
  module Llm
5
- VERSION = "0.1.3"
5
+ VERSION = '0.1.4'
6
6
  end
7
7
  end
data/lib/durable/llm.rb CHANGED
@@ -1,8 +1,8 @@
1
- require "zeitwerk"
1
+ require 'zeitwerk'
2
2
  loader = Zeitwerk::Loader.new
3
- loader.tag = File.basename(__FILE__, ".rb")
3
+ loader.tag = File.basename(__FILE__, '.rb')
4
4
  loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
5
- loader.push_dir(File.dirname(__FILE__) + '/..' )
5
+ loader.push_dir(File.dirname(__FILE__) + '/..')
6
6
 
7
7
  require 'durable/llm/configuration'
8
8
 
@@ -10,6 +10,7 @@ module Durable
10
10
  module Llm
11
11
  class << self
12
12
  attr_accessor :configuration
13
+
13
14
  def config
14
15
  configuration
15
16
  end
@@ -24,4 +25,3 @@ end
24
25
 
25
26
  Durable::Llm.configure do
26
27
  end
27
-
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: durable-llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Durable Programming Team
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-10-09 00:00:00.000000000 Z
11
+ date: 2024-10-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -174,6 +174,7 @@ extra_rdoc_files: []
174
174
  files:
175
175
  - CHANGELOG.md
176
176
  - CLI.md
177
+ - CONFIGURE.md
177
178
  - Gemfile
178
179
  - Gemfile.lock
179
180
  - LICENSE.txt
@@ -192,6 +193,7 @@ files:
192
193
  - lib/durable/llm/providers.rb
193
194
  - lib/durable/llm/providers/anthropic.rb
194
195
  - lib/durable/llm/providers/base.rb
196
+ - lib/durable/llm/providers/cohere.rb
195
197
  - lib/durable/llm/providers/groq.rb
196
198
  - lib/durable/llm/providers/huggingface.rb
197
199
  - lib/durable/llm/providers/openai.rb