llm.rb 4.8.0 → 4.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +356 -583
  3. data/data/anthropic.json +770 -0
  4. data/data/deepseek.json +75 -0
  5. data/data/google.json +1050 -0
  6. data/data/openai.json +1421 -0
  7. data/data/xai.json +792 -0
  8. data/data/zai.json +330 -0
  9. data/lib/llm/agent.rb +42 -41
  10. data/lib/llm/bot.rb +1 -263
  11. data/lib/llm/buffer.rb +7 -0
  12. data/lib/llm/{session → context}/deserializer.rb +4 -3
  13. data/lib/llm/context.rb +292 -0
  14. data/lib/llm/cost.rb +26 -0
  15. data/lib/llm/error.rb +8 -0
  16. data/lib/llm/function/array.rb +61 -0
  17. data/lib/llm/function/fiber_group.rb +91 -0
  18. data/lib/llm/function/task_group.rb +89 -0
  19. data/lib/llm/function/thread_group.rb +94 -0
  20. data/lib/llm/function.rb +75 -10
  21. data/lib/llm/mcp/command.rb +108 -0
  22. data/lib/llm/mcp/error.rb +31 -0
  23. data/lib/llm/mcp/pipe.rb +82 -0
  24. data/lib/llm/mcp/rpc.rb +118 -0
  25. data/lib/llm/mcp/transport/http/event_handler.rb +66 -0
  26. data/lib/llm/mcp/transport/http.rb +122 -0
  27. data/lib/llm/mcp/transport/stdio.rb +85 -0
  28. data/lib/llm/mcp.rb +116 -0
  29. data/lib/llm/message.rb +13 -11
  30. data/lib/llm/model.rb +2 -2
  31. data/lib/llm/prompt.rb +17 -7
  32. data/lib/llm/provider.rb +32 -17
  33. data/lib/llm/providers/anthropic/files.rb +3 -3
  34. data/lib/llm/providers/anthropic.rb +19 -4
  35. data/lib/llm/providers/deepseek.rb +10 -3
  36. data/lib/llm/providers/{gemini → google}/audio.rb +6 -6
  37. data/lib/llm/providers/{gemini → google}/error_handler.rb +2 -2
  38. data/lib/llm/providers/{gemini → google}/files.rb +11 -11
  39. data/lib/llm/providers/{gemini → google}/images.rb +7 -7
  40. data/lib/llm/providers/{gemini → google}/models.rb +5 -5
  41. data/lib/llm/providers/{gemini → google}/request_adapter/completion.rb +7 -3
  42. data/lib/llm/providers/{gemini → google}/request_adapter.rb +1 -1
  43. data/lib/llm/providers/{gemini → google}/response_adapter/completion.rb +7 -7
  44. data/lib/llm/providers/{gemini → google}/response_adapter/embedding.rb +1 -1
  45. data/lib/llm/providers/{gemini → google}/response_adapter/file.rb +1 -1
  46. data/lib/llm/providers/{gemini → google}/response_adapter/files.rb +1 -1
  47. data/lib/llm/providers/{gemini → google}/response_adapter/image.rb +1 -1
  48. data/lib/llm/providers/{gemini → google}/response_adapter/models.rb +1 -1
  49. data/lib/llm/providers/{gemini → google}/response_adapter/web_search.rb +2 -2
  50. data/lib/llm/providers/{gemini → google}/response_adapter.rb +8 -8
  51. data/lib/llm/providers/{gemini → google}/stream_parser.rb +3 -3
  52. data/lib/llm/providers/{gemini.rb → google.rb} +41 -26
  53. data/lib/llm/providers/llamacpp.rb +10 -3
  54. data/lib/llm/providers/ollama.rb +19 -4
  55. data/lib/llm/providers/openai/files.rb +3 -3
  56. data/lib/llm/providers/openai/response_adapter/completion.rb +9 -1
  57. data/lib/llm/providers/openai/response_adapter/responds.rb +9 -1
  58. data/lib/llm/providers/openai/responses.rb +9 -1
  59. data/lib/llm/providers/openai/stream_parser.rb +2 -0
  60. data/lib/llm/providers/openai.rb +19 -4
  61. data/lib/llm/providers/xai.rb +10 -3
  62. data/lib/llm/providers/zai.rb +9 -2
  63. data/lib/llm/registry.rb +81 -0
  64. data/lib/llm/schema/all_of.rb +31 -0
  65. data/lib/llm/schema/any_of.rb +31 -0
  66. data/lib/llm/schema/one_of.rb +31 -0
  67. data/lib/llm/schema/parser.rb +145 -0
  68. data/lib/llm/schema.rb +49 -8
  69. data/lib/llm/server_tool.rb +5 -5
  70. data/lib/llm/session.rb +10 -1
  71. data/lib/llm/tool.rb +88 -6
  72. data/lib/llm/tracer/logger.rb +1 -1
  73. data/lib/llm/tracer/telemetry.rb +7 -7
  74. data/lib/llm/tracer.rb +3 -3
  75. data/lib/llm/usage.rb +5 -0
  76. data/lib/llm/version.rb +1 -1
  77. data/lib/llm.rb +39 -6
  78. data/llm.gemspec +45 -8
  79. metadata +86 -28
