llm.rb 4.14.0 → 4.16.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/CHANGELOG.md +83 -0
- data/README.md +93 -28
- data/data/anthropic.json +218 -198
- data/data/deepseek.json +1 -1
- data/data/google.json +481 -429
- data/data/openai.json +742 -704
- data/data/xai.json +277 -277
- data/data/zai.json +160 -126
- data/lib/llm/active_record/acts_as_llm.rb +238 -0
- data/lib/llm/active_record.rb +3 -0
- data/lib/llm/context.rb +15 -10
- data/lib/llm/eventstream/parser.rb +40 -8
- data/lib/llm/provider.rb +16 -1
- data/lib/llm/providers/anthropic/stream_parser.rb +6 -3
- data/lib/llm/providers/google/stream_parser.rb +6 -3
- data/lib/llm/providers/ollama/stream_parser.rb +3 -2
- data/lib/llm/providers/openai/audio.rb +4 -4
- data/lib/llm/providers/openai/files.rb +6 -6
- data/lib/llm/providers/openai/images.rb +4 -4
- data/lib/llm/providers/openai/models.rb +2 -2
- data/lib/llm/providers/openai/moderations.rb +2 -2
- data/lib/llm/providers/openai/responses/stream_parser.rb +216 -91
- data/lib/llm/providers/openai/responses.rb +4 -4
- data/lib/llm/providers/openai/stream_parser.rb +111 -57
- data/lib/llm/providers/openai/vector_stores.rb +12 -12
- data/lib/llm/providers/openai.rb +4 -4
- data/lib/llm/response.rb +12 -4
- data/lib/llm/sequel/plugin.rb +252 -0
- data/lib/llm/stream/queue.rb +2 -2
- data/lib/llm/stream.rb +2 -2
- data/lib/llm/version.rb +1 -1
- data/lib/sequel/plugins/llm.rb +8 -0
- metadata +5 -1
|
@@ -4,6 +4,8 @@ class LLM::OpenAI
|
|
|
4
4
|
##
|
|
5
5
|
# @private
|
|
6
6
|
class Responses::StreamParser
|
|
7
|
+
EMPTY_HASH = {}.freeze
|
|
8
|
+
|
|
7
9
|
##
|
|
8
10
|
# Returns the fully constructed response body
|
|
9
11
|
# @return [Hash]
|
|
@@ -16,7 +18,15 @@ class LLM::OpenAI
|
|
|
16
18
|
def initialize(stream)
|
|
17
19
|
@body = {"output" => []}
|
|
18
20
|
@stream = stream
|
|
19
|
-
@emits = {tools:
|
|
21
|
+
@emits = {tools: {}}
|
|
22
|
+
@can_emit_content = stream.respond_to?(:on_content)
|
|
23
|
+
@can_emit_reasoning_content = stream.respond_to?(:on_reasoning_content)
|
|
24
|
+
@can_emit_tool_call = stream.respond_to?(:on_tool_call)
|
|
25
|
+
@can_push_content = stream.respond_to?(:<<)
|
|
26
|
+
@cached_output_index = nil
|
|
27
|
+
@cached_output_item = nil
|
|
28
|
+
@cached_content_index = nil
|
|
29
|
+
@cached_content_part = nil
|
|
20
30
|
end
|
|
21
31
|
|
|
22
32
|
##
|
|
@@ -31,126 +41,238 @@ class LLM::OpenAI
|
|
|
31
41
|
# @return [void]
|
|
32
42
|
def free
|
|
33
43
|
@emits.clear
|
|
44
|
+
clear_cache!
|
|
34
45
|
end
|
|
35
46
|
|
|
36
47
|
private
|
|
37
48
|
|
|
49
|
+
##
|
|
50
|
+
# @group Dispatchers
|
|
51
|
+
|
|
38
52
|
def handle_event(chunk)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
output = @body["output"]
|
|
54
|
+
type = chunk["type"]
|
|
55
|
+
if type == "response.output_text.delta"
|
|
56
|
+
merge_output_text_delta!(output, chunk)
|
|
57
|
+
elsif type == "response.content_part.added"
|
|
58
|
+
merge_content_part!(output, chunk)
|
|
59
|
+
elsif type == "response.output_item.added"
|
|
60
|
+
merge_output_item!(output, chunk)
|
|
61
|
+
elsif type == "response.function_call_arguments.delta"
|
|
62
|
+
merge_function_call_arguments_delta!(output, chunk)
|
|
63
|
+
elsif type == "response.function_call_arguments.done"
|
|
64
|
+
merge_function_call_arguments_done!(output, chunk)
|
|
65
|
+
elsif type == "response.output_item.done"
|
|
66
|
+
merge_output_item!(output, chunk)
|
|
67
|
+
elsif type == "response.content_part.done"
|
|
68
|
+
merge_content_part!(output, chunk, part_key: "part")
|
|
69
|
+
else
|
|
70
|
+
case type
|
|
71
|
+
when "response.created"
|
|
72
|
+
merge_response_created!(chunk)
|
|
73
|
+
when "response.in_progress", "response.completed"
|
|
74
|
+
merge_response_state!(output, chunk)
|
|
75
|
+
when "response.reasoning_summary_text.delta"
|
|
76
|
+
merge_reasoning_summary_text_delta!(output, chunk)
|
|
77
|
+
when "response.reasoning_summary_text.done"
|
|
78
|
+
merge_reasoning_summary_text_done!(output, chunk)
|
|
51
79
|
end
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
@body[
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
##
|
|
84
|
+
# @endgroup
|
|
85
|
+
|
|
86
|
+
##
|
|
87
|
+
# @group Mergers
|
|
88
|
+
|
|
89
|
+
def merge_response_created!(chunk)
|
|
90
|
+
clear_cache!
|
|
91
|
+
chunk.each do |k, v|
|
|
92
|
+
next if k == "type"
|
|
93
|
+
@body[k] = v
|
|
94
|
+
end
|
|
95
|
+
@body["output"] ||= []
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def merge_response_state!(output, chunk)
|
|
99
|
+
clear_cache!
|
|
100
|
+
response = chunk["response"] || EMPTY_HASH
|
|
101
|
+
response.each do |k, v|
|
|
102
|
+
next if k == "output" && Array === output && output.any?
|
|
103
|
+
@body[k] = v
|
|
104
|
+
end
|
|
105
|
+
@body["output"] ||= response["output"] || []
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def merge_output_item!(output, chunk)
|
|
109
|
+
output_index = chunk["output_index"]
|
|
110
|
+
item = chunk["item"]
|
|
111
|
+
output[output_index] = item
|
|
112
|
+
item["content"] ||= [] if item["type"] == "message" || item.key?("content")
|
|
113
|
+
item["summary"] ||= [] if item["type"] == "reasoning"
|
|
114
|
+
cache_output_item!(output_index, item)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def merge_content_part!(output, chunk, part_key: "part")
|
|
118
|
+
output_index = chunk["output_index"]
|
|
119
|
+
content_index = chunk["content_index"]
|
|
120
|
+
part = chunk[part_key]
|
|
121
|
+
output_item = output_item_at(output, output_index)
|
|
122
|
+
unless output_item
|
|
123
|
+
output_item = {"content" => []}
|
|
124
|
+
output[output_index] = output_item
|
|
125
|
+
cache_output_item!(output_index, output_item)
|
|
126
|
+
end
|
|
127
|
+
content = output_item["content"] ||= []
|
|
128
|
+
content[content_index] = part
|
|
129
|
+
cache_content_part!(content_index, part)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def merge_output_text_delta!(output, chunk)
|
|
133
|
+
content_part = content_part_at(output, chunk["output_index"], chunk["content_index"])
|
|
134
|
+
if content_part && content_part["type"] == "output_text"
|
|
88
135
|
delta_text = chunk["delta"]
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
content_part["text"] ||= ""
|
|
94
|
-
content_part["text"] << delta_text
|
|
95
|
-
emit_content(delta_text)
|
|
96
|
-
end
|
|
136
|
+
if text = content_part["text"]
|
|
137
|
+
text << delta_text
|
|
138
|
+
else
|
|
139
|
+
content_part["text"] = delta_text
|
|
97
140
|
end
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
141
|
+
emit_content(delta_text)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def merge_reasoning_summary_text_delta!(output, chunk)
|
|
146
|
+
output_item = output_item_at(output, chunk["output_index"])
|
|
147
|
+
if output_item && output_item["type"] == "reasoning"
|
|
148
|
+
summary_index = chunk["summary_index"] || 0
|
|
149
|
+
delta = chunk["delta"]
|
|
150
|
+
summary = output_item["summary"] ||= []
|
|
151
|
+
if summary_item = summary[summary_index]
|
|
152
|
+
summary_item["text"] << delta
|
|
153
|
+
else
|
|
154
|
+
summary[summary_index] = {"type" => "summary_text", "text" => delta}
|
|
103
155
|
end
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
156
|
+
emit_reasoning_content(delta)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def merge_reasoning_summary_text_done!(output, chunk)
|
|
161
|
+
output_item = output_item_at(output, chunk["output_index"])
|
|
162
|
+
if output_item && output_item["type"] == "reasoning"
|
|
163
|
+
summary_index = chunk["summary_index"] || 0
|
|
164
|
+
output_item["summary"] ||= []
|
|
165
|
+
output_item["summary"][summary_index] = {
|
|
166
|
+
"type" => "summary_text",
|
|
167
|
+
"text" => chunk["text"]
|
|
168
|
+
}
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def merge_function_call_arguments_delta!(output, chunk)
|
|
173
|
+
output_item = output_item_at(output, chunk["output_index"])
|
|
174
|
+
if output_item && output_item["type"] == "function_call"
|
|
175
|
+
if arguments = output_item["arguments"]
|
|
176
|
+
arguments << chunk["delta"]
|
|
177
|
+
else
|
|
178
|
+
output_item["arguments"] = chunk["delta"]
|
|
109
179
|
end
|
|
110
|
-
when "response.output_item.done"
|
|
111
|
-
output_index = chunk["output_index"]
|
|
112
|
-
item = chunk["item"]
|
|
113
|
-
@body["output"][output_index] = item
|
|
114
|
-
when "response.content_part.done"
|
|
115
|
-
output_index = chunk["output_index"]
|
|
116
|
-
content_index = chunk["content_index"]
|
|
117
|
-
part = chunk["part"]
|
|
118
|
-
@body["output"][output_index] ||= {"content" => []}
|
|
119
|
-
@body["output"][output_index]["content"] ||= []
|
|
120
|
-
@body["output"][output_index]["content"][content_index] = part
|
|
121
180
|
end
|
|
122
181
|
end
|
|
123
182
|
|
|
183
|
+
def merge_function_call_arguments_done!(output, chunk)
|
|
184
|
+
output_item = output_item_at(output, chunk["output_index"])
|
|
185
|
+
if output_item && output_item["type"] == "function_call"
|
|
186
|
+
output_item["arguments"] = chunk["arguments"]
|
|
187
|
+
emit_tool(chunk["output_index"], output_item)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
##
|
|
192
|
+
# @endgroup
|
|
193
|
+
|
|
194
|
+
##
|
|
195
|
+
# @group Cache
|
|
196
|
+
|
|
197
|
+
def output_item_at(output, output_index)
|
|
198
|
+
if @cached_output_index == output_index
|
|
199
|
+
@cached_output_item
|
|
200
|
+
else
|
|
201
|
+
cache_output_item!(output_index, output[output_index])
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def content_part_at(output, output_index, content_index)
|
|
206
|
+
if @cached_output_index == output_index && @cached_content_index == content_index
|
|
207
|
+
@cached_content_part
|
|
208
|
+
else
|
|
209
|
+
output_item = output_item_at(output, output_index)
|
|
210
|
+
content = output_item && output_item["content"]
|
|
211
|
+
cache_content_part!(content_index, content && content[content_index])
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def cache_output_item!(output_index, output_item)
|
|
216
|
+
@cached_output_index = output_index
|
|
217
|
+
@cached_output_item = output_item
|
|
218
|
+
@cached_content_index = nil
|
|
219
|
+
@cached_content_part = nil
|
|
220
|
+
output_item
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def cache_content_part!(content_index, content_part)
|
|
224
|
+
@cached_content_index = content_index
|
|
225
|
+
@cached_content_part = content_part
|
|
226
|
+
content_part
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def clear_cache!
|
|
230
|
+
@cached_output_index = nil
|
|
231
|
+
@cached_output_item = nil
|
|
232
|
+
@cached_content_index = nil
|
|
233
|
+
@cached_content_part = nil
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
##
|
|
237
|
+
# @endgroup
|
|
238
|
+
|
|
239
|
+
##
|
|
240
|
+
# @group Emitters
|
|
241
|
+
|
|
124
242
|
def emit_content(value)
|
|
125
|
-
if @
|
|
243
|
+
if @can_emit_content
|
|
126
244
|
@stream.on_content(value)
|
|
127
|
-
elsif @
|
|
245
|
+
elsif @can_push_content
|
|
128
246
|
@stream << value
|
|
129
247
|
end
|
|
130
248
|
end
|
|
131
249
|
|
|
132
250
|
def emit_reasoning_content(value)
|
|
133
|
-
@stream.on_reasoning_content(value) if @
|
|
251
|
+
@stream.on_reasoning_content(value) if @can_emit_reasoning_content
|
|
134
252
|
end
|
|
135
253
|
|
|
136
254
|
def emit_tool(index, tool)
|
|
137
|
-
return unless @
|
|
138
|
-
return
|
|
139
|
-
return
|
|
140
|
-
|
|
141
|
-
|
|
255
|
+
return unless @can_emit_tool_call
|
|
256
|
+
return if @emits[:tools][index]
|
|
257
|
+
return unless tool["call_id"] && tool["name"]
|
|
258
|
+
arguments = parse_arguments(tool["arguments"])
|
|
259
|
+
return unless arguments
|
|
260
|
+
function, error = resolve_tool(tool, arguments)
|
|
261
|
+
@emits[:tools][index] = true
|
|
142
262
|
@stream.on_tool_call(function, error)
|
|
143
263
|
end
|
|
144
264
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
end
|
|
265
|
+
##
|
|
266
|
+
# @endgroup
|
|
148
267
|
|
|
149
|
-
|
|
268
|
+
##
|
|
269
|
+
# @group Resolvers
|
|
270
|
+
|
|
271
|
+
def resolve_tool(tool, arguments)
|
|
150
272
|
registered = LLM::Function.find_by_name(tool["name"])
|
|
151
273
|
fn = (registered || LLM::Function.new(tool["name"])).dup.tap do |fn|
|
|
152
274
|
fn.id = tool["call_id"]
|
|
153
|
-
fn.arguments =
|
|
275
|
+
fn.arguments = arguments
|
|
154
276
|
end
|
|
155
277
|
[fn, (registered ? nil : @stream.tool_not_found(fn))]
|
|
156
278
|
end
|
|
@@ -162,5 +284,8 @@ class LLM::OpenAI
|
|
|
162
284
|
rescue *LLM.json.parser_error
|
|
163
285
|
nil
|
|
164
286
|
end
|
|
287
|
+
|
|
288
|
+
##
|
|
289
|
+
# @endgroup
|
|
165
290
|
end
|
|
166
291
|
end
|
|
@@ -40,7 +40,7 @@ class LLM::OpenAI
|
|
|
40
40
|
params = [params, adapt_schema(params), adapt_tools(tools)].inject({}, &:merge!).compact
|
|
41
41
|
role, stream = params.delete(:role), params.delete(:stream)
|
|
42
42
|
params[:stream] = true if @provider.streamable?(stream) || stream == true
|
|
43
|
-
req = Net::HTTP::Post.new("/
|
|
43
|
+
req = Net::HTTP::Post.new(path("/responses"), headers)
|
|
44
44
|
messages = build_complete_messages(prompt, params, role)
|
|
45
45
|
@provider.tracer.set_request_metadata(user_input: extract_user_input(messages, fallback: prompt))
|
|
46
46
|
body = LLM.json.dump({input: [adapt(messages, mode: :response)].flatten}.merge!(params))
|
|
@@ -61,7 +61,7 @@ class LLM::OpenAI
|
|
|
61
61
|
def get(response, **params)
|
|
62
62
|
response_id = response.respond_to?(:id) ? response.id : response
|
|
63
63
|
query = URI.encode_www_form(params)
|
|
64
|
-
req = Net::HTTP::Get.new("/
|
|
64
|
+
req = Net::HTTP::Get.new(path("/responses/#{response_id}?#{query}"), headers)
|
|
65
65
|
res, span, tracer = execute(request: req, operation: "request")
|
|
66
66
|
res = ResponseAdapter.adapt(res, type: :responds)
|
|
67
67
|
tracer.on_request_finish(operation: "request", res:, span:)
|
|
@@ -76,7 +76,7 @@ class LLM::OpenAI
|
|
|
76
76
|
# @return [LLM::Object] Response body
|
|
77
77
|
def delete(response)
|
|
78
78
|
response_id = response.respond_to?(:id) ? response.id : response
|
|
79
|
-
req = Net::HTTP::Delete.new("/
|
|
79
|
+
req = Net::HTTP::Delete.new(path("/responses/#{response_id}"), headers)
|
|
80
80
|
res, span, tracer = execute(request: req, operation: "request")
|
|
81
81
|
res = LLM::Response.new(res)
|
|
82
82
|
tracer.on_request_finish(operation: "request", res:, span:)
|
|
@@ -85,7 +85,7 @@ class LLM::OpenAI
|
|
|
85
85
|
|
|
86
86
|
private
|
|
87
87
|
|
|
88
|
-
[:headers, :execute, :set_body_stream, :resolve_tools].each do |m|
|
|
88
|
+
[:path, :headers, :execute, :set_body_stream, :resolve_tools].each do |m|
|
|
89
89
|
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
|
90
90
|
end
|
|
91
91
|
|
|
@@ -4,6 +4,8 @@ class LLM::OpenAI
|
|
|
4
4
|
##
|
|
5
5
|
# @private
|
|
6
6
|
class StreamParser
|
|
7
|
+
EMPTY_HASH = {}.freeze
|
|
8
|
+
|
|
7
9
|
##
|
|
8
10
|
# Returns the fully constructed response body
|
|
9
11
|
# @return [Hash]
|
|
@@ -14,7 +16,11 @@ class LLM::OpenAI
|
|
|
14
16
|
def initialize(stream)
|
|
15
17
|
@body = {}
|
|
16
18
|
@stream = stream
|
|
17
|
-
@emits = {tools:
|
|
19
|
+
@emits = {tools: {}}
|
|
20
|
+
@can_emit_content = stream.respond_to?(:on_content)
|
|
21
|
+
@can_emit_reasoning_content = stream.respond_to?(:on_reasoning_content)
|
|
22
|
+
@can_emit_tool_call = stream.respond_to?(:on_tool_call)
|
|
23
|
+
@can_push_content = stream.respond_to?(:<<)
|
|
18
24
|
end
|
|
19
25
|
|
|
20
26
|
##
|
|
@@ -45,45 +51,68 @@ class LLM::OpenAI
|
|
|
45
51
|
end
|
|
46
52
|
|
|
47
53
|
def merge_choices!(choices)
|
|
54
|
+
body_choices = @body["choices"]
|
|
48
55
|
choices.each do |choice|
|
|
49
56
|
index = choice["index"]
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
57
|
+
delta = choice["delta"] || EMPTY_HASH
|
|
58
|
+
target_message = if body_choice = body_choices[index]
|
|
59
|
+
body_choice["message"]
|
|
60
|
+
else
|
|
61
|
+
body_choices[index] = {"message" => {"role" => "assistant"}}
|
|
62
|
+
body_choices[index]["message"]
|
|
63
|
+
end
|
|
64
|
+
merge_delta!(target_message, delta)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def merge_delta!(target_message, delta)
|
|
69
|
+
if delta.length == 1
|
|
70
|
+
merge_single_delta!(target_message, delta)
|
|
71
|
+
elsif content = delta["content"]
|
|
72
|
+
if target_content = target_message["content"]
|
|
73
|
+
target_content << content
|
|
74
|
+
else
|
|
75
|
+
target_message["content"] = content
|
|
76
|
+
end
|
|
77
|
+
emit_content(content)
|
|
78
|
+
elsif reasoning = delta["reasoning_content"]
|
|
79
|
+
if target_reasoning = target_message["reasoning_content"]
|
|
80
|
+
target_reasoning << reasoning
|
|
81
|
+
else
|
|
82
|
+
target_message["reasoning_content"] = reasoning
|
|
83
|
+
end
|
|
84
|
+
emit_reasoning_content(reasoning)
|
|
85
|
+
elsif tool_calls = delta["tool_calls"]
|
|
86
|
+
merge_tools!(target_message, tool_calls)
|
|
87
|
+
end
|
|
88
|
+
return if delta.length <= 1
|
|
89
|
+
delta.each do |key, value|
|
|
90
|
+
next if value.nil? || key == "content" || key == "reasoning_content" || key == "tool_calls"
|
|
91
|
+
target_message[key] = value
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def merge_single_delta!(target_message, delta)
|
|
96
|
+
if content = delta["content"]
|
|
97
|
+
if target_content = target_message["content"]
|
|
98
|
+
target_content << content
|
|
99
|
+
else
|
|
100
|
+
target_message["content"] = content
|
|
101
|
+
end
|
|
102
|
+
emit_content(content)
|
|
103
|
+
return
|
|
104
|
+
end
|
|
105
|
+
if reasoning = delta["reasoning_content"]
|
|
106
|
+
if target_reasoning = target_message["reasoning_content"]
|
|
107
|
+
target_reasoning << reasoning
|
|
69
108
|
else
|
|
70
|
-
|
|
71
|
-
@body["choices"][index] = {"message" => message_hash}
|
|
72
|
-
(choice["delta"] || {}).each do |key, value|
|
|
73
|
-
next if value.nil?
|
|
74
|
-
if key == "content"
|
|
75
|
-
emit_content(value)
|
|
76
|
-
message_hash[key] = value
|
|
77
|
-
elsif key == "reasoning_content"
|
|
78
|
-
emit_reasoning_content(value)
|
|
79
|
-
message_hash[key] = value
|
|
80
|
-
elsif key == "tool_calls"
|
|
81
|
-
merge_tools!(message_hash, value)
|
|
82
|
-
else
|
|
83
|
-
message_hash[key] = value
|
|
84
|
-
end
|
|
85
|
-
end
|
|
109
|
+
target_message["reasoning_content"] = reasoning
|
|
86
110
|
end
|
|
111
|
+
emit_reasoning_content(reasoning)
|
|
112
|
+
return
|
|
113
|
+
end
|
|
114
|
+
if tool_calls = delta["tool_calls"]
|
|
115
|
+
merge_tools!(target_message, tool_calls)
|
|
87
116
|
end
|
|
88
117
|
end
|
|
89
118
|
|
|
@@ -93,12 +122,11 @@ class LLM::OpenAI
|
|
|
93
122
|
tindex = toola["index"]
|
|
94
123
|
tindex = index unless Integer === tindex && tindex >= 0
|
|
95
124
|
toolb = target["tool_calls"][tindex]
|
|
96
|
-
|
|
125
|
+
functiona = toola["function"]
|
|
126
|
+
functionb = toolb && toolb["function"]
|
|
127
|
+
if functiona && functionb
|
|
97
128
|
# Append to existing function arguments
|
|
98
|
-
|
|
99
|
-
toolb["function"][func_key] ||= +""
|
|
100
|
-
toolb["function"][func_key] << func_value
|
|
101
|
-
end
|
|
129
|
+
merge_function!(functionb, functiona)
|
|
102
130
|
else
|
|
103
131
|
target["tool_calls"][tindex] = toola
|
|
104
132
|
end
|
|
@@ -106,40 +134,61 @@ class LLM::OpenAI
|
|
|
106
134
|
end
|
|
107
135
|
end
|
|
108
136
|
|
|
137
|
+
def merge_function!(target, source)
|
|
138
|
+
if arguments = source["arguments"]
|
|
139
|
+
if target_arguments = target["arguments"]
|
|
140
|
+
target_arguments << arguments
|
|
141
|
+
else
|
|
142
|
+
target["arguments"] = arguments
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
if name = source["name"]
|
|
146
|
+
if target_name = target["name"]
|
|
147
|
+
target_name << name
|
|
148
|
+
else
|
|
149
|
+
target["name"] = name
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
return if source.length <= 2
|
|
153
|
+
source.each do |func_key, func_value|
|
|
154
|
+
next if func_key == "arguments" || func_key == "name"
|
|
155
|
+
target[func_key] ||= +""
|
|
156
|
+
target[func_key] << func_value
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
109
160
|
def emit_content(value)
|
|
110
|
-
if @
|
|
161
|
+
if @can_emit_content
|
|
111
162
|
@stream.on_content(value)
|
|
112
|
-
elsif @
|
|
163
|
+
elsif @can_push_content
|
|
113
164
|
@stream << value
|
|
114
165
|
end
|
|
115
166
|
end
|
|
116
167
|
|
|
117
168
|
def emit_reasoning_content(value)
|
|
118
|
-
if @
|
|
169
|
+
if @can_emit_reasoning_content
|
|
119
170
|
@stream.on_reasoning_content(value)
|
|
120
171
|
end
|
|
121
172
|
end
|
|
122
173
|
|
|
123
174
|
def emit_tool(tool, tindex)
|
|
124
|
-
return unless @
|
|
125
|
-
return
|
|
126
|
-
return if @emits[:tools].include?(tindex)
|
|
127
|
-
function, error = resolve_tool(tool)
|
|
128
|
-
@emits[:tools] << tindex
|
|
129
|
-
@stream.on_tool_call(function, error)
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def complete_tool?(tool)
|
|
175
|
+
return unless @can_emit_tool_call
|
|
176
|
+
return if @emits[:tools][tindex]
|
|
133
177
|
function = tool["function"]
|
|
134
|
-
function && tool["id"] && function["name"]
|
|
178
|
+
return unless function && tool["id"] && function["name"]
|
|
179
|
+
return unless arguments_complete?(function["arguments"])
|
|
180
|
+
arguments = parse_arguments(function["arguments"])
|
|
181
|
+
return unless arguments
|
|
182
|
+
function, error = resolve_tool(tool, function, arguments)
|
|
183
|
+
@emits[:tools][tindex] = true
|
|
184
|
+
@stream.on_tool_call(function, error)
|
|
135
185
|
end
|
|
136
186
|
|
|
137
|
-
def resolve_tool(tool)
|
|
138
|
-
function = tool["function"]
|
|
187
|
+
def resolve_tool(tool, function, arguments)
|
|
139
188
|
registered = LLM::Function.find_by_name(function["name"])
|
|
140
189
|
fn = (registered || LLM::Function.new(function["name"])).dup.tap do |fn|
|
|
141
190
|
fn.id = tool["id"]
|
|
142
|
-
fn.arguments =
|
|
191
|
+
fn.arguments = arguments
|
|
143
192
|
end
|
|
144
193
|
[fn, (registered ? nil : @stream.tool_not_found(fn))]
|
|
145
194
|
end
|
|
@@ -151,5 +200,10 @@ class LLM::OpenAI
|
|
|
151
200
|
rescue *LLM.json.parser_error
|
|
152
201
|
nil
|
|
153
202
|
end
|
|
203
|
+
|
|
204
|
+
def arguments_complete?(arguments)
|
|
205
|
+
value = arguments.to_s.rstrip
|
|
206
|
+
!value.empty? && value.end_with?("}")
|
|
207
|
+
end
|
|
154
208
|
end
|
|
155
209
|
end
|