llm.rb 0.3.2 → 0.3.3
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/README.md +40 -7
- data/lib/llm/chat.rb +5 -3
- data/lib/llm/core_ext/ostruct.rb +1 -1
- data/lib/llm/file.rb +8 -1
- data/lib/llm/model.rb +27 -2
- data/lib/llm/provider.rb +20 -28
- data/lib/llm/providers/anthropic/format.rb +19 -6
- data/lib/llm/providers/anthropic/models.rb +62 -0
- data/lib/llm/providers/anthropic.rb +23 -8
- data/lib/llm/providers/gemini/format.rb +6 -1
- data/lib/llm/providers/gemini/images.rb +3 -3
- data/lib/llm/providers/gemini/models.rb +69 -0
- data/lib/llm/providers/gemini/response_parser.rb +1 -5
- data/lib/llm/providers/gemini.rb +20 -5
- data/lib/llm/providers/ollama/format.rb +11 -3
- data/lib/llm/providers/ollama/models.rb +66 -0
- data/lib/llm/providers/ollama.rb +23 -8
- data/lib/llm/providers/openai/audio.rb +0 -2
- data/lib/llm/providers/openai/format.rb +6 -1
- data/lib/llm/providers/openai/images.rb +1 -1
- data/lib/llm/providers/openai/models.rb +62 -0
- data/lib/llm/providers/openai/response_parser.rb +1 -5
- data/lib/llm/providers/openai/responses.rb +10 -6
- data/lib/llm/providers/openai.rb +24 -7
- data/lib/llm/response/modellist.rb +18 -0
- data/lib/llm/response.rb +1 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +2 -1
- data/spec/anthropic/completion_spec.rb +36 -0
- data/spec/anthropic/models_spec.rb +21 -0
- data/spec/gemini/images_spec.rb +4 -12
- data/spec/gemini/models_spec.rb +21 -0
- data/spec/llm/conversation_spec.rb +5 -3
- data/spec/ollama/models_spec.rb +20 -0
- data/spec/openai/completion_spec.rb +19 -0
- data/spec/openai/images_spec.rb +2 -6
- data/spec/openai/models_spec.rb +21 -0
- metadata +11 -6
- data/share/llm/models/anthropic.yml +0 -35
- data/share/llm/models/gemini.yml +0 -35
- data/share/llm/models/ollama.yml +0 -155
- data/share/llm/models/openai.yml +0 -46
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c8ce8caa7c769da9197528a864c153071f3c4aca15718efc985e543911c04ce2
|
4
|
+
data.tar.gz: 389ff41e9e2b35782b1484048b7597f5573bf2a86cf9eaff8cfd7c4cb2b19be3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 39c8f71eae878b5787ca839138de07ce06cba4fdee0bafb1bd75a71f3b3e59ee08fa05f5d9f280522ec751722c9a8a15430d1b999b05e14052d85c745bf9781c
|
7
|
+
data.tar.gz: fdb5268b0095f09b41481e6c7071a2dae66cf9a3fd21755834b76040e13a236bc18122dfe95230dd49a827e774487a1add1aa35c8976f284a03afad881321b46
|
data/README.md
CHANGED
@@ -38,7 +38,9 @@ The following example enables lazy mode for a
|
|
38
38
|
object by entering into a "lazy" conversation where messages are buffered and
|
39
39
|
sent to the provider only when necessary. Both lazy and non-lazy conversations
|
40
40
|
maintain a message thread that can be reused as context throughout a conversation.
|
41
|
-
The example
|
41
|
+
The example captures the spirit of llm.rb by demonstrating how objects cooperate
|
42
|
+
together through composition, and it uses the stateless chat completions API that
|
43
|
+
all LLM providers support:
|
42
44
|
|
43
45
|
```ruby
|
44
46
|
#!/usr/bin/env ruby
|
@@ -126,8 +128,7 @@ require "llm"
|
|
126
128
|
|
127
129
|
llm = LLM.openai(ENV["KEY"])
|
128
130
|
res = llm.audio.create_speech(input: "Hello world")
|
129
|
-
|
130
|
-
res.audio.string
|
131
|
+
IO.copy_stream res.audio, File.join(Dir.home, "hello.mp3")
|
131
132
|
```
|
132
133
|
|
133
134
|
#### Transcribe
|
@@ -388,6 +389,38 @@ print res.embeddings[0].size, "\n"
|
|
388
389
|
# 1536
|
389
390
|
```
|
390
391
|
|
392
|
+
### Models
|
393
|
+
|
394
|
+
#### List
|
395
|
+
|
396
|
+
Almost all LLM providers provide a models endpoint that allows a client to
|
397
|
+
query the list of models that are available to use. The list is dynamic,
|
398
|
+
maintained by LLM providers, and it is independent of a specific llm.rb release.
|
399
|
+
True to the llm.rb spirit of small, composable objects that cooperate with
|
400
|
+
each other, a
|
401
|
+
[LLM::Model](https://0x1eef.github.io/x/llm.rb/LLM/Model.html)
|
402
|
+
object can be used instead of a string that describes a model name (although
|
403
|
+
either works). Let's take a look at an example:
|
404
|
+
|
405
|
+
```ruby
|
406
|
+
#!/usr/bin/env ruby
|
407
|
+
require "llm"
|
408
|
+
|
409
|
+
##
|
410
|
+
# List all models
|
411
|
+
llm = LLM.openai(ENV["KEY"])
|
412
|
+
llm.models.all.each do |model|
|
413
|
+
print "model: ", model.id, "\n"
|
414
|
+
end
|
415
|
+
|
416
|
+
##
|
417
|
+
# Select a model
|
418
|
+
model = llm.models.all.find { |m| m.id == "gpt-3.5-turbo" }
|
419
|
+
bot = LLM::Chat.new(llm, model:)
|
420
|
+
bot.chat "Hello #{model.id} :)"
|
421
|
+
bot.messages.select(&:assistant?).each { print "[#{_1.role}] ", _1.content, "\n" }
|
422
|
+
```
|
423
|
+
|
391
424
|
### Memory
|
392
425
|
|
393
426
|
#### Child process
|
@@ -410,7 +443,7 @@ llm = LLM.gemini(ENV["KEY"])
|
|
410
443
|
fork do
|
411
444
|
%w[dog cat sheep goat capybara].each do |animal|
|
412
445
|
res = llm.images.create(prompt: "a #{animal} on a rocket to the moon")
|
413
|
-
|
446
|
+
IO.copy_stream res.images[0], "#{animal}.png"
|
414
447
|
end
|
415
448
|
end
|
416
449
|
Process.wait
|
@@ -440,9 +473,9 @@ magic or complex metaprogramming.
|
|
440
473
|
|
441
474
|
Every part of llm.rb is designed to be explicit, composable, memory-safe,
|
442
475
|
and production-ready without compromise. No unnecessary abstractions,
|
443
|
-
no global configuration, and no dependencies that aren't
|
444
|
-
Ruby. It has been inspired in part by other languages such
|
445
|
-
it is not a port of any other library.
|
476
|
+
no global configuration, no global state, and no dependencies that aren't
|
477
|
+
part of standard Ruby. It has been inspired in part by other languages such
|
478
|
+
as Python, but it is not a port of any other library.
|
446
479
|
|
447
480
|
## License
|
448
481
|
|
data/lib/llm/chat.rb
CHANGED
@@ -27,11 +27,13 @@ module LLM
|
|
27
27
|
##
|
28
28
|
# @param [LLM::Provider] provider
|
29
29
|
# A provider
|
30
|
+
# @param [String] model
|
31
|
+
# The model to maintain throughout the conversation
|
30
32
|
# @param [Hash] params
|
31
|
-
#
|
32
|
-
def initialize(provider,
|
33
|
+
# Other parameters to maintain throughout the conversation
|
34
|
+
def initialize(provider, model: provider.default_model, **params)
|
33
35
|
@provider = provider
|
34
|
-
@params = params
|
36
|
+
@params = params.merge!(model:)
|
35
37
|
@lazy = false
|
36
38
|
@messages = []
|
37
39
|
end
|
data/lib/llm/core_ext/ostruct.rb
CHANGED
data/lib/llm/file.rb
CHANGED
@@ -7,13 +7,20 @@
|
|
7
7
|
class LLM::File
|
8
8
|
##
|
9
9
|
# @return [String]
|
10
|
-
# Returns the path to
|
10
|
+
# Returns the path to the file
|
11
11
|
attr_reader :path
|
12
12
|
|
13
13
|
def initialize(path)
|
14
14
|
@path = path
|
15
15
|
end
|
16
16
|
|
17
|
+
##
|
18
|
+
# @return [String]
|
19
|
+
# Returns basename of the file
|
20
|
+
def basename
|
21
|
+
File.basename(path)
|
22
|
+
end
|
23
|
+
|
17
24
|
##
|
18
25
|
# @return [String]
|
19
26
|
# Returns the MIME type of the file
|
data/lib/llm/model.rb
CHANGED
@@ -1,7 +1,32 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
##
|
4
|
+
# The {LLM::Model LLM::Model} class represents an LLM model that
|
5
|
+
# is available to use. Its properties are delegated to the underlying
|
6
|
+
# response body, and vary by provider.
|
7
|
+
class LLM::Model < OpenStruct
|
8
|
+
##
|
9
|
+
# Returns a subclass of {LLM::Provider LLM::Provider}
|
10
|
+
# @return [LLM::Provider]
|
11
|
+
attr_accessor :provider
|
12
|
+
|
13
|
+
##
|
14
|
+
# Returns the model ID
|
15
|
+
# @return [String]
|
16
|
+
def id
|
17
|
+
case @provider.class.to_s
|
18
|
+
when "LLM::Ollama"
|
19
|
+
self["name"]
|
20
|
+
when "LLM::Gemini"
|
21
|
+
self["name"].sub(%r|\Amodels/|, "")
|
22
|
+
else
|
23
|
+
self["id"]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# @return [String]
|
4
29
|
def to_json(*)
|
5
|
-
|
30
|
+
id.to_json(*)
|
6
31
|
end
|
7
32
|
end
|
data/lib/llm/provider.rb
CHANGED
@@ -44,7 +44,7 @@ class LLM::Provider
|
|
44
44
|
# @raise [NotImplementedError]
|
45
45
|
# When the method is not implemented by a subclass
|
46
46
|
# @return [LLM::Response::Embedding]
|
47
|
-
def embed(input, model
|
47
|
+
def embed(input, model: nil, **params)
|
48
48
|
raise NotImplementedError
|
49
49
|
end
|
50
50
|
|
@@ -69,7 +69,7 @@ class LLM::Provider
|
|
69
69
|
# @raise [NotImplementedError]
|
70
70
|
# When the method is not implemented by a subclass
|
71
71
|
# @return [LLM::Response::Completion]
|
72
|
-
def complete(prompt, role = :user, model:
|
72
|
+
def complete(prompt, role = :user, model: default_model, **params)
|
73
73
|
raise NotImplementedError
|
74
74
|
end
|
75
75
|
|
@@ -85,8 +85,8 @@ class LLM::Provider
|
|
85
85
|
# Other completion parameters to maintain throughout a chat
|
86
86
|
# @raise (see LLM::Provider#complete)
|
87
87
|
# @return [LLM::Chat]
|
88
|
-
def chat(prompt, role = :user, model:
|
89
|
-
LLM::Chat.new(self, params).lazy.chat(prompt, role)
|
88
|
+
def chat(prompt, role = :user, model: default_model, **params)
|
89
|
+
LLM::Chat.new(self, **params.merge(model:)).lazy.chat(prompt, role)
|
90
90
|
end
|
91
91
|
|
92
92
|
##
|
@@ -101,8 +101,8 @@ class LLM::Provider
|
|
101
101
|
# Other completion parameters to maintain throughout a chat
|
102
102
|
# @raise (see LLM::Provider#complete)
|
103
103
|
# @return [LLM::Chat]
|
104
|
-
def chat!(prompt, role = :user, model:
|
105
|
-
LLM::Chat.new(self, params).chat(prompt, role)
|
104
|
+
def chat!(prompt, role = :user, model: default_model, **params)
|
105
|
+
LLM::Chat.new(self, **params.merge(model:)).chat(prompt, role)
|
106
106
|
end
|
107
107
|
|
108
108
|
##
|
@@ -117,8 +117,8 @@ class LLM::Provider
|
|
117
117
|
# Other completion parameters to maintain throughout a chat
|
118
118
|
# @raise (see LLM::Provider#complete)
|
119
119
|
# @return [LLM::Chat]
|
120
|
-
def respond(prompt, role = :user, model:
|
121
|
-
LLM::Chat.new(self, params).lazy.respond(prompt, role)
|
120
|
+
def respond(prompt, role = :user, model: default_model, **params)
|
121
|
+
LLM::Chat.new(self, **params.merge(model:)).lazy.respond(prompt, role)
|
122
122
|
end
|
123
123
|
|
124
124
|
##
|
@@ -133,8 +133,8 @@ class LLM::Provider
|
|
133
133
|
# Other completion parameters to maintain throughout a chat
|
134
134
|
# @raise (see LLM::Provider#complete)
|
135
135
|
# @return [LLM::Chat]
|
136
|
-
def respond!(prompt, role = :user, model:
|
137
|
-
LLM::Chat.new(self, params).respond(prompt, role)
|
136
|
+
def respond!(prompt, role = :user, model: default_model, **params)
|
137
|
+
LLM::Chat.new(self, **params.merge(model:)).respond(prompt, role)
|
138
138
|
end
|
139
139
|
|
140
140
|
##
|
@@ -169,6 +169,13 @@ class LLM::Provider
|
|
169
169
|
raise NotImplementedError
|
170
170
|
end
|
171
171
|
|
172
|
+
##
|
173
|
+
# @return [LLM::OpenAI::Models]
|
174
|
+
# Returns an interface to the models API
|
175
|
+
def models
|
176
|
+
raise NotImplementedError
|
177
|
+
end
|
178
|
+
|
172
179
|
##
|
173
180
|
# @return [String]
|
174
181
|
# Returns the role of the assistant in the conversation.
|
@@ -178,9 +185,9 @@ class LLM::Provider
|
|
178
185
|
end
|
179
186
|
|
180
187
|
##
|
181
|
-
# @return [
|
182
|
-
# Returns
|
183
|
-
def
|
188
|
+
# @return [String]
|
189
|
+
# Returns the default model for chat completions
|
190
|
+
def default_model
|
184
191
|
raise NotImplementedError
|
185
192
|
end
|
186
193
|
|
@@ -228,8 +235,6 @@ class LLM::Provider
|
|
228
235
|
# When the rate limit is exceeded
|
229
236
|
# @raise [LLM::Error::ResponseError]
|
230
237
|
# When any other unsuccessful status code is returned
|
231
|
-
# @raise [LLM::Error::PromptError]
|
232
|
-
# When given an object a provider does not understand
|
233
238
|
# @raise [SystemCallError]
|
234
239
|
# When there is a network error at the operating system level
|
235
240
|
def request(http, req, &b)
|
@@ -250,17 +255,4 @@ class LLM::Provider
|
|
250
255
|
req.body_stream = io
|
251
256
|
req["transfer-encoding"] = "chunked" unless req["content-length"]
|
252
257
|
end
|
253
|
-
|
254
|
-
##
|
255
|
-
# @param [String] provider
|
256
|
-
# The name of the provider
|
257
|
-
# @return [Hash<String, Hash>]
|
258
|
-
def load_models!(provider)
|
259
|
-
require "yaml" unless defined?(YAML)
|
260
|
-
rootdir = File.realpath File.join(__dir__, "..", "..")
|
261
|
-
sharedir = File.join(rootdir, "share", "llm")
|
262
|
-
provider = provider.gsub(/[^a-z0-9]/i, "")
|
263
|
-
yaml = File.join(sharedir, "models", "#{provider}.yml")
|
264
|
-
YAML.safe_load_file(yaml).transform_values { LLM::Model.new(_1) }
|
265
|
-
end
|
266
258
|
end
|
@@ -26,13 +26,26 @@ class LLM::Anthropic
|
|
26
26
|
# @return [String, Hash]
|
27
27
|
# The formatted content
|
28
28
|
def format_content(content)
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
}]
|
29
|
+
case content
|
30
|
+
when Array
|
31
|
+
content.flat_map { format_content(_1) }
|
32
|
+
when URI
|
33
|
+
[{type: :image, source: {type: "url", url: content.to_s}}]
|
34
|
+
when LLM::File
|
35
|
+
if content.image?
|
36
|
+
[{type: :image, source: {type: "base64", media_type: content.mime_type, data: content.to_b64}}]
|
37
|
+
else
|
38
|
+
raise LLM::Error::PromptError, "The given object (an instance of #{content.class}) " \
|
39
|
+
"is not an image, and therefore not supported by the " \
|
40
|
+
"Anthropic API"
|
41
|
+
end
|
42
|
+
when String
|
43
|
+
[{type: :text, text: content}]
|
44
|
+
when LLM::Message
|
45
|
+
format_content(content.content)
|
34
46
|
else
|
35
|
-
content
|
47
|
+
raise LLM::Error::PromptError, "The given object (an instance of #{content.class}) " \
|
48
|
+
"is not supported by the Anthropic API"
|
36
49
|
end
|
37
50
|
end
|
38
51
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class LLM::Anthropic
|
4
|
+
##
|
5
|
+
# The {LLM::Anthropic::Models LLM::Anthropic::Models} class provides a model
|
6
|
+
# object for interacting with [Anthropic's models API](https://platform.anthropic.com/docs/api-reference/models/list).
|
7
|
+
# The models API allows a client to query Anthropic for a list of models
|
8
|
+
# that are available for use with the Anthropic API.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# #!/usr/bin/env ruby
|
12
|
+
# require "llm"
|
13
|
+
#
|
14
|
+
# llm = LLM.anthropic(ENV["KEY"])
|
15
|
+
# res = llm.models.all
|
16
|
+
# res.each do |model|
|
17
|
+
# print "id: ", model.id, "\n"
|
18
|
+
# end
|
19
|
+
class Models
|
20
|
+
##
|
21
|
+
# Returns a new Models object
|
22
|
+
# @param provider [LLM::Provider]
|
23
|
+
# @return [LLM::Anthropic::Files]
|
24
|
+
def initialize(provider)
|
25
|
+
@provider = provider
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# List all models
|
30
|
+
# @example
|
31
|
+
# llm = LLM.anthropic(ENV["KEY"])
|
32
|
+
# res = llm.models.all
|
33
|
+
# res.each do |model|
|
34
|
+
# print "id: ", model.id, "\n"
|
35
|
+
# end
|
36
|
+
# @see https://docs.anthropic.com/en/api/models-list Anthropic docs
|
37
|
+
# @param [Hash] params Other parameters (see Anthropic docs)
|
38
|
+
# @raise (see LLM::Provider#request)
|
39
|
+
# @return [LLM::Response::FileList]
|
40
|
+
def all(**params)
|
41
|
+
query = URI.encode_www_form(params)
|
42
|
+
req = Net::HTTP::Get.new("/v1/models?#{query}", headers)
|
43
|
+
res = request(http, req)
|
44
|
+
LLM::Response::ModelList.new(res).tap { |modellist|
|
45
|
+
models = modellist.body["data"].map do |model|
|
46
|
+
LLM::Model.from_hash(model).tap { _1.provider = @provider }
|
47
|
+
end
|
48
|
+
modellist.models = models
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def http
|
55
|
+
@provider.instance_variable_get(:@http)
|
56
|
+
end
|
57
|
+
|
58
|
+
[:headers, :request].each do |m|
|
59
|
+
define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -8,6 +8,7 @@ module LLM
|
|
8
8
|
require_relative "anthropic/error_handler"
|
9
9
|
require_relative "anthropic/response_parser"
|
10
10
|
require_relative "anthropic/format"
|
11
|
+
require_relative "anthropic/models"
|
11
12
|
include Format
|
12
13
|
|
13
14
|
HOST = "api.anthropic.com"
|
@@ -45,16 +46,28 @@ module LLM
|
|
45
46
|
# @param params (see LLM::Provider#complete)
|
46
47
|
# @example (see LLM::Provider#complete)
|
47
48
|
# @raise (see LLM::Provider#request)
|
49
|
+
# @raise [LLM::Error::PromptError]
|
50
|
+
# When given an object a provider does not understand
|
48
51
|
# @return (see LLM::Provider#complete)
|
49
|
-
def complete(prompt, role = :user, model:
|
50
|
-
params
|
51
|
-
req
|
52
|
+
def complete(prompt, role = :user, model: default_model, max_tokens: 1024, **params)
|
53
|
+
params = {max_tokens:, model:}.merge!(params)
|
54
|
+
req = Net::HTTP::Post.new("/v1/messages", headers)
|
52
55
|
messages = [*(params.delete(:messages) || []), Message.new(role, prompt)]
|
53
|
-
|
54
|
-
|
56
|
+
body = JSON.dump({messages: format(messages)}.merge!(params))
|
57
|
+
set_body_stream(req, StringIO.new(body))
|
58
|
+
|
59
|
+
res = request(@http, req)
|
55
60
|
Response::Completion.new(res).extend(response_parser)
|
56
61
|
end
|
57
62
|
|
63
|
+
##
|
64
|
+
# Provides an interface to Anthropic's models API
|
65
|
+
# @see https://docs.anthropic.com/en/api/models-list
|
66
|
+
# @return [LLM::Anthropic::Models]
|
67
|
+
def models
|
68
|
+
LLM::Anthropic::Models.new(self)
|
69
|
+
end
|
70
|
+
|
58
71
|
##
|
59
72
|
# @return (see LLM::Provider#assistant_role)
|
60
73
|
def assistant_role
|
@@ -62,9 +75,11 @@ module LLM
|
|
62
75
|
end
|
63
76
|
|
64
77
|
##
|
65
|
-
#
|
66
|
-
|
67
|
-
|
78
|
+
# Returns the default model for chat completions
|
79
|
+
# @see https://docs.anthropic.com/en/docs/about-claude/models/all-models#model-comparison-table claude-3-5-sonnet-20240620
|
80
|
+
# @return [String]
|
81
|
+
def default_model
|
82
|
+
"claude-3-5-sonnet-20240620"
|
68
83
|
end
|
69
84
|
|
70
85
|
private
|
@@ -35,8 +35,13 @@ class LLM::Gemini
|
|
35
35
|
when LLM::File
|
36
36
|
file = content
|
37
37
|
{inline_data: {mime_type: file.mime_type, data: file.to_b64}}
|
38
|
-
|
38
|
+
when String
|
39
39
|
{text: content}
|
40
|
+
when LLM::Message
|
41
|
+
format_content(content.content)
|
42
|
+
else
|
43
|
+
raise LLM::Error::PromptError, "The given object (an instance of #{content.class}) " \
|
44
|
+
"is not supported by the Gemini API"
|
40
45
|
end
|
41
46
|
end
|
42
47
|
end
|
@@ -13,7 +13,7 @@ class LLM::Gemini
|
|
13
13
|
#
|
14
14
|
# llm = LLM.gemini(ENV["KEY"])
|
15
15
|
# res = llm.images.create prompt: "A dog on a rocket to the moon"
|
16
|
-
#
|
16
|
+
# IO.copy_stream res.images[0], "rocket.png"
|
17
17
|
class Images
|
18
18
|
include Format
|
19
19
|
|
@@ -30,7 +30,7 @@ class LLM::Gemini
|
|
30
30
|
# @example
|
31
31
|
# llm = LLM.gemini(ENV["KEY"])
|
32
32
|
# res = llm.images.create prompt: "A dog on a rocket to the moon"
|
33
|
-
#
|
33
|
+
# IO.copy_stream res.images[0], "rocket.png"
|
34
34
|
# @see https://ai.google.dev/gemini-api/docs/image-generation Gemini docs
|
35
35
|
# @param [String] prompt The prompt
|
36
36
|
# @param [Hash] params Other parameters (see Gemini docs)
|
@@ -56,7 +56,7 @@ class LLM::Gemini
|
|
56
56
|
# @example
|
57
57
|
# llm = LLM.gemini(ENV["KEY"])
|
58
58
|
# res = llm.images.edit image: LLM::File("cat.png"), prompt: "Add a hat to the cat"
|
59
|
-
#
|
59
|
+
# IO.copy_stream res.images[0], "hatoncat.png"
|
60
60
|
# @see https://ai.google.dev/gemini-api/docs/image-generation Gemini docs
|
61
61
|
# @param [LLM::File] image The image to edit
|
62
62
|
# @param [String] prompt The prompt
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class LLM::Gemini
|
4
|
+
##
|
5
|
+
# The {LLM::Gemini::Models LLM::Gemini::Models} class provides a model
|
6
|
+
# object for interacting with [Gemini's models API](https://ai.google.dev/api/models?hl=en#method:-models.list).
|
7
|
+
# The models API allows a client to query Gemini for a list of models
|
8
|
+
# that are available for use with the Gemini API.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# #!/usr/bin/env ruby
|
12
|
+
# require "llm"
|
13
|
+
#
|
14
|
+
# llm = LLM.gemini(ENV["KEY"])
|
15
|
+
# res = llm.models.all
|
16
|
+
# res.each do |model|
|
17
|
+
# print "id: ", model.id, "\n"
|
18
|
+
# end
|
19
|
+
class Models
|
20
|
+
include LLM::Utils
|
21
|
+
|
22
|
+
##
|
23
|
+
# Returns a new Models object
|
24
|
+
# @param provider [LLM::Provider]
|
25
|
+
# @return [LLM::Gemini::Models]
|
26
|
+
def initialize(provider)
|
27
|
+
@provider = provider
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# List all models
|
32
|
+
# @example
|
33
|
+
# llm = LLM.gemini(ENV["KEY"])
|
34
|
+
# res = llm.models.all
|
35
|
+
# res.each do |model|
|
36
|
+
# print "id: ", model.id, "\n"
|
37
|
+
# end
|
38
|
+
# @see https://ai.google.dev/api/models?hl=en#method:-models.list Gemini docs
|
39
|
+
# @param [Hash] params Other parameters (see Gemini docs)
|
40
|
+
# @raise (see LLM::Provider#request)
|
41
|
+
# @return [LLM::Response::ModelList]
|
42
|
+
def all(**params)
|
43
|
+
query = URI.encode_www_form(params.merge!(key: secret))
|
44
|
+
req = Net::HTTP::Get.new("/v1beta/models?#{query}", headers)
|
45
|
+
res = request(http, req)
|
46
|
+
LLM::Response::ModelList.new(res).tap { |modellist|
|
47
|
+
models = modellist.body["models"].map do |model|
|
48
|
+
model = model.transform_keys { snakecase(_1) }
|
49
|
+
LLM::Model.from_hash(model).tap { _1.provider = @provider }
|
50
|
+
end
|
51
|
+
modellist.models = models
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def http
|
58
|
+
@provider.instance_variable_get(:@http)
|
59
|
+
end
|
60
|
+
|
61
|
+
def secret
|
62
|
+
@provider.instance_variable_get(:@secret)
|
63
|
+
end
|
64
|
+
|
65
|
+
[:headers, :request].each do |m|
|
66
|
+
define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -44,11 +44,7 @@ class LLM::Gemini
|
|
44
44
|
images: body["candidates"].flat_map do |candidate|
|
45
45
|
candidate["content"]["parts"].filter_map do
|
46
46
|
next unless _1.dig("inlineData", "data")
|
47
|
-
|
48
|
-
mime_type: _1["inlineData"]["mimeType"],
|
49
|
-
encoded: _1["inlineData"]["data"],
|
50
|
-
binary: _1["inlineData"]["data"].unpack1("m0")
|
51
|
-
)
|
47
|
+
StringIO.new(_1["inlineData"]["data"].unpack1("m0"))
|
52
48
|
end
|
53
49
|
end
|
54
50
|
}
|
data/lib/llm/providers/gemini.rb
CHANGED
@@ -34,6 +34,7 @@ module LLM
|
|
34
34
|
require_relative "gemini/images"
|
35
35
|
require_relative "gemini/files"
|
36
36
|
require_relative "gemini/audio"
|
37
|
+
require_relative "gemini/models"
|
37
38
|
include Format
|
38
39
|
|
39
40
|
HOST = "generativelanguage.googleapis.com"
|
@@ -52,6 +53,7 @@ module LLM
|
|
52
53
|
# @raise (see LLM::Provider#request)
|
53
54
|
# @return (see LLM::Provider#embed)
|
54
55
|
def embed(input, model: "text-embedding-004", **params)
|
56
|
+
model = model.respond_to?(:id) ? model.id : model
|
55
57
|
path = ["/v1beta/models/#{model}", "embedContent?key=#{@secret}"].join(":")
|
56
58
|
req = Net::HTTP::Post.new(path, headers)
|
57
59
|
req.body = JSON.dump({content: {parts: [{text: input}]}})
|
@@ -68,13 +70,17 @@ module LLM
|
|
68
70
|
# @param params (see LLM::Provider#complete)
|
69
71
|
# @example (see LLM::Provider#complete)
|
70
72
|
# @raise (see LLM::Provider#request)
|
73
|
+
# @raise [LLM::Error::PromptError]
|
74
|
+
# When given an object a provider does not understand
|
71
75
|
# @return (see LLM::Provider#complete)
|
72
|
-
def complete(prompt, role = :user, model:
|
76
|
+
def complete(prompt, role = :user, model: default_model, **params)
|
77
|
+
model.respond_to?(:id) ? model.id : model
|
73
78
|
path = ["/v1beta/models/#{model}", "generateContent?key=#{@secret}"].join(":")
|
74
79
|
req = Net::HTTP::Post.new(path, headers)
|
75
80
|
messages = [*(params.delete(:messages) || []), LLM::Message.new(role, prompt)]
|
76
|
-
body = JSON.dump({contents: format(messages)})
|
81
|
+
body = JSON.dump({contents: format(messages)})
|
77
82
|
set_body_stream(req, StringIO.new(body))
|
83
|
+
|
78
84
|
res = request(@http, req)
|
79
85
|
Response::Completion.new(res).extend(response_parser)
|
80
86
|
end
|
@@ -101,6 +107,13 @@ module LLM
|
|
101
107
|
LLM::Gemini::Files.new(self)
|
102
108
|
end
|
103
109
|
|
110
|
+
##
|
111
|
+
# Provides an interface to Gemini's models API
|
112
|
+
# @see https://ai.google.dev/gemini-api/docs/models Gemini docs
|
113
|
+
def models
|
114
|
+
LLM::Gemini::Models.new(self)
|
115
|
+
end
|
116
|
+
|
104
117
|
##
|
105
118
|
# @return (see LLM::Provider#assistant_role)
|
106
119
|
def assistant_role
|
@@ -108,9 +121,11 @@ module LLM
|
|
108
121
|
end
|
109
122
|
|
110
123
|
##
|
111
|
-
#
|
112
|
-
|
113
|
-
|
124
|
+
# Returns the default model for chat completions
|
125
|
+
# @see https://ai.google.dev/gemini-api/docs/models#gemini-1.5-flash gemini-1.5-flash
|
126
|
+
# @return [String]
|
127
|
+
def default_model
|
128
|
+
"gemini-1.5-flash"
|
114
129
|
end
|
115
130
|
|
116
131
|
private
|
@@ -28,14 +28,22 @@ class LLM::Ollama
|
|
28
28
|
# @return [String, Hash]
|
29
29
|
# The formatted content
|
30
30
|
def format_content(content)
|
31
|
-
|
31
|
+
case content
|
32
|
+
when LLM::File
|
32
33
|
if content.image?
|
33
34
|
{content: "This message has an image associated with it", images: [content.to_b64]}
|
34
35
|
else
|
35
|
-
raise
|
36
|
+
raise LLM::Error::PromptError, "The given object (an instance of #{content.class}) " \
|
37
|
+
"is not an image, and therefore not supported by the " \
|
38
|
+
"Ollama API"
|
36
39
|
end
|
37
|
-
|
40
|
+
when String
|
38
41
|
{content:}
|
42
|
+
when LLM::Message
|
43
|
+
format_content(content.content)
|
44
|
+
else
|
45
|
+
raise LLM::Error::PromptError, "The given object (an instance of #{content.class}) " \
|
46
|
+
"is not supported by the Ollama API"
|
39
47
|
end
|
40
48
|
end
|
41
49
|
end
|