data/lib/llm/provider.rb CHANGED
@@ -50,6 +50,15 @@ class LLM::Provider
50
50
  "#<#{self.class.name}:0x#{object_id.to_s(16)} @key=[REDACTED] @client=#{@client.inspect} @tracer=#{tracer.inspect}>"
51
51
  end
52
52
 
53
+ ##
54
+ # @raise [NotImplementedError]
55
+ # When the method is not implemented by a subclass
56
+ # @return [Symbol]
57
+ # Returns the provider's name
58
+ def name
59
+ raise NotImplementedError
60
+ end
61
+
53
62
  ##
54
63
  # Provides an embedding
55
64
  # @param [String, Array<String>] input
@@ -93,10 +102,10 @@ class LLM::Provider
93
102
  # Starts a new chat powered by the chat completions API
94
103
  # @param prompt (see LLM::Provider#complete)
95
104
  # @param params (see LLM::Provider#complete)
96
- # @return [LLM::Session]
105
+ # @return [LLM::Context]
97
106
  def chat(prompt, params = {})
98
107
  role = params.delete(:role)
99
- LLM::Session.new(self, params).talk(prompt, role:)
108
+ LLM::Context.new(self, params).talk(prompt, role:)
100
109
  end
101
110
 
102
111
  ##
@@ -104,10 +113,10 @@ class LLM::Provider
104
113
  # @param prompt (see LLM::Provider#complete)
105
114
  # @param params (see LLM::Provider#complete)
106
115
  # @raise (see LLM::Provider#complete)
107
- # @return [LLM::Session]
116
+ # @return [LLM::Context]
108
117
  def respond(prompt, params = {})
109
118
  role = params.delete(:role)
110
- LLM::Session.new(self, params).respond(prompt, role:)
119
+ LLM::Context.new(self, params).respond(prompt, role:)
111
120
  end
112
121
 
113
122
  ##
@@ -122,7 +131,7 @@ class LLM::Provider
122
131
  end
123
132
 
124
133
  ##
125
- # @return [LLM::OpenAI::Images, LLM::Gemini::Images]
134
+ # @return [LLM::OpenAI::Images, LLM::Google::Images]
126
135
  # Returns an interface to the images API
127
136
  def images
128
137
  raise NotImplementedError
@@ -264,13 +273,13 @@ class LLM::Provider
264
273
 
265
274
  ##
266
275
  # @return [LLM::Tracer]
267
- # Returns a thread-local tracer
276
+ # Returns a fiber-local tracer
268
277
  def tracer
269
278
  weakmap[self] || LLM::Tracer::Null.new(self)
270
279
  end
271
280
 
272
281
  ##
273
- # Set a thread-local tracer
282
+ # Set a fiber-local tracer
274
283
  # @example
275
284
  # llm = LLM.openai(key: ENV["KEY"])
276
285
  # Thread.new do
@@ -367,16 +376,22 @@ class LLM::Provider
367
376
  args = (Net::HTTP === http) ? [request] : [URI.join(base_uri, request.path), request]
368
377
  res = if stream
369
378
  http.request(*args) do |res|
