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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module LLM::OpenAI::
|
|
3
|
+
module LLM::OpenAI::RequestAdapter
|
|
4
4
|
##
|
|
5
5
|
# @private
|
|
6
|
-
class
|
|
6
|
+
class Completion
|
|
7
7
|
##
|
|
8
8
|
# @param [LLM::Message, Hash] message
|
|
9
9
|
# The message to format
|
|
@@ -12,72 +12,72 @@ module LLM::OpenAI::Format
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
##
|
|
15
|
-
#
|
|
15
|
+
# Adapts the message for the OpenAI 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], content:
|
|
20
|
+
{role: message[:role], content: adapt_content(message[:content])}
|
|
21
21
|
elsif message.tool_call?
|
|
22
22
|
{role: message.role, content: nil, tool_calls: message.extra[:original_tool_calls]}
|
|
23
23
|
else
|
|
24
|
-
|
|
24
|
+
adapt_message
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
private
|
|
30
30
|
|
|
31
|
-
def
|
|
31
|
+
def adapt_message
|
|
32
32
|
case content
|
|
33
33
|
when Array
|
|
34
|
-
|
|
34
|
+
adapt_array
|
|
35
35
|
else
|
|
36
|
-
{role: message.role, content:
|
|
36
|
+
{role: message.role, content: adapt_content(content)}
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
def
|
|
40
|
+
def adapt_array
|
|
41
41
|
if content.empty?
|
|
42
42
|
nil
|
|
43
43
|
elsif returns.any?
|
|
44
|
-
returns.map { {role: "tool", tool_call_id: _1.id, content:
|
|
44
|
+
returns.map { {role: "tool", tool_call_id: _1.id, content: LLM.json.dump(_1.value)} }
|
|
45
45
|
else
|
|
46
|
-
{role: message.role, content: content.flat_map {
|
|
46
|
+
{role: message.role, content: content.flat_map { adapt_content(_1) }}
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
def
|
|
50
|
+
def adapt_content(content)
|
|
51
51
|
case content
|
|
52
52
|
when LLM::Object
|
|
53
|
-
|
|
53
|
+
adapt_object(content)
|
|
54
54
|
when String
|
|
55
55
|
[{type: :text, text: content.to_s}]
|
|
56
56
|
when LLM::Response
|
|
57
|
-
|
|
57
|
+
adapt_remote_file(content)
|
|
58
58
|
when LLM::Message
|
|
59
|
-
|
|
59
|
+
adapt_content(content.content)
|
|
60
60
|
when LLM::Function::Return
|
|
61
|
-
throw(:abort, {role: "tool", tool_call_id: content.id, content:
|
|
61
|
+
throw(:abort, {role: "tool", tool_call_id: content.id, content: LLM.json.dump(content.value)})
|
|
62
62
|
else
|
|
63
63
|
prompt_error!(content)
|
|
64
64
|
end
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
def
|
|
67
|
+
def adapt_object(object)
|
|
68
68
|
case object.kind
|
|
69
69
|
when :image_url
|
|
70
70
|
[{type: :image_url, image_url: {url: object.value}}]
|
|
71
71
|
when :local_file
|
|
72
|
-
|
|
72
|
+
adapt_local_file(object.value)
|
|
73
73
|
when :remote_file
|
|
74
|
-
|
|
74
|
+
adapt_remote_file(object.value)
|
|
75
75
|
else
|
|
76
76
|
prompt_error!(object)
|
|
77
77
|
end
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
-
def
|
|
80
|
+
def adapt_local_file(file)
|
|
81
81
|
if file.image?
|
|
82
82
|
[{type: :image_url, image_url: {url: file.to_data_uri}}]
|
|
83
83
|
else
|
|
@@ -85,7 +85,7 @@ module LLM::OpenAI::Format
|
|
|
85
85
|
end
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
-
def
|
|
88
|
+
def adapt_remote_file(file)
|
|
89
89
|
if file.file?
|
|
90
90
|
[{type: :file, file: {file_id: file.id}}]
|
|
91
91
|
else
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module LLM::OpenAI::
|
|
3
|
+
module LLM::OpenAI::RequestAdapter
|
|
4
4
|
##
|
|
5
5
|
# @private
|
|
6
|
-
class
|
|
6
|
+
class Moderation
|
|
7
7
|
##
|
|
8
8
|
# @param [String, URI, Array<String, URI>] inputs
|
|
9
9
|
# The inputs to format
|
|
10
|
-
# @return [LLM::OpenAI::
|
|
10
|
+
# @return [LLM::OpenAI::RequestAdapter::Moderation]
|
|
11
11
|
def initialize(inputs)
|
|
12
12
|
@inputs = inputs
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
##
|
|
16
|
-
#
|
|
16
|
+
# Adapts the inputs for the OpenAI moderations API
|
|
17
17
|
# @return [Array<Hash>]
|
|
18
|
-
def
|
|
18
|
+
def adapt
|
|
19
19
|
[*inputs].flat_map do |input|
|
|
20
20
|
if String === input
|
|
21
21
|
{type: :text, text: input}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module LLM::OpenAI::
|
|
3
|
+
module LLM::OpenAI::RequestAdapter
|
|
4
4
|
##
|
|
5
5
|
# @private
|
|
6
|
-
class
|
|
6
|
+
class Respond
|
|
7
7
|
##
|
|
8
8
|
# @param [LLM::Message] message
|
|
9
9
|
# The message to format
|
|
@@ -11,28 +11,28 @@ module LLM::OpenAI::Format
|
|
|
11
11
|
@message = message
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def
|
|
14
|
+
def adapt
|
|
15
15
|
catch(:abort) do
|
|
16
16
|
if Hash === message
|
|
17
|
-
{role: message[:role], content:
|
|
17
|
+
{role: message[:role], content: adapt_content(message[:content])}
|
|
18
18
|
else
|
|
19
|
-
|
|
19
|
+
adapt_message
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
private
|
|
25
25
|
|
|
26
|
-
def
|
|
26
|
+
def adapt_content(content)
|
|
27
27
|
case content
|
|
28
28
|
when String
|
|
29
29
|
[{type: :input_text, text: content.to_s}]
|
|
30
|
-
when LLM::Response then
|
|
31
|
-
when LLM::Message then
|
|
30
|
+
when LLM::Response then adapt_remote_file(content)
|
|
31
|
+
when LLM::Message then adapt_content(content.content)
|
|
32
32
|
when LLM::Object
|
|
33
33
|
case content.kind
|
|
34
34
|
when :image_url then [{type: :image_url, image_url: {url: content.value.to_s}}]
|
|
35
|
-
when :remote_file then
|
|
35
|
+
when :remote_file then adapt_remote_file(content.value)
|
|
36
36
|
when :local_file then prompt_error!(content)
|
|
37
37
|
else prompt_error!(content)
|
|
38
38
|
end
|
|
@@ -41,26 +41,26 @@ module LLM::OpenAI::Format
|
|
|
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, content:
|
|
49
|
+
{role: message.role, content: 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 { {type: "function_call_output", call_id: _1.id, output:
|
|
57
|
+
returns.map { {type: "function_call_output", call_id: _1.id, output: LLM.json.dump(_1.value)} }
|
|
58
58
|
else
|
|
59
|
-
{role: message.role, content: content.flat_map {
|
|
59
|
+
{role: message.role, content: content.flat_map { adapt_content(_1) }}
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
def
|
|
63
|
+
def adapt_remote_file(content)
|
|
64
64
|
prompt_error!(content) unless content.file?
|
|
65
65
|
file = LLM::File(content.filename)
|
|
66
66
|
if file.image?
|
|
@@ -3,23 +3,23 @@
|
|
|
3
3
|
class LLM::OpenAI
|
|
4
4
|
##
|
|
5
5
|
# @private
|
|
6
|
-
module
|
|
7
|
-
require_relative "
|
|
8
|
-
require_relative "
|
|
9
|
-
require_relative "
|
|
6
|
+
module RequestAdapter
|
|
7
|
+
require_relative "request_adapter/completion"
|
|
8
|
+
require_relative "request_adapter/respond"
|
|
9
|
+
require_relative "request_adapter/moderation"
|
|
10
10
|
|
|
11
11
|
##
|
|
12
12
|
# @param [Array<LLM::Message>] messages
|
|
13
|
-
# The messages to
|
|
13
|
+
# The messages to adapt
|
|
14
14
|
# @param [Symbol] mode
|
|
15
|
-
# The mode to
|
|
15
|
+
# The mode to adapt the messages for
|
|
16
16
|
# @return [Array<Hash>]
|
|
17
|
-
def
|
|
17
|
+
def adapt(messages, mode: :complete)
|
|
18
18
|
messages.filter_map do |message|
|
|
19
19
|
if mode == :complete
|
|
20
|
-
|
|
20
|
+
Completion.new(message).adapt
|
|
21
21
|
else
|
|
22
|
-
|
|
22
|
+
Respond.new(message).adapt
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
end
|
|
@@ -29,7 +29,7 @@ class LLM::OpenAI
|
|
|
29
29
|
##
|
|
30
30
|
# @param [Hash] params
|
|
31
31
|
# @return [Hash]
|
|
32
|
-
def
|
|
32
|
+
def adapt_schema(params)
|
|
33
33
|
return {} unless params and params[:schema]
|
|
34
34
|
schema = params.delete(:schema)
|
|
35
35
|
schema = schema.respond_to?(:object) ? schema.object : schema
|
|
@@ -44,11 +44,11 @@ class LLM::OpenAI
|
|
|
44
44
|
##
|
|
45
45
|
# @param [Hash] params
|
|
46
46
|
# @return [Hash]
|
|
47
|
-
def
|
|
47
|
+
def adapt_tools(tools)
|
|
48
48
|
if tools.nil? || tools.empty?
|
|
49
49
|
{}
|
|
50
50
|
else
|
|
51
|
-
{tools: tools.map { _1.respond_to?(:
|
|
51
|
+
{tools: tools.map { _1.respond_to?(:adapt) ? _1.adapt(self) : _1 }}
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LLM::OpenAI::ResponseAdapter
|
|
4
|
+
module Completion
|
|
5
|
+
##
|
|
6
|
+
# (see LLM::Contract::Completion#messages)
|
|
7
|
+
def messages
|
|
8
|
+
body.choices.map.with_index do |choice, index|
|
|
9
|
+
message = choice.message
|
|
10
|
+
extra = {
|
|
11
|
+
index:, response: self,
|
|
12
|
+
logprobs: choice.logprobs,
|
|
13
|
+
tool_calls: adapt_tool_calls(message.tool_calls),
|
|
14
|
+
original_tool_calls: message.tool_calls
|
|
15
|
+
}
|
|
16
|
+
LLM::Message.new(message.role, message.content, extra)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
alias_method :choices, :messages
|
|
20
|
+
|
|
21
|
+
##
|
|
22
|
+
# (see LLM::Contract::Completion#input_tokens)
|
|
23
|
+
def input_tokens
|
|
24
|
+
body.usage["prompt_tokens"] || 0
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
##
|
|
28
|
+
# (see LLM::Contract::Completion#output_tokens)
|
|
29
|
+
def output_tokens
|
|
30
|
+
body.usage["completion_tokens"] || 0
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# (see LLM::Contract::Completion#total_tokens)
|
|
35
|
+
def total_tokens
|
|
36
|
+
body.usage["total_tokens"] || 0
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
##
|
|
40
|
+
# (see LLM::Contract::Completion#usage)
|
|
41
|
+
def usage
|
|
42
|
+
super
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
##
|
|
46
|
+
# (see LLM::Contract::Completion#model)
|
|
47
|
+
def model
|
|
48
|
+
body.model
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def adapt_tool_calls(tools)
|
|
54
|
+
(tools || []).filter_map do |tool|
|
|
55
|
+
next unless tool.function
|
|
56
|
+
{id: tool.id, name: tool.function.name, arguments: LLM.json.load(tool.function.arguments)}
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
include LLM::Contract::Completion
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module LLM::OpenAI::
|
|
3
|
+
module LLM::OpenAI::ResponseAdapter
|
|
4
4
|
module Responds
|
|
5
5
|
def model = body.model
|
|
6
6
|
def response_id = respond_to?(:response) ? response["id"] : id
|
|
7
|
-
def choices = [
|
|
7
|
+
def choices = [adapt_message]
|
|
8
8
|
def annotations = choices[0].annotations
|
|
9
9
|
|
|
10
10
|
def prompt_tokens = body.usage&.input_tokens
|
|
@@ -20,11 +20,11 @@ module LLM::OpenAI::Response
|
|
|
20
20
|
|
|
21
21
|
private
|
|
22
22
|
|
|
23
|
-
def
|
|
23
|
+
def adapt_message
|
|
24
24
|
message = LLM::Message.new("assistant", +"", {response: self, tool_calls: []})
|
|
25
25
|
output.each.with_index do |choice, index|
|
|
26
26
|
if choice.type == "function_call"
|
|
27
|
-
message.extra[:tool_calls] <<
|
|
27
|
+
message.extra[:tool_calls] << adapt_tool(choice)
|
|
28
28
|
elsif choice.content
|
|
29
29
|
choice.content.each do |c|
|
|
30
30
|
next unless c["type"] == "output_text"
|
|
@@ -37,12 +37,8 @@ module LLM::OpenAI::Response
|
|
|
37
37
|
message
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
def
|
|
41
|
-
LLM
|
|
42
|
-
id: tool.call_id,
|
|
43
|
-
name: tool.name,
|
|
44
|
-
arguments: JSON.parse(tool.arguments)
|
|
45
|
-
)
|
|
40
|
+
def adapt_tool(tool)
|
|
41
|
+
{id: tool.call_id, name: tool.name, arguments: LLM.json.load(tool.arguments)}
|
|
46
42
|
end
|
|
47
43
|
end
|
|
48
44
|
end
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module LLM::OpenAI::
|
|
3
|
+
module LLM::OpenAI::ResponseAdapter
|
|
4
4
|
##
|
|
5
|
-
# The {LLM::OpenAI::
|
|
5
|
+
# The {LLM::OpenAI::ResponseAdapter::WebSearch LLM::OpenAI::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.
|
|
@@ -11,7 +11,7 @@ module LLM::OpenAI::Response
|
|
|
11
11
|
# Returns one or more search results
|
|
12
12
|
# @return [Array<LLM::Object>]
|
|
13
13
|
def search_results
|
|
14
|
-
LLM::Object.
|
|
14
|
+
LLM::Object.from(
|
|
15
15
|
choices[0]
|
|
16
16
|
.annotations
|
|
17
17
|
.map { _1.slice(:title, :url) }
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::OpenAI
|
|
4
|
+
##
|
|
5
|
+
# @private
|
|
6
|
+
module ResponseAdapter
|
|
7
|
+
require_relative "response_adapter/audio"
|
|
8
|
+
require_relative "response_adapter/completion"
|
|
9
|
+
require_relative "response_adapter/embedding"
|
|
10
|
+
require_relative "response_adapter/enumerable"
|
|
11
|
+
require_relative "response_adapter/file"
|
|
12
|
+
require_relative "response_adapter/image"
|
|
13
|
+
require_relative "response_adapter/moderations"
|
|
14
|
+
require_relative "response_adapter/responds"
|
|
15
|
+
require_relative "response_adapter/web_search"
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
##
|
|
20
|
+
# @param [LLM::Response, Net::HTTPResponse] res
|
|
21
|
+
# @param [Symbol] type
|
|
22
|
+
# @return [LLM::Response]
|
|
23
|
+
def adapt(res, type:)
|
|
24
|
+
response = (LLM::Response === res) ? res : LLM::Response.new(res)
|
|
25
|
+
adapter = select(type)
|
|
26
|
+
response.extend(adapter)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
##
|
|
30
|
+
# @api private
|
|
31
|
+
def select(type)
|
|
32
|
+
case type
|
|
33
|
+
when :audio then LLM::OpenAI::ResponseAdapter::Audio
|
|
34
|
+
when :completion then LLM::OpenAI::ResponseAdapter::Completion
|
|
35
|
+
when :embedding then LLM::OpenAI::ResponseAdapter::Embedding
|
|
36
|
+
when :enumerable then LLM::OpenAI::ResponseAdapter::Enumerable
|
|
37
|
+
when :file then LLM::OpenAI::ResponseAdapter::File
|
|
38
|
+
when :image then LLM::OpenAI::ResponseAdapter::Image
|
|
39
|
+
when :moderations then LLM::OpenAI::ResponseAdapter::Moderations
|
|
40
|
+
when :responds then LLM::OpenAI::ResponseAdapter::Responds
|
|
41
|
+
when :web_search then LLM::OpenAI::ResponseAdapter::WebSearch
|
|
42
|
+
else
|
|
43
|
+
raise ArgumentError, "Unknown response adapter type: #{type.inspect}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -6,14 +6,14 @@ class LLM::OpenAI
|
|
|
6
6
|
class Responses::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::OpenAI::Responses::StreamParser]
|
|
15
15
|
def initialize(io)
|
|
16
|
-
@body =
|
|
16
|
+
@body = {"output" => []}
|
|
17
17
|
@io = io
|
|
18
18
|
end
|
|
19
19
|
|
|
@@ -33,43 +33,43 @@ class LLM::OpenAI
|
|
|
33
33
|
next if k == "type"
|
|
34
34
|
@body[k] = v
|
|
35
35
|
end
|
|
36
|
-
@body
|
|
36
|
+
@body["output"] ||= []
|
|
37
37
|
when "response.output_item.added"
|
|
38
38
|
output_index = chunk["output_index"]
|
|
39
|
-
item =
|
|
40
|
-
@body
|
|
41
|
-
@body
|
|
39
|
+
item = chunk["item"]
|
|
40
|
+
@body["output"][output_index] = item
|
|
41
|
+
@body["output"][output_index]["content"] ||= []
|
|
42
42
|
when "response.content_part.added"
|
|
43
43
|
output_index = chunk["output_index"]
|
|
44
44
|
content_index = chunk["content_index"]
|
|
45
|
-
part =
|
|
46
|
-
@body
|
|
47
|
-
@body
|
|
48
|
-
@body
|
|
45
|
+
part = chunk["part"]
|
|
46
|
+
@body["output"][output_index] ||= {"content" => []}
|
|
47
|
+
@body["output"][output_index]["content"] ||= []
|
|
48
|
+
@body["output"][output_index]["content"][content_index] = part
|
|
49
49
|
when "response.output_text.delta"
|
|
50
50
|
output_index = chunk["output_index"]
|
|
51
51
|
content_index = chunk["content_index"]
|
|
52
52
|
delta_text = chunk["delta"]
|
|
53
|
-
output_item = @body
|
|
54
|
-
if output_item
|
|
55
|
-
content_part = output_item
|
|
56
|
-
if content_part && content_part
|
|
57
|
-
content_part
|
|
58
|
-
content_part
|
|
53
|
+
output_item = @body["output"][output_index]
|
|
54
|
+
if output_item && output_item["content"]
|
|
55
|
+
content_part = output_item["content"][content_index]
|
|
56
|
+
if content_part && content_part["type"] == "output_text"
|
|
57
|
+
content_part["text"] ||= ""
|
|
58
|
+
content_part["text"] << delta_text
|
|
59
59
|
@io << delta_text if @io.respond_to?(:<<)
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
62
|
when "response.output_item.done"
|
|
63
63
|
output_index = chunk["output_index"]
|
|
64
|
-
item =
|
|
65
|
-
@body
|
|
64
|
+
item = chunk["item"]
|
|
65
|
+
@body["output"][output_index] = item
|
|
66
66
|
when "response.content_part.done"
|
|
67
67
|
output_index = chunk["output_index"]
|
|
68
68
|
content_index = chunk["content_index"]
|
|
69
|
-
part =
|
|
70
|
-
@body
|
|
71
|
-
@body
|
|
72
|
-
@body
|
|
69
|
+
part = chunk["part"]
|
|
70
|
+
@body["output"][output_index] ||= {"content" => []}
|
|
71
|
+
@body["output"][output_index]["content"] ||= []
|
|
72
|
+
@body["output"][output_index]["content"][content_index] = part
|
|
73
73
|
end
|
|
74
74
|
end
|
|
75
75
|
end
|
|
@@ -14,9 +14,8 @@ class LLM::OpenAI
|
|
|
14
14
|
# res2 = llm.responses.create "5 + 5 = X ?", role: :user, previous_response_id: res1.id
|
|
15
15
|
# [res1, res2].each { llm.responses.delete(_1) }
|
|
16
16
|
class Responses
|
|
17
|
-
require_relative "response/responds"
|
|
18
17
|
require_relative "responses/stream_parser"
|
|
19
|
-
include
|
|
18
|
+
include RequestAdapter
|
|
20
19
|
|
|
21
20
|
##
|
|
22
21
|
# Returns a new Responses object
|
|
@@ -38,16 +37,15 @@ class LLM::OpenAI
|
|
|
38
37
|
def create(prompt, params = {})
|
|
39
38
|
params = {role: :user, model: @provider.default_model}.merge!(params)
|
|
40
39
|
tools = resolve_tools(params.delete(:tools))
|
|
41
|
-
params = [params,
|
|
40
|
+
params = [params, adapt_schema(params), adapt_tools(tools)].inject({}, &:merge!).compact
|
|
42
41
|
role, stream = params.delete(:role), params.delete(:stream)
|
|
43
42
|
params[:stream] = true if stream.respond_to?(:<<) || stream == true
|
|
44
43
|
req = Net::HTTP::Post.new("/v1/responses", headers)
|
|
45
44
|
messages = [*(params.delete(:input) || []), LLM::Message.new(role, prompt)]
|
|
46
|
-
body =
|
|
45
|
+
body = LLM.json.dump({input: [adapt(messages, mode: :response)].flatten}.merge!(params))
|
|
47
46
|
set_body_stream(req, StringIO.new(body))
|
|
48
47
|
res = execute(request: req, stream:, stream_parser:)
|
|
49
|
-
|
|
50
|
-
.extend(LLM::OpenAI::Response::Responds)
|
|
48
|
+
ResponseAdapter.adapt(res, type: :responds)
|
|
51
49
|
.extend(Module.new { define_method(:__tools__) { tools } })
|
|
52
50
|
end
|
|
53
51
|
|
|
@@ -62,7 +60,7 @@ class LLM::OpenAI
|
|
|
62
60
|
query = URI.encode_www_form(params)
|
|
63
61
|
req = Net::HTTP::Get.new("/v1/responses/#{response_id}?#{query}", headers)
|
|
64
62
|
res = execute(request: req)
|
|
65
|
-
|
|
63
|
+
ResponseAdapter.adapt(res, type: :responds)
|
|
66
64
|
end
|
|
67
65
|
|
|
68
66
|
##
|
|
@@ -84,7 +82,7 @@ class LLM::OpenAI
|
|
|
84
82
|
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
|
85
83
|
end
|
|
86
84
|
|
|
87
|
-
def
|
|
85
|
+
def adapt_schema(params)
|
|
88
86
|
return {} unless params && params[:schema]
|
|
89
87
|
schema = params.delete(:schema)
|
|
90
88
|
schema = schema.to_h.merge(additionalProperties: false)
|
|
@@ -6,13 +6,13 @@ class LLM::OpenAI
|
|
|
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
|
|
|
@@ -38,8 +38,9 @@ class LLM::OpenAI
|
|
|
38
38
|
|
|
39
39
|
def merge_choices!(choices)
|
|
40
40
|
choices.each do |choice|
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
index = choice["index"]
|
|
42
|
+
if @body["choices"][index]
|
|
43
|
+
target_message = @body["choices"][index]["message"]
|
|
43
44
|
delta = choice["delta"]
|
|
44
45
|
delta.each do |key, value|
|
|
45
46
|
if key == "content"
|
|
@@ -54,7 +55,7 @@ class LLM::OpenAI
|
|
|
54
55
|
end
|
|
55
56
|
else
|
|
56
57
|
message_hash = {"role" => "assistant"}
|
|
57
|
-
@body["choices"][
|
|
58
|
+
@body["choices"][index] = {"message" => message_hash}
|
|
58
59
|
choice["delta"].each do |key, value|
|
|
59
60
|
if key == "content"
|
|
60
61
|
@io << value if @io.respond_to?(:<<)
|