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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9073b7495fb9bdad2deec1d2c086b6d3b554c5a440dd884108a2fa8d12f7c8a9
4
- data.tar.gz: 514902fc97de61dc18df8c22d51d9e86472a62e1ffb0c4ce4394b0684cddbd8a
3
+ metadata.gz: 3c55653b476d2fe6fe9457c89bc430c698668312ce89660a1d69abd8adf338eb
4
+ data.tar.gz: fe7d456bbb739eb091e82351839baef4c64d1d108a2c4cd7de3eb1b478982631
5
5
  SHA512:
6
- metadata.gz: 0d0c35fa38ed3481872e29131d15e03e5a4bf0ad8a96c42ba64a5f48ed32584973d39b53ca630c966d54b6700a83a44abb1f4224c1bb9c1ca7f9e7a2d953e1c3
7
- data.tar.gz: 8889034558c56a2bc1ff5321cf0ca45d82ac83ac7122c741e859caed7d060b34b99824cc53d20a5add4949dd135cf65c383f8400e7c112a44110fc1d4e0d2f4d
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 no dependencies outside Ruby's standard library.
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 text from speech, transcribe audio to text, or translate
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 BadResponse < Error
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(BadResponse)
27
+ Unauthorized = Class.new(ResponseError)
23
28
 
24
29
  ##
25
30
  # HTTPTooManyRequests
26
- RateLimit = Class.new(BadResponse)
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
- [*parts, "--#{@boundary}--\r\n"].inject(&:<<)
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
- _1 << File.binread(file.path)
87
- _1 << "\r\n"
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
- _1 << value.to_s
95
- _1 << "\r\n"
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
- require_relative "http_client"
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:, **params)
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::BadResponse.new { _1.response = res }, "Unexpected response"
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::HTTPClient#request)
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::HTTPClient#request)
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::HTTPClient#request)
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::HTTPClient#request)
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::BadResponse.new { _1.response = res }, "Unexpected response"
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::BadResponse.new { _1.response = res }, "Unexpected response"
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 = LLM.gemini(ENV["KEY"])
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 = LLM.gemini(ENV["KEY"])
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::HTTPClient#request)
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::HTTPClient#request)
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
- req.body = File.binread(file.path)
86
- res = request(http, req)
87
- LLM::Response::File.new(res)
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::HTTPClient#request)
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::HTTPClient#request)
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::HTTPClient#request)
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 = Net::HTTP::Post.new("/v1beta/models/#{model}:generateContent?key=#{secret}", headers)
45
- req.body = JSON.dump({
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::HTTPClient#request)
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 = Net::HTTP::Post.new("/v1beta/models/#{model}:generateContent?key=#{secret}", headers)
68
- req.body = JSON.dump({
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
@@ -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::HTTPClient#request)
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::HTTPClient#request)
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 = ["/v1beta/models/#{model}", "generateContent?key=#{@secret}"].join(":")
74
- req = Net::HTTP::Post.new(path, headers)
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
- req.body = JSON.dump({contents: format(messages)})
77
- res = request(@http, req)
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::BadResponse.new { _1.response = res }, "Unexpected response"
30
+ raise LLM::Error::ResponseError.new { _1.response = res }, "Unexpected response"
31
31
  end
32
32
  end
33
33
  end
@@ -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::HTTPClient#request)
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::HTTPClient#request)
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)