370
- handler = event_handler.new stream_parser.new(stream)
371
- parser = LLM::EventStream::Parser.new
372
- parser.register(handler)
373
- res.read_body(parser)
374
- # If the handler body is empty, the response was
375
- # most likely not streamed or parsing failed.
376
- # Preserve the raw body in that case so standard
377
- # JSON/error handling can parse it later.
378
- body = handler.body.empty? ? parser.body : handler.body
379
- res.body = Hash === body || Array === body ? LLM::Object.from(body) : body
379
+ if Net::HTTPSuccess === res
380
+ handler = event_handler.new stream_parser.new(stream)
381
+ parser = LLM::EventStream::Parser.new
382
+ parser.register(handler)
383
+ res.read_body(parser)
384
+ # If the handler body is empty, the response was
385
+ # most likely not streamed or parsing failed.
386
+ # Preserve the raw body in that case so standard
387
+ # JSON/error handling can parse it later.
388
+ body = handler.body.empty? ? parser.body : handler.body
389
+ res.body = Hash === body || Array === body ? LLM::Object.from(body) : body
390
+ else
391
+ body = +""
392
+ res.read_body { body << _1 }
393
+ res.body = body
394
+ end
380
395
  ensure
381
396
  parser&.free
382
397
  end
@@ -10,10 +10,10 @@ class LLM::Anthropic
10
10
  # require "llm"
11
11
  #
12
12
  # llm = LLM.anthropic(key: ENV["KEY"])
13
- # ses = LLM::Session.new(llm)
13
+ # ctx = LLM::Context.new(llm)
14
14
  # file = llm.files.create file: "/books/goodread.pdf"
15
- # ses.talk ["Tell me about this PDF", file]
16
- # ses.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
15
+ # ctx.talk ["Tell me about this PDF", file]
16
+ # ctx.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
17
17
  class Files
18
18
  ##
19
19
  # Returns a new Files object
@@ -10,9 +10,9 @@ module LLM
10
10
  # require "llm"
11
11
  #
12
12
  # llm = LLM.anthropic(key: ENV["KEY"])
13
- # ses = LLM::Session.new(llm)
14
- # ses.talk ["Tell me about this photo", ses.local_file("/images/photo.png")]
15
- # ses.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
13
+ # ctx = LLM::Context.new(llm)
14
+ # ctx.talk ["Tell me about this photo", ctx.local_file("/images/photo.png")]
15
+ # ctx.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
16
16
  class Anthropic < Provider
17
17
  require_relative "anthropic/error_handler"
18
18
  require_relative "anthropic/request_adapter"
@@ -30,6 +30,13 @@ module LLM
30
30
  super(host: HOST, **)
31
31
  end
32
32
 
33
+ ##
34
+ # @return [Symbol]
35
+ # Returns the provider's name
36
+ def name
37
+ :anthropic
38
+ end
39
+
33
40
  ##
34
41
  # Provides an interface to the chat completions API
35
42
  # @see https://docs.anthropic.com/en/api/messages Anthropic docs
@@ -139,12 +146,20 @@ module LLM
139
146
  end
140
147
 
141
148
  def build_complete_request(prompt, params, role)
142
- messages = [*(params.delete(:messages) || []), Message.new(role, prompt)]
149
+ messages = build_complete_messages(prompt, params, role)
143
150
  payload = adapt(messages)
144
151
  body = LLM.json.dump(payload.merge!(params))
145
152
  req = Net::HTTP::Post.new("/v1/messages", headers)
146
153
  set_body_stream(req, StringIO.new(body))
147
154
  req
148
155
  end
156
+
157
+ def build_complete_messages(prompt, params, role)
158
+ if LLM::Prompt === prompt
159
+ [*(params.delete(:messages) || []), *prompt.to_a]
160
+ else
161
+ [*(params.delete(:messages) || []), Message.new(role, prompt)]
162
+ end
163
+ end
149
164
  end
150
165
  end
@@ -14,9 +14,9 @@ module LLM
14
14
  # require "llm"
15
15
  #
16
16
  # llm = LLM.deepseek(key: ENV["KEY"])
17
- # ses = LLM::Session.new(llm)
18
- # ses.talk ["Tell me about this photo", ses.local_file("/images/photo.png")]
19
- # ses.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
17
+ # ctx = LLM::Context.new(llm)
18
+ # ctx.talk ["Tell me about this photo", ctx.local_file("/images/photo.png")]
19
+ # ctx.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
20
20
  class DeepSeek < OpenAI
21
21
  require_relative "deepseek/request_adapter"
22
22
  include DeepSeek::RequestAdapter
@@ -28,6 +28,13 @@ module LLM
28
28
  super
29
29
  end
30
30
 
31
+ ##
32
+ # @return [Symbol]
33
+ # Returns the provider's name
34
+ def name
35
+ :deepseek
36
+ end
37
+
31
38
  ##
