llm.rb 0.1.0 → 0.2.1

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +85 -24
  3. data/lib/llm/conversation.rb +62 -10
  4. data/lib/llm/core_ext/ostruct.rb +0 -0
  5. data/lib/llm/error.rb +0 -0
  6. data/lib/llm/file.rb +0 -0
  7. data/lib/llm/http_client.rb +0 -0
  8. data/lib/llm/message.rb +1 -1
  9. data/lib/llm/message_queue.rb +18 -11
  10. data/lib/llm/model.rb +7 -0
  11. data/lib/llm/provider.rb +144 -98
  12. data/lib/llm/providers/anthropic/error_handler.rb +1 -1
  13. data/lib/llm/providers/anthropic/format.rb +7 -1
  14. data/lib/llm/providers/anthropic/response_parser.rb +0 -0
  15. data/lib/llm/providers/anthropic.rb +31 -15
  16. data/lib/llm/providers/gemini/error_handler.rb +0 -0
  17. data/lib/llm/providers/gemini/format.rb +7 -1
  18. data/lib/llm/providers/gemini/response_parser.rb +0 -0
  19. data/lib/llm/providers/gemini.rb +25 -14
  20. data/lib/llm/providers/ollama/error_handler.rb +0 -0
  21. data/lib/llm/providers/ollama/format.rb +7 -1
  22. data/lib/llm/providers/ollama/response_parser.rb +13 -0
  23. data/lib/llm/providers/ollama.rb +32 -8
  24. data/lib/llm/providers/openai/error_handler.rb +0 -0
  25. data/lib/llm/providers/openai/format.rb +7 -1
  26. data/lib/llm/providers/openai/response_parser.rb +5 -3
  27. data/lib/llm/providers/openai.rb +22 -12
  28. data/lib/llm/providers/voyageai/error_handler.rb +32 -0
  29. data/lib/llm/providers/voyageai/response_parser.rb +13 -0
  30. data/lib/llm/providers/voyageai.rb +44 -0
  31. data/lib/llm/response/completion.rb +0 -0
  32. data/lib/llm/response/embedding.rb +0 -0
  33. data/lib/llm/response.rb +0 -0
  34. data/lib/llm/version.rb +1 -1
  35. data/lib/llm.rb +19 -9
  36. data/llm.gemspec +6 -1
  37. data/share/llm/models/anthropic.yml +35 -0
  38. data/share/llm/models/gemini.yml +35 -0
  39. data/share/llm/models/ollama.yml +155 -0
  40. data/share/llm/models/openai.yml +46 -0
  41. data/spec/anthropic/completion_spec.rb +11 -27
  42. data/spec/anthropic/embedding_spec.rb +25 -0
  43. data/spec/gemini/completion_spec.rb +34 -29
  44. data/spec/gemini/embedding_spec.rb +4 -12
  45. data/spec/llm/conversation_spec.rb +93 -1
  46. data/spec/ollama/completion_spec.rb +7 -16
  47. data/spec/ollama/embedding_spec.rb +14 -5
  48. data/spec/openai/completion_spec.rb +40 -43
  49. data/spec/openai/embedding_spec.rb +4 -12
  50. data/spec/readme_spec.rb +9 -12
  51. data/spec/setup.rb +7 -16
  52. metadata +81 -4
  53. data/lib/llm/lazy_conversation.rb +0 -39
  54. data/spec/llm/lazy_conversation_spec.rb +0 -110
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5cd6331d31fab0e7582d9a0bef7a10b45e910fff70faace6bae774dd2757cff6
4
- data.tar.gz: 42f0d68055c3f4aa732bb31b7dddd8d72b8e4fa90ba8727e871a43b736060e8f
3
+ metadata.gz: 732a483717a5ec8e443077fb71294b1e301c3a8867b225c1fc2a58bd02fe3130
4
+ data.tar.gz: a1c2591a07c413cebfdffa99d133855bb177cc4a6607860333dbc9991da8d33e
5
5
  SHA512:
6
- metadata.gz: b3158f6d82d9f344deef6727ae2e74f2d7fc8637c46188d2aaf7c92d395cd50e737f39674d283decb17bcd9b7c45f5a0b592bf2fe3d48a8f121e89cdc0b5be35
7
- data.tar.gz: 74a9dbe6e7a2082cf764db07058761ed2ca5fb36916e0f78d487b4648fd34b531ca801e2202706a138d45191ed8f0f7e05db357f400aa05569403eadfb8fc887
6
+ metadata.gz: 4f5983f97b3c1e25f4147ec81f6d91df5073ea03dc4031690979f68b2053bf73bae07f7c57c3f7c9813dfd5b43eb1bd7364d5f5929234013d6b19bb49f9271ec
7
+ data.tar.gz: 286f560ce2d9e048e481796d27fd6c9a658b92cec0267f9527fad49bf5349ece0d05b943c086cce3c12bbd82f53dbf086ed91c516d39b815bd6106edca21914a
data/README.md CHANGED
@@ -31,8 +31,8 @@ llm = LLM.ollama(nil)
31
31
 
32
32
  The
33
33
  [LLM::Provider#chat](https://0x1eef.github.io/x/llm/LLM/Provider.html#chat-instance_method)
34
- method returns a
35
- [LLM::LazyConversation](https://0x1eef.github.io/x/llm/LLM/LazyConversation.html)
34
+ method returns a lazy-variant of a
35
+ [LLM::Conversation](https://0x1eef.github.io/x/llm/LLM/Conversation.html)
36
36
  object, and it allows for a "lazy" conversation where messages are batched and
37
37
  sent to the provider only when necessary. The non-lazy counterpart is available via the
38
38
  [LLM::Provider#chat!](https://0x1eef.github.io/x/llm/LLM/Provider.html#chat!-instance_method)
@@ -51,26 +51,26 @@ belonging to a lazy conversation:
51
51
  require "llm"
52
52
 
53
53
  llm = LLM.openai(ENV["KEY"])
54
- bot = llm.chat File.read("./share/llm/prompts/system.txt"), :system
55
- bot.chat "What color is the sky?"
56
- bot.chat "What color is an orange?"
57
- bot.chat "I like Ruby"
58
- bot.messages.each { print "[#{_1.role}] ", _1.content, "\n" }
54
+ convo = llm.chat File.read("./share/llm/prompts/system.txt"), :system
55
+ convo.chat "Tell me the answer to 5 + 15"
56
+ convo.chat "Tell me the answer to (5 + 15) * 2"
57
+ convo.chat "Tell me the answer to ((5 + 15) * 2) / 10"
58
+ convo.messages.each { print "[#{_1.role}] ", _1.content, "\n" }
59
59
 
60
60
  ##
61
- # [system] You are a friendly chatbot. Sometimes, you like to tell a joke.
62
- # But the joke must be based on the given inputs.
61
+ # [system] You are my math assistant.
62
+ # I will provide you with (simple) equations.
63
+ # You will provide answers in the format "The answer to <equation> is <answer>".
63
64
  # I will provide you a set of messages. Reply to all of them.
64
65
  # A message is considered unanswered if there is no corresponding assistant response.
65
66
  #
66
- # [user] What color is the sky?
67
- # [user] What color is an orange?
68
- # [user] I like Ruby
67
+ # [user] Tell me the answer to 5 + 15
68
+ # [user] Tell me the answer to (5 + 15) * 2
69
+ # [user] Tell me the answer to ((5 + 15) * 2) / 10
69
70
  #
70
- # [assistant] The sky is typically blue during the day. As for an orange,
71
- # it is usually orange in color—funny how that works, right?
72
- # I love Ruby too! Speaking of colors, why did the orange stop?
73
- # Because it ran out of juice! 🍊😂
71
+ # [assistant] The answer to 5 + 15 is 20.
72
+ # The answer to (5 + 15) * 2 is 40.
73
+ # The answer to ((5 + 15) * 2) / 10 is 4.
74
74
  ```
75
75
 
76
76
  #### Prompts
@@ -99,28 +99,87 @@ provider accepts:
99
99
 
100
100
  The
101
101
  [`LLM::Provider#embed`](https://0x1eef.github.io/x/llm/LLM/Provider.html#embed-instance_method)
102
- method generates a vector representation of a given piece of text.
103
- Embeddings capture the semantic meaning of text, and they are
104
- commonly used in tasks such as text similarity comparison (e.g., finding related documents),
105
- semantic search in vector databases, and the clustering and classification
106
- of text-based data:
102
+ method generates a vector representation of one or more chunks
103
+ of text. Embeddings capture the semantic meaning of text &ndash;
104
+ a common use-case for them is to store chunks of text in a
105
+ vector database, and then to query the database for *semantically
106
+ similar* text. These chunks of similar text can then support the
107
+ generation of a prompt that is used to query a large language model,
108
+ which will go on to generate a response.
109
+
110
+ For example, a user query might find similar text that adds important
111
+ context to the prompt that informs the large language model in how to respond.
112
+ The chunks of text may also carry metadata that can be used to further filter
113
+ and contextualize the search results. This technique is popularly known as
114
+ retrieval-augmented generation (RAG). Embeddings can also be used for
115
+ other purposes as well &ndash; RAG is just one of the most popular use-cases.
116
+
117
+ Let's take a look at an example that generates a couple of vectors
118
+ for two chunks of text:
107
119
 
108
120
  ```ruby
109
121
  #!/usr/bin/env ruby
110
122
  require "llm"
111
123
 
112
124
  llm = LLM.openai(ENV["KEY"])
113
- res = llm.embed("Hello, world!")
125
+ res = llm.embed(["programming is fun", "ruby is a programming language"])
114
126
  print res.class, "\n"
115
127
  print res.embeddings.size, "\n"
116
128
  print res.embeddings[0].size, "\n"
117
129
 
118
130
  ##
119
131
  # LLM::Response::Embedding
120
- # 1
132
+ # 2
121
133
  # 1536
122
134
  ```
123
135
 
136
+ ### LLM
137
+
138
+ #### Timeouts
139
+
140
+ When running the ollama provider locally it might take a while for
141
+ the language model to reply &ndash; depending on hardware and the
142
+ size of the model. The following example demonstrates how to wait
143
+ a longer period of time for a response through the use of the
144
+ `timeout` configuration option with the `qwq` model. The following
145
+ example waits up to 15 minutes for a response:
146
+
147
+ ```ruby
148
+ #!/usr/bin/env ruby
149
+ require "llm"
150
+
151
+ llm = LLM.ollama(nil, timeout: 60*15)
152
+ llm.chat "What is the meaning of life ?", model: "qwq"
153
+ llm.last_message.tap { print "[assistant] ", _1.content, "\n" }
154
+ ```
155
+
156
+ #### Models
157
+
158
+ Generally each Large Language Model provides multiple models to choose
159
+ from, and each model has its own set of capabilities and limitations.
160
+ The following example demonstrates how to query the list of models
161
+ through the
162
+ [LLM::Provider#models](http://0x1eef.github.io/x/llm/LLM/Provider.html#models-instance_method)
163
+ method &ndash; the example happens to use the ollama provider but
164
+ this can be done for any provider:
165
+
166
+ ```ruby
167
+ #!/usr/bin/env ruby
168
+ require "llm"
169
+
170
+ ##
171
+ # List models
172
+ llm = LLM.ollama(nil)
173
+ llm.models.each { print "#{_2.name}: #{_2.description}", "\n" }
174
+
175
+ ##
176
+ # Select a model
177
+ llm.chat "Hello, world!", model: llm.models["qwq"]
178
+
179
+ ##
180
+ # This also works
181
+ llm.chat "Hello, world!", model: "qwq"
182
+ ```
124
183
  ## Providers
125
184
 
126
185
  - [x] [Anthropic](https://www.anthropic.com/)
@@ -139,7 +198,9 @@ A complete API reference is available at [0x1eef.github.io/x/llm](https://0x1eef
139
198
 
140
199
  ## Install
141
200
 
142
- LLM has not been published to RubyGems.org yet. Stay tuned
201
+ llm.rb can be installed via rubygems.org:
202
+
203
+ gem install llm.rb
143
204
 
144
205
  ## License
145
206
 
@@ -3,14 +3,14 @@
3
3
  module LLM
4
4
  ##
5
5
  # {LLM::Conversation LLM::Conversation} provides a conversation
6
- # object that maintains a thread of messages that act as the
7
- # context of the conversation.
8
- #
6
+ # object that maintains a thread of messages that acts as context
7
+ # throughout the conversation.
9
8
  # @example
10
- # llm = LLM.openai(key)
11
- # bot = llm.chat("What is the capital of France?")
12
- # bot.chat("What should we eat in Paris?")
13
- # bot.chat("What is the weather like in Paris?")
9
+ # llm = LLM.openai(ENV["KEY"])
10
+ # convo = llm.chat("You are my climate expert", :system)
11
+ # convo.chat("What's the climate like in Rio de Janerio?", :user)
12
+ # convo.chat("What's the climate like in Algiers?", :user)
13
+ # convo.chat("What's the climate like in Tokyo?", :user)
14
14
  # p bot.messages.map { [_1.role, _1.content] }
15
15
  class Conversation
16
16
  ##
@@ -20,8 +20,12 @@ module LLM
20
20
  ##
21
21
  # @param [LLM::Provider] provider
22
22
  # A provider
23
- def initialize(provider)
23
+ # @param [Hash] params
24
+ # The parameters to maintain throughout the conversation
25
+ def initialize(provider, params = {})
24
26
  @provider = provider
27
+ @params = params
28
+ @lazy = false
25
29
  @messages = []
26
30
  end
27
31
 
@@ -30,9 +34,57 @@ module LLM
30
34
  # @return [LLM::Conversation]
31
35
  def chat(prompt, role = :user, **params)
32
36
  tap do
33
- completion = @provider.complete(prompt, role, **params.merge(messages:))
34
- @messages.concat [Message.new(role, prompt), completion.choices[0]]
37
+ if lazy?
38
+ @messages << [LLM::Message.new(role, prompt), @params.merge(params)]
39
+ else
40
+ completion = complete(prompt, role, params)
41
+ @messages.concat [Message.new(role, prompt), completion.choices[0]]
42
+ end
35
43
  end
36
44
  end
45
+
46
+ ##
47
+ # @note
48
+ # The `read_response` and `recent_message` methods are aliases of
49
+ # the `last_message` method, and you can choose the name that best
50
+ # fits your context or code style.
51
+ # @param [#to_s] role
52
+ # The role of the last message.
53
+ # Defaults to the LLM's assistant role (eg "assistant" or "model")
54
+ # @return [LLM::Message]
55
+ # The last message for the given role
56
+ def last_message(role: @provider.assistant_role)
57
+ messages.reverse_each.find { _1.role == role.to_s }
58
+ end
59
+ alias_method :recent_message, :last_message
60
+ alias_method :read_response, :last_message
61
+
62
+ ##
63
+ # Enables lazy mode for the conversation.
64
+ # @return [LLM::Conversation]
65
+ def lazy
66
+ tap do
67
+ next if lazy?
68
+ @lazy = true
69
+ @messages = LLM::MessageQueue.new(@provider)
70
+ end
71
+ end
72
+
73
+ ##
74
+ # @return [Boolean]
75
+ # Returns true if the conversation is lazy
76
+ def lazy?
77
+ @lazy
78
+ end
79
+
80
+ private
81
+
82
+ def complete(prompt, role, params)
83
+ @provider.complete(
84
+ prompt,
85
+ role,
86
+ **@params.merge(params.merge(messages:))
87
+ )
88
+ end
37
89
  end
38
90
  end
File without changes
data/lib/llm/error.rb CHANGED
File without changes
data/lib/llm/file.rb CHANGED
File without changes
File without changes
data/lib/llm/message.rb CHANGED
@@ -20,7 +20,7 @@ module LLM
20
20
  # @param [Hash] extra
21
21
  # @return [LLM::Message]
22
22
  def initialize(role, content, extra = {})
23
- @role = role
23
+ @role = role.to_s
24
24
  @content = content
25
25
  @extra = extra
26
26
  end
@@ -13,7 +13,8 @@ module LLM
13
13
  # @return [LLM::MessageQueue]
14
14
  def initialize(provider)
15
15
  @provider = provider
16
- @messages = []
16
+ @pending = []
17
+ @completed = []
17
18
  end
18
19
 
19
20
  ##
@@ -22,26 +23,32 @@ module LLM
22
23
  # @raise (see LLM::Provider#complete)
23
24
  # @return [void]
24
25
  def each
25
- @messages = complete! unless @messages.grep(LLM::Message).size == @messages.size
26
- @messages.each { yield(_1) }
26
+ complete! unless @pending.empty?
27
+ @completed.each { yield(_1) }
27
28
  end
28
29
 
29
30
  ##
30
- # @param message [Object]
31
- # A message to add to the conversation thread
31
+ # @param [[LLM::Message, Hash]] item
32
+ # A message and its parameters
32
33
  # @return [void]
33
- def <<(message)
34
- @messages << message
34
+ def <<(item)
35
+ @pending << item
36
+ self
35
37
  end
36
38
  alias_method :push, :<<
37
39
 
38
40
  private
39
41
 
40
42
  def complete!
41
- prompt, role, params = @messages[-1]
42
- rest = @messages[0..-2].map { (Array === _1) ? LLM::Message.new(_1[1], _1[0]) : _1 }
43
- comp = @provider.complete(prompt, role, **params.merge(messages: rest)).choices.last
44
- [*rest, LLM::Message.new(role, prompt), comp]
43
+ message, params = @pending[-1]
44
+ messages = @pending[0..-2].map { _1[0] }
45
+ completion = @provider.complete(
46
+ message.content,
47
+ message.role,
48
+ **params.merge(messages:)
49
+ )
50
+ @completed.concat([*messages, message, completion.choices[0]])
51
+ @pending.clear
45
52
  end
46
53
  end
47
54
  end
data/lib/llm/model.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Model < Struct.new(:name, :parameters, :description, :to_param, keyword_init: true)
4
+ def to_json(*)
5
+ to_param.to_json(*)
6
+ end
7
+ end
data/lib/llm/provider.rb CHANGED
@@ -1,114 +1,160 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM
4
- require "llm/http_client"
3
+ ##
4
+ # The Provider class represents an abstract class for
5
+ # LLM (Language Model) providers.
6
+ #
7
+ # @note
8
+ # This class is not meant to be instantiated directly.
9
+ # Instead, use one of the subclasses that implement
10
+ # the methods defined here.
11
+ #
12
+ # @abstract
13
+ # @see LLM::Provider::OpenAI
14
+ # @see LLM::Provider::Anthropic
15
+ # @see LLM::Provider::Gemini
16
+ # @see LLM::Provider::Ollama
17
+ class LLM::Provider
18
+ require_relative "http_client"
19
+ include LLM::HTTPClient
20
+
5
21
  ##
6
- # The Provider class represents an abstract class for
7
- # LLM (Language Model) providers
8
- class Provider
9
- include HTTPClient
10
- ##
11
- # @param [String] secret
12
- # The secret key for authentication
13
- # @param [String] host
14
- # The host address of the LLM provider
15
- # @param [Integer] port
16
- # The port number
17
- def initialize(secret, host:, port: 443, ssl: true)
18
- @secret = secret
19
- @http = Net::HTTP.new(host, port).tap do |http|
20
- http.use_ssl = ssl
21
- end
22
+ # @param [String] secret
23
+ # The secret key for authentication
24
+ # @param [String] host
25
+ # The host address of the LLM provider
26
+ # @param [Integer] port
27
+ # The port number
28
+ # @param [Integer] timeout
29
+ # The number of seconds to wait for a response
30
+ def initialize(secret, host:, port: 443, timeout: 60, ssl: true)
31
+ @secret = secret
32
+ @http = Net::HTTP.new(host, port).tap do |http|
33
+ http.use_ssl = ssl
34
+ http.read_timeout = timeout
22
35
  end
36
+ end
23
37
 
24
- ##
25
- # Returns an inspection of the provider object
26
- # @return [String]
27
- # @note The secret key is redacted in inspect for security reasons
28
- def inspect
29
- "#<#{self.class.name}:0x#{object_id.to_s(16)} @secret=[REDACTED] @http=#{@http.inspect}>"
30
- end
38
+ ##
39
+ # Returns an inspection of the provider object
40
+ # @return [String]
41
+ # @note The secret key is redacted in inspect for security reasons
42
+ def inspect
43
+ "#<#{self.class.name}:0x#{object_id.to_s(16)} @secret=[REDACTED] @http=#{@http.inspect}>"
44
+ end
31
45
 
32
- ##
33
- # @param [String] input
34
- # The input to embed
35
- # @raise [NotImplementedError]
36
- # When the method is not implemented by a subclass
37
- # @return [LLM::Response::Embedding]
38
- def embed(input, **params)
39
- raise NotImplementedError
40
- end
46
+ ##
47
+ # @param [String, Array<String>] input
48
+ # The input to embed
49
+ # @raise [NotImplementedError]
50
+ # When the method is not implemented by a subclass
51
+ # @return [LLM::Response::Embedding]
52
+ def embed(input, **params)
53
+ raise NotImplementedError
54
+ end
41
55
 
42
- ##
43
- # Completes a given prompt using the LLM
44
- # @param [String] prompt
45
- # The input prompt to be completed
46
- # @param [Symbol] role
47
- # The role of the prompt (e.g. :user, :system)
48
- # @raise [NotImplementedError]
49
- # When the method is not implemented by a subclass
50
- # @return [LLM::Response::Completion]
51
- def complete(prompt, role = :user, **params)
52
- raise NotImplementedError
53
- end
56
+ ##
57
+ # Completes a given prompt using the LLM
58
+ # @example
59
+ # llm = LLM.openai(ENV["KEY"])
60
+ # context = [
61
+ # {role: "system", content: "Answer all of my questions"},
62
+ # {role: "system", content: "Your name is Pablo, you are 25 years old and you are my amigo"},
63
+ # ]
64
+ # res = llm.complete "What is your name and what age are you?", :user, messages: context
65
+ # print "[#{res.choices[0].role}]", res.choices[0].content, "\n"
66
+ # @param [String] prompt
67
+ # The input prompt to be completed
68
+ # @param [Symbol] role
69
+ # The role of the prompt (e.g. :user, :system)
70
+ # @param [Array<Hash, LLM::Message>] messages
71
+ # The messages to include in the completion
72
+ # @raise [NotImplementedError]
73
+ # When the method is not implemented by a subclass
74
+ # @return [LLM::Response::Completion]
75
+ def complete(prompt, role = :user, **params)
76
+ raise NotImplementedError
77
+ end
54
78
 
55
- ##
56
- # Starts a new lazy conversation
57
- # @param prompt (see LLM::Provider#complete)
58
- # @param role (see LLM::Provider#complete)
59
- # @raise (see LLM::Provider#complete)
60
- # @return [LLM::LazyConversation]
61
- def chat(prompt, role = :user, **params)
62
- LazyConversation.new(self).chat(prompt, role, **params)
63
- end
79
+ ##
80
+ # Starts a new lazy conversation
81
+ # @note
82
+ # This method creates a lazy variant of a
83
+ # {LLM::Conversation LLM::Conversation} object.
84
+ # @param prompt (see LLM::Provider#complete)
85
+ # @param role (see LLM::Provider#complete)
86
+ # @raise (see LLM::Provider#complete)
87
+ # @return [LLM::LazyConversation]
88
+ def chat(prompt, role = :user, **params)
89
+ LLM::Conversation.new(self, params).lazy.chat(prompt, role)
90
+ end
64
91
 
65
- ##
66
- # Starts a new conversation
67
- # @param prompt (see LLM::Provider#complete)
68
- # @param role (see LLM::Provider#complete)
69
- # @raise (see LLM::Provider#complete)
70
- # @return [LLM::Conversation]
71
- def chat!(prompt, role = :user, **params)
72
- Conversation.new(self).chat(prompt, role, **params)
73
- end
92
+ ##
93
+ # Starts a new conversation
94
+ # @note
95
+ # This method creates a non-lazy variant of a
96
+ # {LLM::Conversation LLM::Conversation} object.
97
+ # @param prompt (see LLM::Provider#complete)
98
+ # @param role (see LLM::Provider#complete)
99
+ # @raise (see LLM::Provider#complete)
100
+ # @return [LLM::Conversation]
101
+ def chat!(prompt, role = :user, **params)
102
+ LLM::Conversation.new(self, params).chat(prompt, role)
103
+ end
74
104
 
75
- private
105
+ ##
106
+ # @return [String]
107
+ # Returns the role of the assistant in the conversation.
108
+ # Usually "assistant" or "model"
109
+ def assistant_role
110
+ raise NotImplementedError
111
+ end
76
112
 
77
- ##
78
- # Prepares a request for authentication
79
- # @param [Net::HTTP::Request] req
80
- # The request to prepare for authentication
81
- # @raise [NotImplementedError]
82
- # (see LLM::Provider#complete)
83
- def auth(req)
84
- raise NotImplementedError
85
- end
113
+ ##
114
+ # @return [Hash<String, LLM::Model>]
115
+ # Returns a hash of available models
116
+ def models
117
+ raise NotImplementedError
118
+ end
86
119
 
87
- ##
88
- # @return [Module]
89
- # Returns the module responsible for parsing a successful LLM response
90
- # @raise [NotImplementedError]
91
- # (see LLM::Provider#complete)
92
- def response_parser
93
- raise NotImplementedError
94
- end
120
+ private
95
121
 
96
- ##
97
- # @return [Class]
98
- # Returns the class responsible for handling an unsuccessful LLM response
99
- # @raise [NotImplementedError]
100
- # (see LLM::Provider#complete)
101
- def error_handler
102
- raise NotImplementedError
103
- end
122
+ ##
123
+ # The headers to include with a request
124
+ # @raise [NotImplementedError]
125
+ # (see LLM::Provider#complete)
126
+ def headers
127
+ raise NotImplementedError
128
+ end
104
129
 
105
- ##
106
- # Prepares a request before sending it
107
- def preflight(req, body)
108
- req.content_type = "application/json"
109
- req.body = JSON.generate(body)
110
- auth(req)
111
- req
112
- end
130
+ ##
131
+ # @return [Module]
132
+ # Returns the module responsible for parsing a successful LLM response
133
+ # @raise [NotImplementedError]
134
+ # (see LLM::Provider#complete)
135
+ def response_parser
136
+ raise NotImplementedError
137
+ end
138
+
139
+ ##
140
+ # @return [Class]
141
+ # Returns the class responsible for handling an unsuccessful LLM response
142
+ # @raise [NotImplementedError]
143
+ # (see LLM::Provider#complete)
144
+ def error_handler
145
+ raise NotImplementedError
146
+ end
147
+
148
+ ##
149
+ # @param [String] provider
150
+ # The name of the provider
151
+ # @return [Hash<String, Hash>]
152
+ def load_models!(provider)
153
+ require "yaml" unless defined?(YAML)
154
+ rootdir = File.realpath File.join(__dir__, "..", "..")
155
+ sharedir = File.join(rootdir, "share", "llm")
156
+ provider = provider.gsub(/[^a-z0-9]/i, "")
157
+ yaml = File.join(sharedir, "models", "#{provider}.yml")
158
+ YAML.safe_load_file(yaml).transform_values { LLM::Model.new(_1) }
113
159
  end
114
160
  end
@@ -20,7 +20,7 @@ class LLM::Anthropic
20
20
  # Raises a subclass of {LLM::Error LLM::Error}
21
21
  def raise_error!
22
22
  case res
23
- when Net::HTTPForbidden
23
+ when Net::HTTPUnauthorized
24
24
  raise LLM::Error::Unauthorized.new { _1.response = res }, "Authentication error"
25
25
  when Net::HTTPTooManyRequests
26
26
  raise LLM::Error::RateLimit.new { _1.response = res }, "Too many requests"
@@ -7,7 +7,13 @@ class LLM::Anthropic
7
7
  # The messages to format
8
8
  # @return [Array<Hash>]
9
9
  def format(messages)
10
- messages.map { {role: _1.role, content: format_content(_1.content)} }
10
+ messages.map do
11
+ if Hash === _1
12
+ {role: _1[:role], content: format_content(_1[:content])}
13
+ else
14
+ {role: _1.role, content: format_content(_1.content)}
15
+ end
16
+ end
11
17
  end
12
18
 
13
19
  private
File without changes