llm.rb 0.1.0 → 0.2.0

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +83 -22
  3. data/lib/llm/conversation.rb +14 -2
  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/lazy_conversation.rb +14 -2
  9. data/lib/llm/message.rb +1 -1
  10. data/lib/llm/message_queue.rb +0 -0
  11. data/lib/llm/model.rb +7 -0
  12. data/lib/llm/provider.rb +117 -98
  13. data/lib/llm/providers/anthropic/error_handler.rb +1 -1
  14. data/lib/llm/providers/anthropic/format.rb +0 -0
  15. data/lib/llm/providers/anthropic/response_parser.rb +0 -0
  16. data/lib/llm/providers/anthropic.rb +31 -15
  17. data/lib/llm/providers/gemini/error_handler.rb +0 -0
  18. data/lib/llm/providers/gemini/format.rb +0 -0
  19. data/lib/llm/providers/gemini/response_parser.rb +0 -0
  20. data/lib/llm/providers/gemini.rb +25 -14
  21. data/lib/llm/providers/ollama/error_handler.rb +0 -0
  22. data/lib/llm/providers/ollama/format.rb +0 -0
  23. data/lib/llm/providers/ollama/response_parser.rb +13 -0
  24. data/lib/llm/providers/ollama.rb +32 -8
  25. data/lib/llm/providers/openai/error_handler.rb +0 -0
  26. data/lib/llm/providers/openai/format.rb +0 -0
  27. data/lib/llm/providers/openai/response_parser.rb +5 -3
  28. data/lib/llm/providers/openai.rb +22 -12
  29. data/lib/llm/providers/voyageai/error_handler.rb +32 -0
  30. data/lib/llm/providers/voyageai/response_parser.rb +13 -0
  31. data/lib/llm/providers/voyageai.rb +44 -0
  32. data/lib/llm/response/completion.rb +0 -0
  33. data/lib/llm/response/embedding.rb +0 -0
  34. data/lib/llm/response.rb +0 -0
  35. data/lib/llm/version.rb +1 -1
  36. data/lib/llm.rb +18 -8
  37. data/llm.gemspec +6 -1
  38. data/share/llm/models/anthropic.yml +35 -0
  39. data/share/llm/models/gemini.yml +35 -0
  40. data/share/llm/models/ollama.yml +155 -0
  41. data/share/llm/models/openai.yml +46 -0
  42. data/spec/anthropic/completion_spec.rb +11 -27
  43. data/spec/anthropic/embedding_spec.rb +25 -0
  44. data/spec/gemini/completion_spec.rb +13 -29
  45. data/spec/gemini/embedding_spec.rb +4 -12
  46. data/spec/llm/lazy_conversation_spec.rb +45 -63
  47. data/spec/ollama/completion_spec.rb +7 -16
  48. data/spec/ollama/embedding_spec.rb +14 -5
  49. data/spec/openai/completion_spec.rb +19 -43
  50. data/spec/openai/embedding_spec.rb +4 -12
  51. data/spec/readme_spec.rb +9 -12
  52. data/spec/setup.rb +7 -16
  53. metadata +81 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5cd6331d31fab0e7582d9a0bef7a10b45e910fff70faace6bae774dd2757cff6
4
- data.tar.gz: 42f0d68055c3f4aa732bb31b7dddd8d72b8e4fa90ba8727e871a43b736060e8f
3
+ metadata.gz: a7f9330df025e16999629b094202d78b504de9bedeafc45ec8d75bf729024567
4
+ data.tar.gz: e3a22daa5ac7add9b815346fe810ed368b9f3e3a3bb135cb4e5bbbb3875eacae
5
5
  SHA512:
6
- metadata.gz: b3158f6d82d9f344deef6727ae2e74f2d7fc8637c46188d2aaf7c92d395cd50e737f39674d283decb17bcd9b7c45f5a0b592bf2fe3d48a8f121e89cdc0b5be35
7
- data.tar.gz: 74a9dbe6e7a2082cf764db07058761ed2ca5fb36916e0f78d487b4648fd34b531ca801e2202706a138d45191ed8f0f7e05db357f400aa05569403eadfb8fc887
6
+ metadata.gz: b985c6f0b967354f3d1e5941ebeb2e5d6623b9d0abc33fd238f3764e72a6a6e10c293e7e61221d1be8ad89e631ecc7236bca597e809f1598ad54f0b305bb1103
7
+ data.tar.gz: 721f373b9eada6417b864e723504e5168df8f4362bef9edb4026aad2b39a3f99de5c72081f40ab80828d2f53d24c62c55e035ccd4964e3dadca2a27f3784e6e6
data/README.md CHANGED
@@ -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
 