32
39
  # @raise [NotImplementedError]
33
40
  def files
@@ -1,21 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LLM::Gemini
3
+ class LLM::Google
4
4
  ##
5
- # The {LLM::Gemini::Audio LLM::Gemini::Audio} class provides an audio
5
+ # The {LLM::Google::Audio LLM::Google::Audio} class provides an audio
6
6
  # object for interacting with [Gemini's audio API](https://ai.google.dev/gemini-api/docs/audio).
7
7
  # @example
8
8
  # #!/usr/bin/env ruby
9
9
  # require "llm"
10
10
  #
11
- # llm = LLM.gemini(key: ENV["KEY"])
11
+ # llm = LLM.google(key: ENV["KEY"])
12
12
  # res = llm.audio.create_transcription(input: "/audio/rocket.mp3")
13
13
  # res.text # => "A dog on a rocket to the moon"
14
14
  class Audio
15
15
  ##
16
16
  # Returns a new Audio object
17
17
  # @param provider [LLM::Provider]
18
- # @return [LLM::Gemini::Responses]
18
+ # @return [LLM::Google::Audio]
19
19
  def initialize(provider)
20
20
  @provider = provider
21
21
  end
@@ -30,7 +30,7 @@ class LLM::Gemini
30
30
  ##
31
31
  # Create an audio transcription
32
32
  # @example
33
- # llm = LLM.gemini(key: ENV["KEY"])
33
+ # llm = LLM.google(key: ENV["KEY"])
34
34
  # res = llm.audio.create_transcription(file: "/audio/rocket.mp3")
35
35
  # res.text # => "A dog on a rocket to the moon"
36
36
  # @see https://ai.google.dev/gemini-api/docs/audio Gemini docs
@@ -52,7 +52,7 @@ class LLM::Gemini
52
52
  # Create an audio translation (in English)
53
53
  # @example
54
54
  # # Arabic => English
55
- # llm = LLM.gemini(key: ENV["KEY"])
55
+ # llm = LLM.google(key: ENV["KEY"])
56
56
  # res = llm.audio.create_translation(file: "/audio/bismillah.mp3")
57
57
  # res.text # => "In the name of Allah, the Beneficent, the Merciful."
58
58
  # @see https://ai.google.dev/gemini-api/docs/audio Gemini docs
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LLM::Gemini
3
+ class LLM::Google
4
4
  ##
5
5
  # @private
6
6
  class ErrorHandler
@@ -21,7 +21,7 @@ class LLM::Gemini
21
21
  # The span
22
22
  # @param [Net::HTTPResponse] res
23
23
  # The response from the server
24
- # @return [LLM::Gemini::ErrorHandler]
24
+ # @return [LLM::Google::ErrorHandler]
25
25
  def initialize(tracer, span, res)
26
26
  @tracer = tracer
27
27
  @span = span
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LLM::Gemini
3
+ class LLM::Google
4
4
  ##
5
- # The {LLM::Gemini::Files LLM::Gemini::Files} class provides a files
5
+ # The {LLM::Google::Files LLM::Google::Files} class provides a files
6
6
  # object for interacting with [Gemini's Files API](https://ai.google.dev/gemini-api/docs/files).
7
7
  # The files API allows a client to reference media files in prompts
8
8
  # where they can be referenced by their URL.
@@ -17,16 +17,16 @@ class LLM::Gemini
17
17
  # #!/usr/bin/env ruby
18
18
  # require "llm"
19
19
  #
20
- # llm = LLM.gemini(key: ENV["KEY"])
21
- # ses = LLM::Session.new(llm)
20
+ # llm = LLM.google(key: ENV["KEY"])
21
+ # ctx = LLM::Context.new(llm)
22
22
  # file = llm.files.create(file: "/audio/haiku.mp3")
23
- # ses.talk ["Tell me about this file", file]
24
- # ses.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
23
+ # ctx.talk ["Tell me about this file", file]
24
+ # ctx.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
25
25
  class Files
26
26
  ##
27
27
  # Returns a new Files object
28
28
  # @param provider [LLM::Provider]
29
- # @return [LLM::Gemini::Files]
29
+ # @return [LLM::Google::Files]
30
30
  def initialize(provider)
31
31
  @provider = provider
32
32
  end
@@ -34,7 +34,7 @@ class LLM::Gemini
34
34
  ##
