llm.rb 4.7.0 → 4.9.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 +335 -587
- 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/eventstream/parser.rb +0 -5
- 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/stdio.rb +85 -0
- data/lib/llm/mcp.rb +102 -0
- data/lib/llm/message.rb +13 -11
- data/lib/llm/model.rb +115 -0
- data/lib/llm/prompt.rb +17 -7
- data/lib/llm/provider.rb +60 -32
- data/lib/llm/providers/anthropic/error_handler.rb +1 -1
- data/lib/llm/providers/anthropic/files.rb +3 -3
- data/lib/llm/providers/anthropic/models.rb +1 -1
- data/lib/llm/providers/anthropic/request_adapter.rb +20 -3
- data/lib/llm/providers/anthropic/response_adapter/models.rb +13 -0
- data/lib/llm/providers/anthropic/response_adapter.rb +2 -0
- data/lib/llm/providers/anthropic.rb +21 -5
- 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 +20 -5
- 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/google/response_adapter/models.rb +13 -0
- 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/error_handler.rb +1 -1
- data/lib/llm/providers/ollama/models.rb +1 -1
- data/lib/llm/providers/ollama/response_adapter/models.rb +13 -0
- data/lib/llm/providers/ollama/response_adapter.rb +2 -0
- data/lib/llm/providers/ollama.rb +19 -4
- data/lib/llm/providers/openai/error_handler.rb +18 -3
- data/lib/llm/providers/openai/files.rb +3 -3
- data/lib/llm/providers/openai/images.rb +17 -11
- data/lib/llm/providers/openai/models.rb +1 -1
- data/lib/llm/providers/openai/response_adapter/completion.rb +9 -1
- data/lib/llm/providers/openai/response_adapter/models.rb +13 -0
- data/lib/llm/providers/openai/response_adapter/responds.rb +9 -1
- data/lib/llm/providers/openai/response_adapter.rb +2 -0
- data/lib/llm/providers/openai/responses.rb +16 -1
- data/lib/llm/providers/openai/stream_parser.rb +2 -0
- data/lib/llm/providers/openai.rb +28 -6
- data/lib/llm/providers/xai/images.rb +7 -6
- 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/enum.rb +16 -0
- data/lib/llm/schema/parser.rb +109 -0
- data/lib/llm/schema.rb +5 -0
- data/lib/llm/server_tool.rb +5 -5
- data/lib/llm/session.rb +10 -1
- data/lib/llm/tool/param.rb +1 -1
- data/lib/llm/tool.rb +86 -5
- data/lib/llm/tracer/langsmith.rb +144 -0
- data/lib/llm/tracer/logger.rb +9 -1
- data/lib/llm/tracer/null.rb +8 -0
- data/lib/llm/tracer/telemetry.rb +98 -78
- data/lib/llm/tracer.rb +108 -4
- data/lib/llm/usage.rb +5 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +40 -6
- data/llm.gemspec +45 -8
- metadata +87 -28
- data/lib/llm/providers/gemini/response_adapter/models.rb +0 -15
|
@@ -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
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module LLM
|
|
4
4
|
##
|
|
5
|
-
# The
|
|
6
|
-
# [Gemini](https://ai.google.dev/). The
|
|
5
|
+
# The Google class implements a provider for
|
|
6
|
+
# [Gemini](https://ai.google.dev/). The Google provider
|
|
7
7
|
# can accept multiple inputs (text, images, audio, and video).
|
|
8
8
|
# The inputs can be provided inline via the prompt for files
|
|
9
9
|
# under 20MB or via the Gemini Files API for files
|
|
@@ -13,19 +13,19 @@ module LLM
|
|
|
13
13
|
# #!/usr/bin/env ruby
|
|
14
14
|
# require "llm"
|
|
15
15
|
#
|
|
16
|
-
# llm = LLM.
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
class
|
|
21
|
-
require_relative "
|
|
22
|
-
require_relative "
|
|
23
|
-
require_relative "
|
|
24
|
-
require_relative "
|
|
25
|
-
require_relative "
|
|
26
|
-
require_relative "
|
|
27
|
-
require_relative "
|
|
28
|
-
require_relative "
|
|
16
|
+
# llm = LLM.google(key: ENV["KEY"])
|
|
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
|
+
class Google < Provider
|
|
21
|
+
require_relative "google/error_handler"
|
|
22
|
+
require_relative "google/request_adapter"
|
|
23
|
+
require_relative "google/response_adapter"
|
|
24
|
+
require_relative "google/stream_parser"
|
|
25
|
+
require_relative "google/models"
|
|
26
|
+
require_relative "google/images"
|
|
27
|
+
require_relative "google/audio"
|
|
28
|
+
require_relative "google/files"
|
|
29
29
|
|
|
30
30
|
include RequestAdapter
|
|
31
31
|
|
|
@@ -37,6 +37,13 @@ module LLM
|
|
|
37
37
|
super(host: HOST, **)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
##
|
|
41
|
+
# @return [Symbol]
|
|
42
|
+
# Returns the provider's name
|
|
43
|
+
def name
|
|
44
|
+
:google
|
|
45
|
+
end
|
|
46
|
+
|
|
40
47
|
##
|
|
41
48
|
# Provides an embedding
|
|
42
49
|
# @param input (see LLM::Provider#embed)
|
|
@@ -78,33 +85,33 @@ module LLM
|
|
|
78
85
|
##
|
|
79
86
|
# Provides an interface to Gemini's audio API
|
|
80
87
|
# @see https://ai.google.dev/gemini-api/docs/audio Gemini docs
|
|
81
|
-
# @return [LLM::
|
|
88
|
+
# @return [LLM::Google::Audio]
|
|
82
89
|
def audio
|
|
83
|
-
LLM::
|
|
90
|
+
LLM::Google::Audio.new(self)
|
|
84
91
|
end
|
|
85
92
|
|
|
86
93
|
##
|
|
87
94
|
# Provides an interface to Gemini's image generation API
|
|
88
95
|
# @see https://ai.google.dev/gemini-api/docs/image-generation Gemini docs
|
|
89
|
-
# @return [see LLM::
|
|
96
|
+
# @return [see LLM::Google::Images]
|
|
90
97
|
def images
|
|
91
|
-
LLM::
|
|
98
|
+
LLM::Google::Images.new(self)
|
|
92
99
|
end
|
|
93
100
|
|
|
94
101
|
##
|
|
95
102
|
# Provides an interface to Gemini's file management API
|
|
96
103
|
# @see https://ai.google.dev/gemini-api/docs/files Gemini docs
|
|
97
|
-
# @return [LLM::
|
|
104
|
+
# @return [LLM::Google::Files]
|
|
98
105
|
def files
|
|
99
|
-
LLM::
|
|
106
|
+
LLM::Google::Files.new(self)
|
|
100
107
|
end
|
|
101
108
|
|
|
102
109
|
##
|
|
103
110
|
# Provides an interface to Gemini's models API
|
|
104
111
|
# @see https://ai.google.dev/gemini-api/docs/models Gemini docs
|
|
105
|
-
# @return [LLM::
|
|
112
|
+
# @return [LLM::Google::Models]
|
|
106
113
|
def models
|
|
107
|
-
LLM::
|
|
114
|
+
LLM::Google::Models.new(self)
|
|
108
115
|
end
|
|
109
116
|
|
|
110
117
|
##
|
|
@@ -177,11 +184,11 @@ module LLM
|
|
|
177
184
|
end
|
|
178
185
|
|
|
179
186
|
def stream_parser
|
|
180
|
-
LLM::
|
|
187
|
+
LLM::Google::StreamParser
|
|
181
188
|
end
|
|
182
189
|
|
|
183
190
|
def error_handler
|
|
184
|
-
LLM::
|
|
191
|
+
LLM::Google::ErrorHandler
|
|
185
192
|
end
|
|
186
193
|
|
|
187
194
|
def normalize_complete_params(params)
|
|
@@ -197,10 +204,18 @@ module LLM
|
|
|
197
204
|
model.respond_to?(:id) ? model.id : model
|
|
198
205
|
path = ["/v1beta/models/#{model}", action].join(":")
|
|
199
206
|
req = Net::HTTP::Post.new(path, headers)
|
|
200
|
-
messages =
|
|
207
|
+
messages = build_complete_messages(prompt, params, role)
|
|
201
208
|
body = LLM.json.dump({contents: adapt(messages)}.merge!(params))
|
|
202
209
|
set_body_stream(req, StringIO.new(body))
|
|
203
210
|
req
|
|
204
211
|
end
|
|
212
|
+
|
|
213
|
+
def build_complete_messages(prompt, params, role)
|
|
214
|
+
if LLM::Prompt === prompt
|
|
215
|
+
[*(params.delete(:messages) || []), *prompt.to_a]
|
|
216
|
+
else
|
|
217
|
+
[*(params.delete(:messages) || []), LLM::Message.new(role, prompt)]
|
|
218
|
+
end
|
|
219
|
+
end
|
|
205
220
|
end
|
|
206
221
|
end
|
|
@@ -16,9 +16,9 @@ module LLM
|
|
|
16
16
|
# require "llm"
|
|
17
17
|
#
|
|
18
18
|
# llm = LLM.llamacpp(key: nil)
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
19
|
+
# ctx = LLM::Context.new(llm)
|
|
20
|
+
# ctx.talk ["Tell me about this photo", ctx.local_file("/images/photo.png")]
|
|
21
|
+
# ctx.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
|
22
22
|
class LlamaCpp < OpenAI
|
|
23
23
|
##
|
|
24
24
|
# @param (see LLM::Provider#initialize)
|
|
@@ -27,6 +27,13 @@ module LLM
|
|
|
27
27
|
super
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
##
|
|
31
|
+
# @return [Symbol]
|
|
32
|
+
# Returns the provider's name
|
|
33
|
+
def name
|
|
34
|
+
:llamacpp
|
|
35
|
+
end
|
|
36
|
+
|
|
30
37
|
##
|
|
31
38
|
# @raise [NotImplementedError]
|
|
32
39
|
def files
|
|
@@ -44,7 +44,7 @@ class LLM::Ollama
|
|
|
44
44
|
query = URI.encode_www_form(params)
|
|
45
45
|
req = Net::HTTP::Get.new("/api/tags?#{query}", headers)
|
|
46
46
|
res, span, tracer = execute(request: req, operation: "request")
|
|
47
|
-
res =
|
|
47
|
+
res = ResponseAdapter.adapt(res, type: :models)
|
|
48
48
|
tracer.on_request_finish(operation: "request", res:, span:)
|
|
49
49
|
res
|
|
50
50
|
end
|
|
@@ -6,6 +6,7 @@ class LLM::Ollama
|
|
|
6
6
|
module ResponseAdapter
|
|
7
7
|
require_relative "response_adapter/completion"
|
|
8
8
|
require_relative "response_adapter/embedding"
|
|
9
|
+
require_relative "response_adapter/models"
|
|
9
10
|
|
|
10
11
|
module_function
|
|
11
12
|
|
|
@@ -24,6 +25,7 @@ class LLM::Ollama
|
|
|
24
25
|
case type
|
|
25
26
|
when :completion then LLM::Ollama::ResponseAdapter::Completion
|
|
26
27
|
when :embedding then LLM::Ollama::ResponseAdapter::Embedding
|
|
28
|
+
when :models then LLM::Ollama::ResponseAdapter::Models
|
|
27
29
|
else
|
|
28
30
|
raise ArgumentError, "Unknown response adapter type: #{type.inspect}"
|
|
29
31
|
end
|
data/lib/llm/providers/ollama.rb
CHANGED
|
@@ -12,9 +12,9 @@ module LLM
|
|
|
12
12
|
# require "llm"
|
|
13
13
|
#
|
|
14
14
|
# llm = LLM.ollama(key: nil)
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
15
|
+
# ctx = LLM::Context.new(llm, model: "llava")
|
|
16
|
+
# ctx.talk ["Tell me about this image", ctx.local_file("/images/photo.png")]
|
|
17
|
+
# ctx.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
|
18
18
|
class Ollama < Provider
|
|
19
19
|
require_relative "ollama/error_handler"
|
|
20
20
|
require_relative "ollama/request_adapter"
|
|
@@ -32,6 +32,13 @@ module LLM
|
|
|
32
32
|
super(host: HOST, port: 11434, ssl: false, **)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
##
|
|
36
|
+
# @return [Symbol]
|
|
37
|
+
# Returns the provider's name
|
|
38
|
+
def name
|
|
39
|
+
:ollama
|
|
40
|
+
end
|
|
41
|
+
|
|
35
42
|
##
|
|
36
43
|
# Provides an embedding
|
|
37
44
|
# @param input (see LLM::Provider#embed)
|
|
@@ -120,11 +127,19 @@ module LLM
|
|
|
120
127
|
end
|
|
121
128
|
|
|
122
129
|
def build_complete_request(prompt, params, role)
|
|
123
|
-
messages =
|
|
130
|
+
messages = build_complete_messages(prompt, params, role)
|
|
124
131
|
body = LLM.json.dump({messages: [adapt(messages)].flatten}.merge!(params))
|
|
125
132
|
req = Net::HTTP::Post.new("/api/chat", headers)
|
|
126
133
|
set_body_stream(req, StringIO.new(body))
|
|
127
134
|
req
|
|
128
135
|
end
|
|
136
|
+
|
|
137
|
+
def build_complete_messages(prompt, params, role)
|
|
138
|
+
if LLM::Prompt === prompt
|
|
139
|
+
[*(params.delete(:messages) || []), *prompt.to_a]
|
|
140
|
+
else
|
|
141
|
+
[*(params.delete(:messages) || []), LLM::Message.new(role, prompt)]
|
|
142
|
+
end
|
|
143
|
+
end
|
|
129
144
|
end
|
|
130
145
|
end
|
|
@@ -35,15 +35,15 @@ class LLM::OpenAI
|
|
|
35
35
|
ex = error
|
|
36
36
|
@tracer.on_request_error(ex:, span:)
|
|
37
37
|
ensure
|
|
38
|
-
raise(ex)
|
|
38
|
+
raise(ex) if ex
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
private
|
|
42
42
|
|
|
43
43
|
##
|
|
44
|
-
# @return [LLM::Object]
|
|
44
|
+
# @return [String, LLM::Object]
|
|
45
45
|
def body
|
|
46
|
-
@body ||=
|
|
46
|
+
@body ||= parse_body!
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
##
|
|
@@ -79,5 +79,20 @@ class LLM::OpenAI
|
|
|
79
79
|
LLM::InvalidRequestError.new(error["message"]).tap { _1.response = res }
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
|
+
|
|
83
|
+
##
|
|
84
|
+
# Tries to parse the response body as a LLM::Object
|
|
85
|
+
# @return [String, LLM::Object]
|
|
86
|
+
def parse_body!
|
|
87
|
+
if String === res.body
|
|
88
|
+
LLM::Object.from LLM.json.load(res.body)
|
|
89
|
+
elsif Hash === res.body
|
|
90
|
+
LLM::Object.from(res.body)
|
|
91
|
+
else
|
|
92
|
+
res.body
|
|
93
|
+
end
|
|
94
|
+
rescue
|
|
95
|
+
res.body
|
|
96
|
+
end
|
|
82
97
|
end
|
|
83
98
|
end
|
|
@@ -13,10 +13,10 @@ class LLM::OpenAI
|
|
|
13
13
|
# require "llm"
|
|
14
14
|
#
|
|
15
15
|
# llm = LLM.openai(key: ENV["KEY"])
|
|
16
|
-
#
|
|
16
|
+
# ctx = LLM::Context.new(llm)
|
|
17
17
|
# file = llm.files.create file: "/books/goodread.pdf"
|
|
18
|
-
#
|
|
19
|
-
#
|
|
18
|
+
# ctx.talk ["Tell me about this PDF", file]
|
|
19
|
+
# ctx.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
|
20
20
|
class Files
|
|
21
21
|
##
|
|
22
22
|
# Returns a new Files object
|
|
@@ -5,7 +5,7 @@ class LLM::OpenAI
|
|
|
5
5
|
# The {LLM::OpenAI::Images LLM::OpenAI::Images} class provides an interface
|
|
6
6
|
# for [OpenAI's images API](https://platform.openai.com/docs/api-reference/images).
|
|
7
7
|
# OpenAI supports multiple response formats: temporary URLs, or binary strings
|
|
8
|
-
# encoded in base64. The default is to return
|
|
8
|
+
# encoded in base64. The default is to return base64-encoded image data.
|
|
9
9
|
#
|
|
10
10
|
# @example Temporary URLs
|
|
11
11
|
# #!/usr/bin/env ruby
|
|
@@ -14,7 +14,8 @@ class LLM::OpenAI
|
|
|
14
14
|
# require "fileutils"
|
|
15
15
|
#
|
|
16
16
|
# llm = LLM.openai(key: ENV["KEY"])
|
|
17
|
-
# res = llm.images.create prompt: "A dog on a rocket to the moon"
|
|
17
|
+
# res = llm.images.create prompt: "A dog on a rocket to the moon",
|
|
18
|
+
# response_format: "url"
|
|
18
19
|
# FileUtils.mv OpenURI.open_uri(res.urls[0]).path,
|
|
19
20
|
# "rocket.png"
|
|
20
21
|
#
|
|
@@ -40,16 +41,17 @@ class LLM::OpenAI
|
|
|
40
41
|
# @example
|
|
41
42
|
# llm = LLM.openai(key: ENV["KEY"])
|
|
42
43
|
# res = llm.images.create prompt: "A dog on a rocket to the moon"
|
|
43
|
-
# res.
|
|
44
|
+
# IO.copy_stream res.images[0], "rocket.png"
|
|
44
45
|
# @see https://platform.openai.com/docs/api-reference/images/create OpenAI docs
|
|
45
46
|
# @param [String] prompt The prompt
|
|
46
47
|
# @param [String] model The model to use
|
|
48
|
+
# @param [String] response_format The response format ("b64_json" or "url")
|
|
47
49
|
# @param [Hash] params Other parameters (see OpenAI docs)
|
|
48
50
|
# @raise (see LLM::Provider#request)
|
|
49
51
|
# @return [LLM::Response]
|
|
50
|
-
def create(prompt:, model: "dall-e-3", **params)
|
|
52
|
+
def create(prompt:, model: "dall-e-3", response_format: "b64_json", **params)
|
|
51
53
|
req = Net::HTTP::Post.new("/v1/images/generations", headers)
|
|
52
|
-
req.body = LLM.json.dump({prompt:, n: 1, model:}.merge!(params))
|
|
54
|
+
req.body = LLM.json.dump({prompt:, n: 1, model:, response_format:}.merge!(params))
|
|
53
55
|
res, span, tracer = execute(request: req, operation: "request")
|
|
54
56
|
res = ResponseAdapter.adapt(res, type: :image)
|
|
55
57
|
tracer.on_request_finish(operation: "request", model:, res:, span:)
|
|
@@ -61,16 +63,19 @@ class LLM::OpenAI
|
|
|
61
63
|
# @example
|
|
62
64
|
# llm = LLM.openai(key: ENV["KEY"])
|
|
63
65
|
# res = llm.images.create_variation(image: "/images/hat.png", n: 5)
|
|
64
|
-
#
|
|
66
|
+
# res.images.each.with_index do |image, index|
|
|
67
|
+
# IO.copy_stream image, "variation#{index}.png"
|
|
68
|
+
# end
|
|
65
69
|
# @see https://platform.openai.com/docs/api-reference/images/createVariation OpenAI docs
|
|
66
70
|
# @param [File] image The image to create variations from
|
|
67
71
|
# @param [String] model The model to use
|
|
72
|
+
# @param [String] response_format The response format ("b64_json" or "url")
|
|
68
73
|
# @param [Hash] params Other parameters (see OpenAI docs)
|
|
69
74
|
# @raise (see LLM::Provider#request)
|
|
70
75
|
# @return [LLM::Response]
|
|
71
|
-
def create_variation(image:, model: "dall-e-2", **params)
|
|
76
|
+
def create_variation(image:, model: "dall-e-2", response_format: "b64_json", **params)
|
|
72
77
|
image = LLM.File(image)
|
|
73
|
-
multi = LLM::Multipart.new(params.merge!(image:, model:))
|
|
78
|
+
multi = LLM::Multipart.new(params.merge!(image:, model:, response_format:))
|
|
74
79
|
req = Net::HTTP::Post.new("/v1/images/variations", headers)
|
|
75
80
|
req["content-type"] = multi.content_type
|
|
76
81
|
set_body_stream(req, multi.body)
|
|
@@ -85,17 +90,18 @@ class LLM::OpenAI
|
|
|
85
90
|
# @example
|
|
86
91
|
# llm = LLM.openai(key: ENV["KEY"])
|
|
87
92
|
# res = llm.images.edit(image: "/images/hat.png", prompt: "A cat wearing this hat")
|
|
88
|
-
#
|
|
93
|
+
# IO.copy_stream res.images[0], "hatoncat.png"
|
|
89
94
|
# @see https://platform.openai.com/docs/api-reference/images/createEdit OpenAI docs
|
|
90
95
|
# @param [File] image The image to edit
|
|
91
96
|
# @param [String] prompt The prompt
|
|
92
97
|
# @param [String] model The model to use
|
|
98
|
+
# @param [String] response_format The response format ("b64_json" or "url")
|
|
93
99
|
# @param [Hash] params Other parameters (see OpenAI docs)
|
|
94
100
|
# @raise (see LLM::Provider#request)
|
|
95
101
|
# @return [LLM::Response]
|
|
96
|
-
def edit(image:, prompt:, model: "dall-e-2", **params)
|
|
102
|
+
def edit(image:, prompt:, model: "dall-e-2", response_format: "b64_json", **params)
|
|
97
103
|
image = LLM.File(image)
|
|
98
|
-
multi = LLM::Multipart.new(params.merge!(image:, prompt:, model:))
|
|
104
|
+
multi = LLM::Multipart.new(params.merge!(image:, prompt:, model:, response_format:))
|
|
99
105
|
req = Net::HTTP::Post.new("/v1/images/edits", headers)
|
|
100
106
|
req["content-type"] = multi.content_type
|
|
101
107
|
set_body_stream(req, multi.body)
|
|
@@ -41,7 +41,7 @@ class LLM::OpenAI
|
|
|
41
41
|
query = URI.encode_www_form(params)
|
|
42
42
|
req = Net::HTTP::Get.new("/v1/models?#{query}", headers)
|
|
43
43
|
res, span, tracer = execute(request: req, operation: "request")
|
|
44
|
-
res = ResponseAdapter.adapt(res, type: :
|
|
44
|
+
res = ResponseAdapter.adapt(res, type: :models)
|
|
45
45
|
tracer.on_request_finish(operation: "request", res:, span:)
|
|
46
46
|
res
|
|
47
47
|
end
|
|
@@ -74,10 +74,18 @@ module LLM::OpenAI::ResponseAdapter
|
|
|
74
74
|
def adapt_tool_calls(tools)
|
|
75
75
|
(tools || []).filter_map do |tool|
|
|
76
76
|
next unless tool.function
|
|
77
|
-
{id: tool.id, name: tool.function.name, arguments:
|
|
77
|
+
{id: tool.id, name: tool.function.name, arguments: parse_tool_arguments(tool.function.arguments)}
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
+
def parse_tool_arguments(arguments)
|
|
82
|
+
return {} if arguments.to_s.empty?
|
|
83
|
+
parsed = LLM.json.load(arguments)
|
|
84
|
+
Hash === parsed ? parsed : {}
|
|
85
|
+
rescue *LLM.json.parser_error
|
|
86
|
+
{}
|
|
87
|
+
end
|
|
88
|
+
|
|
81
89
|
include LLM::Contract::Completion
|
|
82
90
|
end
|
|
83
91
|
end
|
|
@@ -38,7 +38,15 @@ module LLM::OpenAI::ResponseAdapter
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def adapt_tool(tool)
|
|
41
|
-
{id: tool.call_id, name: tool.name, arguments:
|
|
41
|
+
{id: tool.call_id, name: tool.name, arguments: parse_tool_arguments(tool.arguments)}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def parse_tool_arguments(arguments)
|
|
45
|
+
return {} if arguments.to_s.empty?
|
|
46
|
+
parsed = LLM.json.load(arguments)
|
|
47
|
+
Hash === parsed ? parsed : {}
|
|
48
|
+
rescue *LLM.json.parser_error
|
|
49
|
+
{}
|
|
42
50
|
end
|
|
43
51
|
end
|
|
44
52
|
end
|
|
@@ -11,6 +11,7 @@ class LLM::OpenAI
|
|
|
11
11
|
require_relative "response_adapter/file"
|
|
12
12
|
require_relative "response_adapter/image"
|
|
13
13
|
require_relative "response_adapter/moderations"
|
|
14
|
+
require_relative "response_adapter/models"
|
|
14
15
|
require_relative "response_adapter/responds"
|
|
15
16
|
require_relative "response_adapter/web_search"
|
|
16
17
|
|
|
@@ -37,6 +38,7 @@ class LLM::OpenAI
|
|
|
37
38
|
when :file then LLM::OpenAI::ResponseAdapter::File
|
|
38
39
|
when :image then LLM::OpenAI::ResponseAdapter::Image
|
|
39
40
|
when :moderations then LLM::OpenAI::ResponseAdapter::Moderations
|
|
41
|
+
when :models then LLM::OpenAI::ResponseAdapter::Models
|
|
40
42
|
when :responds then LLM::OpenAI::ResponseAdapter::Responds
|
|
41
43
|
when :web_search then LLM::OpenAI::ResponseAdapter::WebSearch
|
|
42
44
|
else
|