@@ -20,8 +20,9 @@ module LLM
20
20
  ##
21
21
  # @param [LLM::Provider] provider
22
22
  # A provider
23
- def initialize(provider)
23
+ def initialize(provider, params = {})
24
24
  @provider = provider
25
+ @params = params
25
26
  @messages = []
26
27
  end
27
28
 
@@ -30,9 +31,20 @@ module LLM
30
31
  # @return [LLM::Conversation]
31
32
  def chat(prompt, role = :user, **params)
32
33
  tap do
33
- completion = @provider.complete(prompt, role, **params.merge(messages:))
34
+ completion = @provider.complete(prompt, role, **@params.merge(params.merge(messages:)))
34
35
  @messages.concat [Message.new(role, prompt), completion.choices[0]]
35
36
  end
36
37
  end
38
+
39
+ ##
40
+ # @param [#to_s] role
41
+ # The role of the last message.
42
+ # Defaults to the LLM's assistant role (eg "assistant" or "model")
43
+ # @return [LLM::Message]
44
+ # The last message for the given role
45
+ def last_message(role: @provider.assistant_role)
46
+ messages.reverse_each.find { _1.role == role.to_s }
47
+ end
48
+ alias_method :recent_message, :last_message
37
49
  end
38
50
  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
@@ -24,8 +24,9 @@ module LLM
24
24
  ##
25
25
  # @param [LLM::Provider] provider
26
26
  # A provider
27
- def initialize(provider)
27
+ def initialize(provider, params = {})
28
28
  @provider = provider
29
+ @params = params
29
30
  @messages = LLM::MessageQueue.new(provider)
30
31
  end
31
32
 
@@ -33,7 +34,18 @@ module LLM
33
34
  # @param prompt (see LLM::Provider#prompt)
34
35
  # @return [LLM::Conversation]
35
36
  def chat(prompt, role = :user, **params)
36
- tap { @messages << [prompt, role, params] }
37
+ tap { @messages << [prompt, role, @params.merge(params)] }
37
38
  end
39
+
40
+ ##
41
+ # @param [#to_s] role
42
+ # The role of the last message.
43
+ # Defaults to the LLM's assistant role (eg "assistant" or "model")
44
+ # @return [LLM::Message]
45
+ # The last message for the given role
46
+ def last_message(role: @provider.assistant_role)
47
+ messages.reverse_each.find { _1.role == role.to_s }
48
+ end
49
+ alias_method :recent_message, :last_message
38
50
  end
39
51
  end
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
File without changes
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,133 @@
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
+ class LLM::Provider
7
+ require_relative "http_client"
8
+ include LLM::HTTPClient
9
+
5
10
  ##
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
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
+ # @param [Integer] timeout
18
+ # The number of seconds to wait for a response
19
+ def initialize(secret, host:, port: 443, timeout: 60, ssl: true)
20
+ @secret = secret
21
+ @http = Net::HTTP.new(host, port).tap do |http|
22
+ http.use_ssl = ssl
23
+ http.read_timeout = timeout
22
24
  end
25
+ end
23
26
 
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
27
+ ##
28
+ # Returns an inspection of the provider object
29
+ # @return [String]
30
+ # @note The secret key is redacted in inspect for security reasons
31
+ def inspect
32
+ "#<#{self.class.name}:0x#{object_id.to_s(16)} @secret=[REDACTED] @http=#{@http.inspect}>"
33
+ end
31
34
 
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
35
+ ##
36
+ # @param [String, Array<String>] input
37
+ # The input to embed
38
+ # @raise [NotImplementedError]
39
+ # When the method is not implemented by a subclass
40
+ # @return [LLM::Response::Embedding]
41
+ def embed(input, **params)
42
+ raise NotImplementedError
43
+ end
41
44
 
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
45
+ ##
46
+ # Completes a given prompt using the LLM
47
+ # @param [String] prompt
48
+ # The input prompt to be completed
49
+ # @param [Symbol] role
50
+ # The role of the prompt (e.g. :user, :system)
51
+ # @raise [NotImplementedError]
52
+ # When the method is not implemented by a subclass
53
+ # @return [LLM::Response::Completion]
54
+ def complete(prompt, role = :user, **params)
55
+ raise NotImplementedError
56
+ end
54
57
 
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
58
+ ##
59
+ # Starts a new lazy conversation
60
+ # @param prompt (see LLM::Provider#complete)
61
+ # @param role (see LLM::Provider#complete)
62
+ # @raise (see LLM::Provider#complete)
63
+ # @return [LLM::LazyConversation]
64
+ def chat(prompt, role = :user, **params)
65
+ LLM::LazyConversation.new(self, params).chat(prompt, role)
66
+ end
64
67
 
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
68
+ ##
69
+ # Starts a new conversation
70
+ # @param prompt (see LLM::Provider#complete)
71
+ # @param role (see LLM::Provider#complete)
72
+ # @raise (see LLM::Provider#complete)
73
+ # @return [LLM::Conversation]
74
+ def chat!(prompt, role = :user, **params)
75
+ LLM::Conversation.new(self, params).chat(prompt, role)
76
+ end
74
77
 