35
35
  # List all files
36
36
  # @example
37
- # llm = LLM.gemini(key: ENV["KEY"])
37
+ # llm = LLM.google(key: ENV["KEY"])
38
38
  # res = llm.files.all
39
39
  # res.each do |file|
40
40
  # print "name: ", file.name, "\n"
@@ -55,7 +55,7 @@ class LLM::Gemini
55
55
  ##
56
56
  # Create a file
57
57
  # @example
58
- # llm = LLM.gemini(key: ENV["KEY"])
58
+ # llm = LLM.google(key: ENV["KEY"])
59
59
  # res = llm.files.create(file: "/audio/haiku.mp3")
60
60
  # @see https://ai.google.dev/gemini-api/docs/files Gemini docs
61
61
  # @param [String, LLM::File] file The file
@@ -80,7 +80,7 @@ class LLM::Gemini
80
80
  ##
81
81
  # Get a file
82
82
  # @example
83
- # llm = LLM.gemini(key: ENV["KEY"])
83
+ # llm = LLM.google(key: ENV["KEY"])
84
84
  # res = llm.files.get(file: "files/1234567890")
85
85
  # print "name: ", res.name, "\n"
86
86
  # @see https://ai.google.dev/gemini-api/docs/files Gemini docs
@@ -101,7 +101,7 @@ class LLM::Gemini
101
101
  ##
102
102
  # Delete a file
103
103
  # @example
104
- # llm = LLM.gemini(key: ENV["KEY"])
104
+ # llm = LLM.google(key: ENV["KEY"])
105
105
  # res = llm.files.delete(file: "files/1234567890")
106
106
  # @see https://ai.google.dev/gemini-api/docs/files Gemini docs
107
107
  # @param [#name, String] file The file to delete
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LLM::Gemini
3
+ class LLM::Google
4
4
  ##
5
- # The {LLM::Gemini::Images LLM::Gemini::Images} class provides an images
5
+ # The {LLM::Google::Images LLM::Google::Images} class provides an images
6
6
  # object for interacting with Google's Imagen text-to-image models via the
7
7
  # Imagen API: https://ai.google.dev/gemini-api/docs/imagen
8
8
  #
9
9
  # @example
10
10
  # #!/usr/bin/env ruby
11
11
  # require "llm"
12
- # llm = LLM.gemini(key: ENV["KEY"])
12
+ # llm = LLM.google(key: ENV["KEY"])
13
13
  # res = llm.images.create prompt: "A dog on a rocket to the moon"
14
14
  # IO.copy_stream res.images[0], "rocket.png"
15
15
  class Images
@@ -18,7 +18,7 @@ class LLM::Gemini
18
18
  ##
19
19
  # Returns a new Images object
20
20
  # @param provider [LLM::Provider]
21
- # @return [LLM::Gemini::Responses]
21
+ # @return [LLM::Google::Images]
22
22
  def initialize(provider)
23
23
  @provider = provider
24
24
  end
@@ -26,7 +26,7 @@ class LLM::Gemini
26
26
  ##
27
27
  # Create an image
28
28
  # @example
29
- # llm = LLM.gemini(key: ENV["KEY"])
29
+ # llm = LLM.google(key: ENV["KEY"])
30
30
  # res = llm.images.create prompt: "A dog on a rocket to the moon"
31
31
  # IO.copy_stream res.images[0], "rocket.png"
32
32
  # @see https://ai.google.dev/gemini-api/docs/imagen Imagen docs
@@ -60,7 +60,7 @@ class LLM::Gemini
60
60
  ##
61
61
  # Edit an image
62
62
  # @example
63
- # llm = LLM.gemini(key: ENV["KEY"])
63
+ # llm = LLM.google(key: ENV["KEY"])
64
64
  # res = llm.images.edit image: "cat.png", prompt: "Add a hat to the cat"
65
65
  # IO.copy_stream res.images[0], "hatoncat.png"
66
66
  # @see https://ai.google.dev/gemini-api/docs/image-generation Gemini docs
@@ -68,7 +68,7 @@ class LLM::Gemini
68
68
  # @param [String] prompt The prompt
69
69
  # @param [Hash] params Other parameters (see Gemini docs)
70
70
  # @raise (see LLM::Provider#request)
71
- # @note (see LLM::Gemini::Images#create)
71
+ # @note (see LLM::Google::Images#create)
72
72
  # @return [LLM::Response]
