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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +40 -7
  3. data/lib/llm/chat.rb +5 -3
  4. data/lib/llm/core_ext/ostruct.rb +1 -1
  5. data/lib/llm/file.rb +8 -1
  6. data/lib/llm/model.rb +27 -2
  7. data/lib/llm/provider.rb +20 -28
  8. data/lib/llm/providers/anthropic/format.rb +19 -6
  9. data/lib/llm/providers/anthropic/models.rb +62 -0
  10. data/lib/llm/providers/anthropic.rb +23 -8
  11. data/lib/llm/providers/gemini/format.rb +6 -1
  12. data/lib/llm/providers/gemini/images.rb +3 -3
  13. data/lib/llm/providers/gemini/models.rb +69 -0
  14. data/lib/llm/providers/gemini/response_parser.rb +1 -5
  15. data/lib/llm/providers/gemini.rb +20 -5
  16. data/lib/llm/providers/ollama/format.rb +11 -3
  17. data/lib/llm/providers/ollama/models.rb +66 -0
  18. data/lib/llm/providers/ollama.rb +23 -8
  19. data/lib/llm/providers/openai/audio.rb +0 -2
  20. data/lib/llm/providers/openai/format.rb +6 -1
  21. data/lib/llm/providers/openai/images.rb +1 -1
  22. data/lib/llm/providers/openai/models.rb +62 -0
  23. data/lib/llm/providers/openai/response_parser.rb +1 -5
  24. data/lib/llm/providers/openai/responses.rb +10 -6
  25. data/lib/llm/providers/openai.rb +24 -7
  26. data/lib/llm/response/modellist.rb +18 -0
  27. data/lib/llm/response.rb +1 -0
  28. data/lib/llm/version.rb +1 -1
  29. data/lib/llm.rb +2 -1
  30. data/spec/anthropic/completion_spec.rb +36 -0
  31. data/spec/anthropic/models_spec.rb +21 -0
  32. data/spec/gemini/images_spec.rb +4 -12
  33. data/spec/gemini/models_spec.rb +21 -0
  34. data/spec/llm/conversation_spec.rb +5 -3
  35. data/spec/ollama/models_spec.rb +20 -0
  36. data/spec/openai/completion_spec.rb +19 -0
  37. data/spec/openai/images_spec.rb +2 -6
  38. data/spec/openai/models_spec.rb +21 -0
  39. metadata +11 -6
  40. data/share/llm/models/anthropic.yml +0 -35
  41. data/share/llm/models/gemini.yml +0 -35
  42. data/share/llm/models/ollama.yml +0 -155
  43. data/share/llm/models/openai.yml +0 -46
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3c55653b476d2fe6fe9457c89bc430c698668312ce89660a1d69abd8adf338eb
4
- data.tar.gz: fe7d456bbb739eb091e82351839baef4c64d1d108a2c4cd7de3eb1b478982631
3
+ metadata.gz: c8ce8caa7c769da9197528a864c153071f3c4aca15718efc985e543911c04ce2
4
+ data.tar.gz: 389ff41e9e2b35782b1484048b7597f5573bf2a86cf9eaff8cfd7c4cb2b19be3
5
5
  SHA512:
6
- metadata.gz: 8cd55bb28eb92fea745d8b11062b2442bf4b2de88ecfb0b7dc99cfefd293bd45113088dd13ccfe7e251d2e369459da700f15725bae51c3d31d4bf68e19953138
7
- data.tar.gz: dab47021b94d00e51e7d0ca3f92e2966170b9fd8ce7138e0728d2be7fb83da03104ff93cd7c54b760acca62dd03adf16462069db9eb5c30185743c25259105aa
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 uses the stateless chat completions API that all LLM providers support:
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
- File.binwrite File.join(Dir.home, "hello.mp3"),
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
- File.binwrite "#{animal}.png", res.images[0].binary
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 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.
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
- # The parameters to maintain throughout the conversation
32
- def initialize(provider, params = {})
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
@@ -18,7 +18,7 @@ class OpenStruct
18
18
  hash_obj.each do |key, value|
19
19
  visited_object[key] = walk(value)
20
20
  end
21
- OpenStruct.new(visited_object)
21
+ new(visited_object)
22
22
  end
23
23
 
24
24
  private
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 a file
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
- class LLM::Model < Struct.new(:name, :parameters, :description, :to_param, keyword_init: true)
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
- to_param.to_json(*)
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:, **params)
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: nil, **params)
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: nil, **params)
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: nil, **params)
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: nil, **params)
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: nil, **params)
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 [Hash<String, LLM::Model>]
182
- # Returns a hash of available models
183
- def models
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
- if URI === content
30
- [{
31
- type: :image,
32
- source: {type: :base64, media_type: LLM::File(content.to_s).mime_type, data: [content.to_s].pack("m0")}
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: "claude-3-5-sonnet-20240620", max_tokens: 1024, **params)
50
- params = {max_tokens:, model:}.merge!(params)
51
- req = Net::HTTP::Post.new("/v1/messages", headers)
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
- req.body = JSON.dump({messages: format(messages)}.merge!(params))
54
- res = request(@http, req)
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
- # @return (see LLM::Provider#models)
66
- def models
67
- @models ||= load_models!("anthropic")
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
- else
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
- # File.binwrite "rocket.png", res.images[0].binary
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
- # File.binwrite "rocket.png", res.images[0].binary
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
- # File.binwrite "hatoncat.png", res.images[0].binary
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
- OpenStruct.from_hash(
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
  }
@@ -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: "gemini-1.5-flash", **params)
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)}).b
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
- # @return (see LLM::Provider#models)
112
- def models
113
- @models ||= load_models!("gemini")
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
- if LLM::File === content
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 TypeError, "'#{content.path}' was not recognized as an image file."
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
- else
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