75
- private
78
+ ##
79
+ # @return [String]
80
+ # Returns the role of the assistant in the conversation.
81
+ # Usually "assistant" or "model"
82
+ def assistant_role
83
+ raise NotImplementedError
84
+ end
76
85
 
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
86
+ ##
87
+ # @return [Hash<String, LLM::Model>]
88
+ # Returns a hash of available models
89
+ def models
90
+ raise NotImplementedError
91
+ end
86
92
 
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
93
+ private
95
94
 
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
95
+ ##
96
+ # The headers to include with a request
97
+ # @raise [NotImplementedError]
98
+ # (see LLM::Provider#complete)
99
+ def headers
100
+ raise NotImplementedError
101
+ end
104
102
 
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
103
+ ##
104
+ # @return [Module]
105
+ # Returns the module responsible for parsing a successful LLM response
106
+ # @raise [NotImplementedError]
107
+ # (see LLM::Provider#complete)
108
+ def response_parser
109
+ raise NotImplementedError
110
+ end
111
+
112
+ ##
113
+ # @return [Class]
114
+ # Returns the class responsible for handling an unsuccessful LLM response
115
+ # @raise [NotImplementedError]
116
+ # (see LLM::Provider#complete)
117
+ def error_handler
118
+ raise NotImplementedError
119
+ end
120
+
121
+ ##
122
+ # @param [String] provider
123
+ # The name of the provider
124
+ # @return [Hash<String, Hash>]
125
+ def load_models!(provider)
126
+ require "yaml" unless defined?(YAML)
127
+ rootdir = File.realpath File.join(__dir__, "..", "..")
128
+ sharedir = File.join(rootdir, "share", "llm")
129
+ provider = provider.gsub(/[^a-z0-9]/i, "")
130
+ yaml = File.join(sharedir, "models", "#{provider}.yml")
131
+ YAML.safe_load_file(yaml).transform_values { LLM::Model.new(_1) }
113
132
  end
114
133
  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"
File without changes
File without changes
@@ -11,7 +11,6 @@ module LLM
11
11
  include Format
12
12
 
13
13
  HOST = "api.anthropic.com"
14
- DEFAULT_PARAMS = {max_tokens: 1024, model: "claude-3-5-sonnet-20240620"}.freeze
15
14
 
16
15
  ##
17
16
  # @param secret (see LLM::Provider#initialize)
@@ -20,14 +19,17 @@ module LLM
20
19
  end
21
20
 
22
21
  ##
22
+ # Provides an embedding via VoyageAI per
23
+ # [Anthropic's recommendation](https://docs.anthropic.com/en/docs/build-with-claude/embeddings)
23
24
  # @param input (see LLM::Provider#embed)
25
+ # @param [String] token
26
+ # Valid token for the VoyageAI API
27
+ # @param [Hash] params
28
+ # Additional parameters to pass to the API
24
29
  # @return (see LLM::Provider#embed)
25
- def embed(input, **params)
26
- req = Net::HTTP::Post.new ["api.voyageai.com/v1", "embeddings"].join("/")
27
- body = {input:, model: "voyage-2"}.merge!(params)
28
- req = preflight(req, body)
29
- res = request(@http, req)
30
- Response::Embedding.new(res).extend(response_parser)
30
+ def embed(input, token:, **params)
31
+ llm = LLM.voyageai(token)
32
+ llm.embed(input, **params)
31
33
  end
32
34
 
33
35
  ##
@@ -36,20 +38,34 @@ module LLM
36
38
  # @param role (see LLM::Provider#complete)
37
39
  # @return (see LLM::Provider#complete)
38
40
  def complete(prompt, role = :user, **params)