73
73
  def edit(image:, prompt:, model: "gemini-2.5-flash-image", **params)
74
74
  raise NotImplementedError, "image editing is not yet supported by Gemini"
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LLM::Gemini
3
+ class LLM::Google
4
4
  ##
5
- # The {LLM::Gemini::Models LLM::Gemini::Models} class provides a model
5
+ # The {LLM::Google::Models LLM::Google::Models} class provides a model
6
6
  # object for interacting with [Gemini's models API](https://ai.google.dev/api/models?hl=en#method:-models.list).
7
7
  # The models API allows a client to query Gemini for a list of models
8
8
  # that are available for use with the Gemini API.
@@ -11,7 +11,7 @@ class LLM::Gemini
11
11
  # #!/usr/bin/env ruby
12
12
  # require "llm"
13
13
  #
14
- # llm = LLM.gemini(key: ENV["KEY"])
14
+ # llm = LLM.google(key: ENV["KEY"])
15
15
  # res = llm.models.all
16
16
  # res.each do |model|
17
17
  # print "id: ", model.id, "\n"
@@ -22,7 +22,7 @@ class LLM::Gemini
22
22
  ##
23
23
  # Returns a new Models object
24
24
  # @param provider [LLM::Provider]
25
- # @return [LLM::Gemini::Models]
25
+ # @return [LLM::Google::Models]
26
26
  def initialize(provider)
27
27
  @provider = provider
28
28
  end
@@ -30,7 +30,7 @@ class LLM::Gemini
30
30
  ##
31
31
  # List all models
32
32
  # @example
33
- # llm = LLM.gemini(key: ENV["KEY"])
33
+ # llm = LLM.google(key: ENV["KEY"])
34
34
  # res = llm.models.all
35
35
  # res.each do |model|
36
36
  # print "id: ", model.id, "\n"
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::Gemini::RequestAdapter
3
+ module LLM::Google::RequestAdapter
4
4
  ##
5
5
  # @private
6
6
  class Completion
@@ -19,7 +19,7 @@ module LLM::Gemini::RequestAdapter
19
19
  if Hash === message
20
20
  {role: message[:role], parts: adapt_content(message[:content])}
21
21
  elsif message.tool_call?
22
- {role: message.role, parts: message.extra[:original_tool_calls].map { {"functionCall" => _1} }}
22
+ {role: message.role, parts: message.extra.original_tool_calls}
23
23
  else
24
24
  {role: message.role, parts: adapt_content(message.content)}
25
25
  end
@@ -37,7 +37,7 @@ module LLM::Gemini::RequestAdapter
37
37
  when LLM::Message
38
38
  adapt_content(content.content)
39
39
  when LLM::Function::Return
40
- [{functionResponse: {name: content.name, response: content.value}}]
40
+ [{functionResponse: {name: content.name, response: adapt_function_response(content.value)}}]
41
41
  when LLM::Object
42
42
  adapt_object(content)
43
43
  else
@@ -64,6 +64,10 @@ module LLM::Gemini::RequestAdapter
64
64
  [{file_data: {mime_type: file.mime_type, file_uri: file.uri}}]
65
65
  end
66
66
 
67
+ def adapt_function_response(value)
68
+ Hash === value ? value : {result: value}
69
+ end
70
+
67
71
  def prompt_error!(object)
68
72
  if LLM::Object === object
69
73
  raise LLM::PromptError, "The given LLM::Object with kind '#{content.kind}' is not " \
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LLM::Gemini
3
+ class LLM::Google
4
4
  ##
5
5
  # @private
6
6
  module RequestAdapter
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::Gemini::ResponseAdapter
3
+ module LLM::Google::ResponseAdapter
4
4
  module Completion
5
5
  ##
6
6
  # (see LLM::Contract::Completion#messages)
@@ -64,17 +64,17 @@ module LLM::Gemini::ResponseAdapter
64
64
  content = choice.content || LLM::Object.new
65
65
  role = content.role || "model"
66
66
  parts = content.parts || [{"text" => choice.finishReason}]
67
- text = parts.filter_map { _1["text"] }.join
68
- tools = parts.filter_map { _1["functionCall"] }
67
+ text = parts.filter_map { _1["text"] }.join
68
+ tools = parts.select { _1["functionCall"] }
69
69
  extra = {index:, response: self, tool_calls: adapt_tool_calls(tools), original_tool_calls: tools}
