llm.rb 0.3.0 → 0.3.2
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 +99 -9
- data/lib/llm/error.rb +9 -4
- data/lib/llm/file.rb +17 -0
- data/lib/llm/message.rb +7 -0
- data/lib/llm/multipart.rb +11 -9
- data/lib/llm/provider.rb +41 -12
- data/lib/llm/providers/anthropic/error_handler.rb +1 -1
- data/lib/llm/providers/anthropic.rb +2 -2
- data/lib/llm/providers/gemini/audio.rb +2 -2
- data/lib/llm/providers/gemini/error_handler.rb +2 -2
- data/lib/llm/providers/gemini/files.rb +14 -12
- data/lib/llm/providers/gemini/images.rb +11 -11
- data/lib/llm/providers/gemini.rb +7 -6
- data/lib/llm/providers/ollama/error_handler.rb +1 -1
- data/lib/llm/providers/ollama.rb +2 -2
- data/lib/llm/providers/openai/audio.rb +6 -6
- data/lib/llm/providers/openai/error_handler.rb +1 -1
- data/lib/llm/providers/openai/files.rb +7 -7
- data/lib/llm/providers/openai/format.rb +42 -10
- data/lib/llm/providers/openai/images.rb +6 -6
- data/lib/llm/providers/openai/responses.rb +24 -3
- data/lib/llm/providers/openai.rb +2 -2
- data/lib/llm/providers/voyageai/error_handler.rb +1 -1
- data/lib/llm/version.rb +1 -1
- data/spec/gemini/files_spec.rb +2 -2
- data/spec/llm/conversation_spec.rb +8 -3
- data/spec/openai/completion_spec.rb +5 -4
- data/spec/openai/files_spec.rb +71 -17
- metadata +2 -3
- data/lib/llm/http_client.rb +0 -34
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3c55653b476d2fe6fe9457c89bc430c698668312ce89660a1d69abd8adf338eb
|
4
|
+
data.tar.gz: fe7d456bbb739eb091e82351839baef4c64d1d108a2c4cd7de3eb1b478982631
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8cd55bb28eb92fea745d8b11062b2442bf4b2de88ecfb0b7dc99cfefd293bd45113088dd13ccfe7e251d2e369459da700f15725bae51c3d31d4bf68e19953138
|
7
|
+
data.tar.gz: dab47021b94d00e51e7d0ca3f92e2966170b9fd8ce7138e0728d2be7fb83da03104ff93cd7c54b760acca62dd03adf16462069db9eb5c30185743c25259105aa
|
data/README.md
CHANGED
@@ -3,7 +3,9 @@
|
|
3
3
|
llm.rb is a lightweight library that provides a common interface
|
4
4
|
and set of functionality for multiple Large Language Models (LLMs). It
|
5
5
|
is designed to be simple, flexible, and easy to use – and it has been
|
6
|
-
implemented with
|
6
|
+
implemented with zero dependencies outside Ruby's standard library. See the
|
7
|
+
[philosophy](#philosophy) section for more information on the design principles
|
8
|
+
behind llm.rb.
|
7
9
|
|
8
10
|
## Examples
|
9
11
|
|
@@ -24,6 +26,7 @@ llm = LLM.openai("yourapikey")
|
|
24
26
|
llm = LLM.gemini("yourapikey")
|
25
27
|
llm = LLM.anthropic("yourapikey")
|
26
28
|
llm = LLM.ollama(nil)
|
29
|
+
llm = LLM.voyageai("yourapikey")
|
27
30
|
```
|
28
31
|
|
29
32
|
### Conversations
|
@@ -110,7 +113,7 @@ bot.messages.each { print "[#{_1.role}] ", _1.content, "\n" }
|
|
110
113
|
#### Speech
|
111
114
|
|
112
115
|
Some but not all providers implement audio generation capabilities that
|
113
|
-
can create
|
116
|
+
can create speech from text, transcribe audio to text, or translate
|
114
117
|
audio to text (usually English). The following example uses the OpenAI provider
|
115
118
|
to create an audio file from a text prompt. The audio is then moved to
|
116
119
|
`${HOME}/hello.mp3` as the final step. As always, consult the provider's
|
@@ -120,8 +123,6 @@ for more information on how to use the audio generation API:
|
|
120
123
|
```ruby
|
121
124
|
#!/usr/bin/env ruby
|
122
125
|
require "llm"
|
123
|
-
require "open-uri"
|
124
|
-
require "fileutils"
|
125
126
|
|
126
127
|
llm = LLM.openai(ENV["KEY"])
|
127
128
|
res = llm.audio.create_speech(input: "Hello world")
|
@@ -149,8 +150,6 @@ examples and documentation
|
|
149
150
|
```ruby
|
150
151
|
#!/usr/bin/env ruby
|
151
152
|
require "llm"
|
152
|
-
require "open-uri"
|
153
|
-
require "fileutils"
|
154
153
|
|
155
154
|
llm = LLM.openai(ENV["KEY"])
|
156
155
|
res = llm.audio.create_transcription(
|
@@ -178,9 +177,8 @@ examples and documentation
|
|
178
177
|
|
179
178
|
|
180
179
|
```ruby
|
180
|
+
#!/usr/bin/env ruby
|
181
181
|
require "llm"
|
182
|
-
require "open-uri"
|
183
|
-
require "fileutils"
|
184
182
|
|
185
183
|
llm = LLM.openai(ENV["KEY"])
|
186
184
|
res = llm.audio.create_translation(
|
@@ -193,7 +191,7 @@ print res.text, "\n" # => "Good morning."
|
|
193
191
|
|
194
192
|
#### Create
|
195
193
|
|
196
|
-
Some but all LLM providers implement image generation capabilities that
|
194
|
+
Some but not all LLM providers implement image generation capabilities that
|
197
195
|
can create new images from a prompt, or edit an existing image with a
|
198
196
|
prompt. The following example uses the OpenAI provider to create an
|
199
197
|
image of a dog on a rocket to the moon. The image is then moved to
|
@@ -282,6 +280,84 @@ res.urls.each.with_index do |url, index|
|
|
282
280
|
end
|
283
281
|
```
|
284
282
|
|
283
|
+
### Files
|
284
|
+
|
285
|
+
#### Create
|
286
|
+
|
287
|
+
Most LLM providers provide a Files API where you can upload files
|
288
|
+
that can be referenced from a prompt and llm.rb has first-class support
|
289
|
+
for this feature. The following example uses the OpenAI provider to describe
|
290
|
+
the contents of a PDF file after it has been uploaded. The file (an instance
|
291
|
+
of [LLM::Response::File](https://0x1eef.github.io/x/llm.rb/LLM/Response/File.html))
|
292
|
+
is passed directly to the chat method, and generally any object a prompt supports
|
293
|
+
can be given to the chat method.
|
294
|
+
|
295
|
+
Please also see provider-specific documentation for more provider-specific
|
296
|
+
examples and documentation
|
297
|
+
(eg
|
298
|
+
[LLM::Gemini::Files](https://0x1eef.github.io/x/llm.rb/LLM/Gemini/Files.html),
|
299
|
+
[LLM::OpenAI::Files](https://0x1eef.github.io/x/llm.rb/LLM/OpenAI/Files.html)):
|
300
|
+
|
301
|
+
```ruby
|
302
|
+
#!/usr/bin/env ruby
|
303
|
+
require "llm"
|
304
|
+
|
305
|
+
llm = LLM.openai(ENV["KEY"])
|
306
|
+
bot = LLM::Chat.new(llm).lazy
|
307
|
+
file = llm.files.create(file: LLM::File("/documents/openbsd_is_awesome.pdf"))
|
308
|
+
bot.chat(file)
|
309
|
+
bot.chat("What is this file about?")
|
310
|
+
bot.messages.select(&:assistant?).each { print "[#{_1.role}] ", _1.content, "\n" }
|
311
|
+
|
312
|
+
##
|
313
|
+
# [assistant] This file is about OpenBSD, a free and open-source Unix-like operating system
|
314
|
+
# based on the Berkeley Software Distribution (BSD). It is known for its
|
315
|
+
# emphasis on security, code correctness, and code simplicity. The file
|
316
|
+
# contains information about the features, installation, and usage of OpenBSD.
|
317
|
+
```
|
318
|
+
|
319
|
+
### Prompts
|
320
|
+
|
321
|
+
#### Multimodal
|
322
|
+
|
323
|
+
Generally all providers accept text prompts but some providers can
|
324
|
+
also understand URLs, and various file types (eg images, audio, video,
|
325
|
+
etc). The llm.rb approach to multimodal prompts is to let you pass `URI`
|
326
|
+
objects to describe links, `LLM::File` / `LLM::Response::File` objects
|
327
|
+
to describe files, `String` objects to describe text blobs, or an array
|
328
|
+
of the forementioned objects to describe multiple objects in a single
|
329
|
+
prompt. Each object is a first class citizen that can be passed directly
|
330
|
+
to a prompt.
|
331
|
+
|
332
|
+
For more depth and examples on how to use the multimodal API, please see
|
333
|
+
the [provider-specific documentation](https://0x1eef.github.io/x/llm.rb/)
|
334
|
+
for more provider-specific examples – there can be subtle differences
|
335
|
+
between providers and even between APIs from the same provider that are
|
336
|
+
not covered in the README:
|
337
|
+
|
338
|
+
```ruby
|
339
|
+
#!/usr/bin/env ruby
|
340
|
+
require "llm"
|
341
|
+
|
342
|
+
llm = LLM.openai(ENV["KEY"])
|
343
|
+
bot = LLM::Chat.new(llm).lazy
|
344
|
+
|
345
|
+
bot.chat URI("https://example.com/path/to/image.png")
|
346
|
+
bot.chat "Describe the above image"
|
347
|
+
bot.messages.select(&:assistant?).each { print "[#{_1.role}] ", _1.content, "\n" }
|
348
|
+
|
349
|
+
file = bot.files.create(file: LLM::File("/documents/openbsd_is_awesome.pdf"))
|
350
|
+
bot.chat file
|
351
|
+
bot.chat "What is this file about?"
|
352
|
+
bot.messages.select(&:assistant?).each { print "[#{_1.role}] ", _1.content, "\n" }
|
353
|
+
|
354
|
+
bot.chat [LLM::File("/images/puffy.png"), "What is this image about?"]
|
355
|
+
bot.messages.select(&:assistant?).each { print "[#{_1.role}] ", _1.content, "\n" }
|
356
|
+
|
357
|
+
bot.chat [LLM::File("/images/beastie.png"), "What is this image about?"]
|
358
|
+
bot.messages.select(&:assistant?).each { print "[#{_1.role}] ", _1.content, "\n" }
|
359
|
+
```
|
360
|
+
|
285
361
|
### Embeddings
|
286
362
|
|
287
363
|
#### Text
|
@@ -354,6 +430,20 @@ llm.rb can be installed via rubygems.org:
|
|
354
430
|
|
355
431
|
gem install llm.rb
|
356
432
|
|
433
|
+
## Philosophy
|
434
|
+
|
435
|
+
llm.rb was built for developers who believe that simplicity can be challenging
|
436
|
+
but it is always worth it. It provides a clean, dependency-free interface to
|
437
|
+
Large Language Models, treating Ruby itself as the primary platform –
|
438
|
+
not Rails or any other specific framework or library. There is no hidden
|
439
|
+
magic or complex metaprogramming.
|
440
|
+
|
441
|
+
Every part of llm.rb is designed to be explicit, composable, memory-safe,
|
442
|
+
and production-ready without compromise. No unnecessary abstractions,
|
443
|
+
no global configuration, and no dependencies that aren't part of standard
|
444
|
+
Ruby. It has been inspired in part by other languages such as Python, but
|
445
|
+
it is not a port of any other library.
|
446
|
+
|
357
447
|
## License
|
358
448
|
|
359
449
|
[BSD Zero Clause](https://choosealicense.com/licenses/0bsd/)
|
data/lib/llm/error.rb
CHANGED
@@ -4,25 +4,30 @@ module LLM
|
|
4
4
|
##
|
5
5
|
# The superclass of all LLM errors
|
6
6
|
class Error < RuntimeError
|
7
|
-
def initialize
|
7
|
+
def initialize(...)
|
8
8
|
block_given? ? yield(self) : nil
|
9
|
+
super
|
9
10
|
end
|
10
11
|
|
11
12
|
##
|
12
13
|
# The superclass of all HTTP protocol errors
|
13
|
-
class
|
14
|
+
class ResponseError < Error
|
14
15
|
##
|
15
16
|
# @return [Net::HTTPResponse]
|
16
17
|
# Returns the response associated with an error
|
17
18
|
attr_accessor :response
|
18
19
|
end
|
19
20
|
|
21
|
+
##
|
22
|
+
# When a prompt is given an object that's not understood
|
23
|
+
PromptError = Class.new(Error)
|
24
|
+
|
20
25
|
##
|
21
26
|
# HTTPUnauthorized
|
22
|
-
Unauthorized = Class.new(
|
27
|
+
Unauthorized = Class.new(ResponseError)
|
23
28
|
|
24
29
|
##
|
25
30
|
# HTTPTooManyRequests
|
26
|
-
RateLimit = Class.new(
|
31
|
+
RateLimit = Class.new(ResponseError)
|
27
32
|
end
|
28
33
|
end
|
data/lib/llm/file.rb
CHANGED
@@ -41,6 +41,23 @@ class LLM::File
|
|
41
41
|
def to_b64
|
42
42
|
[File.binread(path)].pack("m0")
|
43
43
|
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# @return [String]
|
47
|
+
# Returns the file contents in base64 URL format
|
48
|
+
def to_data_uri
|
49
|
+
"data:#{mime_type};base64,#{to_b64}"
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# @return [File]
|
54
|
+
# Yields an IO object suitable to be streamed
|
55
|
+
def with_io
|
56
|
+
io = File.open(path, "rb")
|
57
|
+
yield(io)
|
58
|
+
ensure
|
59
|
+
io.close
|
60
|
+
end
|
44
61
|
end
|
45
62
|
|
46
63
|
##
|
data/lib/llm/message.rb
CHANGED
@@ -50,6 +50,13 @@ module LLM
|
|
50
50
|
end
|
51
51
|
alias_method :eql?, :==
|
52
52
|
|
53
|
+
##
|
54
|
+
# Returns true when the message is from the LLM
|
55
|
+
# @return [Boolean]
|
56
|
+
def assistant?
|
57
|
+
role == "assistant" || role == "model"
|
58
|
+
end
|
59
|
+
|
53
60
|
##
|
54
61
|
# Returns a string representation of the message
|
55
62
|
# @return [String]
|
data/lib/llm/multipart.rb
CHANGED
@@ -45,7 +45,9 @@ class LLM::Multipart
|
|
45
45
|
# Returns the multipart request body
|
46
46
|
# @return [String]
|
47
47
|
def body
|
48
|
-
|
48
|
+
io = StringIO.new("".b)
|
49
|
+
[*parts, StringIO.new("--#{@boundary}--\r\n".b)].each { IO.copy_stream(_1.tap(&:rewind), io) }
|
50
|
+
io.tap(&:rewind)
|
49
51
|
end
|
50
52
|
|
51
53
|
private
|
@@ -61,7 +63,7 @@ class LLM::Multipart
|
|
61
63
|
|
62
64
|
def multipart_header(type:, locals:)
|
63
65
|
if type == :file
|
64
|
-
str = "".b
|
66
|
+
str = StringIO.new("".b)
|
65
67
|
str << "--#{locals[:boundary]}" \
|
66
68
|
"\r\n" \
|
67
69
|
"Content-Disposition: form-data; name=\"#{locals[:key]}\";" \
|
@@ -70,7 +72,7 @@ class LLM::Multipart
|
|
70
72
|
"Content-Type: #{locals[:content_type]}" \
|
71
73
|
"\r\n\r\n"
|
72
74
|
elsif type == :data
|
73
|
-
str = "".b
|
75
|
+
str = StringIO.new("".b)
|
74
76
|
str << "--#{locals[:boundary]}" \
|
75
77
|
"\r\n" \
|
76
78
|
"Content-Disposition: form-data; name=\"#{locals[:key]}\"" \
|
@@ -82,17 +84,17 @@ class LLM::Multipart
|
|
82
84
|
|
83
85
|
def file_part(key, file, locals)
|
84
86
|
locals = locals.merge(attributes(file))
|
85
|
-
multipart_header(type: :file, locals:).tap do
|
86
|
-
|
87
|
-
|
87
|
+
multipart_header(type: :file, locals:).tap do |io|
|
88
|
+
IO.copy_stream(file.path, io)
|
89
|
+
io << "\r\n"
|
88
90
|
end
|
89
91
|
end
|
90
92
|
|
91
93
|
def data_part(key, value, locals)
|
92
94
|
locals = locals.merge(value:)
|
93
|
-
multipart_header(type: :data, locals:).tap do
|
94
|
-
|
95
|
-
|
95
|
+
multipart_header(type: :data, locals:).tap do |io|
|
96
|
+
io << value.to_s
|
97
|
+
io << "\r\n"
|
96
98
|
end
|
97
99
|
end
|
98
100
|
end
|
data/lib/llm/provider.rb
CHANGED
@@ -4,19 +4,9 @@
|
|
4
4
|
# The Provider class represents an abstract class for
|
5
5
|
# LLM (Language Model) providers.
|
6
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
7
|
# @abstract
|
13
|
-
# @see LLM::Provider::OpenAI
|
14
|
-
# @see LLM::Provider::Anthropic
|
15
|
-
# @see LLM::Provider::Gemini
|
16
|
-
# @see LLM::Provider::Ollama
|
17
8
|
class LLM::Provider
|
18
|
-
|
19
|
-
include LLM::HTTPClient
|
9
|
+
require "net/http"
|
20
10
|
|
21
11
|
##
|
22
12
|
# @param [String] secret
|
@@ -79,7 +69,7 @@ class LLM::Provider
|
|
79
69
|
# @raise [NotImplementedError]
|
80
70
|
# When the method is not implemented by a subclass
|
81
71
|
# @return [LLM::Response::Completion]
|
82
|
-
def complete(prompt, role = :user, model
|
72
|
+
def complete(prompt, role = :user, model: nil, **params)
|
83
73
|
raise NotImplementedError
|
84
74
|
end
|
85
75
|
|
@@ -222,6 +212,45 @@ class LLM::Provider
|
|
222
212
|
raise NotImplementedError
|
223
213
|
end
|
224
214
|
|
215
|
+
##
|
216
|
+
# Initiates a HTTP request
|
217
|
+
# @param [Net::HTTP] http
|
218
|
+
# The HTTP object to use for the request
|
219
|
+
# @param [Net::HTTPRequest] req
|
220
|
+
# The request to send
|
221
|
+
# @param [Proc] b
|
222
|
+
# A block to yield the response to (optional)
|
223
|
+
# @return [Net::HTTPResponse]
|
224
|
+
# The response from the server
|
225
|
+
# @raise [LLM::Error::Unauthorized]
|
226
|
+
# When authentication fails
|
227
|
+
# @raise [LLM::Error::RateLimit]
|
228
|
+
# When the rate limit is exceeded
|
229
|
+
# @raise [LLM::Error::ResponseError]
|
230
|
+
# When any other unsuccessful status code is returned
|
231
|
+
# @raise [LLM::Error::PromptError]
|
232
|
+
# When given an object a provider does not understand
|
233
|
+
# @raise [SystemCallError]
|
234
|
+
# When there is a network error at the operating system level
|
235
|
+
def request(http, req, &b)
|
236
|
+
res = http.request(req, &b)
|
237
|
+
case res
|
238
|
+
when Net::HTTPOK then res
|
239
|
+
else error_handler.new(res).raise_error!
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
##
|
244
|
+
# @param [Net::HTTPRequest] req
|
245
|
+
# The request to set the body stream for
|
246
|
+
# @param [IO] io
|
247
|
+
# The IO object to set as the body stream
|
248
|
+
# @return [void]
|
249
|
+
def set_body_stream(req, io)
|
250
|
+
req.body_stream = io
|
251
|
+
req["transfer-encoding"] = "chunked" unless req["content-length"]
|
252
|
+
end
|
253
|
+
|
225
254
|
##
|
226
255
|
# @param [String] provider
|
227
256
|
# The name of the provider
|
@@ -27,7 +27,7 @@ class LLM::Anthropic
|
|
27
27
|
when Net::HTTPTooManyRequests
|
28
28
|
raise LLM::Error::RateLimit.new { _1.response = res }, "Too many requests"
|
29
29
|
else
|
30
|
-
raise LLM::Error::
|
30
|
+
raise LLM::Error::ResponseError.new { _1.response = res }, "Unexpected response"
|
31
31
|
end
|
32
32
|
end
|
33
33
|
end
|
@@ -28,7 +28,7 @@ module LLM
|
|
28
28
|
# The embedding model to use
|
29
29
|
# @param [Hash] params
|
30
30
|
# Other embedding parameters
|
31
|
-
# @raise (see LLM::
|
31
|
+
# @raise (see LLM::Provider#request)
|
32
32
|
# @return (see LLM::Provider#embed)
|
33
33
|
def embed(input, token:, model: "voyage-2", **params)
|
34
34
|
llm = LLM.voyageai(token)
|
@@ -44,7 +44,7 @@ module LLM
|
|
44
44
|
# @param max_tokens The maximum number of tokens to generate
|
45
45
|
# @param params (see LLM::Provider#complete)
|
46
46
|
# @example (see LLM::Provider#complete)
|
47
|
-
# @raise (see LLM::
|
47
|
+
# @raise (see LLM::Provider#request)
|
48
48
|
# @return (see LLM::Provider#complete)
|
49
49
|
def complete(prompt, role = :user, model: "claude-3-5-sonnet-20240620", max_tokens: 1024, **params)
|
50
50
|
params = {max_tokens:, model:}.merge!(params)
|
@@ -37,7 +37,7 @@ class LLM::Gemini
|
|
37
37
|
# @param [LLM::File, LLM::Response::File] file The input audio
|
38
38
|
# @param [String] model The model to use
|
39
39
|
# @param [Hash] params Other parameters (see Gemini docs)
|
40
|
-
# @raise (see LLM::
|
40
|
+
# @raise (see LLM::Provider#request)
|
41
41
|
# @return [LLM::Response::AudioTranscription]
|
42
42
|
def create_transcription(file:, model: "gemini-1.5-flash", **params)
|
43
43
|
res = @provider.complete [
|
@@ -61,7 +61,7 @@ class LLM::Gemini
|
|
61
61
|
# @param [LLM::File, LLM::Response::File] file The input audio
|
62
62
|
# @param [String] model The model to use
|
63
63
|
# @param [Hash] params Other parameters (see Gemini docs)
|
64
|
-
# @raise (see LLM::
|
64
|
+
# @raise (see LLM::Provider#request)
|
65
65
|
# @return [LLM::Response::AudioTranslation]
|
66
66
|
def create_translation(file:, model: "gemini-1.5-flash", **params)
|
67
67
|
res = @provider.complete [
|
@@ -27,12 +27,12 @@ class LLM::Gemini
|
|
27
27
|
if reason == "API_KEY_INVALID"
|
28
28
|
raise LLM::Error::Unauthorized.new { _1.response = res }, "Authentication error"
|
29
29
|
else
|
30
|
-
raise LLM::Error::
|
30
|
+
raise LLM::Error::ResponseError.new { _1.response = res }, "Unexpected response"
|
31
31
|
end
|
32
32
|
when Net::HTTPTooManyRequests
|
33
33
|
raise LLM::Error::RateLimit.new { _1.response = res }, "Too many requests"
|
34
34
|
else
|
35
|
-
raise LLM::Error::
|
35
|
+
raise LLM::Error::ResponseError.new { _1.response = res }, "Unexpected response"
|
36
36
|
end
|
37
37
|
end
|
38
38
|
|
@@ -17,9 +17,9 @@ class LLM::Gemini
|
|
17
17
|
# #!/usr/bin/env ruby
|
18
18
|
# require "llm"
|
19
19
|
#
|
20
|
-
# llm
|
21
|
-
# file = llm.files.create file: LLM::File("/audio/haiku.mp3")
|
20
|
+
# llm = LLM.gemini(ENV["KEY"])
|
22
21
|
# bot = LLM::Chat.new(llm).lazy
|
22
|
+
# file = llm.files.create file: LLM::File("/audio/haiku.mp3")
|
23
23
|
# bot.chat(file)
|
24
24
|
# bot.chat("Describe the audio file I sent to you")
|
25
25
|
# bot.chat("The audio file is the first message I sent to you.")
|
@@ -28,9 +28,9 @@ class LLM::Gemini
|
|
28
28
|
# #!/usr/bin/env ruby
|
29
29
|
# require "llm"
|
30
30
|
#
|
31
|
-
# llm
|
32
|
-
# file = llm.files.create file: LLM::File("/audio/haiku.mp3")
|
31
|
+
# llm = LLM.gemini(ENV["KEY"])
|
33
32
|
# bot = LLM::Chat.new(llm).lazy
|
33
|
+
# file = llm.files.create file: LLM::File("/audio/haiku.mp3")
|
34
34
|
# bot.chat(["Describe the audio file I sent to you", file])
|
35
35
|
# bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
36
36
|
class Files
|
@@ -52,7 +52,7 @@ class LLM::Gemini
|
|
52
52
|
# end
|
53
53
|
# @see https://ai.google.dev/gemini-api/docs/files Gemini docs
|
54
54
|
# @param [Hash] params Other parameters (see Gemini docs)
|
55
|
-
# @raise (see LLM::
|
55
|
+
# @raise (see LLM::Provider#request)
|
56
56
|
# @return [LLM::Response::FileList]
|
57
57
|
def all(**params)
|
58
58
|
query = URI.encode_www_form(params.merge!(key: secret))
|
@@ -75,16 +75,18 @@ class LLM::Gemini
|
|
75
75
|
# @see https://ai.google.dev/gemini-api/docs/files Gemini docs
|
76
76
|
# @param [File] file The file
|
77
77
|
# @param [Hash] params Other parameters (see Gemini docs)
|
78
|
-
# @raise (see LLM::
|
78
|
+
# @raise (see LLM::Provider#request)
|
79
79
|
# @return [LLM::Response::File]
|
80
80
|
def create(file:, **params)
|
81
81
|
req = Net::HTTP::Post.new(request_upload_url(file:), {})
|
82
82
|
req["content-length"] = file.bytesize
|
83
83
|
req["X-Goog-Upload-Offset"] = 0
|
84
84
|
req["X-Goog-Upload-Command"] = "upload, finalize"
|
85
|
-
|
86
|
-
|
87
|
-
|
85
|
+
file.with_io do |io|
|
86
|
+
set_body_stream(req, io)
|
87
|
+
res = request(http, req)
|
88
|
+
LLM::Response::File.new(res)
|
89
|
+
end
|
88
90
|
end
|
89
91
|
|
90
92
|
##
|
@@ -96,7 +98,7 @@ class LLM::Gemini
|
|
96
98
|
# @see https://ai.google.dev/gemini-api/docs/files Gemini docs
|
97
99
|
# @param [#name, String] file The file to get
|
98
100
|
# @param [Hash] params Other parameters (see Gemini docs)
|
99
|
-
# @raise (see LLM::
|
101
|
+
# @raise (see LLM::Provider#request)
|
100
102
|
# @return [LLM::Response::File]
|
101
103
|
def get(file:, **params)
|
102
104
|
file_id = file.respond_to?(:name) ? file.name : file.to_s
|
@@ -114,7 +116,7 @@ class LLM::Gemini
|
|
114
116
|
# @see https://ai.google.dev/gemini-api/docs/files Gemini docs
|
115
117
|
# @param [#name, String] file The file to delete
|
116
118
|
# @param [Hash] params Other parameters (see Gemini docs)
|
117
|
-
# @raise (see LLM::
|
119
|
+
# @raise (see LLM::Provider#request)
|
118
120
|
# @return [LLM::Response::File]
|
119
121
|
def delete(file:, **params)
|
120
122
|
file_id = file.respond_to?(:name) ? file.name : file.to_s
|
@@ -153,7 +155,7 @@ class LLM::Gemini
|
|
153
155
|
@provider.instance_variable_get(:@secret)
|
154
156
|
end
|
155
157
|
|
156
|
-
[:headers, :request].each do |m|
|
158
|
+
[:headers, :request, :set_body_stream].each do |m|
|
157
159
|
define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
|
158
160
|
end
|
159
161
|
end
|
@@ -34,18 +34,19 @@ class LLM::Gemini
|
|
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)
|
37
|
-
# @raise (see LLM::
|
37
|
+
# @raise (see LLM::Provider#request)
|
38
38
|
# @note
|
39
39
|
# The prompt should make it clear you want to generate an image, or you
|
40
40
|
# might unexpectedly receive a purely textual response. This is due to how
|
41
41
|
# Gemini implements image generation under the hood.
|
42
42
|
# @return [LLM::Response::Image]
|
43
43
|
def create(prompt:, model: "gemini-2.0-flash-exp-image-generation", **params)
|
44
|
-
req
|
45
|
-
|
44
|
+
req = Net::HTTP::Post.new("/v1beta/models/#{model}:generateContent?key=#{secret}", headers)
|
45
|
+
body = JSON.dump({
|
46
46
|
contents: [{parts: {text: prompt}}],
|
47
47
|
generationConfig: {responseModalities: ["TEXT", "IMAGE"]}
|
48
48
|
}.merge!(params))
|
49
|
+
req.body = body
|
49
50
|
res = request(http, req)
|
50
51
|
LLM::Response::Image.new(res).extend(response_parser)
|
51
52
|
end
|
@@ -60,17 +61,16 @@ class LLM::Gemini
|
|
60
61
|
# @param [LLM::File] image The image to edit
|
61
62
|
# @param [String] prompt The prompt
|
62
63
|
# @param [Hash] params Other parameters (see Gemini docs)
|
63
|
-
# @raise (see LLM::
|
64
|
+
# @raise (see LLM::Provider#request)
|
64
65
|
# @note (see LLM::Gemini::Images#create)
|
65
66
|
# @return [LLM::Response::Image]
|
66
67
|
def edit(image:, prompt:, model: "gemini-2.0-flash-exp-image-generation", **params)
|
67
|
-
req
|
68
|
-
|
69
|
-
contents: [
|
70
|
-
{parts: [{text: prompt}, format_content(image)]}
|
71
|
-
],
|
68
|
+
req = Net::HTTP::Post.new("/v1beta/models/#{model}:generateContent?key=#{secret}", headers)
|
69
|
+
body = JSON.dump({
|
70
|
+
contents: [{parts: [{text: prompt}, format_content(image)]}],
|
72
71
|
generationConfig: {responseModalities: ["TEXT", "IMAGE"]}
|
73
|
-
}.merge!(params))
|
72
|
+
}.merge!(params)).b
|
73
|
+
set_body_stream(req, StringIO.new(body))
|
74
74
|
res = request(http, req)
|
75
75
|
LLM::Response::Image.new(res).extend(response_parser)
|
76
76
|
end
|
@@ -92,7 +92,7 @@ class LLM::Gemini
|
|
92
92
|
@provider.instance_variable_get(:@http)
|
93
93
|
end
|
94
94
|
|
95
|
-
[:response_parser, :headers, :request].each do |m|
|
95
|
+
[:response_parser, :headers, :request, :set_body_stream].each do |m|
|
96
96
|
define_method(m) { |*args, &b| @provider.send(m, *args, &b) }
|
97
97
|
end
|
98
98
|
end
|
data/lib/llm/providers/gemini.rb
CHANGED
@@ -49,7 +49,7 @@ module LLM
|
|
49
49
|
# @param input (see LLM::Provider#embed)
|
50
50
|
# @param model (see LLM::Provider#embed)
|
51
51
|
# @param params (see LLM::Provider#embed)
|
52
|
-
# @raise (see LLM::
|
52
|
+
# @raise (see LLM::Provider#request)
|
53
53
|
# @return (see LLM::Provider#embed)
|
54
54
|
def embed(input, model: "text-embedding-004", **params)
|
55
55
|
path = ["/v1beta/models/#{model}", "embedContent?key=#{@secret}"].join(":")
|
@@ -67,14 +67,15 @@ module LLM
|
|
67
67
|
# @param model (see LLM::Provider#complete)
|
68
68
|
# @param params (see LLM::Provider#complete)
|
69
69
|
# @example (see LLM::Provider#complete)
|
70
|
-
# @raise (see LLM::
|
70
|
+
# @raise (see LLM::Provider#request)
|
71
71
|
# @return (see LLM::Provider#complete)
|
72
72
|
def complete(prompt, role = :user, model: "gemini-1.5-flash", **params)
|
73
|
-
path
|
74
|
-
req
|
73
|
+
path = ["/v1beta/models/#{model}", "generateContent?key=#{@secret}"].join(":")
|
74
|
+
req = Net::HTTP::Post.new(path, headers)
|
75
75
|
messages = [*(params.delete(:messages) || []), LLM::Message.new(role, prompt)]
|
76
|
-
|
77
|
-
|
76
|
+
body = JSON.dump({contents: format(messages)}).b
|
77
|
+
set_body_stream(req, StringIO.new(body))
|
78
|
+
res = request(@http, req)
|
78
79
|
Response::Completion.new(res).extend(response_parser)
|
79
80
|
end
|
80
81
|
|
@@ -27,7 +27,7 @@ class LLM::Ollama
|
|
27
27
|
when Net::HTTPTooManyRequests
|
28
28
|
raise LLM::Error::RateLimit.new { _1.response = res }, "Too many requests"
|
29
29
|
else
|
30
|
-
raise LLM::Error::
|
30
|
+
raise LLM::Error::ResponseError.new { _1.response = res }, "Unexpected response"
|
31
31
|
end
|
32
32
|
end
|
33
33
|
end
|
data/lib/llm/providers/ollama.rb
CHANGED
@@ -37,7 +37,7 @@ module LLM
|
|
37
37
|
# @param input (see LLM::Provider#embed)
|
38
38
|
# @param model (see LLM::Provider#embed)
|
39
39
|
# @param params (see LLM::Provider#embed)
|
40
|
-
# @raise (see LLM::
|
40
|
+
# @raise (see LLM::Provider#request)
|
41
41
|
# @return (see LLM::Provider#embed)
|
42
42
|
def embed(input, model: "llama3.2", **params)
|
43
43
|
params = {model:}.merge!(params)
|
@@ -55,7 +55,7 @@ module LLM
|
|
55
55
|
# @param model (see LLM::Provider#complete)
|
56
56
|
# @param params (see LLM::Provider#complete)
|
57
57
|
# @example (see LLM::Provider#complete)
|
58
|
-
# @raise (see LLM::
|
58
|
+
# @raise (see LLM::Provider#request)
|
59
59
|
# @return (see LLM::Provider#complete)
|
60
60
|
def complete(prompt, role = :user, model: "llama3.2", **params)
|
61
61
|
params = {model:, stream: false}.merge!(params)
|