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.
- checksums.yaml +4 -4
- data/README.md +356 -583
- data/data/anthropic.json +770 -0
- data/data/deepseek.json +75 -0
- data/data/google.json +1050 -0
- data/data/openai.json +1421 -0
- data/data/xai.json +792 -0
- data/data/zai.json +330 -0
- data/lib/llm/agent.rb +42 -41
- data/lib/llm/bot.rb +1 -263
- data/lib/llm/buffer.rb +7 -0
- data/lib/llm/{session → context}/deserializer.rb +4 -3
- data/lib/llm/context.rb +292 -0
- data/lib/llm/cost.rb +26 -0
- data/lib/llm/error.rb +8 -0
- data/lib/llm/function/array.rb +61 -0
- data/lib/llm/function/fiber_group.rb +91 -0
- data/lib/llm/function/task_group.rb +89 -0
- data/lib/llm/function/thread_group.rb +94 -0
- data/lib/llm/function.rb +75 -10
- data/lib/llm/mcp/command.rb +108 -0
- data/lib/llm/mcp/error.rb +31 -0
- data/lib/llm/mcp/pipe.rb +82 -0
- data/lib/llm/mcp/rpc.rb +118 -0
- data/lib/llm/mcp/transport/http/event_handler.rb +66 -0
- data/lib/llm/mcp/transport/http.rb +122 -0
- data/lib/llm/mcp/transport/stdio.rb +85 -0
- data/lib/llm/mcp.rb +116 -0
- data/lib/llm/message.rb +13 -11
- data/lib/llm/model.rb +2 -2
- data/lib/llm/prompt.rb +17 -7
- data/lib/llm/provider.rb +32 -17
- data/lib/llm/providers/anthropic/files.rb +3 -3
- data/lib/llm/providers/anthropic.rb +19 -4
- data/lib/llm/providers/deepseek.rb +10 -3
- data/lib/llm/providers/{gemini → google}/audio.rb +6 -6
- data/lib/llm/providers/{gemini → google}/error_handler.rb +2 -2
- data/lib/llm/providers/{gemini → google}/files.rb +11 -11
- data/lib/llm/providers/{gemini → google}/images.rb +7 -7
- data/lib/llm/providers/{gemini → google}/models.rb +5 -5
- data/lib/llm/providers/{gemini → google}/request_adapter/completion.rb +7 -3
- data/lib/llm/providers/{gemini → google}/request_adapter.rb +1 -1
- data/lib/llm/providers/{gemini → google}/response_adapter/completion.rb +7 -7
- data/lib/llm/providers/{gemini → google}/response_adapter/embedding.rb +1 -1
- data/lib/llm/providers/{gemini → google}/response_adapter/file.rb +1 -1
- data/lib/llm/providers/{gemini → google}/response_adapter/files.rb +1 -1
- data/lib/llm/providers/{gemini → google}/response_adapter/image.rb +1 -1
- data/lib/llm/providers/{gemini → google}/response_adapter/models.rb +1 -1
- data/lib/llm/providers/{gemini → google}/response_adapter/web_search.rb +2 -2
- data/lib/llm/providers/{gemini → google}/response_adapter.rb +8 -8
- data/lib/llm/providers/{gemini → google}/stream_parser.rb +3 -3
- data/lib/llm/providers/{gemini.rb → google.rb} +41 -26
- data/lib/llm/providers/llamacpp.rb +10 -3
- data/lib/llm/providers/ollama.rb +19 -4
- data/lib/llm/providers/openai/files.rb +3 -3
- data/lib/llm/providers/openai/response_adapter/completion.rb +9 -1
- data/lib/llm/providers/openai/response_adapter/responds.rb +9 -1
- data/lib/llm/providers/openai/responses.rb +9 -1
- data/lib/llm/providers/openai/stream_parser.rb +2 -0
- data/lib/llm/providers/openai.rb +19 -4
- data/lib/llm/providers/xai.rb +10 -3
- data/lib/llm/providers/zai.rb +9 -2
- data/lib/llm/registry.rb +81 -0
- data/lib/llm/schema/all_of.rb +31 -0
- data/lib/llm/schema/any_of.rb +31 -0
- data/lib/llm/schema/one_of.rb +31 -0
- data/lib/llm/schema/parser.rb +145 -0
- data/lib/llm/schema.rb +49 -8
- data/lib/llm/server_tool.rb +5 -5
- data/lib/llm/session.rb +10 -1
- data/lib/llm/tool.rb +88 -6
- data/lib/llm/tracer/logger.rb +1 -1
- data/lib/llm/tracer/telemetry.rb +7 -7
- data/lib/llm/tracer.rb +3 -3
- data/lib/llm/usage.rb +5 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +39 -6
- data/llm.gemspec +45 -8
- 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::
|
|
105
|
+
# @return [LLM::Context]
|
|
97
106
|
def chat(prompt, params = {})
|
|
98
107
|
role = params.delete(:role)
|
|
99
|
-
LLM::
|
|
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::
|
|
116
|
+
# @return [LLM::Context]
|
|
108
117
|
def respond(prompt, params = {})
|
|
109
118
|
role = params.delete(:role)
|
|
110
|
-
LLM::
|
|
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::
|
|
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
|
|
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
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
#
|
|
13
|
+
# ctx = LLM::Context.new(llm)
|
|
14
14
|
# file = llm.files.create file: "/books/goodread.pdf"
|
|
15
|
-
#
|
|
16
|
-
#
|
|
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
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
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 =
|
|
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
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
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::
|
|
3
|
+
class LLM::Google
|
|
4
4
|
##
|
|
5
|
-
# The {LLM::
|
|
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.
|
|
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::
|
|
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.
|
|
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.
|
|
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::
|
|
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::
|
|
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::
|
|
3
|
+
class LLM::Google
|
|
4
4
|
##
|
|
5
|
-
# The {LLM::
|
|
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.
|
|
21
|
-
#
|
|
20
|
+
# llm = LLM.google(key: ENV["KEY"])
|
|
21
|
+
# ctx = LLM::Context.new(llm)
|
|
22
22
|
# file = llm.files.create(file: "/audio/haiku.mp3")
|
|
23
|
-
#
|
|
24
|
-
#
|
|
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::
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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::
|
|
3
|
+
class LLM::Google
|
|
4
4
|
##
|
|
5
|
-
# The {LLM::
|
|
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.
|
|
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::
|
|
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.
|
|
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.
|
|
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::
|
|
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::
|
|
3
|
+
class LLM::Google
|
|
4
4
|
##
|
|
5
|
-
# The {LLM::
|
|
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.
|
|
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::
|
|
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.
|
|
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::
|
|
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
|
|
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
|
-
module LLM::
|
|
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
|
|
68
|
-
tools = parts.
|
|
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(
|
|
75
|
-
(
|
|
76
|
-
|
|
77
|
-
|
|
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,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module LLM::
|
|
3
|
+
module LLM::Google::ResponseAdapter
|
|
4
4
|
##
|
|
5
|
-
# The {LLM::
|
|
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::
|
|
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::
|
|
31
|
-
when :embedding then LLM::
|
|
32
|
-
when :file then LLM::
|
|
33
|
-
when :files then LLM::
|
|
34
|
-
when :image then LLM::
|
|
35
|
-
when :models then LLM::
|
|
36
|
-
when :web_search then LLM::
|
|
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::
|
|
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::
|
|
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::
|
|
22
|
+
# @return [LLM::Google::StreamParser]
|
|
23
23
|
def parse!(chunk)
|
|
24
24
|
tap { merge_chunk!(chunk) }
|
|
25
25
|
end
|