70
70
  LLM::Message.new(role, text, extra)
71
71
  end
72
72
  end
73
73
 
74
- def adapt_tool_calls(tools)
75
- (tools || []).map do |tool|
76
- function = {name: tool.name, arguments: tool.args}
77
- function
74
+ def adapt_tool_calls(parts)
75
+ (parts || []).map do |part|
76
+ tool = part["functionCall"]
77
+ {name: tool.name, arguments: tool.args}
78
78
  end
79
79
  end
80
80
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::Gemini::ResponseAdapter
3
+ module LLM::Google::ResponseAdapter
4
4
  module Embedding
5
5
  def model = "text-embedding-004"
6
6
  def embeddings = body.dig("embedding", "values")
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::Gemini::ResponseAdapter
3
+ module LLM::Google::ResponseAdapter
4
4
  module File
5
5
  def name = respond_to?(:file) ? file.name : body.name
6
6
  def display_name = respond_to?(:file) ? file.displayName : body.displayName
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::Gemini::ResponseAdapter
3
+ module LLM::Google::ResponseAdapter
4
4
  module Files
5
5
  include ::Enumerable
6
6
  def each(&)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::Gemini::ResponseAdapter
3
+ module LLM::Google::ResponseAdapter
4
4
  module Image
5
5
  ##
6
6
  # @return [Array<StringIO>]
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::Gemini::ResponseAdapter
3
+ module LLM::Google::ResponseAdapter
4
4
  module Models
5
5
  include LLM::Model::Collection
6
6
 
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module LLM::Gemini::ResponseAdapter
3
+ module LLM::Google::ResponseAdapter
4
4
  ##
5
- # The {LLM::Gemini::ResponseAdapter::WebSearch LLM::Gemini::ResponseAdapter::WebSearch}
5
+ # The {LLM::Google::ResponseAdapter::WebSearch LLM::Google::ResponseAdapter::WebSearch}
6
6
  # module provides methods for accessing web search results from a web search
7
7
  # tool call made via the {LLM::Provider#web_search LLM::Provider#web_search}
8
8
  # method.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LLM::Gemini
3
+ class LLM::Google
4
4
  ##
5
5
  # @private
6
6
  module ResponseAdapter
@@ -27,13 +27,13 @@ class LLM::Gemini
27
27
  # @api private
28
28
  def select(type)
29
29
  case type
30
- when :completion then LLM::Gemini::ResponseAdapter::Completion
31
- when :embedding then LLM::Gemini::ResponseAdapter::Embedding
32
- when :file then LLM::Gemini::ResponseAdapter::File
33
- when :files then LLM::Gemini::ResponseAdapter::Files
34
- when :image then LLM::Gemini::ResponseAdapter::Image
35
- when :models then LLM::Gemini::ResponseAdapter::Models
36
- when :web_search then LLM::Gemini::ResponseAdapter::WebSearch
30
+ when :completion then LLM::Google::ResponseAdapter::Completion
31
+ when :embedding then LLM::Google::ResponseAdapter::Embedding
32
+ when :file then LLM::Google::ResponseAdapter::File
33
+ when :files then LLM::Google::ResponseAdapter::Files
34
+ when :image then LLM::Google::ResponseAdapter::Image
35
+ when :models then LLM::Google::ResponseAdapter::Models
36
+ when :web_search then LLM::Google::ResponseAdapter::WebSearch
37
37
  else
38
38
  raise ArgumentError, "Unknown response adapter type: #{type.inspect}"
39
39
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class LLM::Gemini
3
+ class LLM::Google
4
4
  ##
5
5
  # @private
6
6
  class StreamParser
@@ -11,7 +11,7 @@ class LLM::Gemini
11
11
 
12
12
  ##
13
13
  # @param [#<<] io An IO-like object
14
- # @return [LLM::Gemini::StreamParser]
14
+ # @return [LLM::Google::StreamParser]
15
15
  def initialize(io)
16
16
  @body = {"candidates" => []}
17
17
  @io = io
@@ -19,7 +19,7 @@ class LLM::Gemini
19
19
 
20
20
  ##
21
21
  # @param [Hash] chunk
22
- # @return [LLM::Gemini::StreamParser]
22
+ # @return [LLM::Google::StreamParser]
23
23
  def parse!(chunk)
24
24
  tap { merge_chunk!(chunk) }
25
25
  end