39
- req = Net::HTTP::Post.new ["/v1", "messages"].join("/")
41
+ params = {max_tokens: 1024, model: "claude-3-5-sonnet-20240620"}.merge!(params)
42
+ req = Net::HTTP::Post.new("/v1/messages", headers)
40
43
  messages = [*(params.delete(:messages) || []), Message.new(role, prompt)]
41
- params = DEFAULT_PARAMS.merge(params)
42
- body = {messages: format(messages)}.merge!(params)
43
- req = preflight(req, body)
44
- res = request(@http, req)
44
+ req.body = JSON.dump({messages: format(messages)}.merge!(params))
45
+ res = request(@http, req)
45
46
  Response::Completion.new(res).extend(response_parser)
46
47
  end
47
48
 
49
+ ##
50
+ # @return (see LLM::Provider#assistant_role)
51
+ def assistant_role
52
+ "assistant"
53
+ end
54
+
55
+ ##
56
+ # @return (see LLM::Provider#models)
57
+ def models
58
+ @models ||= load_models!("anthropic")
59
+ end
60
+
48
61
  private
49
62
 
50
- def auth(req)
51
- req["anthropic-version"] = "2023-06-01"
52
- req["x-api-key"] = @secret
63
+ def headers
64
+ {
65
+ "Content-Type" => "application/json",
66
+ "x-api-key" => @secret,
67
+ "anthropic-version" => "2023-06-01"
68
+ }
53
69
  end
54
70
 
55
71
  def response_parser
File without changes
File without changes
File without changes
@@ -11,7 +11,6 @@ module LLM
11
11
  include Format
12
12
 
13
13
  HOST = "generativelanguage.googleapis.com"
14
- DEFAULT_PARAMS = {model: "gemini-1.5-flash"}.freeze
15
14
 
16
15
  ##
17
16
  # @param secret (see LLM::Provider#initialize)
@@ -23,11 +22,10 @@ module LLM
23
22
  # @param input (see LLM::Provider#embed)
24
23
  # @return (see LLM::Provider#embed)
25
24
  def embed(input, **params)
26
- path = ["/v1beta/models", "text-embedding-004"].join("/")
27
- req = Net::HTTP::Post.new [path, "embedContent"].join(":")
28
- body = {content: {parts: [{text: input}]}}
29
- req = preflight(req, body)
30
- res = request @http, req
25
+ path = ["/v1beta/models/text-embedding-004", "embedContent?key=#{@secret}"].join(":")
26
+ req = Net::HTTP::Post.new(path, headers)
27
+ req.body = JSON.dump({content: {parts: [{text: input}]}})
28
+ res = request(@http, req)
31
29
  Response::Embedding.new(res).extend(response_parser)
32
30
  end
33
31
 
@@ -37,20 +35,33 @@ module LLM
37
35
  # @param role (see LLM::Provider#complete)
38
36
  # @return (see LLM::Provider#complete)
39
37
  def complete(prompt, role = :user, **params)
40
- params = DEFAULT_PARAMS.merge(params)
41
- path = ["/v1beta/models", params.delete(:model)].join("/")
42
- req = Net::HTTP::Post.new [path, "generateContent"].join(":")
38
+ params = {model: "gemini-1.5-flash"}.merge!(params)
39
+ path = ["/v1beta/models/#{params.delete(:model)}", "generateContent?key=#{@secret}"].join(":")
40
+ req = Net::HTTP::Post.new(path, headers)
43
41
  messages = [*(params.delete(:messages) || []), LLM::Message.new(role, prompt)]
44
- body = {contents: format(messages)}
45
- req = preflight(req, body)
46
- res = request(@http, req)
42
+ req.body = JSON.dump({contents: format(messages)})
43
+ res = request(@http, req)
47
44
  Response::Completion.new(res).extend(response_parser)
48
45
  end
49
46
 
47
+ ##
48
+ # @return (see LLM::Provider#assistant_role)
49
+ def assistant_role
50
+ "model"
51
+ end
52
+
53
+ ##
54
+ # @return (see LLM::Provider#models)
55
+ def models
56
+ @models ||= load_models!("gemini")
57
+ end
58
+
50
59
  private
51
60
 
52
- def auth(req)
53
- req.path.replace [req.path, URI.encode_www_form(key: @secret)].join("?")
61
+ def headers
62
+ {
63
+ "Content-Type" => "application/json"
64
+ }
54
65
  end
55
66
 
56
67
  def response_parser
File without changes
File without changes