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 +4 -4
- data/CONFIGURE.md +132 -0
- data/Gemfile.lock +1 -1
- data/lib/durable/llm/cli.rb +39 -45
- data/lib/durable/llm/client.rb +8 -6
- data/lib/durable/llm/configuration.rb +13 -15
- data/lib/durable/llm/providers/anthropic.rb +23 -21
- data/lib/durable/llm/providers/base.rb +24 -9
- data/lib/durable/llm/providers/cohere.rb +100 -0
- data/lib/durable/llm/providers/groq.rb +3 -6
- data/lib/durable/llm/providers/huggingface.rb +2 -1
- data/lib/durable/llm/providers/openai.rb +13 -22
- data/lib/durable/llm/providers.rb +6 -8
- data/lib/durable/llm/version.rb +1 -1
- data/lib/durable/llm.rb +4 -4
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 41b81b7592103ed34098a473f004036c0a9ed22a6f43cd764c32d057ac840cd9
|
4
|
+
data.tar.gz: 0caf0d378409b4d025261049c41deb769b111eb556a77843495202cc58cea430
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
data/lib/durable/llm/cli.rb
CHANGED
@@ -10,29 +10,26 @@ module Durable
|
|
10
10
|
true
|
11
11
|
end
|
12
12
|
|
13
|
-
desc
|
14
|
-
option :model, aliases:
|
15
|
-
option :system, aliases:
|
16
|
-
option :continue, aliases:
|
17
|
-
option :conversation, aliases:
|
18
|
-
option :no_stream, type: :boolean, desc:
|
19
|
-
option :option, aliases:
|
20
|
-
|
21
|
-
def prompt(prompt)
|
22
|
-
|
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:
|
35
|
-
messages << { role:
|
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
|
42
|
+
puts response.choices.first
|
46
43
|
else
|
47
44
|
client.stream(params) do |chunk|
|
48
|
-
print chunk
|
45
|
+
print chunk
|
49
46
|
$stdout.flush
|
50
47
|
end
|
51
48
|
end
|
52
49
|
end
|
53
50
|
|
54
|
-
desc
|
55
|
-
option :model, aliases:
|
56
|
-
option :system, aliases:
|
57
|
-
option :continue, aliases:
|
58
|
-
option :conversation, aliases:
|
59
|
-
option :option, aliases:
|
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
|
-
|
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:
|
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 [
|
76
|
+
input = cli.ask('> ')
|
77
|
+
break if %w[exit quit].include?(input.downcase)
|
84
78
|
|
85
|
-
if input ==
|
79
|
+
if input == '!multi'
|
86
80
|
input = cli.ask("Enter multiple lines. Type '!end' to finish:") do |q|
|
87
|
-
q.gather =
|
81
|
+
q.gather = '!end'
|
88
82
|
end
|
89
83
|
end
|
90
84
|
|
91
|
-
messages << { role:
|
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:
|
94
|
+
messages << { role: 'assistant', content: response.choices.first.to_s }
|
101
95
|
end
|
102
96
|
end
|
103
97
|
|
104
|
-
desc
|
105
|
-
option :options, type: :boolean, desc:
|
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(
|
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.
|
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}")
|
data/lib/durable/llm/client.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
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
|
-
|
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 =
|
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
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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(
|
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,
|
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,
|
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,
|
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,
|
50
|
+
raise NotImplementedError, 'Subclasses must implement stream'
|
36
51
|
end
|
37
52
|
|
38
53
|
def embedding(model:, input:, **options)
|
39
|
-
raise NotImplementedError,
|
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,
|
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(
|
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
|
-
[
|
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
|
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 =
|
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(
|
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 ==
|
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 =
|
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
|
-
|
14
|
-
|
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
|
data/lib/durable/llm/version.rb
CHANGED
data/lib/durable/llm.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
require
|
1
|
+
require 'zeitwerk'
|
2
2
|
loader = Zeitwerk::Loader.new
|
3
|
-
loader.tag = File.basename(__FILE__,
|
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.
|
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-
|
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
|