llm.rb 2.1.0 → 3.0.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 +6 -0
- data/lib/llm/bot.rb +4 -4
- data/lib/llm/buffer.rb +0 -9
- data/lib/llm/contract/completion.rb +57 -0
- data/lib/llm/contract.rb +48 -0
- data/lib/llm/error.rb +22 -14
- data/lib/llm/eventhandler.rb +6 -4
- data/lib/llm/eventstream/parser.rb +18 -13
- data/lib/llm/function.rb +1 -1
- data/lib/llm/json_adapter.rb +109 -0
- data/lib/llm/message.rb +7 -28
- data/lib/llm/multipart/enumerator_io.rb +86 -0
- data/lib/llm/multipart.rb +32 -51
- data/lib/llm/object/builder.rb +6 -6
- data/lib/llm/object/kernel.rb +2 -2
- data/lib/llm/object.rb +23 -8
- data/lib/llm/provider.rb +11 -3
- data/lib/llm/providers/anthropic/error_handler.rb +1 -1
- data/lib/llm/providers/anthropic/files.rb +4 -5
- data/lib/llm/providers/anthropic/models.rb +1 -2
- data/lib/llm/providers/anthropic/{format/completion_format.rb → request_adapter/completion.rb} +19 -19
- data/lib/llm/providers/anthropic/{format.rb → request_adapter.rb} +7 -7
- data/lib/llm/providers/anthropic/response_adapter/completion.rb +66 -0
- data/lib/llm/providers/anthropic/{response → response_adapter}/enumerable.rb +1 -1
- data/lib/llm/providers/anthropic/{response → response_adapter}/file.rb +1 -1
- data/lib/llm/providers/anthropic/{response → response_adapter}/web_search.rb +3 -3
- data/lib/llm/providers/anthropic/response_adapter.rb +36 -0
- data/lib/llm/providers/anthropic/stream_parser.rb +6 -6
- data/lib/llm/providers/anthropic.rb +8 -11
- data/lib/llm/providers/deepseek/{format/completion_format.rb → request_adapter/completion.rb} +15 -15
- data/lib/llm/providers/deepseek/{format.rb → request_adapter.rb} +7 -7
- data/lib/llm/providers/deepseek.rb +2 -2
- data/lib/llm/providers/gemini/audio.rb +2 -2
- data/lib/llm/providers/gemini/error_handler.rb +3 -3
- data/lib/llm/providers/gemini/files.rb +4 -7
- data/lib/llm/providers/gemini/images.rb +9 -14
- data/lib/llm/providers/gemini/models.rb +1 -2
- data/lib/llm/providers/gemini/{format/completion_format.rb → request_adapter/completion.rb} +14 -14
- data/lib/llm/providers/gemini/{format.rb → request_adapter.rb} +8 -8
- data/lib/llm/providers/gemini/response_adapter/completion.rb +67 -0
- data/lib/llm/providers/gemini/{response → response_adapter}/embedding.rb +1 -1
- data/lib/llm/providers/gemini/{response → response_adapter}/file.rb +1 -1
- data/lib/llm/providers/gemini/{response → response_adapter}/files.rb +1 -1
- data/lib/llm/providers/gemini/{response → response_adapter}/image.rb +3 -3
- data/lib/llm/providers/gemini/{response → response_adapter}/models.rb +1 -1
- data/lib/llm/providers/gemini/{response → response_adapter}/web_search.rb +3 -3
- data/lib/llm/providers/gemini/response_adapter.rb +42 -0
- data/lib/llm/providers/gemini/stream_parser.rb +37 -32
- data/lib/llm/providers/gemini.rb +10 -14
- data/lib/llm/providers/ollama/error_handler.rb +1 -1
- data/lib/llm/providers/ollama/{format/completion_format.rb → request_adapter/completion.rb} +19 -19
- data/lib/llm/providers/ollama/{format.rb → request_adapter.rb} +7 -7
- data/lib/llm/providers/ollama/response_adapter/completion.rb +61 -0
- data/lib/llm/providers/ollama/{response → response_adapter}/embedding.rb +1 -1
- data/lib/llm/providers/ollama/response_adapter.rb +32 -0
- data/lib/llm/providers/ollama/stream_parser.rb +2 -2
- data/lib/llm/providers/ollama.rb +8 -10
- data/lib/llm/providers/openai/audio.rb +1 -1
- data/lib/llm/providers/openai/error_handler.rb +12 -2
- data/lib/llm/providers/openai/files.rb +3 -6
- data/lib/llm/providers/openai/images.rb +4 -5
- data/lib/llm/providers/openai/models.rb +1 -3
- data/lib/llm/providers/openai/moderations.rb +3 -5
- data/lib/llm/providers/openai/{format/completion_format.rb → request_adapter/completion.rb} +22 -22
- data/lib/llm/providers/openai/{format/moderation_format.rb → request_adapter/moderation.rb} +5 -5
- data/lib/llm/providers/openai/{format/respond_format.rb → request_adapter/respond.rb} +16 -16
- data/lib/llm/providers/openai/{format.rb → request_adapter.rb} +12 -12
- data/lib/llm/providers/openai/{response → response_adapter}/audio.rb +1 -1
- data/lib/llm/providers/openai/response_adapter/completion.rb +62 -0
- data/lib/llm/providers/openai/{response → response_adapter}/embedding.rb +1 -1
- data/lib/llm/providers/openai/{response → response_adapter}/enumerable.rb +1 -1
- data/lib/llm/providers/openai/{response → response_adapter}/file.rb +1 -1
- data/lib/llm/providers/openai/{response → response_adapter}/image.rb +1 -1
- data/lib/llm/providers/openai/{response → response_adapter}/moderations.rb +1 -1
- data/lib/llm/providers/openai/{response → response_adapter}/responds.rb +6 -10
- data/lib/llm/providers/openai/{response → response_adapter}/web_search.rb +3 -3
- data/lib/llm/providers/openai/response_adapter.rb +47 -0
- data/lib/llm/providers/openai/responses/stream_parser.rb +22 -22
- data/lib/llm/providers/openai/responses.rb +6 -8
- data/lib/llm/providers/openai/stream_parser.rb +6 -5
- data/lib/llm/providers/openai/vector_stores.rb +8 -9
- data/lib/llm/providers/openai.rb +12 -14
- data/lib/llm/response.rb +2 -5
- data/lib/llm/usage.rb +10 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +33 -1
- metadata +44 -35
- data/lib/llm/providers/anthropic/response/completion.rb +0 -39
- data/lib/llm/providers/gemini/response/completion.rb +0 -35
- data/lib/llm/providers/ollama/response/completion.rb +0 -28
- data/lib/llm/providers/openai/response/completion.rb +0 -40
|
@@ -6,14 +6,14 @@ class LLM::Gemini
|
|
|
6
6
|
class StreamParser
|
|
7
7
|
##
|
|
8
8
|
# Returns the fully constructed response body
|
|
9
|
-
# @return [
|
|
9
|
+
# @return [Hash]
|
|
10
10
|
attr_reader :body
|
|
11
11
|
|
|
12
12
|
##
|
|
13
13
|
# @param [#<<] io An IO-like object
|
|
14
14
|
# @return [LLM::Gemini::StreamParser]
|
|
15
15
|
def initialize(io)
|
|
16
|
-
@body =
|
|
16
|
+
@body = {"candidates" => []}
|
|
17
17
|
@io = io
|
|
18
18
|
end
|
|
19
19
|
|
|
@@ -21,35 +21,37 @@ class LLM::Gemini
|
|
|
21
21
|
# @param [Hash] chunk
|
|
22
22
|
# @return [LLM::Gemini::StreamParser]
|
|
23
23
|
def parse!(chunk)
|
|
24
|
-
tap { merge_chunk!(
|
|
24
|
+
tap { merge_chunk!(chunk) }
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
private
|
|
28
28
|
|
|
29
29
|
def merge_chunk!(chunk)
|
|
30
30
|
chunk.each do |key, value|
|
|
31
|
-
|
|
31
|
+
k = key.to_s
|
|
32
|
+
if k == "candidates"
|
|
32
33
|
merge_candidates!(value)
|
|
33
|
-
elsif
|
|
34
|
-
@body
|
|
35
|
-
value.is_a?(
|
|
36
|
-
@body
|
|
34
|
+
elsif k == "usageMetadata" &&
|
|
35
|
+
@body["usageMetadata"].is_a?(Hash) &&
|
|
36
|
+
value.is_a?(Hash)
|
|
37
|
+
@body["usageMetadata"] = @body["usageMetadata"].merge(value)
|
|
37
38
|
else
|
|
38
|
-
@body[
|
|
39
|
+
@body[k] = value
|
|
39
40
|
end
|
|
40
41
|
end
|
|
41
42
|
end
|
|
42
43
|
|
|
43
44
|
def merge_candidates!(deltas)
|
|
44
45
|
deltas.each do |delta|
|
|
45
|
-
index = delta
|
|
46
|
-
@body
|
|
47
|
-
candidate = @body
|
|
46
|
+
index = delta["index"]
|
|
47
|
+
@body["candidates"][index] ||= {"content" => {"parts" => []}}
|
|
48
|
+
candidate = @body["candidates"][index]
|
|
48
49
|
delta.each do |key, value|
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
k = key.to_s
|
|
51
|
+
if k == "content"
|
|
52
|
+
merge_candidate_content!(candidate["content"], value) if value
|
|
51
53
|
else
|
|
52
|
-
candidate[
|
|
54
|
+
candidate[k] = value # Overwrite other fields
|
|
53
55
|
end
|
|
54
56
|
end
|
|
55
57
|
end
|
|
@@ -57,26 +59,27 @@ class LLM::Gemini
|
|
|
57
59
|
|
|
58
60
|
def merge_candidate_content!(content, delta)
|
|
59
61
|
delta.each do |key, value|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
k = key.to_s
|
|
63
|
+
if k == "parts"
|
|
64
|
+
content["parts"] ||= []
|
|
65
|
+
merge_content_parts!(content["parts"], value) if value
|
|
63
66
|
else
|
|
64
|
-
content[
|
|
67
|
+
content[k] = value
|
|
65
68
|
end
|
|
66
69
|
end
|
|
67
70
|
end
|
|
68
71
|
|
|
69
72
|
def merge_content_parts!(parts, deltas)
|
|
70
73
|
deltas.each do |delta|
|
|
71
|
-
if delta
|
|
74
|
+
if delta["text"]
|
|
72
75
|
merge_text!(parts, delta)
|
|
73
|
-
elsif delta
|
|
76
|
+
elsif delta["functionCall"]
|
|
74
77
|
merge_function_call!(parts, delta)
|
|
75
|
-
elsif delta
|
|
78
|
+
elsif delta["inlineData"]
|
|
76
79
|
parts << delta
|
|
77
|
-
elsif delta
|
|
80
|
+
elsif delta["functionResponse"]
|
|
78
81
|
parts << delta
|
|
79
|
-
elsif delta
|
|
82
|
+
elsif delta["fileData"]
|
|
80
83
|
parts << delta
|
|
81
84
|
end
|
|
82
85
|
end
|
|
@@ -84,21 +87,23 @@ class LLM::Gemini
|
|
|
84
87
|
|
|
85
88
|
def merge_text!(parts, delta)
|
|
86
89
|
last_existing_part = parts.last
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
+
text = delta["text"]
|
|
91
|
+
if last_existing_part.is_a?(Hash) && last_existing_part["text"]
|
|
92
|
+
last_existing_part["text"] ||= +""
|
|
93
|
+
last_existing_part["text"] << text
|
|
94
|
+
@io << text if @io.respond_to?(:<<)
|
|
90
95
|
else
|
|
91
96
|
parts << delta
|
|
92
|
-
@io <<
|
|
97
|
+
@io << text if @io.respond_to?(:<<)
|
|
93
98
|
end
|
|
94
99
|
end
|
|
95
100
|
|
|
96
101
|
def merge_function_call!(parts, delta)
|
|
97
102
|
last_existing_part = parts.last
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
)
|
|
103
|
+
last_call = last_existing_part.is_a?(Hash) ? last_existing_part["functionCall"] : nil
|
|
104
|
+
delta_call = delta["functionCall"]
|
|
105
|
+
if last_call.is_a?(Hash) && delta_call.is_a?(Hash)
|
|
106
|
+
last_existing_part["functionCall"] = last_call.merge(delta_call)
|
|
102
107
|
else
|
|
103
108
|
parts << delta
|
|
104
109
|
end
|
data/lib/llm/providers/gemini.rb
CHANGED
|
@@ -18,18 +18,16 @@ module LLM
|
|
|
18
18
|
# bot.chat ["Tell me about this photo", File.open("/images/horse.jpg", "rb")]
|
|
19
19
|
# bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
|
20
20
|
class Gemini < Provider
|
|
21
|
-
require_relative "gemini/response/embedding"
|
|
22
|
-
require_relative "gemini/response/completion"
|
|
23
|
-
require_relative "gemini/response/web_search"
|
|
24
21
|
require_relative "gemini/error_handler"
|
|
25
|
-
require_relative "gemini/
|
|
22
|
+
require_relative "gemini/request_adapter"
|
|
23
|
+
require_relative "gemini/response_adapter"
|
|
26
24
|
require_relative "gemini/stream_parser"
|
|
27
25
|
require_relative "gemini/models"
|
|
28
26
|
require_relative "gemini/images"
|
|
29
|
-
require_relative "gemini/files"
|
|
30
27
|
require_relative "gemini/audio"
|
|
28
|
+
require_relative "gemini/files"
|
|
31
29
|
|
|
32
|
-
include
|
|
30
|
+
include RequestAdapter
|
|
33
31
|
|
|
34
32
|
HOST = "generativelanguage.googleapis.com"
|
|
35
33
|
|
|
@@ -50,9 +48,9 @@ module LLM
|
|
|
50
48
|
model = model.respond_to?(:id) ? model.id : model
|
|
51
49
|
path = ["/v1beta/models/#{model}", "embedContent?key=#{@key}"].join(":")
|
|
52
50
|
req = Net::HTTP::Post.new(path, headers)
|
|
53
|
-
req.body =
|
|
51
|
+
req.body = LLM.json.dump({content: {parts: [{text: input}]}})
|
|
54
52
|
res = execute(request: req)
|
|
55
|
-
|
|
53
|
+
ResponseAdapter.adapt(res, type: :embedding)
|
|
56
54
|
end
|
|
57
55
|
|
|
58
56
|
##
|
|
@@ -68,18 +66,17 @@ module LLM
|
|
|
68
66
|
def complete(prompt, params = {})
|
|
69
67
|
params = {role: :user, model: default_model}.merge!(params)
|
|
70
68
|
tools = resolve_tools(params.delete(:tools))
|
|
71
|
-
params = [params,
|
|
69
|
+
params = [params, adapt_schema(params), adapt_tools(tools)].inject({}, &:merge!).compact
|
|
72
70
|
role, model, stream = [:role, :model, :stream].map { params.delete(_1) }
|
|
73
71
|
action = stream ? "streamGenerateContent?key=#{@key}&alt=sse" : "generateContent?key=#{@key}"
|
|
74
72
|
model.respond_to?(:id) ? model.id : model
|
|
75
73
|
path = ["/v1beta/models/#{model}", action].join(":")
|
|
76
74
|
req = Net::HTTP::Post.new(path, headers)
|
|
77
75
|
messages = [*(params.delete(:messages) || []), LLM::Message.new(role, prompt)]
|
|
78
|
-
body =
|
|
76
|
+
body = LLM.json.dump({contents: adapt(messages)}.merge!(params))
|
|
79
77
|
set_body_stream(req, StringIO.new(body))
|
|
80
78
|
res = execute(request: req, stream:)
|
|
81
|
-
|
|
82
|
-
.extend(LLM::Gemini::Response::Completion)
|
|
79
|
+
ResponseAdapter.adapt(res, type: :completion)
|
|
83
80
|
.extend(Module.new { define_method(:__tools__) { tools } })
|
|
84
81
|
end
|
|
85
82
|
|
|
@@ -150,8 +147,7 @@ module LLM
|
|
|
150
147
|
# @param query [String] The search query.
|
|
151
148
|
# @return [LLM::Response] The response from the LLM provider.
|
|
152
149
|
def web_search(query:)
|
|
153
|
-
complete(query, tools: [server_tools[:google_search]])
|
|
154
|
-
.extend(LLM::Gemini::Response::WebSearch)
|
|
150
|
+
ResponseAdapter.adapt(complete(query, tools: [server_tools[:google_search]]), type: :web_search)
|
|
155
151
|
end
|
|
156
152
|
|
|
157
153
|
private
|
|
@@ -29,7 +29,7 @@ class LLM::Ollama
|
|
|
29
29
|
when Net::HTTPTooManyRequests
|
|
30
30
|
raise LLM::RateLimitError.new { _1.response = res }, "Too many requests"
|
|
31
31
|
else
|
|
32
|
-
raise LLM::
|
|
32
|
+
raise LLM::Error.new { _1.response = res }, "Unexpected response"
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
end
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module LLM::Ollama::
|
|
3
|
+
module LLM::Ollama::RequestAdapter
|
|
4
4
|
##
|
|
5
5
|
# @private
|
|
6
|
-
class
|
|
6
|
+
class Completion
|
|
7
7
|
##
|
|
8
8
|
# @param [LLM::Message] message
|
|
9
9
|
# The message to format
|
|
@@ -12,64 +12,64 @@ module LLM::Ollama::Format
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
##
|
|
15
|
-
#
|
|
15
|
+
# Adapts the message for the Ollama chat completions API
|
|
16
16
|
# @return [Hash]
|
|
17
|
-
def
|
|
17
|
+
def adapt
|
|
18
18
|
catch(:abort) do
|
|
19
19
|
if Hash === message
|
|
20
|
-
{role: message[:role]}.merge(
|
|
20
|
+
{role: message[:role]}.merge(adapt_content(message[:content]))
|
|
21
21
|
else
|
|
22
|
-
|
|
22
|
+
adapt_message
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
private
|
|
28
28
|
|
|
29
|
-
def
|
|
29
|
+
def adapt_content(content)
|
|
30
30
|
case content
|
|
31
31
|
when String
|
|
32
32
|
{content:}
|
|
33
33
|
when LLM::Message
|
|
34
|
-
|
|
34
|
+
adapt_content(content.content)
|
|
35
35
|
when LLM::Function::Return
|
|
36
|
-
throw(:abort, {role: "tool", tool_call_id: content.id, content:
|
|
36
|
+
throw(:abort, {role: "tool", tool_call_id: content.id, content: LLM.json.dump(content.value)})
|
|
37
37
|
when LLM::Object
|
|
38
|
-
|
|
38
|
+
adapt_object(content)
|
|
39
39
|
else
|
|
40
40
|
prompt_error!(content)
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
def
|
|
44
|
+
def adapt_message
|
|
45
45
|
case content
|
|
46
46
|
when Array
|
|
47
|
-
|
|
47
|
+
adapt_array
|
|
48
48
|
else
|
|
49
|
-
{role: message.role}.merge(
|
|
49
|
+
{role: message.role}.merge(adapt_content(content))
|
|
50
50
|
end
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
-
def
|
|
53
|
+
def adapt_array
|
|
54
54
|
if content.empty?
|
|
55
55
|
nil
|
|
56
56
|
elsif returns.any?
|
|
57
|
-
returns.map { {role: "tool", tool_call_id: _1.id, content:
|
|
57
|
+
returns.map { {role: "tool", tool_call_id: _1.id, content: LLM.json.dump(_1.value)} }
|
|
58
58
|
else
|
|
59
|
-
content.flat_map { {role: message.role}.merge(
|
|
59
|
+
content.flat_map { {role: message.role}.merge(adapt_content(_1)) }
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
def
|
|
63
|
+
def adapt_object(object)
|
|
64
64
|
case object.kind
|
|
65
|
-
when :local_file then
|
|
65
|
+
when :local_file then adapt_local_file(object.value)
|
|
66
66
|
when :remote_file then prompt_error!(object)
|
|
67
67
|
when :image_url then prompt_error!(object)
|
|
68
68
|
else prompt_error!(object)
|
|
69
69
|
end
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
-
def
|
|
72
|
+
def adapt_local_file(file)
|
|
73
73
|
if file.image?
|
|
74
74
|
{content: "This message has an image associated with it", images: [file.to_b64]}
|
|
75
75
|
else
|
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
class LLM::Ollama
|
|
4
4
|
##
|
|
5
5
|
# @private
|
|
6
|
-
module
|
|
7
|
-
require_relative "
|
|
6
|
+
module RequestAdapter
|
|
7
|
+
require_relative "request_adapter/completion"
|
|
8
8
|
|
|
9
9
|
##
|
|
10
10
|
# @param [Array<LLM::Message>] messages
|
|
11
|
-
# The messages to
|
|
11
|
+
# The messages to adapt
|
|
12
12
|
# @return [Array<Hash>]
|
|
13
|
-
def
|
|
13
|
+
def adapt(messages, mode: nil)
|
|
14
14
|
messages.filter_map do |message|
|
|
15
|
-
|
|
15
|
+
Completion.new(message).adapt
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
|
|
@@ -21,9 +21,9 @@ class LLM::Ollama
|
|
|
21
21
|
##
|
|
22
22
|
# @param [Hash] params
|
|
23
23
|
# @return [Hash]
|
|
24
|
-
def
|
|
24
|
+
def adapt_tools(tools)
|
|
25
25
|
return {} unless tools&.any?
|
|
26
|
-
{tools: tools.map { _1.
|
|
26
|
+
{tools: tools.map { _1.adapt(self) }}
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LLM::Ollama::ResponseAdapter
|
|
4
|
+
module Completion
|
|
5
|
+
##
|
|
6
|
+
# (see LLM::Contract::Completion#messages)
|
|
7
|
+
def messages
|
|
8
|
+
adapt_choices
|
|
9
|
+
end
|
|
10
|
+
alias_method :choices, :messages
|
|
11
|
+
|
|
12
|
+
##
|
|
13
|
+
# (see LLM::Contract::Completion#input_tokens)
|
|
14
|
+
def input_tokens
|
|
15
|
+
body.prompt_eval_count || 0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
##
|
|
19
|
+
# (see LLM::Contract::Completion#output_tokens)
|
|
20
|
+
def output_tokens
|
|
21
|
+
body.eval_count || 0
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
##
|
|
25
|
+
# (see LLM::Contract::Completion#total_tokens)
|
|
26
|
+
def total_tokens
|
|
27
|
+
input_tokens + output_tokens
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
##
|
|
31
|
+
# (see LLM::Contract::Completion#usage)
|
|
32
|
+
def usage
|
|
33
|
+
super
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
##
|
|
37
|
+
# (see LLM::Contract::Completion#model)
|
|
38
|
+
def model
|
|
39
|
+
body.model
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def adapt_choices
|
|
45
|
+
message = body.message
|
|
46
|
+
role, content, calls = message.role, message.content, message.tool_calls
|
|
47
|
+
extra = {response: self, tool_calls: adapt_tool_calls(calls)}
|
|
48
|
+
[LLM::Message.new(role, content, extra)]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def adapt_tool_calls(tools)
|
|
52
|
+
return [] unless tools
|
|
53
|
+
tools.filter_map do |tool|
|
|
54
|
+
next unless tool["function"]
|
|
55
|
+
tool["function"]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
include LLM::Contract::Completion
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Ollama
|
|
4
|
+
##
|
|
5
|
+
# @private
|
|
6
|
+
module ResponseAdapter
|
|
7
|
+
require_relative "response_adapter/completion"
|
|
8
|
+
require_relative "response_adapter/embedding"
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
##
|
|
13
|
+
# @param [LLM::Response, Net::HTTPResponse] res
|
|
14
|
+
# @param [Symbol] type
|
|
15
|
+
# @return [LLM::Response]
|
|
16
|
+
def adapt(res, type:)
|
|
17
|
+
response = (LLM::Response === res) ? res : LLM::Response.new(res)
|
|
18
|
+
response.extend(select(type))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
# @api private
|
|
23
|
+
def select(type)
|
|
24
|
+
case type
|
|
25
|
+
when :completion then LLM::Ollama::ResponseAdapter::Completion
|
|
26
|
+
when :embedding then LLM::Ollama::ResponseAdapter::Embedding
|
|
27
|
+
else
|
|
28
|
+
raise ArgumentError, "Unknown response adapter type: #{type.inspect}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -6,13 +6,13 @@ class LLM::Ollama
|
|
|
6
6
|
class StreamParser
|
|
7
7
|
##
|
|
8
8
|
# Returns the fully constructed response body
|
|
9
|
-
# @return [
|
|
9
|
+
# @return [Hash]
|
|
10
10
|
attr_reader :body
|
|
11
11
|
|
|
12
12
|
##
|
|
13
13
|
# @return [LLM::OpenAI::Chunk]
|
|
14
14
|
def initialize(io)
|
|
15
|
-
@body =
|
|
15
|
+
@body = {}
|
|
16
16
|
@io = io
|
|
17
17
|
end
|
|
18
18
|
|
data/lib/llm/providers/ollama.rb
CHANGED
|
@@ -16,14 +16,13 @@ module LLM
|
|
|
16
16
|
# bot.chat ["Tell me about this image", File.open("/images/parrot.png", "rb")]
|
|
17
17
|
# bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
|
18
18
|
class Ollama < Provider
|
|
19
|
-
require_relative "ollama/response/embedding"
|
|
20
|
-
require_relative "ollama/response/completion"
|
|
21
19
|
require_relative "ollama/error_handler"
|
|
22
|
-
require_relative "ollama/
|
|
20
|
+
require_relative "ollama/request_adapter"
|
|
21
|
+
require_relative "ollama/response_adapter"
|
|
23
22
|
require_relative "ollama/stream_parser"
|
|
24
23
|
require_relative "ollama/models"
|
|
25
24
|
|
|
26
|
-
include
|
|
25
|
+
include RequestAdapter
|
|
27
26
|
|
|
28
27
|
HOST = "localhost"
|
|
29
28
|
|
|
@@ -43,9 +42,9 @@ module LLM
|
|
|
43
42
|
def embed(input, model: default_model, **params)
|
|
44
43
|
params = {model:}.merge!(params)
|
|
45
44
|
req = Net::HTTP::Post.new("/v1/embeddings", headers)
|
|
46
|
-
req.body =
|
|
45
|
+
req.body = LLM.json.dump({input:}.merge!(params))
|
|
47
46
|
res = execute(request: req)
|
|
48
|
-
|
|
47
|
+
ResponseAdapter.adapt(res, type: :embedding)
|
|
49
48
|
end
|
|
50
49
|
|
|
51
50
|
##
|
|
@@ -61,16 +60,15 @@ module LLM
|
|
|
61
60
|
def complete(prompt, params = {})
|
|
62
61
|
params = {role: :user, model: default_model, stream: true}.merge!(params)
|
|
63
62
|
tools = resolve_tools(params.delete(:tools))
|
|
64
|
-
params = [params, {format: params[:schema]},
|
|
63
|
+
params = [params, {format: params[:schema]}, adapt_tools(tools)].inject({}, &:merge!).compact
|
|
65
64
|
role, stream = params.delete(:role), params.delete(:stream)
|
|
66
65
|
params[:stream] = true if stream.respond_to?(:<<) || stream == true
|
|
67
66
|
req = Net::HTTP::Post.new("/api/chat", headers)
|
|
68
67
|
messages = [*(params.delete(:messages) || []), LLM::Message.new(role, prompt)]
|
|
69
|
-
body =
|
|
68
|
+
body = LLM.json.dump({messages: [adapt(messages)].flatten}.merge!(params))
|
|
70
69
|
set_body_stream(req, StringIO.new(body))
|
|
71
70
|
res = execute(request: req, stream:)
|
|
72
|
-
|
|
73
|
-
.extend(LLM::Ollama::Response::Completion)
|
|
71
|
+
ResponseAdapter.adapt(res, type: :completion)
|
|
74
72
|
.extend(Module.new { define_method(:__tools__) { tools } })
|
|
75
73
|
end
|
|
76
74
|
|
|
@@ -33,7 +33,7 @@ class LLM::OpenAI
|
|
|
33
33
|
# @return [LLM::Response]
|
|
34
34
|
def create_speech(input:, voice: "alloy", model: "gpt-4o-mini-tts", response_format: "mp3", **params)
|
|
35
35
|
req = Net::HTTP::Post.new("/v1/audio/speech", headers)
|
|
36
|
-
req.body =
|
|
36
|
+
req.body = LLM.json.dump({input:, voice:, model:, response_format:}.merge!(params))
|
|
37
37
|
io = StringIO.new("".b)
|
|
38
38
|
res = execute(request: req) { _1.read_body { |chunk| io << chunk } }
|
|
39
39
|
LLM::Response.new(res).tap { _1.define_singleton_method(:audio) { io } }
|
|
@@ -31,16 +31,26 @@ class LLM::OpenAI
|
|
|
31
31
|
else
|
|
32
32
|
error = body["error"] || {}
|
|
33
33
|
case error["type"]
|
|
34
|
+
when "invalid_request_error" then handle_invalid_request(error)
|
|
34
35
|
when "server_error" then raise LLM::ServerError.new { _1.response = res }, error["message"]
|
|
35
|
-
else raise LLM::
|
|
36
|
+
else raise LLM::Error.new { _1.response = res }, error["message"] || "Unexpected response"
|
|
36
37
|
end
|
|
37
38
|
end
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
private
|
|
41
42
|
|
|
43
|
+
def handle_invalid_request(error)
|
|
44
|
+
case error["code"]
|
|
45
|
+
when "context_length_exceeded"
|
|
46
|
+
raise LLM::ContextWindowError.new { _1.response = res }, error["message"]
|
|
47
|
+
else
|
|
48
|
+
raise LLM::InvalidRequestError.new { _1.response = res }, error["message"]
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
42
52
|
def body
|
|
43
|
-
@body ||=
|
|
53
|
+
@body ||= LLM.json.load(res.body)
|
|
44
54
|
end
|
|
45
55
|
end
|
|
46
56
|
end
|
|
@@ -18,9 +18,6 @@ class LLM::OpenAI
|
|
|
18
18
|
# bot.chat ["Tell me about this PDF", file]
|
|
19
19
|
# bot.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
|
20
20
|
class Files
|
|
21
|
-
require_relative "response/enumerable"
|
|
22
|
-
require_relative "response/file"
|
|
23
|
-
|
|
24
21
|
##
|
|
25
22
|
# Returns a new Files object
|
|
26
23
|
# @param provider [LLM::Provider]
|
|
@@ -45,7 +42,7 @@ class LLM::OpenAI
|
|
|
45
42
|
query = URI.encode_www_form(params)
|
|
46
43
|
req = Net::HTTP::Get.new("/v1/files?#{query}", headers)
|
|
47
44
|
res = execute(request: req)
|
|
48
|
-
|
|
45
|
+
ResponseAdapter.adapt(res, type: :enumerable)
|
|
49
46
|
end
|
|
50
47
|
|
|
51
48
|
##
|
|
@@ -65,7 +62,7 @@ class LLM::OpenAI
|
|
|
65
62
|
req["content-type"] = multi.content_type
|
|
66
63
|
set_body_stream(req, multi.body)
|
|
67
64
|
res = execute(request: req)
|
|
68
|
-
|
|
65
|
+
ResponseAdapter.adapt(res, type: :file)
|
|
69
66
|
end
|
|
70
67
|
|
|
71
68
|
##
|
|
@@ -84,7 +81,7 @@ class LLM::OpenAI
|
|
|
84
81
|
query = URI.encode_www_form(params)
|
|
85
82
|
req = Net::HTTP::Get.new("/v1/files/#{file_id}?#{query}", headers)
|
|
86
83
|
res = execute(request: req)
|
|
87
|
-
|
|
84
|
+
ResponseAdapter.adapt(res, type: :file)
|
|
88
85
|
end
|
|
89
86
|
|
|
90
87
|
##
|
|
@@ -27,7 +27,6 @@ class LLM::OpenAI
|
|
|
27
27
|
# response_format: "b64_json"
|
|
28
28
|
# IO.copy_stream res.images[0], "rocket.png"
|
|
29
29
|
class Images
|
|
30
|
-
require_relative "response/image"
|
|
31
30
|
##
|
|
32
31
|
# Returns a new Images object
|
|
33
32
|
# @param provider [LLM::Provider]
|
|
@@ -50,9 +49,9 @@ class LLM::OpenAI
|
|
|
50
49
|
# @return [LLM::Response]
|
|
51
50
|
def create(prompt:, model: "dall-e-3", **params)
|
|
52
51
|
req = Net::HTTP::Post.new("/v1/images/generations", headers)
|
|
53
|
-
req.body =
|
|
52
|
+
req.body = LLM.json.dump({prompt:, n: 1, model:}.merge!(params))
|
|
54
53
|
res = execute(request: req)
|
|
55
|
-
|
|
54
|
+
ResponseAdapter.adapt(res, type: :image)
|
|
56
55
|
end
|
|
57
56
|
|
|
58
57
|
##
|
|
@@ -74,7 +73,7 @@ class LLM::OpenAI
|
|
|
74
73
|
req["content-type"] = multi.content_type
|
|
75
74
|
set_body_stream(req, multi.body)
|
|
76
75
|
res = execute(request: req)
|
|
77
|
-
|
|
76
|
+
ResponseAdapter.adapt(res, type: :image)
|
|
78
77
|
end
|
|
79
78
|
|
|
80
79
|
##
|
|
@@ -97,7 +96,7 @@ class LLM::OpenAI
|
|
|
97
96
|
req["content-type"] = multi.content_type
|
|
98
97
|
set_body_stream(req, multi.body)
|
|
99
98
|
res = execute(request: req)
|
|
100
|
-
|
|
99
|
+
ResponseAdapter.adapt(res, type: :image)
|
|
101
100
|
end
|
|
102
101
|
|
|
103
102
|
private
|
|
@@ -17,8 +17,6 @@ class LLM::OpenAI
|
|
|
17
17
|
# print "id: ", model.id, "\n"
|
|
18
18
|
# end
|
|
19
19
|
class Models
|
|
20
|
-
require_relative "response/enumerable"
|
|
21
|
-
|
|
22
20
|
##
|
|
23
21
|
# Returns a new Models object
|
|
24
22
|
# @param provider [LLM::Provider]
|
|
@@ -43,7 +41,7 @@ class LLM::OpenAI
|
|
|
43
41
|
query = URI.encode_www_form(params)
|
|
44
42
|
req = Net::HTTP::Get.new("/v1/models?#{query}", headers)
|
|
45
43
|
res = execute(request: req)
|
|
46
|
-
|
|
44
|
+
ResponseAdapter.adapt(res, type: :enumerable)
|
|
47
45
|
end
|
|
48
46
|
|
|
49
47
|
private
|
|
@@ -31,8 +31,6 @@ class LLM::OpenAI
|
|
|
31
31
|
# @see https://platform.openai.com/docs/api-reference/moderations/create OpenAI docs
|
|
32
32
|
# @see https://platform.openai.com/docs/models#moderation OpenAI moderation models
|
|
33
33
|
class Moderations
|
|
34
|
-
require_relative "response/moderations"
|
|
35
|
-
|
|
36
34
|
##
|
|
37
35
|
# Returns a new Moderations object
|
|
38
36
|
# @param [LLM::Provider] provider
|
|
@@ -50,10 +48,10 @@ class LLM::OpenAI
|
|
|
50
48
|
# @return [LLM::Response]
|
|
51
49
|
def create(input:, model: "omni-moderation-latest", **params)
|
|
52
50
|
req = Net::HTTP::Post.new("/v1/moderations", headers)
|
|
53
|
-
input =
|
|
54
|
-
req.body =
|
|
51
|
+
input = RequestAdapter::Moderation.new(input).adapt
|
|
52
|
+
req.body = LLM.json.dump({input:, model:}.merge!(params))
|
|
55
53
|
res = execute(request: req)
|
|
56
|
-
|
|
54
|
+
ResponseAdapter.adapt(res, type: :moderations)
|
|
57
55
|
end
|
|
58
56
|
|
|
59
57
|
private
|